dsh-wkgeo-ops 0.2.2 → 0.2.4

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.
package/client/client.js CHANGED
@@ -131,6 +131,25 @@ window.__ModuleLoader__.load({
131
131
  initTitle: '第一步:先选站点根目录',
132
132
  initHint: '所有站点的本地文件夹都放在这个根目录下。可以直接粘路径,也可以点「选择根目录」一层层点进来。',
133
133
  initNote: '根目录只存在本机(DSH profile 里),不上传;只有根目录里面的文件夹能绑定给站点。',
134
+ // ---- v5:首次进入三步(① 选页根目录 → ② 授权 → ③ 建站,老板 2026-10-30 定) ----
135
+ stepRoot: '选页根目录',
136
+ stepAuth: '授权',
137
+ stepCreate: '建站',
138
+ stepRootDone: '页根目录:{dir}',
139
+ stepHint: '首次进入按顺序走:第 1 步选页根目录 → 第 2 步授权 → 第 3 步建站。',
140
+ authTitle: '第二步:授权 wkgeo 账号',
141
+ authHint: '建站要走 wkgeo CLI 的写接口,必须先授权。在终端里执行下面这条命令,浏览器会打开授权页;完成后回到这里点「我已授权,重新检查」。',
142
+ authCommand: 'wkgeo login',
143
+ authCheck: '我已授权,重新检查',
144
+ authChecking: '正在检查授权…',
145
+ authOk: '已授权:能读到站点列表',
146
+ authNote: '授权凭据由 wkgeo CLI 自己保存,本插件不读取、不显示、不上传账号密码或 token。',
147
+ needRoot: '先去第 1 步选页根目录,选定后才能建站。',
148
+ needAuth: '先去第 2 步完成授权(wkgeo login),之后才能建站。',
149
+ blockedTitle: '还差一步才能建站',
150
+ crashTitle: '悟空GEO-网站运营 · 面板异常',
151
+ crashRefresh: '刷新界面',
152
+ regWarn: '面板自检:',
134
153
  rootLabel: '站点根目录',
135
154
  rootUnset: '未设置根目录',
136
155
  rootPick: '选择根目录',
@@ -248,6 +267,25 @@ window.__ModuleLoader__.load({
248
267
  initTitle: 'Step 1: choose a site root folder',
249
268
  initHint: 'Every site keeps its local folder under this root. Paste a path, or click "Choose root" and browse into it.',
250
269
  initNote: 'The root is stored locally (in your DSH profile), never uploaded. Only folders inside the root can be bound to a site.',
270
+ // ---- v5: first run = root folder -> authorization -> create ----
271
+ stepRoot: 'root folder',
272
+ stepAuth: 'authorize',
273
+ stepCreate: 'create',
274
+ stepRootDone: 'Root folder: {dir}',
275
+ stepHint: 'First run goes in order: step 1 pick a root folder, step 2 authorize, step 3 create.',
276
+ authTitle: 'Step 2: authorize the wkgeo account',
277
+ authHint: 'Creating a site uses the wkgeo CLI write API, so authorization is required. Run the command below in a terminal; a browser page opens for authorization, then come back and click "I have authorized, check again".',
278
+ authCommand: 'wkgeo login',
279
+ authCheck: 'I have authorized, check again',
280
+ authChecking: 'Checking authorization…',
281
+ authOk: 'Authorized: the site list is readable',
282
+ authNote: 'Credentials are kept by the wkgeo CLI itself; this plugin never reads, shows or uploads your password or token.',
283
+ needRoot: 'Pick a root folder in step 1 first, then you can create a site.',
284
+ needAuth: 'Finish authorization in step 2 (wkgeo login) before creating a site.',
285
+ blockedTitle: 'One more step before creating a site',
286
+ crashTitle: 'wkgeo site ops · panel error',
287
+ crashRefresh: 'Reload UI',
288
+ regWarn: 'Panel self-check: ',
251
289
  rootLabel: 'Site root folder',
252
290
  rootUnset: 'no root folder yet',
253
291
  rootPick: 'Choose root',
@@ -331,7 +369,9 @@ window.__ModuleLoader__.load({
331
369
  height: '100%',
332
370
  width: '100%',
333
371
  minWidth: 0,
334
- minHeight: 0,
372
+ // 老板 2026-10-30 白屏复盘:主区容器是 flex 项,历史上有过「高度塌成 0、看起来全白」的情况,
373
+ // 这里给一个最小高度兜底;真渲染出来了,内容就一定会占可见高度。
374
+ minHeight: '320px',
335
375
  flex: '1 1 auto',
336
376
  gap: '10px',
337
377
  padding: '12px',
@@ -956,12 +996,8 @@ window.__ModuleLoader__.load({
956
996
  }
957
997
  render() {
958
998
  if (this.state.error) {
959
- const msg = String((this.state.error && this.state.error.message) || this.state.error || 'unknown')
960
- return h(
961
- 'div',
962
- { style: { ...STYLE.err, margin: '12px', fontSize: '12px', whiteSpace: 'pre-wrap' } },
963
- '面板渲染出错:' + msg.slice(0, 600),
964
- )
999
+ // 老板 2026-10-30:渲染异常不许变成一片空白 —— 消息 + 堆栈摘要直接打在面板上。
1000
+ return h(PanelCrash, { error: this.state.error, where: '面板渲染(PanelBoundary 捕获)' })
965
1001
  }
966
1002
  return this.props.children
967
1003
  }
@@ -1512,6 +1548,107 @@ window.__ModuleLoader__.load({
1512
1548
  )
1513
1549
  }
1514
1550
 
1551
+ /**
1552
+ * 面板自检记录(老板 2026-10-30 白屏复盘):
1553
+ * 旧代码里注册失败被 `try{...}catch{return undefined}` 静默吞掉 —— 左栏有图标、点进去一片空白,
1554
+ * 界面上一个字都没有,只能靠猜。现在所有注册失败都记进这里,面板会把它打在首屏/错误页上。
1555
+ */
1556
+ const REGISTRATION = { ok: {}, errors: [] }
1557
+
1558
+ function noteRegistration(label, err) {
1559
+ if (!err) {
1560
+ REGISTRATION.ok[label] = true
1561
+ return
1562
+ }
1563
+ const msg = String((err && err.message) || err || 'unknown')
1564
+ REGISTRATION.ok[label] = false
1565
+ const entry = label + ' → ' + msg
1566
+ if (REGISTRATION.errors[REGISTRATION.errors.length - 1] !== entry) {
1567
+ REGISTRATION.errors.push(entry)
1568
+ if (REGISTRATION.errors.length > 6) REGISTRATION.errors.shift()
1569
+ }
1570
+ try {
1571
+ console.error('[wkgeo-ops] slot register failed · ' + label, err)
1572
+ } catch (_consoleErr) {
1573
+ /* 忽略 */
1574
+ }
1575
+ }
1576
+
1577
+ /** 错误兜底页:异常消息 + 堆栈摘要 + 注册自检,保证「绝不静默空白」。 */
1578
+ function PanelCrash(props) {
1579
+ const err = props ? props.error : null
1580
+ const where = (props && props.where) || '面板'
1581
+ const msg = String((err && err.message) || err || 'unknown')
1582
+ const stack = err && err.stack ? String(err.stack) : ''
1583
+ const trace = stack ? stack.split('\n').slice(0, 8).join('\n') : ''
1584
+ return h(
1585
+ 'div',
1586
+ { style: STYLE.wrap },
1587
+ h('div', { style: STYLE.bar }, h('span', { style: STYLE.barTitle }, MESSAGES.zh.crashTitle)),
1588
+ h(
1589
+ 'div',
1590
+ {
1591
+ style: {
1592
+ ...STYLE.err,
1593
+ whiteSpace: 'pre-wrap',
1594
+ wordBreak: 'break-word',
1595
+ fontFamily: 'ui-monospace, SFMono-Regular, Consolas, monospace',
1596
+ fontSize: '12px',
1597
+ lineHeight: '1.5',
1598
+ padding: '10px',
1599
+ border: '1px solid var(--dsw-alias-border-l2)',
1600
+ borderRadius: '8px',
1601
+ background: 'var(--dsw-alias-bg-layer-2)',
1602
+ },
1603
+ },
1604
+ '位置:' + where + '\n' + msg + (trace ? '\n\n堆栈摘要:\n' + trace : ''),
1605
+ ),
1606
+ REGISTRATION.errors.length
1607
+ ? h(
1608
+ 'div',
1609
+ { style: { ...STYLE.warn, whiteSpace: 'pre-wrap', fontSize: '11px' } },
1610
+ MESSAGES.zh.regWarn + '\n' + REGISTRATION.errors.slice(-4).join('\n'),
1611
+ )
1612
+ : null,
1613
+ h(
1614
+ 'div',
1615
+ { style: STYLE.actions },
1616
+ h(
1617
+ 'button',
1618
+ {
1619
+ type: 'button',
1620
+ style: STYLE.btnPrimary,
1621
+ onClick: () => {
1622
+ try {
1623
+ if (typeof window !== 'undefined' && window.location) window.location.reload()
1624
+ } catch (_reloadErr) {
1625
+ /* 忽略 */
1626
+ }
1627
+ },
1628
+ },
1629
+ MESSAGES.zh.crashRefresh,
1630
+ ),
1631
+ ),
1632
+ )
1633
+ }
1634
+
1635
+ /** 首次进入的三步指示条:① 选页根目录 → ② 授权 → ③ 建站 */
1636
+ function stepChip(t, label, done, active) {
1637
+ const mark = done ? '✓ ' : active ? '● ' : '○ '
1638
+ return h(
1639
+ 'span',
1640
+ {
1641
+ style: {
1642
+ ...STYLE.pill,
1643
+ color: done ? '#15803d' : active ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-tertiary)',
1644
+ borderColor: done ? '#15803d' : undefined,
1645
+ opacity: done || active ? 1 : 0.75,
1646
+ },
1647
+ },
1648
+ mark + label,
1649
+ )
1650
+ }
1651
+
1515
1652
  /** 面板主体 */
1516
1653
  function PanelBody(ctx) {
1517
1654
  const t = makeT(ctx)
@@ -1570,6 +1707,18 @@ window.__ModuleLoader__.load({
1570
1707
  }, [])
1571
1708
 
1572
1709
  const openWizard = () => {
1710
+ // 三步顺序(老板 2026-10-30):① 页根目录 → ② 授权 → ③ 建站。前两步没完成不许进向导。
1711
+ if (!rootReady) {
1712
+ setView('init')
1713
+ setInitError(t('needRoot'))
1714
+ return
1715
+ }
1716
+ if (!authed) {
1717
+ setInitError('')
1718
+ setView('list')
1719
+ load(true)
1720
+ return
1721
+ }
1573
1722
  setWizard(defaultWizard())
1574
1723
  setWizardError('')
1575
1724
  setRun({ busy: false, ok: false, siteId: '', log: [], unfilled: [], gaps: [] })
@@ -1824,12 +1973,25 @@ window.__ModuleLoader__.load({
1824
1973
  const rootReady = Boolean(cfg.rootDir)
1825
1974
  const boundCount = Object.keys(folders || {}).length
1826
1975
  const cfgDown = cfg.phase === 'error'
1976
+ // 第 2 步「授权」的判定:站点接口能拿到 ok 数据即视为已授权;明确返回 NOT_LOGGED_IN 即未授权。
1977
+ const authed = Boolean(data) && data.ok !== false && !isLoginIssue
1978
+ const needAuth = Boolean(data) && isLoginIssue
1979
+ const canCreate = rootReady && authed
1980
+ const blockedHint = !rootReady ? t('needRoot') : !authed ? t('needAuth') : ''
1981
+
1982
+ const steps = h(
1983
+ 'div',
1984
+ { style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '6px' } },
1985
+ stepChip(t, '1 · ' + t('stepRoot'), rootReady, !rootReady),
1986
+ stepChip(t, '2 · ' + t('stepAuth'), authed, rootReady && !authed),
1987
+ stepChip(t, '3 · ' + t('stepCreate'), false, canCreate),
1988
+ )
1827
1989
 
1828
1990
  const head = h(
1829
1991
  'div',
1830
1992
  { style: STYLE.bar },
1831
1993
  h('span', { style: STYLE.barTitle }, t('title')),
1832
- h('span', { style: { ...STYLE.barMeta, fontSize: '10px', opacity: 0.65 }, title: 'client build' }, 'v5'),
1994
+ h('span', { style: { ...STYLE.barMeta, fontSize: '10px', opacity: 0.65 }, title: 'client build' }, 'v6'),
1833
1995
  h(
1834
1996
  'span',
1835
1997
  { style: { ...STYLE.badge, cursor: 'pointer' }, title: cfg.rootDir || t('rootUnset'), onClick: openBrowserRoot },
@@ -1837,7 +1999,17 @@ window.__ModuleLoader__.load({
1837
1999
  ),
1838
2000
  h('button', { type: 'button', style: STYLE.btn, disabled: busy, onClick: () => load(true), title: t('refresh') }, busy ? t('refreshing') : t('refresh')),
1839
2001
  sites.length > 0 && (view === 'list' || view === 'detail')
1840
- ? h('button', { type: 'button', style: STYLE.btn, onClick: openWizard }, t('create'))
2002
+ ? h(
2003
+ 'button',
2004
+ {
2005
+ type: 'button',
2006
+ style: canCreate ? STYLE.btn : { ...STYLE.btn, opacity: 0.55, cursor: 'not-allowed' },
2007
+ disabled: !canCreate,
2008
+ title: blockedHint || t('create'),
2009
+ onClick: openWizard,
2010
+ },
2011
+ t('create'),
2012
+ )
1841
2013
  : null,
1842
2014
  h(
1843
2015
  'span',
@@ -1847,6 +2019,34 @@ window.__ModuleLoader__.load({
1847
2019
  ),
1848
2020
  )
1849
2021
 
2022
+ // 第 2 步:授权卡片(放在建站之前,未授权时首屏就是它,建站按钮全部禁用并提示去哪一步)
2023
+ const authPanel = h(
2024
+ 'div',
2025
+ { style: STYLE.initCard },
2026
+ h('div', { style: STYLE.bar }, h('span', { style: STYLE.barTitle }, t('authTitle'))),
2027
+ h('div', { style: STYLE.hint }, t('authHint')),
2028
+ h('div', { style: STYLE.growRow }, h('code', { style: STYLE.code }, t('authCommand'))),
2029
+ h(
2030
+ 'div',
2031
+ { style: STYLE.actions },
2032
+ h(
2033
+ 'button',
2034
+ { type: 'button', style: STYLE.btnPrimary, disabled: busy, onClick: () => load(true) },
2035
+ busy ? t('authChecking') : t('authCheck'),
2036
+ ),
2037
+ h('button', { type: 'button', style: STYLE.btn, onClick: () => setView('init') }, t('rootChange')),
2038
+ // 第 3 步的入口先亮出来但禁用:未授权时点不动,title 说明缺的是哪一步
2039
+ h(
2040
+ 'button',
2041
+ { type: 'button', style: STYLE.btn, disabled: true, title: blockedHint || t('needAuth') },
2042
+ t('create'),
2043
+ ),
2044
+ ),
2045
+ needAuth && !busy ? h('div', { style: STYLE.err }, t('loginHint')) : null,
2046
+ state.error ? h('div', { style: STYLE.err }, String(state.error).slice(0, 300)) : null,
2047
+ h('div', { style: STYLE.note }, t('authNote')),
2048
+ )
2049
+
1850
2050
  const initPanel = h(
1851
2051
  'div',
1852
2052
  { style: STYLE.initCard },
@@ -1945,6 +2145,9 @@ window.__ModuleLoader__.load({
1945
2145
  })
1946
2146
  } else if (!rootReady && !cfgDown) {
1947
2147
  body = initPanel
2148
+ } else if (needAuth) {
2149
+ // 第 2 步:目录已选、但还没授权 → 先授权,建站按钮此时是禁用的
2150
+ body = authPanel
1948
2151
  } else if (state.phase === 'loading' && !data) {
1949
2152
  body = h('div', { style: STYLE.state }, h('div', { style: STYLE.stateTitle }, t('refreshing')))
1950
2153
  } else if (state.error) {
@@ -1953,9 +2156,19 @@ window.__ModuleLoader__.load({
1953
2156
  { style: STYLE.state },
1954
2157
  h('div', { style: STYLE.stateTitle }, isLoginIssue ? t('loginHint') : t('error')),
1955
2158
  h('div', null, String(state.error).slice(0, 300)),
1956
- isLoginIssue ? h('div', null, t('emptyHint')) : null,
2159
+ !canCreate ? h('div', { style: STYLE.hint }, blockedHint) : null,
1957
2160
  h('button', { type: 'button', style: STYLE.btn, onClick: () => load(true) }, t('retry')),
1958
- h('button', { type: 'button', style: STYLE.btnPrimary, onClick: openWizard }, t('create')),
2161
+ h(
2162
+ 'button',
2163
+ {
2164
+ type: 'button',
2165
+ style: canCreate ? STYLE.btnPrimary : { ...STYLE.btnPrimary, opacity: 0.55, cursor: 'not-allowed' },
2166
+ disabled: !canCreate,
2167
+ title: blockedHint || t('create'),
2168
+ onClick: openWizard,
2169
+ },
2170
+ t('create'),
2171
+ ),
1959
2172
  )
1960
2173
  } else if (sites.length === 0) {
1961
2174
  body = h(
@@ -1963,7 +2176,18 @@ window.__ModuleLoader__.load({
1963
2176
  { style: STYLE.state },
1964
2177
  h('div', { style: STYLE.stateTitle }, t('empty')),
1965
2178
  h('div', null, t('emptyHint')),
1966
- h('button', { type: 'button', style: { ...STYLE.btnPrimary, ...STYLE.btnBlock }, onClick: openWizard }, t('createFirst')),
2179
+ !canCreate ? h('div', { style: STYLE.hint }, blockedHint) : null,
2180
+ h(
2181
+ 'button',
2182
+ {
2183
+ type: 'button',
2184
+ style: canCreate ? { ...STYLE.btnPrimary, ...STYLE.btnBlock } : { ...STYLE.btn, ...STYLE.btnBlock, opacity: 0.55, cursor: 'not-allowed' },
2185
+ disabled: !canCreate,
2186
+ title: blockedHint || t('createFirst'),
2187
+ onClick: openWizard,
2188
+ },
2189
+ t('createFirst'),
2190
+ ),
1967
2191
  h('div', { style: STYLE.hint }, '没登录会取不到站点:', h('code', { style: STYLE.code }, 'wkgeo login')),
1968
2192
  )
1969
2193
  } else {
@@ -1980,7 +2204,16 @@ window.__ModuleLoader__.load({
1980
2204
  onPickFolder: openBrowserSite,
1981
2205
  }),
1982
2206
  ),
1983
- h('div', { key: '__new__', style: STYLE.newCard, onClick: openWizard }, t('newCard')),
2207
+ h(
2208
+ 'div',
2209
+ {
2210
+ key: '__new__',
2211
+ style: canCreate ? STYLE.newCard : { ...STYLE.newCard, opacity: 0.55, cursor: 'not-allowed' },
2212
+ title: blockedHint || t('newCard'),
2213
+ onClick: canCreate ? openWizard : undefined,
2214
+ },
2215
+ canCreate ? t('newCard') : t('blockedTitle') + ':' + blockedHint,
2216
+ ),
1984
2217
  )
1985
2218
  }
1986
2219
 
@@ -1988,6 +2221,8 @@ window.__ModuleLoader__.load({
1988
2221
  'div',
1989
2222
  { style: STYLE.wrap },
1990
2223
  head,
2224
+ steps,
2225
+ rootReady ? h('div', { style: { ...STYLE.hint, fontSize: '11px' }, title: cfg.rootDir }, t('stepRootDone', { dir: cfg.rootDir })) : null,
1991
2226
  cfgDown ? h('div', { style: STYLE.warn }, t('cfgUnavailable')) : null,
1992
2227
  rootReady && cfg.exists === false ? h('div', { style: STYLE.warn }, t('rootMissingNow')) : null,
1993
2228
  folderError ? h('div', { style: STYLE.err }, String(folderError).slice(0, 300)) : null,
@@ -1998,25 +2233,98 @@ window.__ModuleLoader__.load({
1998
2233
  data && data.ageMs != null ? t('updated', { t: new Date().toLocaleTimeString() }) : '',
1999
2234
  boundCount > 0 ? ' · 📁 ' + boundCount : '',
2000
2235
  ),
2236
+ REGISTRATION.errors.length
2237
+ ? h(
2238
+ 'div',
2239
+ { style: { ...STYLE.warn, whiteSpace: 'pre-wrap', fontSize: '11px' } },
2240
+ t('regWarn') + REGISTRATION.errors.slice(-3).join(' | '),
2241
+ )
2242
+ : null,
2001
2243
  )
2002
2244
  }
2003
2245
 
2004
2246
  function apply(ctx) {
2005
2247
  // 面板入口:错误边界包住面板本身 —— 渲染异常变成可复制的报错文本,绝不整块空白。
2006
- const OpsPanel = () => h(PanelBoundary, null, h(PanelBody, ctx))
2248
+ // 面板入口:① 函数级 try/catch 兜住「构造元素树就抛错」;② PanelBoundary 兜住渲染期异常。
2249
+ // 两条都不许再变成空白 —— 出错就打错误页(异常消息 + 堆栈摘要 + 注册自检)。
2250
+ const OpsPanel = () => {
2251
+ try {
2252
+ return h(PanelBoundary, null, h(PanelBody, ctx))
2253
+ } catch (err) {
2254
+ return h(PanelCrash, { error: err, where: 'OpsPanel 构造元素树' })
2255
+ }
2256
+ }
2007
2257
 
2008
- const scope = (label, fn) => {
2258
+ /** 注册期兜底:失败必须记账(旧代码在这里静默 return undefined,正是白屏查不出来的原因)。 */
2259
+ const guard = (label, fn) => {
2009
2260
  try {
2010
2261
  if (typeof ctx.effect === 'function') return ctx.effect(fn, label)
2011
2262
  return fn()
2012
- } catch (_err) {
2263
+ } catch (err) {
2264
+ noteRegistration(label, err)
2013
2265
  return undefined
2014
2266
  }
2015
2267
  }
2016
2268
 
2017
- scope('wkgeo-ops:locale', () => ctx.locale.register(NS, { zh: MESSAGES.zh, en: MESSAGES.en }))
2269
+ const SLOT_RETRY_MAX = 20
2270
+ const SLOT_RETRY_MS = 300
2271
+
2272
+ /**
2273
+ * 槽位注册(白屏根因修复,老板 2026-10-30):
2274
+ * DSH 的 SlotCore.register 在「槽位还没被父 entry 的 children 表声明」时会**同步抛错**
2275
+ * (`slot "main" is not declared (a parent entry's children table must declare it)`)。
2276
+ * 旧代码是「直接 register + catch 静默吞掉」:只要那次 register 抛了,`main` 就永远不存在,
2277
+ * 结果正是「左栏图标在、点进去一片空白、界面没有任何报错」。
2278
+ * 官方范式(DSH 内置插件全部如此)是 ctx.slots.inject(name, () => ctx.slots.register(...)):
2279
+ * 声明前挂起、声明后自动补注册;这里再叠加定时重试,双保险。
2280
+ */
2281
+ const slotRegister = (label, options, component) => {
2282
+ const name = options && options.name
2283
+ let off = null
2284
+ let timer = null
2285
+ let tries = 0
2286
+ let disposed = false
2287
+ const attempt = () => {
2288
+ if (disposed) return undefined
2289
+ try {
2290
+ off = ctx.slots.register(options, component)
2291
+ noteRegistration(label, null)
2292
+ return off
2293
+ } catch (err) {
2294
+ noteRegistration(label, err)
2295
+ tries += 1
2296
+ if (tries <= SLOT_RETRY_MAX && !timer) {
2297
+ timer = setTimeout(() => {
2298
+ timer = null
2299
+ attempt()
2300
+ }, SLOT_RETRY_MS)
2301
+ }
2302
+ return undefined
2303
+ }
2304
+ }
2305
+ let offInject = null
2306
+ if (typeof ctx.slots.inject === 'function') {
2307
+ try {
2308
+ offInject = ctx.slots.inject(name, attempt)
2309
+ } catch (err) {
2310
+ noteRegistration(label, err)
2311
+ attempt()
2312
+ }
2313
+ } else {
2314
+ attempt()
2315
+ }
2316
+ return () => {
2317
+ disposed = true
2318
+ if (timer) clearTimeout(timer)
2319
+ timer = null
2320
+ if (typeof offInject === 'function') offInject()
2321
+ if (typeof off === 'function') off()
2322
+ }
2323
+ }
2324
+
2325
+ guard('wkgeo-ops:locale', () => ctx.locale.register(NS, { zh: MESSAGES.zh, en: MESSAGES.en }))
2018
2326
 
2019
- scope('wkgeo-ops:tab', () =>
2327
+ guard('wkgeo-ops:tab', () =>
2020
2328
  ctx.sidebarRightTabs.register({
2021
2329
  id: PANEL_ID,
2022
2330
  kind: 'wkgeo-ops',
@@ -2043,18 +2351,19 @@ window.__ModuleLoader__.load({
2043
2351
  }),
2044
2352
  )
2045
2353
 
2046
- scope('wkgeo-ops:pane', () =>
2047
- ctx.slots.inject('sidebar.right.pane.tab', () =>
2048
- ctx.slots.register(
2049
- { name: 'sidebar.right.pane.tab', key: PANEL_ID, locale: NS, inject: () => ({}) },
2050
- OpsPanel,
2051
- ),
2354
+ guard('wkgeo-ops:pane', () =>
2355
+ slotRegister(
2356
+ 'sidebar.right.pane.tab',
2357
+ { name: 'sidebar.right.pane.tab', key: PANEL_ID, locale: NS, inject: () => ({}) },
2358
+ OpsPanel,
2052
2359
  ),
2053
2360
  )
2054
2361
 
2055
- scope('wkgeo-ops:pane-title', () =>
2056
- ctx.slots.inject('sidebar.right.pane.tab.title', () =>
2057
- ctx.slots.register({ name: 'sidebar.right.pane.tab.title', key: PANEL_ID }, () => h('span', null, MESSAGES.zh.title)),
2362
+ guard('wkgeo-ops:pane-title', () =>
2363
+ slotRegister(
2364
+ 'sidebar.right.pane.tab.title',
2365
+ { name: 'sidebar.right.pane.tab.title', key: PANEL_ID },
2366
+ () => h('span', null, SIDEBAR_ROW_LABEL),
2058
2367
  ),
2059
2368
  )
2060
2369
 
@@ -2063,8 +2372,9 @@ window.__ModuleLoader__.load({
2063
2372
  // ① sidebar.panellist —— 左栏里的一行图标(id = PANEL_ID)
2064
2373
  // ② main —— 点该图标后中央区域渲染的面板(key = PANEL_ID)
2065
2374
  // 少一条就看不到入口(只有①会点不开,只有②没有图标)。停用插件时把界面交还对话。
2066
- scope('wkgeo-ops:sidebar-entry', () => {
2067
- const offRow = ctx.slots.register(
2375
+ guard('wkgeo-ops:sidebar-row', () =>
2376
+ slotRegister(
2377
+ 'sidebar.panellist',
2068
2378
  {
2069
2379
  name: 'sidebar.panellist',
2070
2380
  id: PANEL_ID,
@@ -2073,24 +2383,23 @@ window.__ModuleLoader__.load({
2073
2383
  locale: NS,
2074
2384
  },
2075
2385
  (owner = {}) => h(OpsIcon, { size: owner.size }),
2076
- )
2077
- const offPanel = ctx.slots.register(
2078
- { name: 'main', key: PANEL_ID, locale: NS, inject: () => ({}) },
2079
- OpsPanel,
2080
- )
2081
- return () => {
2082
- try {
2083
- const layout = typeof ctx.get === 'function' ? ctx.get('layout') : null
2084
- const info = layout && layout.panelInfo
2085
- if (info && typeof info.getSnapshot === 'function') {
2086
- const snapshot = info.getSnapshot()
2087
- if (snapshot && snapshot.activePanelId === PANEL_ID) layout.selectPanel(null)
2088
- }
2089
- } catch (_err) {
2090
- /* 尽力而为 */
2386
+ ),
2387
+ )
2388
+
2389
+ guard('wkgeo-ops:main', () =>
2390
+ slotRegister('main', { name: 'main', key: PANEL_ID, locale: NS, inject: () => ({}) }, OpsPanel),
2391
+ )
2392
+
2393
+ guard('wkgeo-ops:cleanup', () => () => {
2394
+ try {
2395
+ const layout = typeof ctx.get === 'function' ? ctx.get('layout') : null
2396
+ const info = layout && layout.panelInfo
2397
+ if (info && typeof info.getSnapshot === 'function') {
2398
+ const snapshot = info.getSnapshot()
2399
+ if (snapshot && snapshot.activePanelId === PANEL_ID) layout.selectPanel(null)
2091
2400
  }
2092
- offRow()
2093
- offPanel()
2401
+ } catch (_err) {
2402
+ /* 尽力而为 */
2094
2403
  }
2095
2404
  })
2096
2405
  }