修页面白屏:十一个显示函数没交给模板,并加一道接线守卫
现象。持仓管理系统页面整页白屏,控制台只有一句
propVerdictLine is not a function。
根因。这批函数在 setup() 里定义好了、模板里也在用,但没有写进 setup()
末尾那份 return 清单,模板拿到的是 undefined,当函数调用就抛异常、整棵
组件树渲染失败。一共漏了十一个:propVerdictLine、propBasis、propLogic、
propLogicMore、propText、propHeads、hardCn、opCn、paramsCn、dropCn、
plainCheck,全部出自 2026-09-04 的 bcf5783 那批文案改造。
为什么隔了六天才炸。这几行都挂着 v-if,条件是「这条提议带选股判决」。
平时提议队列是空的、条件不成立,函数一次都不会被调用。09-10 早上队列里
出现八条新建仓提议(09:32 到 09:52,全是 WAIT_USER),第一次真的调到它。
修法。把十一个名字补进 return 清单。页面走 FileResponse 每次读盘,
155 是源码挂载,所以 git pull 之后浏览器强刷即生效,不用重启容器。
守卫。新增 scripts/test_page_wiring_guard.py,查两件事:模板引用了、
setup 里定义了、却没 return 的名字;以及 return 清单里写了、setup 里
找不到的名字。不用维护名单,从文件本身推。已在把修复回退的副本上验过,
报满十一处、退出码 1。已登记进 run_tests.py 的 SUITES 与清单说明。
index.html 二十七万字节、没有任何单测,原有的 test_page_enum_guard.py
守的是文案有没有翻译,守不到接线,所以要单独这一道。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4455002a4a
commit
9359056d04
|
|
@ -3906,6 +3906,12 @@ createApp({
|
|||
loadChanges,
|
||||
mode, tx, nm, fullName, nameMap, attention, heldPositions, traderParams,
|
||||
propWhy, propReasons, nextStep, scrollTo, actExitStock, actReduceStock, rowMore,
|
||||
// 2026-09-10 修白屏。下面这十一个显示函数在 setup 里定义了、模板里也在用,
|
||||
// 但从 2026-09-04 那批文案改造起就一直没写进这份清单, 模板拿到的是 undefined。
|
||||
// 平时提议队列为空、那几行 v-if 不成立所以从不调用, 09-10 早上队列里出现
|
||||
// 八条新建仓提议, 第一次真的调到它, 整页渲染当场炸掉 —— 表现是整个页面白屏。
|
||||
propVerdictLine, propBasis, propLogic, propLogicMore, propText, propHeads,
|
||||
hardCn, opCn, paramsCn, dropCn, plainCheck,
|
||||
pHaltBuy, pResumeBuy, pHaltAll, pResumeAll, pReduce, pIncrease, pLiquidate, pSectorExit,
|
||||
pctOf, tgtPos, posMoveValid, posMovePreview, doPosMove, heldSectors, secSel, doSectorExit,
|
||||
pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy,
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@
|
|||
页面枚举名单 scripts/test_page_enum_guard.py 顶部 ENUM_FIELDS —— 后端新增会送到页面的
|
||||
枚举字段就登记一行, 并在 index.html 的 T 表里加对应的组。不登记等于不检查。
|
||||
index.html 没有别的测试, 这道扫描是它唯一的护栏。
|
||||
页面接线 scripts/test_page_wiring_guard.py —— 不用维护名单, 它从文件本身推。
|
||||
查的是 setup() 里定义给模板用的东西有没有真的 return 出去。
|
||||
2026-09-10 白屏 (propVerdictLine is not a function) 就是漏了十一个,
|
||||
而那批从 09-04 埋下、每天都在用的页面上藏了六天才炸。
|
||||
动作求值器 scripts/test_batch12_units.py 用例「回归·既有四类动作的判据与研判范围没被
|
||||
动过」—— action_engine.EVALUATORS 的名单与次序钉在那里 (次序有意义: 减持排
|
||||
在买入前面, 同轮买卖互斥才让得了路)
|
||||
|
|
@ -121,6 +125,7 @@ SUITES = ["test_core_units.py", "test_batch2_units.py", "test_batch3_units.py",
|
|||
"test_batch21_units.py", "test_batch22_units.py", "test_batch23_units.py",
|
||||
"test_batch24_units.py", "test_batch25_units.py",
|
||||
"test_page_enum_guard.py",
|
||||
"test_page_wiring_guard.py",
|
||||
"test_wiring.py"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""页面接线守卫:模板里用到的东西,必须真的交给模板(静态扫描,不连库、不起浏览器)。
|
||||
|
||||
## 为什么要有它
|
||||
|
||||
2026-09-10 上午,持仓管理系统的页面整页白屏,浏览器控制台只有一句
|
||||
`propVerdictLine is not a function`。
|
||||
|
||||
根因是:这个函数在 setup() 里定义好了、模板里也在用,但**没有写进 setup()
|
||||
末尾那份 return 清单**。Vue 的组合式写法里,只有 return 出去的东西模板才拿得到,
|
||||
没 return 的在模板里就是 undefined,当成函数调用就抛异常,整棵组件树渲染失败。
|
||||
|
||||
它坏得很安静。那几行都挂着 v-if,条件是「这条提议带选股判决」。平时提议队列是空的、
|
||||
条件不成立,函数一次都不会被调用,页面看上去完全正常。这个洞从 2026-09-04 那批
|
||||
文案改造就埋下了,一直到 09-10 早上队列里出现八条新建仓提议,第一次真的调到它,
|
||||
页面当场白屏。也就是说:**它从埋下到爆炸隔了六天,而这六天里页面每天都在用。**
|
||||
|
||||
同一批一共漏了十一个(propVerdictLine、propBasis、propLogic、propLogicMore、
|
||||
propText、propHeads、hardCn、opCn、paramsCn、dropCn、plainCheck),
|
||||
说明这不是一次手误,是这种错误没有任何东西拦得住。
|
||||
|
||||
index.html 是二十七万字节的单文件页面,没有任何单测,也没有构建期检查。
|
||||
scripts/test_page_enum_guard.py 守的是「文案有没有翻译」,守不到接线。所以要有这一道。
|
||||
|
||||
## 它检查什么
|
||||
|
||||
一、**漏 return**:模板里作为标识符引用、且在 setup() 里定义过、却不在 return
|
||||
清单里的名字。这一条就是白屏那个 bug。
|
||||
二、**清单里有、setup 里没有**:return 清单里写了一个 setup 里根本没定义的名字,
|
||||
JS 会直接抛 ReferenceError,整个 setup 挂掉,页面同样白屏,而且是打开就白。
|
||||
改名或删函数时忘了改清单就会这样。
|
||||
|
||||
两条都只看静态文本,不执行任何 JS。
|
||||
|
||||
## 它检查不到什么
|
||||
|
||||
模板里引用了一个**任何地方都没定义**的名字(既不在 setup 里也不在清单里),
|
||||
这道扫描看不出来 —— 因为它无从判断那是笔误还是 Vue 的内置项、还是 v-for 的循环变量。
|
||||
那类错误要靠真在浏览器里打开页面才看得见。
|
||||
|
||||
## 怎么维护
|
||||
|
||||
不用维护名单,它是从文件本身推出来的。setup 里新加一个给模板用的函数或状态,
|
||||
照常写进 return 清单就行,忘了写这道检查会喊。
|
||||
|
||||
跑法:python3 scripts/test_page_wiring_guard.py
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PAGE = os.path.join(os.path.dirname(HERE), "app", "web", "static", "index.html")
|
||||
|
||||
# Vue 模板里合法但不由 setup 提供的东西,出现在清单差集里也不算问题。
|
||||
# (目前用不上,留着是因为将来引入 defineExpose 或全局属性时这里是唯一的落点。)
|
||||
ALLOW_MISSING = set()
|
||||
|
||||
|
||||
def _strip_line_comments(text):
|
||||
"""剥掉 // 行注释。return 清单里现在有中文注释,注释里的英文词不能被当成导出名。"""
|
||||
out = []
|
||||
for line in text.split("\n"):
|
||||
i = line.find("//")
|
||||
out.append(line[:i] if i >= 0 else line)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _strip_strings(text):
|
||||
"""剥掉字符串字面量。模板表达式里的 'trader'、"ACCEPTED" 这些不是标识符引用。"""
|
||||
text = re.sub(r"'[^'\n]*'", "''", text)
|
||||
text = re.sub(r'"[^"\n]*"', '""', text)
|
||||
return text
|
||||
|
||||
|
||||
def _idents(expr):
|
||||
"""从一段表达式里取出被当作标识符引用的名字。
|
||||
|
||||
排除两类:紧跟在点号后面的(那是属性名,不是标识符),以及后面紧跟冒号的
|
||||
(那是对象字面量的键)。不排除的话 p.verdict 里的 verdict 会被误当成引用。
|
||||
"""
|
||||
expr = _strip_strings(expr)
|
||||
names = set()
|
||||
for m in re.finditer(r"(?<![.\w$])([A-Za-z_$][\w$]*)\s*(:?)", expr):
|
||||
if m.group(2) == ":":
|
||||
continue
|
||||
names.add(m.group(1))
|
||||
return names
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(PAGE):
|
||||
print(f"找不到页面文件: {PAGE}")
|
||||
sys.exit(1)
|
||||
src = open(PAGE, encoding="utf-8").read()
|
||||
|
||||
m_app = re.search(r'<div id="app"', src)
|
||||
i_setup = src.find("createApp({")
|
||||
if not m_app or i_setup < 0:
|
||||
print("页面结构变了:找不到 <div id=\"app\"> 或 createApp({,"
|
||||
"这道检查的前提没了,先修这里")
|
||||
sys.exit(1)
|
||||
tpl = src[m_app.start():i_setup]
|
||||
body = src[i_setup:]
|
||||
|
||||
# setup() 里的顶层定义。缩进恰好四格的那一层才是 setup 的直接作用域,
|
||||
# 更深的缩进是别的函数内部的局部变量,模板本来就够不到。
|
||||
defined = set()
|
||||
for m in re.finditer(r"^ (?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=", body, re.M):
|
||||
defined.add(m.group(1))
|
||||
for m in re.finditer(r"^ function\s+([A-Za-z_$][\w$]*)", body, re.M):
|
||||
defined.add(m.group(1))
|
||||
if not defined:
|
||||
print("在 setup() 里一个顶层定义都没找到,缩进约定或文件结构变了,先修这里")
|
||||
sys.exit(1)
|
||||
|
||||
# setup() 末尾那份 return 清单。
|
||||
m_ret = re.search(r"\n return \{(.*?)\};\n \}\n\}\)\.use", body, re.S)
|
||||
if not m_ret:
|
||||
print("找不到 setup() 末尾的 return 清单,这道检查的前提没了,先修这里")
|
||||
sys.exit(1)
|
||||
ret_src = _strip_line_comments(m_ret.group(1))
|
||||
exported = set(re.findall(r"[A-Za-z_$][\w$]*", ret_src))
|
||||
|
||||
# 模板里的引用:插值 {{ }}、指令 v-xxx="…"、属性绑定 :x="…"、事件 @x="…"。
|
||||
used = set()
|
||||
for chunk in re.findall(r"\{\{(.*?)\}\}", tpl, re.S):
|
||||
used |= _idents(chunk)
|
||||
for chunk in re.findall(r'(?:v-[\w:.\-]+|[:@][\w:.\-]+)="([^"]*)"', tpl):
|
||||
used |= _idents(chunk)
|
||||
|
||||
bad = []
|
||||
|
||||
# 一、模板在用、setup 里有、却没 return
|
||||
for n in sorted((used & defined) - exported - ALLOW_MISSING):
|
||||
dl = next((i for i, l in enumerate(src.split("\n"), 1)
|
||||
if re.match(r"^ (?:const|let|var|function)\s+" + re.escape(n) + r"\b", l)), "?")
|
||||
ul = next((i for i, l in enumerate(tpl.split("\n"), 1)
|
||||
if re.search(r"\b" + re.escape(n) + r"\b", l)), "?")
|
||||
bad.append(f"{n} 在 setup() 第 {dl} 行定义、模板第 {ul} 行左右在用,"
|
||||
f"但没写进 return 清单。模板拿到的是 undefined,"
|
||||
f"当函数调用就整页白屏。")
|
||||
|
||||
# 二、清单里写了、setup 里却没定义
|
||||
for n in sorted(exported - defined):
|
||||
# 清单里的名字未必都是顶层 const/function,也可能来自解构(如 Vue 的 ref)。
|
||||
# 只有在 setup 体内完全找不到这个词时才算真的没有。
|
||||
if re.search(r"\b" + re.escape(n) + r"\b", body[:m_ret.start()]):
|
||||
continue
|
||||
bad.append(f"return 清单里有 {n},但 setup() 里找不到它。"
|
||||
f"JS 会抛 ReferenceError,setup 整个挂掉、页面打开就白。"
|
||||
f"多半是改名或删函数时忘了改清单。")
|
||||
|
||||
if bad:
|
||||
print(f"页面接线守卫:发现 {len(bad)} 处")
|
||||
for b in bad:
|
||||
print(f" - {b}")
|
||||
sys.exit(1)
|
||||
print(f"ALL OK — 页面接线守卫:模板引用 {len(used & defined)} 个 setup 名字,"
|
||||
f"return 清单 {len(exported)} 项,两侧对得上")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue