修页面白屏:十一个显示函数没交给模板,并加一道接线守卫

现象。持仓管理系统页面整页白屏,控制台只有一句
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:
zlt 2026-09-10 11:06:39 +08:00
parent 4455002a4a
commit 9359056d04
3 changed files with 175 additions and 0 deletions

View File

@ -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,

View File

@ -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"]

View File

@ -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 早上队列里出现八条新建仓提议第一次真的调到它
页面当场白屏也就是说**它从埋下到爆炸隔了六天而这六天里页面每天都在用**
同一批一共漏了十一个propVerdictLinepropBasispropLogicpropLogicMore
propTextpropHeadshardCnopCnparamsCndropCnplainCheck
说明这不是一次手误是这种错误没有任何东西拦得住
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 会抛 ReferenceErrorsetup 整个挂掉、页面打开就白。"
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()