Files
jcgl-dev/docs/frontend/_src/build_jcxx_optimized.py
T

505 lines
20 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""
由 YktVue/src/views/JcXx.vue 生成 JcXx.optimized.vue。
设计约束(与用户需求一一对应):
1. <script> 段逐字节不变 —— 功能集合 100% 保留;
2. <template> 只增加语义 class、修正一处事件名拼写,不增删任何元素/指令/绑定;
3. <style> 原规则全部保留,仅修正 4 处明显错误,其余以「追加优化层」覆盖;
4. 不引入新配色 / 新字体 / 新组件 / 新字段,全部取自本页既有设计语言。
同时输出校验与布局快照所需的中间产物到 docs/frontend/_src/。
"""
import io
import os
import re
import hashlib
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
SRC = os.path.join(ROOT, 'YktVue', 'src', 'views', 'JcXx.vue')
OUT = os.path.join(ROOT, 'YktVue', 'src', 'views', 'JcXx.optimized.vue')
WORK = os.path.join(os.path.dirname(__file__))
src = io.open(SRC, encoding='utf-8').read()
script_before = src[src.index('<script>'):src.index('</script>') + len('</script>')]
s = src
def rep(text, old, new, tag, count=1):
n = text.count(old)
assert n == count, '[%s] 期望 %d 处,实际 %d 处' % (tag, n, count)
return text.replace(old, new)
# ============================================================ 1. template:语义 class
s = rep(s, '<div class="app_contianer">',
'<div class="app_contianer jc-opt-root">', '根容器 class')
s = rep(s, '<el-row style="height: 150px;" :gutter="15">',
'<el-row class="jc-header-row" style="height: 150px;" :gutter="15">',
'顶部行 class')
s = rep(s, '<el-col :span="9" style="height: 100%; display: flex; flex-direction: column; justify-content: center;">',
'<el-col class="jc-header-search" :span="9" style="height: 100%; display: flex; flex-direction: column; justify-content: center;">',
'顶部左列 class')
s = rep(s, '<el-col :span="6" style="height: 100%;">',
'<el-col class="jc-header-time" :span="6" style="height: 100%;">',
'顶部中列 class')
s = rep(s, '<el-col :span="9" style="height: 100%;">',
'<el-col class="jc-header-actions" :span="9" style="height: 100%;">',
'顶部右列 class')
s = rep(s, '<span style="font-size: 12px; color: #666;width: 45%;">当前用户: {{ loginAccount }}</span>',
'<span class="jc-user-label" style="font-size: 12px; color: #666;width: 45%;">当前用户: {{ loginAccount }}</span>',
'当前用户标签 class')
# Element UI 派发的是 tab-click;原写法 tab_click 永远不会被触发(处理函数体为空,修正后行为不变)
s = rep(s, '@tab_click=handleTabClick', '@tab-click=handleTabClick', 'tab 事件名拼写')
# 状态列:仅加 class,文本与 v-if 链完全不变(行内底色已是状态语义色,
# 这里只提升文字对比与字重,不把纯文字改成标签,保持原有信息呈现方式)
old_state = """ <el-table-column prop="zt" label="状态" min-width="7%" align="center">
<template slot-scope="scope">
<span v-if="scope.row.ryzt == '0'">待叫乘</span>
<span v-else-if="scope.row.ryzt == '1'">正在呼叫</span>
<span v-else-if="scope.row.ryzt == '5'">正在通话</span>
<span v-else-if="scope.row.ryzt == '2'">叫乘报警</span>
<span v-else-if="scope.row.ryzt == '3'">待离寓</span>
<span v-else-if="scope.row.ryzt == '4'">离寓报警</span>
<span v-else>已入寓</span>
</template>
</el-table-column>"""
new_state = """ <el-table-column prop="zt" label="状态" min-width="7%" align="center">
<template slot-scope="scope">
<span class="jc-st jc-st-normal" v-if="scope.row.ryzt == '0'">待叫乘</span>
<span class="jc-st jc-st-normal" v-else-if="scope.row.ryzt == '1'">正在呼叫</span>
<span class="jc-st jc-st-normal" v-else-if="scope.row.ryzt == '5'">正在通话</span>
<span class="jc-st jc-st-alarm" v-else-if="scope.row.ryzt == '2'">叫乘报警</span>
<span class="jc-st jc-st-normal" v-else-if="scope.row.ryzt == '3'">待离寓</span>
<span class="jc-st jc-st-alarm" v-else-if="scope.row.ryzt == '4'">离寓报警</span>
<span class="jc-st jc-st-muted" v-else>已入寓</span>
</template>
</el-table-column>"""
s = rep(s, old_state, new_state, '主表状态列 class')
old_dev = """ <el-table-column prop="heartBeatTime" label="状态" min-width="8%" align="center">
<template slot-scope="scope">
<span v-if="scope.row.isOffline == '0'">在线</span>
<span v-else-if="scope.row.isOffline == '1'">掉线</span>
</template>
</el-table-column>"""
new_dev = """ <el-table-column prop="heartBeatTime" label="状态" min-width="8%" align="center">
<template slot-scope="scope">
<span class="jc-st jc-st-normal" v-if="scope.row.isOffline == '0'">在线</span>
<span class="jc-st jc-st-alarm" v-else-if="scope.row.isOffline == '1'">掉线</span>
</template>
</el-table-column>"""
s = rep(s, old_dev, new_dev, '设备状态列 class')
old_hist = """ <el-table-column prop="is_jcbj" label="叫乘报警" min-width="4%" align="center">
<template slot-scope="scope">
<span v-if="scope.row.is_jcbj == '1'">是</span>
<span v-else>否</span>
</template>
</el-table-column>
<el-table-column prop="is_lybj" label="离寓报警" min-width="4%" align="center">
<template slot-scope="scope">
<span v-if="scope.row.is_lybj == '1'">是</span>
<span v-else>否</span>
</template>
</el-table-column>"""
new_hist = """ <el-table-column prop="is_jcbj" label="叫乘报警" min-width="4%" align="center">
<template slot-scope="scope">
<span class="jc-st jc-st-alarm" v-if="scope.row.is_jcbj == '1'">是</span>
<span class="jc-st jc-st-muted" v-else>否</span>
</template>
</el-table-column>
<el-table-column prop="is_lybj" label="离寓报警" min-width="4%" align="center">
<template slot-scope="scope">
<span class="jc-st jc-st-alarm" v-if="scope.row.is_lybj == '1'">是</span>
<span class="jc-st jc-st-muted" v-else>否</span>
</template>
</el-table-column>"""
s = rep(s, old_hist, new_hist, '历史报警列 class')
# 所有弹窗统一挂 jc-dialog(append-to-body 后仍在 body 下,必须靠自身 class 收口样式);
# 1800/1700px 的宽弹窗额外挂 jc-wide-dialog 以做视口约束。
dialog_re = re.compile(r'<el-dialog\b[^>]*?>', re.S)
dialog_tags = dialog_re.findall(s)
expected = s.count('<el-dialog')
assert len(dialog_tags) == expected, 'el-dialog 标签解析数 %d != %d' % (len(dialog_tags), expected)
def decorate_dialog(m):
tag = m.group(0)
wide = bool(re.search(r'width="(?:1800|1700)px"', tag))
extra = 'jc-dialog' + (' jc-wide-dialog' if wide else '')
cm = re.search(r'\bclass="([^"]*)"', tag)
if cm:
merged = extra + (' ' + cm.group(1) if cm.group(1).strip() else '')
return tag[:cm.start()] + 'class="' + merged + '"' + tag[cm.end():]
return tag.replace('<el-dialog', '<el-dialog class="' + extra + '"', 1)
s = dialog_re.sub(decorate_dialog, s)
print('弹窗装饰:%d 个(其中宽弹窗 %d 个)' % (
expected, s.count('jc-wide-dialog')))
# ============================================================ 2. style:4 处错误修正
# (a) margin-top: -2 缺单位,是无效声明(2 处)
s = rep(s, 'margin-top: -2;', 'margin-top: 0;', '无效负边距归零', count=2)
# (b) 同一 :hover 位移被重复定义为 -1px / -2px,统一为 -1px
s = rep(s, 'transform: translateY(-2px) !important;',
'transform: translateY(-1px) !important;', 'hover 位移统一')
# (c) 所有渐变按钮 hover 被强制刷成蓝色渐变,破坏橙/深青/红按钮的语义色
old_hover = """/* 查询按钮特殊样式 */
.el-button[style*="linear-gradient"]:hover {
background: linear-gradient(135deg, #66b3ff, #409EFF) !important;
box-shadow: 0 8px 25px rgba(64, 158, 255, 0.5) !important;
}"""
new_hover = """/* 查询按钮特殊样式(修正:保留按钮自身渐变,只做亮度与阴影反馈) */
.el-button[style*="linear-gradient"]:hover {
filter: brightness(1.08) !important;
box-shadow: 0 8px 25px rgba(11, 88, 88, 0.28) !important;
}"""
s = rep(s, old_hover, new_hover, '渐变按钮 hover 配色修正')
# ============================================================ 3. 追加体验优化层
OPT_CSS = r"""
/* ==========================================================================
JcXx.vue 体验与可用性优化层 v1
--------------------------------------------------------------------------
只修六类问题:布局拥挤/留白失衡、对齐间距不一致、信息层级、
交互反馈缺失、状态提示不明确、多尺寸适配。
· 不新增/删减/替换任何功能,<script> 逐字节未改;
· 不引入新配色:所有色值取自本页原有样式
(#029191 深青 / #0b5858 深青 / #fff45c 黄 / #2c3e50 墨 /
#c45656 红 / #606266 灰 / #dcdfe6 边框 / #1990f1 选中蓝);
· 不改动字号基准、组件形态与信息呈现方式,仅统一不一致处并补齐断点;
· 新增规则统一以 .jc-opt-root / .jc-dialog 收口,不扩大原全局样式的影响面。
原样式规则完整保留在前,本层可整段移除以回到优化前效果。
========================================================================== */
/* ---- O1 顶部:固定 150px + 40px 跑马灯上边距造成的拥挤与留白失衡 ---- */
.jc-header-row{
height:auto !important;
min-height:150px;
display:flex;
flex-wrap:wrap;
align-items:stretch;
}
.jc-header-row > .el-col{
height:auto !important;
min-height:150px;
float:none;
}
.jc-header-search{
justify-content:flex-start !important;
padding:4px 0 8px;
}
.jc-header-time{
display:flex !important;
align-items:center;
justify-content:center;
}
.jc-header-time > .el-row{ width:100%; }
.jc-header-actions{ padding:2px 0; }
.top-left-marquee-row{ margin-top:10px !important; margin-bottom:8px !important; }
.top-left-search-area{
padding:8px 14px 10px !important;
justify-content:flex-start !important;
}
.top-left-input-row{ margin-top:0 !important; margin-bottom:10px !important; }
.top-left-action-row{ margin-top:0 !important; }
.form-item-container{ padding:0 6px; }
/* 原跑马灯 padding-left:100% + translateX(0) 起步,开屏数秒内整条提示为空。
用负动画延时把滚动相位提前,文案/配色/组件/速度全部不变。 */
.marquee-content{ animation-delay:-4s; }
.center-time-section{
width:calc(100% - 30px);
box-sizing:border-box;
margin:0 15px;
}
.date-time{ width:100%; }
/* 右上按钮组:等间距、窄屏可换行,避免互相挤压与文字截断 */
.jc-header-actions .el-row > .el-col > div{
flex-wrap:wrap;
gap:8px !important;
}
.jc-header-actions .el-button{ flex:0 0 auto; }
.jc-user-label{
width:auto !important;
white-space:nowrap;
color:#606266 !important;
font-size:12px !important;
}
/* ---- O2 表单/弹窗:label 宽度 25%/20%/96px 混用导致对齐不一致 ---- */
.jc-opt-root .el-form label[style*="width:25%"],
.jc-opt-root .el-form label[style*="width:20%"],
.jc-dialog .el-form label[style*="width:25%"],
.jc-dialog .el-form label[style*="width:20%"]{
flex:0 0 96px;
width:96px !important;
min-width:96px;
text-align:center;
white-space:nowrap;
line-height:20px;
}
.jc-opt-root .el-form .el-col > .el-input,
.jc-opt-root .el-form .el-col > .el-select,
.jc-opt-root .el-form .el-col > div > .el-input,
.jc-opt-root .el-form .el-col > div > .el-select,
.jc-dialog .el-form .el-col > .el-input,
.jc-dialog .el-form .el-col > .el-select,
.jc-dialog .el-form .el-col > div > .el-input,
.jc-dialog .el-form .el-col > div > .el-select{
flex:1 1 0;
min-width:0;
}
.jc-dialog .el-form-item{ margin-bottom:16px; }
.jc-dialog .el-dialog__footer .el-button + .el-button{ margin-left:12px; }
/* 宽弹窗 1800/1700px 在 1366 及以下屏幕会横向越界 */
.jc-wide-dialog .el-dialog{
max-width:calc(100vw - 32px) !important;
}
/* ---- O3 交互反馈:焦点、hover、禁用 ---- */
.el-button:hover{ transform:translateY(-1px) !important; }
/* 焦点轮廓:element-ui 自身把 outline 压掉了,这里用本页作用域 + !important 补回 */
.jc-opt-root .el-button:focus,
.jc-opt-root .el-button:focus-visible,
.jc-opt-root .el-input__inner:focus,
.jc-opt-root .el-input__inner:focus-visible,
.jc-opt-root .el-checkbox__inner:focus,
.jc-dialog .el-button:focus,
.jc-dialog .el-button:focus-visible,
.jc-dialog .el-input__inner:focus,
.jc-dialog .el-input__inner:focus-visible,
.jc-dialog .el-checkbox__inner:focus{
outline:2px solid #029191 !important;
outline-offset:2px !important;
}
/* 部分浏览器对 input 的 outline 计算值有限制,补一层主色光晕保证焦点可见 */
.jc-opt-root .el-input__inner:focus,
.jc-opt-root .el-input__inner:focus-visible,
.jc-dialog .el-input__inner:focus,
.jc-dialog .el-input__inner:focus-visible{
box-shadow:0 0 0 3px rgba(2,145,145,.22) !important;
}
.el-button.is-disabled,
.el-button[disabled]{
cursor:not-allowed !important;
opacity:.55;
filter:grayscale(.25);
}
.el-button.is-disabled:hover,
.el-button[disabled]:hover{
transform:none !important;
box-shadow:none !important;
}
/* ---- O4 表格:保留行内状态底色,补齐 hover / 选中反馈与列层级 ----
原 .el-table__body tr:hover > td 用 background-color:inherit!important
是为了让 td 继承 tr 上的行内状态色(黄/绿/红/蓝),机制必须保留;
这里只叠加一层半透明 background-image 提供 hover 反馈,不覆盖底色。 */
.jc-opt-root .main-jcxx-table .el-table__body tr:hover > td,
.jc-dialog .el-table__body tr:hover > td{
background-image:linear-gradient(rgba(2,145,145,.10), rgba(2,145,145,.10)) !important;
}
/* 选中行原先用纯蓝底 !important 覆盖,会把报警红/到点黄整行抹掉。
改为「继承行内状态色 + 叠加蓝层 + 首列蓝条」,选中态与状态色可同时辨识。 */
.jc-opt-root .el-table__body tr.current-row > td,
.jc-dialog .el-table__body tr.current-row > td,
.jc-dialog .history-jcxx-table.el-table--striped .el-table__body tr.current-row > td,
.jc-dialog .history-jcxx-table.el-table--striped .el-table__body tr.current-row.el-table__row--striped > td{
background-color:inherit !important;
background-image:linear-gradient(rgba(25,144,241,.30), rgba(25,144,241,.30)) !important;
color:inherit !important;
}
.jc-opt-root .el-table__body tr.current-row > td:first-child,
.jc-dialog .el-table__body tr.current-row > td:first-child{
box-shadow:inset 3px 0 0 #1990f1;
}
/* 状态文字:行内底色已承担语义色,这里只提升字重与对比,不改成标签 */
.jc-st{ font-weight:700; letter-spacing:.3px; }
.jc-st-normal{ color:#2c3e50; }
.jc-st-alarm{ color:#ffffff; text-shadow:0 1px 2px rgba(0,0,0,.35); }
.jc-st-muted{ color:#606266; }
.jc-opt-root .main-jcxx-table th.el-table__cell{
font-weight:700 !important;
border-bottom:2px solid #029191 !important;
vertical-align:middle;
}
.jc-opt-root .main-jcxx-table .cell{ padding-left:6px; padding-right:6px; }
.jc-opt-root .el-tabs__item{ font-weight:600; }
/* ---- O5 吸底值班条:absolute 居中在窄屏与左右内容重叠 ---- */
.jc-opt-root{ padding-bottom:62px; } /* 防止 50px 吸底条遮住表格最后一行 */
.bottom-duty-bar{
display:flex;
align-items:center;
gap:16px;
padding:0 20px;
box-sizing:border-box;
}
.bottom-stay-summary{
flex:1 1 auto;
gap:36px;
padding-left:0;
min-width:0;
}
.latest-sync-time{
position:static !important;
transform:none !important;
flex:0 1 auto;
white-space:nowrap;
}
.bottom-duty-info{
flex:1 1 auto;
display:flex;
align-items:center;
justify-content:flex-end;
gap:16px;
min-width:0;
}
.bottom-duty-info .el-button{
position:static !important;
right:auto !important;
top:auto !important;
flex:0 0 auto;
}
.bottom-duty-info > span{
margin-right:0 !important;
position:static !important;
top:auto !important;
white-space:nowrap;
}
/* ---- O6 右下角「有新的交路信息」提示:改为固定定位并补齐点击反馈 ----
原为 200x200 的 absolute 大色块,滚动后位置漂移且易被吸底条压住。 */
.bottom-right-div{
position:fixed !important;
bottom:72px;
right:20px;
width:200px;
height:auto;
box-sizing:border-box;
padding:14px 16px;
line-height:1.5;
text-align:center;
border:2px solid #029191;
box-shadow:0 6px 18px rgba(0,0,0,.18);
z-index:1200;
cursor:pointer;
transition:transform .25s ease, box-shadow .25s ease;
}
.bottom-right-div:hover{
transform:translateY(-2px);
box-shadow:0 10px 24px rgba(0,0,0,.24);
}
/* ---- O7 响应式:原页只有 1200/768 两档,且顶部 9/6/9 在中屏严重挤压 ---- */
@media (max-width:1440px){
.jc-header-row{ min-height:0; }
.jc-header-row > .el-col{ min-height:0; }
.jc-header-row > .jc-header-search{ flex:1 1 100%; width:100%; }
.jc-header-row > .jc-header-time{ flex:0 0 320px; width:320px; }
.jc-header-row > .jc-header-actions{ flex:1 1 520px; width:auto; }
.center-time-section{ min-width:0; }
}
@media (max-width:1200px){
.bottom-stay-summary{ gap:20px; }
.bottom-stay-summary,.latest-sync-time{ font-size:20px; }
.bottom-duty-info > span{ font-size:20px !important; }
/* 窄屏下 200px 卡片会压住表格右下数据,收紧为贴边小卡 */
.bottom-right-div{
width:150px;
right:8px;
bottom:62px;
padding:8px 10px;
font-size:14px !important;
line-height:1.35;
opacity:.96;
}
}
@media (max-width:992px){
.jc-header-row > .jc-header-time,
.jc-header-row > .jc-header-actions{ flex:1 1 100%; width:100%; }
.jc-header-row .top-left-input-row .el-col-6{ width:50%; }
.bottom-duty-bar{
flex-wrap:wrap;
height:auto !important;
min-height:50px;
padding:6px 12px;
gap:4px 12px;
}
.bottom-stay-summary,.latest-sync-time,.bottom-duty-info > span{
font-size:16px !important;
line-height:1.4;
}
.latest-sync-time{ flex:1 1 100%; order:3; text-align:right; }
.jc-opt-root{ padding-bottom:88px; }
.bottom-right-div{ bottom:96px; }
}
@media (max-width:768px){
.jc-header-row .top-left-input-row .el-col-6{ width:100%; }
.bottom-right-div{ width:160px; font-size:16px !important; }
}
/* 矮屏:600px 固定表高 + 50px 吸底条会溢出,改为可滚动,不改变数据/列/操作 */
@media (max-height:900px){
.jc-opt-root .main-jcxx-table{
height:auto !important;
max-height:calc(100vh - 260px);
}
.jc-opt-root .main-jcxx-table .el-table__body-wrapper{
height:auto !important;
max-height:calc(100vh - 320px);
overflow-y:auto;
}
.jc-dialog .el-dialog__body{
max-height:calc(100vh - 170px);
overflow-y:auto;
}
}
@media (prefers-reduced-motion:reduce){
.marquee-content{
animation:none !important;
padding-left:0 !important; /* 停动画时必须去掉右推,否则整条提示不可见 */
}
.init-success-content i{ animation:none !important; }
.el-button,
.bottom-right-div{ transition:none !important; }
.el-button:hover{ transform:none !important; }
}
/* ===================== 体验优化层结束 ===================== */
"""
s = rep(s, '</style>', OPT_CSS + '</style>', '追加优化层')
# ============================================================ 4. 写出
# 原文件为 CRLF;写出时保持同一换行,避免全文件 diff / 逐字节比对误报
out_text = s.replace('\n', '\r\n')
io.open(OUT, 'w', encoding='utf-8', newline='').write(out_text)
script_after = s[s.index('<script>'):s.index('</script>') + len('</script>')]
assert script_before == script_after, '<script> 段发生变化,已中止'
print('<script> 段逐字节一致:%d 字节 sha256=%s' % (
len(script_before.encode('utf-8')),
hashlib.sha256(script_before.encode('utf-8')).hexdigest()[:16]))
# 中间产物:优化前 / 优化后的 style 源(供 sass 编译与前后对比快照使用)
def extract_style(text):
a = text.index('<style')
b = text.index('</style>')
return text[text.index('>', a) + 1:b]
io.open(os.path.join(WORK, 'style-before.scss'), 'w', encoding='utf-8').write(extract_style(src))
io.open(os.path.join(WORK, 'style-after.scss'), 'w', encoding='utf-8').write(extract_style(s))
print('输出:%s(%d 行 / %d 字节 / CRLF)' % (OUT, len(s.splitlines()), len(out_text.encode('utf-8'))))
print('原文件:%d 行 / %d 字节' % (len(src.splitlines()), os.path.getsize(SRC)))