# DESIGN —— ek-ui(S1b 常驻观测界面)
Frozen ref: docs/ek-ui-station-plan.md(DV 表 + S1b 段验收)。本档只记**结构决定**与
执行期钉。形式沿用 ek-dm/ek-station 的 BD-U1.. 编号加序。
## 结构
- `state.rs`:`UiShared`(总线面写 / 窗口面读)+ `LogRing`(cap 512,挤旧 +
spilled 单调计数)+ `BusSnap`(svctl.list 行 + 逐插件服务 id 目录)。
锁粒度:每次操作 lock/clone,不跨帧持锁;`snapshot()` 返回 `Arc<BusSnap>` 零拷贝。
- `plugin.rs`:`UiPlugin`("ui"):routes = `eLog`/`ePlugUp`/`ePlugDown`/`eTimer` +
`eTick.`(行情带唯一源)+ `eContract.`/`eContractFinish`(查询合约窗数据流,
ek-ctp 登录后自动查询,UI 只收不催)。
- eLog:payload `LogData`,`is_warning_or_above` 打 WARN 标签(+UCH 转储环形);
- ePlugUp/Down:payload `PlugNotice` → 单行 host 日志 + rev;
- eTick./eContract.:payload 直落 `tick_store`/`contract_store`(键 EX.symbol);
- eTimer:`ask(svctl.list, 2s)` + 逐插件 `ctx.services_of` → `set_snapshot`;
ask 失败进日志面,不堆 panic。
- `render.rs`:`render_body(ctx, &UiShared)` 唯一渲染入口(L2;v5 面板化,
BD-UI-27/29):TopBottomPanel(菜单)→ TopBottomPanel(行情带,resizable)→
SidePanel(下单面板 + 盘口,resizable)→ CentralPanel(2×2 监控格,响应式)→
弹窗面(K线/运维/CTP/查询合约,egui::Window,面板之后)。
- `runtime.rs`:
- `ui_kinds(&Arc<UiShared>)` = station 缺省表 + ui 工厂;工厂闭包绑定**同一个**
shared → ui 插件被 svctl 摘除/再注,窗口观测态不丢(状态在 shared 不在插件体);
- `spawn_host_watcher`:`sleep 120ms` 轮询 stop 位 → `engine.close_all(3s)`;
close 顺序(逆 start 序)仍由宿主引擎决定,观察线程只送信号;
- `run_station_ui`:boot(不 start 由 boot 内部? 否—— boot 只装配)→ `engine.start()`
→ 观察线程 → eframe `run_native`;关窗 `on_close_event → request_stop` →
join 观察线程 → 返回。启动失败(start Err)以 `Err(String)` 上抛(供 S1c
launcher 的失败信号,不在窗口内吞错)。
## 执行期钉
- **BD-UI-9(UI 即插件 / 双线程形貌)**:总线 worker 写、eframe 主线程读,以
`Arc<UiShared>` 相连;不加自制通道、不退回向量共享消息队列。
- **BD-UI-10(L2 单渲染入口)**:`render_body(ctx, &UiShared)` 被真实 App 与
kittest 同调;面板组合在 render_body 内(BD-UI-29),测试走
`Harness::builder().build(|ctx| …)`(ctx 级,不再包 CentralPanel)。
- **BD-UI-7(四件套版本锁)**:eframe = egui = egui_kittest = `0.33.3`;
egui-charts 0.2.0 进入点定为 S4(K 线页),进入时与四件套重验一版。
- **BD-UI-11(关窗协议)**:`App::on_close_event` 只置 stop 位;close_all 由
观察线程完成 —— 窗口永不直接持有 `MainEngine::close_all` 调用链。
- **BD-UI-12(kittest 用法纪律,源自官方 README)**:优先节点/文本断言,少用
图像快照;快照只对静态首帧;更新基线仅经 `UPDATE_SNAPSHOTS='true'`;冲突
先 diff 图后压缩容差。
- **BD-UI-13(纯观测零服务)**:ui 插件 `services()` 返回空;管制面唯一在 svctl
—— 界面无特权路径。动态 add 的 ask 窗口期(`on_init` 后)与 headless 同一契约。
- **BD-UI-14(启动器协议,与 ek-station bin `--from/--ready-file` 同口径)**:
清单 = 行式文本(`#` 注释;空行跳过;行内空白拒);就绪标记 = `ready\n`;
spawn 分离式(stdin/stdout 静默 + Windows `CREATE_NO_WINDOW` + stderr 管道,
**stderr 生存期 ≤ 64KB** —— 子进程不朝 stderr 泼流,违约即协议故障);
等待 = 50ms 轮询标记 + 存活检查,30s 上限(超时 kill);失败分类 `Exited{code,
stderr_tail}` / `NoReadySignal{timeout}`;成功路径 **drop Handle = 分离脱管**。
- **BD-UI-15(清单解析地面)**:pick.rs 与 ek-station main.rs 的 `--from` 解析
S1c 各自落地(等语义);S3 toml-lite 重构时统一抽到共享模块(单点 PR),避免
现在跨 crate 发版拖 III 制订的节奏。
- **BD-UI-16(pick 窗两线程形貌)**:UI 线程只画与翻位;spawn+wait 在工作线程
(30s 不冻 UI);结果回槽 `RunShared`;App 见果即关窗,
收场归 `run_pick_launcher`(join + 读槽)。ui 插件自动补入 pick 后配置,
其余勾选即装配面(与 boot 同表)。
- **BD-UI-17(隔离红标的闭环面)**:`ePlugQDown.<名>` 是定向广播(未知名无法
预订阅 —— 严格 exact-type 路由),UI 侧红标走快照行状态扫描
(`is_quarantined`,1s 节拍内落实);隔离行仍可重注入(svctl.start 对隔离行
被 Rejected(重名)拒;既定语义链 = 先 remove 再 start)。红标卡件 trace:
`ek-core::lifecycle::EVENT_PLUG_DOWN_PREFIX("ePlugQDown.")` 常量名与后缀型
一致,但仅作 reference,不预订阅。
- **BD-UI-18(操作指令的唯一通道)**:窗口面动作只 `push_cmd(UiCmd)`,
NOT 直 ask —— `PluginContext` 只在 ui 插件 worker 可用;eTimer 拍先辞行
指令再拉快照;移除走 `pending_confirm` 单槽二步(Armed→Fired;Switched =
目标迁移),取消钮可擦;重注入为单步(加性动作);拒绝类回执
(`Rejected/NoSuchName`)进日志面板,不在 UI 抛错。
- **BD-UI-19(配置面 → 2026-10-17 用户定盘撤销,零配置口径)**:原 S3 落地过
toml-lite + `setting/global.toml` 融入读法 + 状态条 + 脱敏(cfg.rs/cfg 测试
9 例/s7 kittest 1 例)。**现行口径:ek-ui 零配置、宿主零配置** —— 不读任何
配置文件,界面本身无设置;插件结构只由 `--from` 清单或 demo 缺省决定。
需要配置的插件(唯一 = ek-ctp 虚拟账号),配置面长在插件自身,激活时在插件
内完成(见「配置面撤销记录」);宿主/界面不代读不转发。cfg.rs 整块删除;
脱敏纪律(redact)随配置面一同撤销(无配置输入即无秘密暴露面;插件自带
配置面若需展示账号,脱敏规则届时由该插件自持)。
- **BD-UI-20(S4 行情面口径)**:主题面(数据)与身体面(交互)分离——
`MarketStore`持直播线上屏(eBar./eBarUpdate.),(symbol,interval) 格缓存
4096 丢头保尾,帧/更新/回灌三计数 = 「刷新不丢帧统计可见」的三处钉面;
K线 chart 实例**每帧重建**(v1 不保 pan/缩放;首帧快照入库即 review 约定
—— BD-UI-8);图表层引用在本 crate 集中(`render::render_kline_cell`
一点换器,egui_charts→egui_plot 退守路线只动此处,BD-UI-4)。
- **BD-UI-21(S4 直播面工厂纪律)**:UI 宿主的 dm 工厂在 `ui_kinds` 内换建
(real_time=true),ek-station headless 的 rt-off 工厂**不动** ——
宿主差异收在各自工厂表,不回写 ek-station、不新增 svctl 控制面;
dm 工厂 real_time 门为装配参数(缺省 true;UI 观测面要实时门 —— 非配置文件
输入,零配置口径,BD-UI-19 现行版)。
- **BD-UI-15 兑现记录(S3)**:清单地面统一抽世到 `ek-station 0.1.1::pickfile`
(已发布;`ek-ui::pick` 变薄转发,launcher/bin/测试共一处,双实现消灭)。
- **BD-UI-22(S5 纸面网关契约纪律)**:sim 前三口直接复用 ek-ctp 的
Req/Reply 类型(SubscribeReq/SendOrderReq/CancelOrderReq 及回包)——
同构 = 同一类型面,接线换 `sim.` 即插真网关;第四口 `query_rate` 为 sim
自定(ek-ctp 0.2.0 无此口,CTP 流控语义镜像)。撮合规则钉:限价成交于
委托价、市价成交于 close/last、bar.volume = 本拍成交预算、挂单 FIFO ——
手数排队由此可断言。撮合器 `SimMatcher` 纯逻辑零总线(金样本直断),
插件壳只做路由/锁/上总线(锁内撮合锁外 put,与 DmPlugin 同形)。
sim 不引 ek-oms:成交经 eOrder./eTrade./ePosition. 总线入账由 OmsPlugin
被动完成(插件不直调铁律不破)。
- **BD-UI-23(S5 监控页读数纪律)**:监控页读数 = ui 插件 eTimer 拍 ask
`oms.get_all_*` 全量查询帧(与 headless 同源契约;oms 不在 → 空帧不抛错);
下单/撤单/流控三口走 UiCmd → ask `sim.*`(窗口面零直调)。撤单钮只挂
ACTIVE_STATUSES 行(与 ek-data 同口径)。kittest 点击纪律:**`click_accesskit`
取代坐标 `click`** —— 坐标点击对折叠线下(clip 外)节点无效,accesskit
动作可达不可见控件;`get_all_by_label` 精确匹配、`query_by_label_contains`
子串匹配且多命中即炸,撞名列按语义选面。
- **BD-UI-24(S6 CTP 手工配置面,2026-10-17 集成测试战役)**:「插件需要配置 →
配置面在插件自身」的正向界面落地 —— CTP 段 9 输入位全部**人敲**(界面与宿主
不读文件;密码/AuthCode `password(true)` 掩码不回显),`连接 CTP` →
`UiCmd::CtpConfigure(CtpForm)` → ask `ctp.configure`(ek-ctp 0.3.0
D-CONFIGURE:校验/守卫复位/发起归插件,md 账号回落也在插件侧裁决 —— 表单
`to_configure_req` 不预处理);`订阅行情` → ask `ctp.subscribe`(订阅位
逗号/空白解析,空位红字不入列)。`UiCmd` 携 `CtpForm`(全 String 可 PartialEq;
ek-ctp `ConfigureReq` 无 PartialEq,抬线在 ui 插件出队点一次转换,装版不动)。
资金小节 = `oms.get_all_accounts` 帧(可用 = balance−frozen 由 AccountData
自算)。native 经 feature `ctp-native` 透传(默认关;实连战役开,工厂
`with_native` 在 ek-station 侧)。装配序纪律:要观察插件 on_start 的日志,
ui 须在清单里先于该插件(观察序 = 装配序,c0 钉)。
- **BD-UI-25(票据网关选择子,2026-10-17 战役)**:票据新增「网关」位(空 =
`sim` 纸面;填 `ctp` = ask `ctp.send_order`/`ctp.cancel_order`);`UiCmd`
随单携 `gateway`;行级撤单钮跟随委托来路 `gateway_name`(空兜底 sim)。
- **BD-UI-26(CJK 字体回退,2026-10-18 乱码根因)**:epaint 0.33 缺省字体组
(Hack/Ubuntu-Light/NotoEmoji/emoji-icon)**无 CJK 字形** → 中文豆腐块。
`fonts::apply_cjk_fonts` 启动时从系统字体候选表装载一枚 CJK 字体追加
Proportional/Monospace 两族末位(**只收纯 .ttf/.otf** —— epaint 走 ab_glyph,
不吃 .ttc 集合且解析失败即 panic;VF 按缺省实例解析,排位靠后);两处
run_native 取帧闭包接线(runtime + pick 窗)。验收三钉:
f0 字形 API(22 常用汉字 has_glyph)、s16 汉字密集页快照 baseline、实窗
stderr 证据行 `[ek-ui] CJK 回退字体已装载: <path>`。界面文案保持中文单语
(用户定盘:要的是不乱码,不是双语切换)。
- **BD-UI-27(v4 带框架 + 订阅两入口,2026-10-18 布局重做定稿)**:
① **带框架(Python 主界面等价)**:菜单栏(运维|网关|查询合约|帮助)→ 行情带
(全宽,tick 唯一源,交易所二级 tab,双击行弹 K线窗)→ 交易带(左下单面板
9 字段 + 委托/全撤 + 右 2×2 监控格「委托|未成交」「成交」「持仓」「资金|日志」);
回灌/票据界面面退役(总线面 UiCmd::Reload 保留);默认窗 1280×860。
② **订阅两入口(用户裁定「订阅不在网关窗定死」)**:下单面板代码框回车 =
CtpSubscribe(bare symbol,Python returnPressed 等价);菜单「查询合约」弹窗
(Python ContractManager 等价:查询框留空 = 全部 + 勾选 + 订阅选中)。CTP 网关窗
无订阅字段。UI 对合约流只收不催(无催查询总线口)。
③ **egui 0.33 帧内 sizing 退让钉(实窗截图审出)**:帧内 Grid 的 sizing 预估遇
ComboBox 高行会退让,网格后挂的面板内容会叠回网格 → 下单表单 = 网格(按钮/
提示语都是网格行,网格后不挂内容),盘口独立帧,交易带整体包 ScrollArea 保小窗
可达(D7 口径)。④ **kittest 钉**:建构面统一 `harness_for()` 1280×860(实窗
同源);点击帧断言用 `harness.step()` 单帧(`run()` 空闲帧会撤走单帧红字);
撞名 label(委托钮/tab/表题)用 `get_all_by_label` 按渲染序取。
- **BD-UI-28(第五轮审计,2026-10-18 复审修正)**:
① **回车订阅真缺陷(实码修复)**:egui 单行 TextEdit 遇 Enter 即
`surrender_focus`,`has_focus()` 在检查时已 false —— 原 `has_focus +
key_pressed` 口径下回车订阅**实窗从不触发**;改规范口径 `lost_focus() +
key_pressed(Enter)`(s20 钉实测)。回车输入带 `EX.` 前缀则剥成裸码并回写
表单(与查询合约窗同口径,ctp.subscribe 收裸码)。
② **悬空面清除**:`selected_symbol` 四处写零读(注释声称「联动下单面板」
未兑现)→ 字段/访问器删除;行情带行**单击 = 带入下单面板**(方案「行单击
= 选中合约」落地,s8 钉);委托行/持仓行单击无动作(Python 同口径)。
③ **委托行双击 = 撤单**(Python OrderMonitor `itemDoubleClicked→
cancel_order` 等价,活动行限定,s11 钉;原实现双击弹 K线为偏离,已撤 ——
K线入口唯一在行情带双击)。④ **表列补全(Python 蓝图逐值抄录)**:委托表
13→17 列(+名称/交易所/类型/来源=reference)、成交表 10→12(+名称/交易所)、
持仓表 11→16(+名称/交易所/在途/市值=market_value/成本=price);数据面
OrderData/TradeData/PositionData 字段全在场,零新增依赖。⑤ **tick_of
交易所感知**:交易所非空 = EX.symbol 精确匹配(跨所同码不串),空 = 仅按
代码。⑥ **kittest 双击口径**:`step()` 每事件一帧且缺省 step_dt=0.25s →
两击释放间隔 0.5s > egui `max_double_click_delay` 0.3s → 双击钉用
`with_step_dt(0.1)` 建构(s11);同帧四连击不可用(每事件一帧)。Node 借用
harness 树,取 rect/id 后再 step(借用检查);`Role::TextInput` 非 TextBox。
⑦ **Frame 布局泄漏钉(s21 满表快照显形)**:`Frame::show` 继承父级
`ui.horizontal` 的横向布局 → 2×2 格内 tab 行/表题/宽网格横流挤同一行
(空表期隐形,满表现形)→ `group_frame` 内容强制 `ui.vertical`(行情带同款
口径:短行成行,宽网格末位溢出走横向滚动);下单面板帧不动(其内容 = 网格 +
罕见 err label,横流反而护住 BD-UI-27 叠行钉)。委托表表题撤除(tab
「委托|未成交」即 Python 表身份,三重撞名降为两面,s12 口径同步)。
满表视觉钉 s21(委托 17/成交 12/持仓 16 列入 baseline)。
- **BD-UI-29(面板化重构,布局 v5;实窗演示暴露 + 官方文档核对)**:实窗
1280 演示暴露三症:① 交易带定宽 250+140+480+480+间距 ≈ 1438 > 1280 →
右列裁切且带级 ScrollArea 只开纵向 → 裁掉内容不可达(违 BD-UI-27 可达
口径);② 2×2 格定高 250 实际只渲染 ~85px → 底部死区 ~300px;③ 菜单/两带
全挤单 CentralPanel 手工嵌套,无 Python splitter 语义。
- **机制实锤(dbg 探针)**:`[row] avail=[9984, 18]` —— egui horizontal 行
成员的可用高度初始只有一行高(interact_size),**高度分配不沿横向向下
传播**,行按内容收缩 → 格内 `ScrollArea.max_height` 被钳到最小滚动尺寸,
定高失效(②的根因;①为定宽算术,1438>1280 可复现)。
- **官方口径核对**(egui 0.33.3 源码模块文档 = docs.rs 同源;crates.io 包
不含 examples/,egui.rs 书为 JS 渲染页):panel.rs「面板是放控件的正规
区域;顶层面板不得互相嵌套;CentralPanel 必须最后加;Window 挂面板之后」;
`TopBottomPanel::resizable/default_height`、`SidePanel::resizable/
default_width` = QSplitter 官方等价;resizable 前提 = 内容吃满可用空间
(ScrollArea auto_shrink(false) 满足)。
- **落地**:render_body 改收 `&egui::Context` 自开四面板(菜单 → 行情带
240 可拖 → 下单侧栏 250 可拖[下单面板 + 盘口纵排,Python TradingWidget
内序] → CentralPanel 最后 → 弹窗);2×2 格响应式 `allocate_exact_size`
预留 (可用宽-间距)/2 × (可用高-间距)/2 矩形 + `new_child` 在预留矩形内
开 Frame+ScrollArea(高度才吃满;`add_sized` 收 impl Widget 不适用闭包
内容,已试撤);`group_frame` 保留 `ui.vertical` 泄漏钉(BD-UI-28⑦ 延续)。
- **BD-UI-28⑦「下单面板帧不动」被本钉取代**:面板化后下单侧栏内容天然
纵向,err label 后置改由 ScrollArea 吸收 sizing 退让(BD-UI-27 口径不变,
护垫从「横流侥幸」换成「官方吸收面」)。
- **钉**:s3 加结构钉(右列存在贴边不裁切/底行沉底无死区/侧栏在左/两列
等宽随窗分配,先于字体让位执行);快照 s3/s10/s16/s21 重基线;行为钉
s0–s20 全数不动全绿;kittest 建构面 `build_ui` → `build`(ctx 级)。
## 与 ek-station 的边界
ek-ui 依赖 `ek-station = "0.1.7"` / `ek-ctp = "0.5.0"`(装版;禁 path —— 同纪律)。ek-station
本 bin 保持 headless 不引 ek-ui;UI 总装配在 ek-ui(`ui_kinds` 复用 `default_kinds`
+ ui 工厂),launcher(S1c)也在 ek-ui 侧。
## 审计修正记录(2026-10-17 全面复审)
- **BD-UI-22 补钉(撮合两守卫)**:① **tick/bar 双重计价防护** —— tick 成交
挂账 `tick_filled`,bar 收口预算 = bar.volume − 本段 tick 已成(同一笔市场
量只吃一次;u5 钉);② **bar 单调守卫** —— 同 symbol `bar.datetime` 非严格
递增的帧(重复/重放)直接忽略,不重复撮合(u6 钉);③ ask 臂锁面开块
(match 暂存守卫会跨臂存活 —— 锁内造单锁外上总线,与 on_mail 同形)。
- **ek-station 0.1.2(pickfile BOM 剥离)**:Notepad 系 UTF-8 BOM 使首行注释
识别失效 → 整表 BadLine 拒启(活跑实测撞到);read 剥 BOM,p3 钉。
- 复审确认未动面:ek-station headless 工厂表(rt-off)未波及;已发布六仓仅
rustfmt 1.8.0 纯格式对齐(无语义变);setting/global.toml 只读;
ek-ctp/ek-oms 契约类型只引用未改。
## 配置面撤销记录(2026-10-17 用户定盘)
- 用户口径:**界面零配置 + 宿主零配置** —— ek-ui 不读任何配置文件;station
侧也不预读配置转发。插件结构只由 `--from` 清单或 demo 缺省决定。
- **配置归插件自身(定盘)**:激活列表(pick 窗勾选 / svctl start 重注入)时,
哪个插件需要配置,配置面就在该插件内部完成(插件自带配置面/配置服务);
宿主与界面都不代读、不转发、不集中。暂时不需要任何多配置机制。
- 全生态唯一需要配置的插件 = ek-ctp(虚拟账号)。其配置面归 ek-ctp 插件自身
(S6 接线时按此定盘落地:激活 ctp 时配置在插件内完成,不落共享配置树)。
- 落地:cfg.rs(toml-lite/UiConfig/locate_setting/redact)+ CfgStatus 状态条 +
`--config` 形态 + 发现链整块删除;cfg 测试 9 例 + s7 kittest 1 例随撤;
测试 51 → 41。**setting/ 目录已整树清理(2026-10-17 定盘)**:global.toml +
backtest/common/live/test 分层样例(含凭据档)全部删除——插件配置自持,
工作区不留共享配置文件;Python 侧 c:\dev\enkeep_trade_setting 原档不在
清理范围(只读纪律,原样未动)。
## BD-UI-30(0.1.1 薄下单者,OMS 中枢链,2026-10-19)
- **裁定**:下单/撤单直发 `{gw}.send_order`/`{gw}.cancel_order` 的旁路口
**删除**——UI 是薄下单者,管制面收敛到 oms 两服务:
`UiCmd::SendOrder{req, gateway, mode}` → `oms.submit_order`(五段管线:
入口校验/拆单槽位/风控槽位/发送段/记账段;parent 聚合 SENT|PARTIAL|REJECTED
逐腿显形进日志);`UiCmd::CancelOrder{orderid, gateway}` → `oms.cancel_order`
(守卫 4000 不可绕过;symbol/exchange 不再随令携带,oms 订单缓存解;
全撤 = 界面侧逐个扇出到同一守卫口)。BD-UI-23/25 的「ask sim.*/ctp.*」
口径自此被取代(历史记录保留)。
- **「自动开平」勾选(OrderForm.auto_split,默认开)**:拆单四态的 UI 映射 =
Auto(勾选)/None(取消,原样旁路);风控与守卫不可旁,拆单可逐请求旁
(DESIGN ek-oms §6.2 口径的 UI 面)。s22 钉勾选双向 → mode 逐位。
- **装配口径统一**:ui_kinds 复用 station 0.1.7 `default_kinds`——oms 已装配
ek-risk 三闸(standard_gates 缺省限额),面板与 headless 同一台 oms。
- **fixture 退役**:ui_converter_chain 从「offset 夹具进链」升格真管线链
(拆单器入驻 oms 管线,夹具失去存在意义;dev-dep ek-converter 删除)。
顺带实锤忠实初始化口径:无成交史的首轮 `ePositionDone.` 只记时刻等下一轮
(原稿 1268-1288/1282-1285),第二轮距首轮 ≥3s 才置位——链测补第二轮。
- **实连钉**:ui_ctp_live 第 13 步(有仓则 CLOSE 经管线 = CLOSETODAY 实连
证据;7×24 快照市无成交 = OBSERVE 如实记档)。13 步 110s 全收场。
- **依赖**:ek-oms 0.2.1→0.3.1、ek-station 0.1.5→0.1.7(强制 bump,防同构建
两份 oms)。
## BD-UI-31(0.2.0 界面体验轮,2026-10-19;用户四点裁定)
- **R1 菜单一键直达**:「运维/网关/查询合约」menu_button→下拉→入口钮的
两层结构废除,菜单钮即入口,单击直接弹窗(「帮助」保留菜单——多行内容)。
破坏性:三个入口钮 pub 常量删除(标签 = 菜单名)。
- **R2 成交表方向上色**:方向列空绿/多红,补齐与委托/持仓表的一致口径
(此前成交表方向为裸文本 = 遗漏,用户实看指出)。
- **R3 表格行选中 + 悬停跳色**(`UiShared::row_selected/select_row` 新口 +
`paint_row_bg` 渲染工具):委托/未成交/成交/持仓/查询合约窗行首格单击 =
选中行(蓝调半透明跳底,每表一行,换行即换选);行情带选中行 = 下单面板
当前合约(带入即跳色,不另存态);全表悬停 = 淡灰底。行首格既有双击动作
(撤单/带入/K线)全保留——egui 双击先经 clicked,选中顺带发生,无冲突。
叠底为半透明(行文字先画,叠后仍可读)。
- **R4 标题文字撤除**:「行情带(唯一源…)」「下单面板」「盘口(合约)」三处
题字整行删(带位/侧栏即身份;Python TickMonitor/TradingWidget 无题同口径;
盘口括号合约代码一并删——选中合约由下单面板代码格与行情带跳色行指认)。
- **不动面(用户裁定)**:K 线窗标题保留合约;运维窗维持浮动弹窗形态
(O1 内嵌页方案否决);「帮助」菜单保留。
- **钉**:s23 行选中(委托换选 + 成交独立槽);s17 改单击直达;快照重基线。
- **0.2.1 续(动作归行,用户裁定)**:行内任何一格单击/双击都触发该行动作
(`click_cell` 标签矩形叠感应,外观不变);行情带盘口四列带方向
(`import_tick`:买1 价/量 → 卖,卖1 价/量 → 买,价格列顺带填价,其余列
方向/价格保持默认)。r8 钉逐列口径。
## BD-UI-32(0.2.2 字号与独立窗轮,2026-10-19;用户三点裁定)
- **R1 全局字号上调**(`apply_ui_style` 新口):body/button 16、small 14、
heading 22、monospace 15(egui 缺省 13/12/18/12)——用户实看「整体字偏小」,
对照 Python 版;render_body 顶部施加,真实 App 与 kittest 同源(快照重基线)。
- **R2/R3 查询合约窗 + K线窗独立 OS 窗**(`show_viewport_immediate`):
原生最小化/最大化/关闭钮(此前 egui::Window 只有关闭钮,且被物理关在
主窗矩形内不能拖出——用户对照 Python 独立窗形态指出);可拖到屏幕任意
位置、独立任务栏条目。OS 关闭钮 = close_requested → 状态位翻负 → 父窗
下一拍不再 show → eframe 销毁窗。Embedded 回退(后端不支持多窗:
kittest/WEB)= 主窗内弹窗同一本体,测试断言面零改动。
- **不动面**:运维窗/CTP 窗维持主窗内弹窗(用户只点了这两个窗)。
- **选型**:immediate 而非 deferred——回调可捕获 `&UiShared`(UiShared 非
Clone,deferred 需 Arc 化会动 render_body 签名 = 无谓破坏性);StationApp
1s 节流重绘节奏下 immediate 的父子联动重绘代价可忽略。
- **0.2.3 勘误(关不掉根因,用户实看指出):** egui 销毁判据 = 子 viewport
本拍未 used——0.2.2 调用点未按状态位守卫,每拍 show = 每拍 used = OS 窗
永不销毁(X 翻位后又被 show 回来,等效 CancelClose),且窗在主窗一开就
自己出现。修复 = show 整段包进 `contracts_window_open()` 守卫。测试面
教训:Embedded 回退掩盖 Immediate 真窗面(21 绿 ≠ 真窗面正确),s24 注入
recording renderer 补钉;复审纪律补条:新机制的每个调用点必须按状态位辞行。
## BD-UI-33(0.2.4 四窗一型独立化,2026-10-19;用户裁定)
- **运维窗 + CTP 网关窗也转独立 OS 窗**(「四窗皆可脱离主窗」):与查询合约/
K线同形态同守卫;Embedded 回退维持主窗内弹窗(kittest 断言面与快照零改动)。
- **合约表吃满窗口:** 撤 max_height(260) 内嵌时代残留;动作行(计数 +
订阅钮)前置,ScrollArea auto_shrink(false) 吃满剩余窗高——放大窗口 =
表格变高(用户「放大窗口意义在哪里」的直接回应)。
- **`show_independent_window` 助手(四窗一型):** 守卫模式内建(0.2.3 教训
固化成调用方纪律:位关不 show = GC 销毁);Embedded/Immediate 两面的
"被关"统一以返回值出表,消除四份 viewport 样板重复。
## BD-UI-34(0.2.5 表头固定 + 主题 + K线最大化/ESC,2026-10-19;用户裁定)
- **表头固定(`pinned_table` 五表全上):** 官方 DIY 模式(scroll_area.rs:412
注释「painting a sticky header on top of it」)——单 grid 实例(头行+体行)
保列对齐,ScrollArea 之后以本拍捕获的表头格矩形重画表头带(背景盖行 +
Label 重画;矩形已含滚动平移 → 横向跟随、纵向盖顶、零拍滞后;Label 无
交互 sense → 点击穿透)。两轮机制实锤:①vertical 嵌 horizontal 高度塌缩
(s3 底行钉 462<500 抓到);②同 id 双 grid 各从空白重建 State,save 交替
request_repaint = 重绘环(kittest max_steps 爆)——两案皆弃,留 overlay 案。
- **主题四套(金融深色默认/egui深/egui浅/Qt灰白):** 菜单钮一键循环;
Theme 枚举 + AtomicU8 运行期态(零配置口径);apply_ui_style_for 统一施加
(主窗/独立子窗/kittest 三处同源)。语义色跨主题恒定。回答用户「颜色是
默认还是自配」:此前 = egui 默认深色骨架 + 自配语义色(非全默认)。
- **K线窗:** maximized 打开即最大化(s25 钉 builder.maximized=Some(true));
ESC 快捷关闭(body 返回 true → close_kline 同链;s25 钉 Embedded 面注入
Event::Key;Immediate 面键按子窗焦点路由)。
- **行情带外层 ScrollArea 不可撤:** resizable 面板吃满前提(BD-UI-27),
本轮实撤一次即 s3 底行钉实测塌到 462——已恢复并注释钉死。
## BD-UI-34 返修(0.2.6,2026-10-19;用户实看三报,全有机制实锤)
- **竖线(资金右):** 覆盖带 ui.put 视口外表头格 → 布局 min_rect 撑宽 →
Frame 描边矩形撑到 x=1222(= 346.9 + 16 列总宽)。二分实锤:关覆盖带线
消失;空内容时线跟内容宽走(1007)。修 = painter.text/rect_filled 纯绘制
(零布局副作用)+ clip 裁越界。教训:覆盖/装饰一律走 painter,不得用布局
调用(put/label)画视口外内容。
- **表头缝:** 带高须 = 表头格高 + item_spacing.y(3px),否则滚动行从间距带
露出。
- **空表缩格:** 空态不得提前 return——pinned_table 照调(空 body),表头与
吃满滚动区在场。s26 钉三表空态表头同在场。
- **group_frame_plain 使用纪律(0.2.6 钉死):** Frame::show 描边矩形按内容
min_rect 收缩(实锤:label 内容时 gw 宽缩到 112)——只配 auto_shrink(false)
填充物(pinned_table)使用;非表格格(资金/日志)必须走带填充 ScrollArea 的
group_frame,否则 Frame 收缩、右边框落行中段。
## BD-UI-35(0.2.8,2026-10-19;用户实看三报,全机制实锤)
- 行情带状态行「tick N 格 / 最后更新」= 用户裁定撤除(考证:从未删过,
0.2.0 撤的是题字)。s8 反向钉防回归。
- 表头缝硬根因 = clip_rect_margin(默认 3.0)外扩裁剪;覆盖带必须按
inner_rect ± margin 起画。s27 滚动快照 + 像素扫描口径入 CHANGELOG。
- 空态提示一律在表头之下(grid body 首格),不得再置于 pinned_table 之前。
- 列头单源纪律扩散到全部四张格表(const HEADERS):本轮自审又抓到两处
空态分支凭记忆写错——手写第二套表头 = 必错模式,单源是唯一防线。
- 快照回读串行化(SNAP_LOCK):本机 wgpu 并发回读会随机成组失败,测试闸
对此环境敏感;新增快照测试必须走 snap() 不得裸调 harness.snapshot。
## BD-UI-36(0.2.9,2026-10-19;纸面网关迁出界面包)
- 用户裁定:纸面网关 = 网关,应与 ek-ctp 同级独立成包(Python 蓝图 PaperAccount
即与 CTP/Xt/Qmt 同级独立 app;SimNow 登不上时切换的本地模拟流)。
曾议落位:留 ek-ui(否决:界面包藏网关,无头测不到)/并入 ek-ctp(否决:污染
纯 CTP 协议面)→ 裁定 = 独立 crate ek-paper。
- 语义零迁移实锤:旧 sim.rs 与新 matcher+plugin 非注释代码行 449↔449,
Compare-Object 仅 4 处 Arc 写法面差异。
- 工厂表归一:sim 登记项上移 ek-station 0.1.8 default_kinds(无头/有头同一张表);
ek-ui ui_kinds/pick_kinds 撤自建 sim 项防双份(注册名冲突 = add_plugin Duplicate)。
- 教训钉(注册表耦合顺序):改工厂表必须【先发布被依赖方】——station 0.1.8
未发布前 ek-ui 测试 UnknownKind("sim")(本地改动对 registry 依赖不可见,
这正是禁 path 耦合的代价与回报)。
## BD-UI-38(0.2.10,2026-10-19;纸面网关整体退场)
- 用户裁定(2026-10-19):纸面账户功能整体撤除,界面不再有 sim——
**ctp/SimNow 两套前置(12 小时/7×24,选前置用 ek-ctp scripts/simnow-fronts.ps1
-Probe)已满足当前需求**。BD-UI-37(sim 对外面对齐 CTP 的改造方案)作废,
未执行半成品整堆丢弃(ek-paper 工作区回滚到 0.1.1 已发布态)。
- 退场涟漪(三包):ek-station 0.1.12 撤 ek-paper 依赖 + 工厂表 sim 项
(pipeline_headless 改 dm/oms/ctp 装配 + CTP 回报行直发注入,账本/行情/因子
钉数值零变);ek-ui 0.2.10 撤 ek-paper 依赖 + 下拉/兜底/钮/测试全清;
ek-paper 0.1.x 档案原样留存(不删仓库不发版不 yank——yank 反咬已发布老版)。
- 界面口径:接口下拉 ∪ 集改 {ctp};parse_gateway 空兜底 sim→ctp;运维窗
「流控查询」钮撤(UiCmd::QueryRate 变体同步撤——公共 API 减项);
demo 窗 = dm/oms/ui(网关经运维窗激活);demo_tour 纸面段整段删(8 步重编)。
- 测试口径:ui_sim.rs 整删(9 钉);ui_converter_chain 网关位改测试本地
"ctp" 受理夹具(受理形制忠实 ek-ctp:SUBMITTING 单上总线 + ok 回执 +
`{gw}.` 全号;成交按 OnOrderTrade/OnRtnTrade 同构行直发)——本钉主体
一直是 oms 五段管线,与网关撮合无涉,换顶替零减钉;kittest 夹具标签
sim→ctp(五张快照重基线:s3/s10/s16/s21/s27,钮撤除 + 标签改名像素变);
71→62 钉(减的 9 = ui_sim)。
- 纪律钉:历史条目(BD-UI-36、CHANGELOG 0.1.8/0.1.9 sim 入表等)是史实,
不改写;退场只记新条目。
## BD-UI-39(多账号面:账户面板取代网关窗,规划 docs/ek-multi-account-plan.md)
> 本条取代的旧口径(史实保留不改写):BD-UI-24 的 `连接 CTP` → 硬编码
> `ctp.configure` / `ctp.subscribe`;BD-UI-27/33 的「CTP 网关配置窗」单账号窗体;
> BD-UI-38 的接口下拉 ∪ {ctp} 与 `parse_gateway` 空兜底 `"ctp"`。
- **菜单「网关」→「账户」**:旧单账号网关窗退役,收纳为账户面板的添加/编辑表单
(9 输入位 + 密码掩码 + 收录勾选原样沿用,`ctp_grid` 未改字段)。旧窗的死面
(`ctp_window` 状态位、`MENU_CTP`、`BTN_CTP_CONNECT`)一并删除,不留"供旧测试引用"。
- **账户清册在界面侧**(`UiShared.accounts`):行 = 派生注册名 + broker + 账号 +
在场位 + 元账号标记 + 表单留档。派生名唯一入口 `ek_ctp::derived_gateway_name`
(界面不自己拼字符串);密码只在内存,本轮不持久化(规划 §10.2)。
- **添加时序钉(规划 §5.A)**:一个钮两条 cmd —— `Start{factory:"ctp", name:<派生名>}`
先装配,`CtpConfigure{gateway:<派生名>, form}` 后注入凭据(凭据不进 svctl.start 通道)。
- **单源纪律在"添加"这一刻定源(规划 §5.C)**:无元账号且无在场账户 → 新账户自动成为
**行情元账号**(md 前置保留;清掉它等于这个账户自己也永远收不到 tick);已有元账号或
已有在场账户 → 按纯 TD 装配(md 前置清空)。订阅两个入口(下单面板代码回车 /
查询合约窗勾选)一律经 `subscribe_target` 路由到元账号,未指定就明确报错不静默发。
- **元账号连接后锁定(用户裁定「账户连接后不允许切换」)**:任何账户已装配即拒切换;
换源流程 = 撤挂单 → 移除 → 改配置 → 重连 → 重放订阅(订阅集 `note_subscriptions`
登记,元账号新装配后自动重放)。
- **移除守卫**:该账户有活跃委托 = 不给移除钮(只给"先撤单才能移除"提示);有持仓 =
可移除但警告"柜台侧不受影响,重加后由轮转查询重建视图";两步确认沿用 `confirm_step`。
- **网关名回落只剩一个阶梯**:`default_gateway()` = 上次手动选择 → 首个已装配 → 首个;
一个账户都没有 = 禁用「委托」钮并给提示(旧"空 → 字面量 ctp"兜底退役,B4)。
撤单三处(行内钮 / 全撤 / 行双击)一律随该单自己的 `gateway_name`。
- **诚实标注钉**:账户表状态列写「已装配 / 未装配」不写「已连接」——界面只能从
`svctl.list` 看见实例在场,登录成败 ek-ctp 无状态口可查(开放项)。
- **测试口径**:s13 重写(添加账户 → 派生名 + 两条 cmd + 首个账户自动定源 +
第二账户 md 清空不被抢源)、s17 菜单改「账户」、s18/s20 订阅路由到元账号、
新增 s28(元账号锁定 + 挂单移除守卫 + 守卫解除);五张快照重基线
(s3/s10/s16/s21/s27,菜单标签与 tick 列名「接口」→「行情源」像素变)。