Files
jcgl-dev/docs/frontend/_src/build_jcxx_optimized.py
T
zfjia 329e27b515 初始提交:三端设计文档与 dev 代码基线
docs/:系统梳理、架构优化方案、CR-02 三端功能设计修订(含 §15 三端一致性同步与平台统计分析)、公寓端/应用端/平台端分册、公寓统计分析与排名报表、公寓端小步迭代开发计划(IT-0~IT-8)

dev/:RuoYi-Vue-master(后端)与 RuoYi-Vue2-master(前端)官方上游基线,未改动
2026-11-10 13:43:05 +08:00

506 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)))