@v587d/capital-generation 2.6.2 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,7 +65,7 @@ const zh = {
65
65
  'command.description': '添加 / 查看 / 删除自选股',
66
66
  title: '自选股',
67
67
  close: '关闭',
68
- searchPlaceholder: '输入证券代码或中文名,支持A股/指数/ETF',
68
+ searchPlaceholder: '输入证券代码或名称,支持A股/港股/美股',
69
69
  add: '添加',
70
70
  added: '已添加',
71
71
  refreshing: '刷新中…',
@@ -79,11 +79,33 @@ const zh = {
79
79
  notRefreshed: '未刷新',
80
80
  /** 陈旧值的 tooltip:「……限流,请稍后再试 · 上一次快照 09-29 19:04」。 */
81
81
  stale: '上一次快照',
82
+ /**
83
+ * 港美股回包带的是**交易所当地时间**(HKT / 美东),与页脚那个"我们几点取的数"是两件事。
84
+ * 只写进 tooltip,不做时区换算(换算要用到我们没有的偏移表与夏令时口径)。
85
+ */
86
+ exchangeTime: '交易所时间',
82
87
  /** 有最新价、但没有涨跌(停牌 / 未开盘)时 tooltip 说的那件事。 */
83
88
  noChangePct: '暂无涨跌数据',
84
- typeStock: '股票',
85
- typeIndex: '指数',
86
- typeEtf: 'ETF',
89
+ /**
90
+ * 徽标说**市场**(2026-10-07 用户点名:「股票」这一档要拆成 A股 / 港股 / 美股,指数三档跟着拆)。
91
+ * 六档一字典,场内 ETF 不单列——它的报价同样是二级市场行情,归 A股那一档("这是 ETF"那一层
92
+ * 留在递进输入框那句话里,见下面 `kindStock` 一族)。
93
+ */
94
+ typeAShare: 'A股',
95
+ typeHkStock: '港股',
96
+ typeUsStock: '美股',
97
+ typeAIndex: 'A指',
98
+ typeHkIndex: '港指',
99
+ typeUsIndex: '美指',
100
+ /**
101
+ * ⛔ 这一族**不是徽标**:它只进「预测」「复盘」递进输入框的那句话,说的是**取数端点那一族**。
102
+ * 徽标按市场配色之后就不说"股票 / 指数 / ETF"了,而 Agent 恰恰靠这三档选端点
103
+ * (A 股股票、A 股指数、场内基金是三个不同上游端点,港美股又各两档)。
104
+ * 市场不在这儿重复:中间那段 thscode 自己带(`.SZ` / `.HK` / `.US`)。
105
+ */
106
+ kindStock: '股票',
107
+ kindIndex: '指数',
108
+ kindEtf: 'ETF',
87
109
  remove: '移除',
88
110
  more: '更多',
89
111
  pin: '置顶',
@@ -109,8 +131,10 @@ const zh = {
109
131
  confirmDescSuffix: ' 从自选中移除吗?',
110
132
  confirmCancel: '取消',
111
133
  confirmOk: '确认移除',
112
- truncated: '候选超过 10 条,请输入准确证券代码或名称。',
113
- noHit: '没有匹配的标的(支持沪深 A 股 / 指数 / 场内 ETF)。',
134
+ /** 故意不写"超过 N 条"里那个 N:它上游是宿主的一个常量,抄进两套字典就迟早只改红一边
135
+ * (`error.list_full` 那条教训)。用户下一步要做的动作跟数字无关。 */
136
+ truncated: '候选不止这些,请输入更准确的证券代码或名称。',
137
+ noHit: '没有匹配的标的(A股 / 港股 / 美股,含指数与场内基金)。',
114
138
  'error.invalid_query': '输入不被接受',
115
139
  'error.not_found': '没有找到该标的',
116
140
  'error.ambiguous': '命中多条,请从候选里选一条',
@@ -122,14 +146,22 @@ const zh = {
122
146
  */
123
147
  'error.list_full': '自选股最多 {max} 条,先删一条再加',
124
148
  'error.list_full_noMax': '自选股已达上限,先删一条再加',
125
- 'error.credential_missing': '未配置 Fuyao 密钥:在「插件」页的 Capital 模式卡片里填 API Key',
149
+ 'error.credential_missing': '未配置 Fuyao 密钥:在「插件」页的 Capital 模式卡片里填 API Key(只影响 A 股那一路,港美股照常用)',
126
150
  'error.rate_limited': '行情服务限流,请稍后再试',
127
- 'error.fuyao_unavailable': '行情服务暂时不可用,请稍后再试',
151
+ 'error.fuyao_unavailable': 'A 股行情暂时取不到,请稍后再试',
152
+ 'error.tencent_unavailable': '港美股行情暂时取不到,请稍后再试',
128
153
  'error.quote_unavailable': '该标的没有可用报价',
129
154
  'error.refresh_timeout': '刷新超时,请稍后再试',
130
155
  'error.store_unavailable': '自选股存储不可用',
131
156
  'error.unauthorized': '登录状态已失效:请先回到宿主窗口重新登录',
132
157
  errorSuffix: '(其他标的仍是上一次成功快照)',
158
+ /**
159
+ * 某一路检索挂了:候选照常给其余路,这一句说**挂掉的那一路**(R12 / 设计文档 §5.4)。
160
+ * 值自带冒号,中英文各按各的标点;接的是同一份 `error.*` 文案,不另起一套说法。
161
+ */
162
+ 'partial.a-share': 'A 股检索不可用:',
163
+ 'partial.hk': '港股检索不可用:',
164
+ 'partial.us': '美股检索不可用:',
133
165
  }
134
166
 
135
167
  const en = {
@@ -137,7 +169,7 @@ const en = {
137
169
  'command.description': 'Add / view / remove watched symbols',
138
170
  title: 'Watchlist',
139
171
  close: 'Close',
140
- searchPlaceholder: 'Symbol code or Chinese name — A-share / index / ETF',
172
+ searchPlaceholder: 'Symbol code or name — A-share / HK / US',
141
173
  add: 'Add',
142
174
  added: 'Added',
143
175
  refreshing: 'Refreshing…',
@@ -150,10 +182,17 @@ const en = {
150
182
  updated: 'Updated',
151
183
  notRefreshed: 'Not refreshed',
152
184
  stale: 'last snapshot',
185
+ exchangeTime: 'exchange time',
153
186
  noChangePct: 'No change data yet',
154
- typeStock: 'Stock',
155
- typeIndex: 'Index',
156
- typeEtf: 'ETF',
187
+ typeAShare: 'A-Share',
188
+ typeHkStock: 'HK',
189
+ typeUsStock: 'US',
190
+ typeAIndex: 'A-Index',
191
+ typeHkIndex: 'HK-Index',
192
+ typeUsIndex: 'US-Index',
193
+ kindStock: 'stock',
194
+ kindIndex: 'index',
195
+ kindEtf: 'ETF',
157
196
  remove: 'Remove',
158
197
  more: 'More',
159
198
  pin: 'Pin to top',
@@ -175,34 +214,75 @@ const en = {
175
214
  confirmDescSuffix: ' from your watchlist?',
176
215
  confirmCancel: 'Cancel',
177
216
  confirmOk: 'Remove',
178
- truncated: 'More than 10 candidates — enter the exact code or name.',
179
- noHit: 'No matching symbol (A-share / index / on-exchange ETF).',
217
+ truncated: 'More candidates exist than shown — enter a more exact code or name.',
218
+ noHit: 'No matching symbol (A-share / HK / US, including indices and listed funds).',
180
219
  'error.invalid_query': 'Input not accepted',
181
220
  'error.not_found': 'Symbol not found',
182
221
  'error.ambiguous': 'Multiple hits — pick one candidate',
183
222
  'error.list_full': 'Watchlist holds at most {max} symbols — remove one first',
184
223
  'error.list_full_noMax': 'Watchlist is full — remove one first',
185
- 'error.credential_missing': 'No Fuyao API key: set it on the Capital card in the Plugins page',
224
+ 'error.credential_missing': 'No Fuyao API key: set it on the Capital card in the Plugins page (A-share quotes only — HK and US keep working)',
186
225
  'error.rate_limited': 'Quote service is rate limited — try again later',
187
- 'error.fuyao_unavailable': 'Quote service unavailable — try again later',
226
+ 'error.fuyao_unavailable': 'A-share quotes unavailable — try again later',
227
+ 'error.tencent_unavailable': 'HK / US quotes unavailable — try again later',
188
228
  'error.quote_unavailable': 'No quote for this symbol',
189
229
  'error.refresh_timeout': 'Refresh timed out — try again later',
190
230
  'error.store_unavailable': 'Watchlist storage unavailable',
191
231
  'error.unauthorized': 'Session expired — sign back in to the host first',
192
232
  errorSuffix: ' (other symbols still show their last good snapshot)',
233
+ 'partial.a-share': 'A-share search unavailable: ',
234
+ 'partial.hk': 'HK search unavailable: ',
235
+ 'partial.us': 'US search unavailable: ',
193
236
  }
194
237
 
195
238
  /**
196
239
  * `asset_type` → 类型徽标文案的**字典键**。文案本身住两套字典里(2.5.2 是在这里写死中文,
197
- * 英文界面于是画着「股票 / 指数」,还绕过了"两套语言 key 必须齐平"那道闸门);
198
- * 「预测」「复盘」递进输入框的那句话也读同一份,英文句子中间不再夹中文类型词。
240
+ * 英文界面于是画着「股票 / 指数」,还绕过了"两套语言 key 必须齐平"那道闸门)。
241
+ *
242
+ * 六档只说**市场**(2026-10-07 用户点名:「股票」拆成 A股 / 港股 / 美股,指数跟着拆三档,
243
+ * 三市三种底色)。两件事因此从徽标上退场,各自另有落点:
244
+ * ① "这是 ETF / 这是股票还是指数"进递进输入框那句话(`KIND_LABEL_KEYS`,Agent 按它选端点);
245
+ * ② 港股 ETF 在检索阶段本来就分不出与个股的差别(实测 `03037` 搜索给 `GP`、快照给 `GP-FUND`),
246
+ * 所以 `港股` 那一档同时盖住两者是**如实**,不是合并出了错。
199
247
  */
200
- const TYPE_LABEL_KEYS = { 'a-share': 'typeStock', 'a-share-index': 'typeIndex', 'fund-etf': 'typeEtf' }
248
+ const TYPE_LABEL_KEYS = {
249
+ 'a-share': 'typeAShare',
250
+ 'hk-stock': 'typeHkStock',
251
+ 'us-stock': 'typeUsStock',
252
+ 'a-share-index': 'typeAIndex',
253
+ 'hk-index': 'typeHkIndex',
254
+ 'us-index': 'typeUsIndex',
255
+ // 场内 ETF 归 A股那一档:徽标说市场,而它交易的就是 A 股二级市场的价格。
256
+ 'fund-etf': 'typeAShare',
257
+ }
258
+ /** 递进输入框那句话里的"这一族走哪个端点",与徽标各管各的(见字典里 `kindStock` 一族的注释)。 */
259
+ const KIND_LABEL_KEYS = {
260
+ 'a-share': 'kindStock',
261
+ 'hk-stock': 'kindStock',
262
+ 'us-stock': 'kindStock',
263
+ 'a-share-index': 'kindIndex',
264
+ 'hk-index': 'kindIndex',
265
+ 'us-index': 'kindIndex',
266
+ 'fund-etf': 'kindEtf',
267
+ }
201
268
  /** 未知类型(上游哪天多一档)照原样画出来,比硬套一个错的中文标签诚实。 */
202
269
  function typeLabel(assetType, t) {
203
270
  const key = TYPE_LABEL_KEYS[assetType]
204
271
  return key === undefined ? String(assetType ?? '') : t(key)
205
272
  }
273
+ function kindLabel(assetType, t) {
274
+ const key = KIND_LABEL_KEYS[assetType]
275
+ return key === undefined ? String(assetType ?? '') : t(key)
276
+ }
277
+
278
+ /**
279
+ * 面板**不画币种**(2026-10-07 用户两次点名:先不要 ISO 码、再不要 `¥` / `$`——"正常人都 get
280
+ * 货币单元的问题")。落点只有这一处:`quote.currency` 照旧取、照旧入库、照旧过 schema,
281
+ * 只是不进 DOM。要恢复显示,只需要在这一处重新读它,上游与宿主都不必动。
282
+ * ⚠️ 已知代价(记录在 `docs/design/watchlist-hk-us.md` R9 与 P10):`80700` 这类 `-R` 柜台是
283
+ * **港币市场、人民币计价**,同一屏里它与 `00700`(HKD) 现在长得一样。用户按市场读币种,
284
+ * 在这一类标的上会读错——这是这一版接受了的取舍,不是没看见。
285
+ */
206
286
 
207
287
  /**
208
288
  * 上游(同花顺搜索结果)的标的名在进面板、进 tooltip、进输入框之前,先剥掉不可见与双向控制字符。
@@ -222,8 +302,19 @@ function scrubInvisible(value) {
222
302
  function cleanRows(rows) {
223
303
  return Array.isArray(rows) ? rows.map((row) => (row === null || typeof row !== 'object' ? row : { ...row, name: scrubInvisible(row.name) })) : rows
224
304
  }
225
- /** 类型徽标的配色:股票=橙、指数=蓝、ETF=绿,与候选行共用同一组类。 */
226
- const TYPE_TONES = { 'a-share': 'capital-watchlist-tag-stock', 'a-share-index': 'capital-watchlist-tag-index', 'fund-etf': 'capital-watchlist-tag-etf' }
305
+ /**
306
+ * 类型徽标的配色:**一色只说一件事 = 市场**(2026-10-07 用户点名:A 股 / 港股 / 美股三种底色;
307
+ * 此前 A 股与港股共用同一支琥珀色,三市在色上分不开)。同一市场里股票与指数同色,
308
+ * 品种那一层只由文字给(`A股` / `A指`)——颜色不再同时表达市场与品种两件事。
309
+ */const TYPE_TONES = {
310
+ 'a-share': 'capital-watchlist-tag-cn',
311
+ 'a-share-index': 'capital-watchlist-tag-cn',
312
+ 'fund-etf': 'capital-watchlist-tag-cn',
313
+ 'hk-stock': 'capital-watchlist-tag-hk',
314
+ 'hk-index': 'capital-watchlist-tag-hk',
315
+ 'us-stock': 'capital-watchlist-tag-us',
316
+ 'us-index': 'capital-watchlist-tag-us',
317
+ }
227
318
 
228
319
  /**
229
320
  * 面板几何。
@@ -310,9 +401,15 @@ const CSS = `
310
401
  /* 类型徽标 = 官方状态徽标那对写法:底色用 state-*-tertiary(主题包为"状态色的底"准备的实心档,
311
402
  自己 color-mix 出来的透明档压在白卡片上等于没混),文字用同色系 primary;
312
403
  amber 例外走 warn-label——warn-primary 压白底只有 2:1,官方"状态色当文字"另有 label 一档。 */
313
- .capital-watchlist-tag-index { color: var(--dsw-alias-state-business-primary); background: var(--dsw-alias-state-business-tertiary); }
314
- .capital-watchlist-tag-stock { color: var(--dsw-alias-state-warn-label); background: var(--dsw-alias-state-warn-tertiary); }
315
- .capital-watchlist-tag-etf { color: var(--dsw-alias-state-success-primary); background: var(--dsw-alias-state-success-tertiary); }
404
+ /* 三档徽标按**市场**配色,取的是"钞票的主色"这把直觉:人民币=红、港币=金琥珀、美元=绿
405
+ (2026-10-07 用户点名,并明确绿那一档"和美元很像")。股票与指数同市同色,品种只由文字分。
406
+ ⚠️ 红档与"红涨"那颗红是两种东西:徽标是整行固定的底,价格只在涨跌时上色。
407
+ state-*-tertiary 是主题包给状态色准备的**实心底**;error 那一族的 tertiary 本仓从未用过、
408
+ 无法在此确认存在,于是给一条 color-mix 兜底——猜错 token 的症状是"底色没了",不会报错。
409
+ ⚠️ 这段 CSS 住在模板字符串里:注释里不许出现反引号,出现就把整段样式截断(本轮踩过)。 */
410
+ .capital-watchlist-tag-cn { color: var(--dsw-alias-state-error-primary); background: var(--dsw-alias-state-error-tertiary, color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)); }
411
+ .capital-watchlist-tag-hk { color: var(--dsw-alias-state-warn-label); background: var(--dsw-alias-state-warn-tertiary); }
412
+ .capital-watchlist-tag-us { color: var(--dsw-alias-state-success-primary); background: var(--dsw-alias-state-success-tertiary); }
316
413
  /* 表头与数据行共用同一个 grid 定义:宽度只在这一处,两处各自排版就会串行(实机踩过)。
317
414
  三列:名称(第二行是「代码 · 徽标」)/ 报价 / 「操作」那一簇(「预测」「复盘」「更多」三枚)。
318
415
  92px 与 84px 必须是**定值**:表头与数据行是两个独立的 grid 容器,只有定值才让两行的列边线对齐;
@@ -334,8 +431,8 @@ const CSS = `
334
431
  .capital-watchlist-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
335
432
  .capital-watchlist-namewrap { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
336
433
  .capital-watchlist-name { font-size: 14px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
337
- /* 代码与类型徽标同处第二行:gap 是这一对唯一的间距来源,徽标自带内边距,别再各写 margin。 */
338
- .capital-watchlist-codeline { display: flex; align-items: center; gap: 6px; min-width: 0; }
434
+ /* 代码与类型徽标同处第二行(2026-10-07 起这一行就这两样,币种不进面板):gap 是这一对唯一的间距来源,徽标自带内边距,别再各写 margin。10px 与候选行 capital-watchlist-option 那条 gap 同值——同一件事的两种排法,不该一处紧一处松;6px 在实机上读起来是"贴着"(2026-10-07 用户点名)。 */
435
+ .capital-watchlist-codeline { display: flex; align-items: center; gap: 10px; min-width: 0; }
339
436
  .capital-watchlist-code { font-size: 11px; color: var(--dsw-alias-label-tertiary); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
340
437
  /* 最新价在上、涨跌幅在下,右对齐:两行都同一列里排,列宽才不需要跟着涨跌位数抖。
341
438
  padding-right 是**数字与动作区之间那口气的唯一来源**(2026-10-01 用户点名:报价和预测/复盘太近,
@@ -508,6 +605,8 @@ const blank = () => ({
508
605
  * 与 `addError` **分开两个字段**是刻意的——两者清空的时机不同(见 `onQueryChange`)。
509
606
  */
510
607
  searchError: null,
608
+ /** 某一路检索挂了:候选照常给其余路,这一句只说挂掉的那一路(R12)。 */
609
+ searchPartial: null,
511
610
  /** 添加动作的失败(list_full / ambiguous):与输入无关,显示在搜索区下方,改字不清它。 */
512
611
  addError: null,
513
612
  /**
@@ -691,7 +790,7 @@ class WatchlistSurface {
691
790
  if (this.searchController !== null) this.searchController.abort()
692
791
  const controller = new AbortController()
693
792
  this.searchController = controller
694
- this.patch({ searching: true, searchError: null })
793
+ this.patch({ searching: true, searchError: null, searchPartial: null })
695
794
  // 回包落地前再看一眼输入框:从发出到返回这段时间里用户可能已经改字(新词只排了个防抖
696
795
  // 定时器,还没出网)。那一串候选属于**上一个词**,画出来就是能被点错的那一行——丢掉,
697
796
  // 让新词的定时器自己去查。
@@ -703,7 +802,7 @@ class WatchlistSurface {
703
802
  this.patch({ searching: false })
704
803
  return
705
804
  }
706
- this.patch({ candidates: cleanRows(payload.items) ?? [], truncated: payload.truncated === true, searching: false, pendingSearch: false })
805
+ this.patch({ candidates: cleanRows(payload.items) ?? [], truncated: payload.truncated === true, searching: false, pendingSearch: false, searchPartial: Array.isArray(payload.partial) && payload.partial.length > 0 ? payload.partial : null })
707
806
  } catch (error) {
708
807
  if (error.name === 'AbortError') return
709
808
  // 失败不记在"这个词查过了"的账上:同样几个字重打一遍还得能再出网。
@@ -711,7 +810,7 @@ class WatchlistSurface {
711
810
  if (!stillCurrent()) return
712
811
  // 走 `searchError` 而不是 `addError`:它占下拉的 hint 位,替换掉「没有匹配的标的」——
713
812
  // 同一屏不许一边说"没有这个票"、一边说"服务限流"(候选清空了,noHit 本来就会亮起来)。
714
- this.patch({ candidates: [], truncated: false, searching: false, pendingSearch: false, searchError: { code: error.code ?? 'fuyao_unavailable' } })
813
+ this.patch({ candidates: [], truncated: false, searching: false, pendingSearch: false, searchError: { code: error.code ?? 'fuyao_unavailable' }, searchPartial: null })
715
814
  }
716
815
  }
717
816
 
@@ -728,7 +827,7 @@ class WatchlistSurface {
728
827
  // try 里:`/add` 已经落库、紧接着清单读回来 503,面板就报"自选股已达上限"式的黄字,
729
828
  // 用户照着再点一次——同一个标的被加了第二遍)。读清单的失败归 `loadError`(列表那
730
829
  // 一片),刷报价的失败归 `error`(页脚那条红字),各自有位置,都不占添加这句话。
731
- this.patch({ addError: null, candidates: [], query: '', dropdownOpen: false, pendingSearch: false })
830
+ this.patch({ addError: null, candidates: [], query: '', dropdownOpen: false, pendingSearch: false, searchError: null, searchPartial: null })
732
831
  this.lastQuery = null
733
832
  await this.load().catch(() => {})
734
833
  // 添加是一次完整动作:读完清单顺手刷一次报价,新行当场就有价。停在「未刷新」
@@ -809,18 +908,19 @@ function formatTime(value) {
809
908
 
810
909
  /** 类型徽标:候选行与数据行共用同一支笔(文案与配色都在这两处一次对齐)。 */
811
910
  function TypeTag({ assetType, t }) {
812
- return h('span', { className: `capital-watchlist-tag ${TYPE_TONES[assetType] ?? 'capital-watchlist-tag-stock'}` }, typeLabel(assetType, t))
911
+ return h('span', { className: `capital-watchlist-tag ${TYPE_TONES[assetType] ?? 'capital-watchlist-tag-cn'}` }, typeLabel(assetType, t))
813
912
  }
814
913
 
815
914
  /**
816
- * 「预测」与「复盘」共用的**形状**:`<前缀>名称 (thscode · 类型)<后缀>`。
915
+ * 「预测」与「复盘」共用的**形状**:`<前缀>名称 (thscode · 品种)<后缀>`。
817
916
  * 括号、空格与中点是结构,写死在这里(中英文同一形状);句子两半住字典的
818
917
  * `<kind>PromptPrefix` / `<kind>PromptSuffix`(kind 是 `predict` / `review`),改文案去那儿改。
819
- * 类型词读字典(2.5.3 起):英文句子中间不许再夹「股票 / 指数」这种中文标签。
918
+ * 中间那个品种词读 `KIND_LABEL_KEYS`,**不是徽标那套市场词**:Agent 靠它选端点
919
+ * (股票 / 指数 / 场内基金是三个不同上游),市场由 thscode 自己带。
820
920
  */
821
921
  function promptOf(item, t, kind) {
822
922
  const name = scrubInvisible(item.name)
823
- return `${t(`${kind}PromptPrefix`)}${name} (${item.thscode} · ${typeLabel(item.asset_type, t)})${t(`${kind}PromptSuffix`)}`
923
+ return `${t(`${kind}PromptPrefix`)}${name} (${item.thscode} · ${kindLabel(item.asset_type, t)})${t(`${kind}PromptSuffix`)}`
824
924
  }
825
925
 
826
926
  /**
@@ -856,11 +956,20 @@ function QuoteCell({ quote, failureCode, refreshing, t }) {
856
956
  : !hasPct
857
957
  ? 'capital-watchlist-nodata'
858
958
  : quote.change_pct > 0 ? 'capital-watchlist-up' : quote.change_pct < 0 ? 'capital-watchlist-down' : 'capital-watchlist-flat'
959
+ /**
960
+ * tooltip 的三句话按本次状态拼:失败原因 + 上一次快照时刻(陈旧行)、"暂无涨跌数据"(价有涨跌没有)、
961
+ * 交易所当地时间(**只有港美股那一路带 `source_time`**,A 股那一路没有这格就不画、不编一句)。
962
+ */
963
+ const exchange = typeof quote.source_time === 'string' && quote.source_time.length > 0
964
+ ? `${t('exchangeTime')} ${quote.source_time}`
965
+ : ''
966
+ const tips = []
967
+ if (stale) tips.push(`${t(`error.${failureCode}`)} · ${t('stale')} ${formatTime(quote.captured_at)}`)
968
+ else if (!hasPct) tips.push(t('noChangePct'))
969
+ if (exchange.length > 0) tips.push(exchange)
859
970
  return h('div', {
860
971
  className: 'capital-watchlist-quote',
861
- title: stale
862
- ? `${t(`error.${failureCode}`)} · ${t('stale')} ${formatTime(quote.captured_at)}`
863
- : hasPct ? undefined : t('noChangePct'),
972
+ title: tips.length === 0 ? undefined : tips.join(' · '),
864
973
  },
865
974
  h('div', { className: stale ? 'capital-watchlist-price capital-watchlist-stale' : 'capital-watchlist-price' }, quote.price.toFixed(2)),
866
975
  h('div', { className: `capital-watchlist-change ${tone}` }, pct))
@@ -953,8 +1062,11 @@ function WatchlistDialog({ useDialog, surface, t, inputActions }) {
953
1062
  },
954
1063
  h('span', { className: 'capital-watchlist-optionmain' },
955
1064
  h('span', { className: 'capital-watchlist-optionname' }, candidate.name),
1065
+ // ⛔ 候选第二行画**完整 canonical 代码**,不是裸码:`00700` 一次同时命中港个股与
1066
+ // `000700.SZ` 模塑科技,搜「恒生指数」还会同时回 `HSI` / `HSIGTR` / `HSINTR` 三条真指数——
1067
+ // 裸码并列时用户没有可核对的信息,点错就是加错票。
956
1068
  h('span', { className: 'capital-watchlist-optioncode' },
957
- candidate.in_list === true ? `${candidate.ticker} · ${t('added')}` : candidate.ticker)),
1069
+ candidate.in_list === true ? `${candidate.thscode} · ${t('added')}` : candidate.thscode)),
958
1070
  h(TypeTag, { assetType: candidate.asset_type, t }))),
959
1071
  state.composing === true
960
1072
  // ⛔ 合成期(拼音还没上屏)不许报「没有匹配的标的」:那是把"还没查"说成"查了,没有",
@@ -971,6 +1083,15 @@ function WatchlistDialog({ useDialog, surface, t, inputActions }) {
971
1083
  : state.candidates.length === 0
972
1084
  ? h('div', { className: 'capital-watchlist-hint' }, t('noHit'))
973
1085
  : null,
1086
+ // 某一路挂了而其余路照常给候选:这一行说的是**那一路**(R12)。它与上面的 hint 各说
1087
+ // 各的事——上面那句讲"这一屏有没有匹到",这句讲"有一路今天没参与",不互相顶替。
1088
+ ...(state.searchPartial ?? []).map((branch) => h('div', {
1089
+ key: `partial-${branch.market}`,
1090
+ className: 'capital-watchlist-hint capital-watchlist-searcherror',
1091
+ role: 'status',
1092
+ 'data-market': branch.market,
1093
+ 'data-code': branch.code,
1094
+ }, `${t(`partial.${branch.market}`)}${t(`error.${branch.code}`)}`)),
974
1095
  state.truncated ? h('div', { className: 'capital-watchlist-hint' }, t('truncated')) : null)
975
1096
 
976
1097
  const head = state.items.length === 0
@@ -1002,7 +1123,7 @@ function WatchlistDialog({ useDialog, surface, t, inputActions }) {
1002
1123
  },
1003
1124
  h('span', { className: 'capital-watchlist-namewrap' },
1004
1125
  h('span', { className: 'capital-watchlist-name', title: `${item.name} ${item.ticker}` }, item.name),
1005
- // 代码与类型徽标同处第二行(用户反馈:类型单占一列太累赘)。
1126
+ // 代码与市场徽标同处第二行(2026-10-07 起这一行只有这两样:币种不进面板,见上面那一族注释)。
1006
1127
  h('span', { className: 'capital-watchlist-codeline' },
1007
1128
  h('span', { className: 'capital-watchlist-code' }, item.ticker),
1008
1129
  h(TypeTag, { assetType: item.asset_type, t }))),