mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 02:16:13 +00:00
refactor(ui): 页面包更名为页面插件
消除断句歧义, 与数据源插件命名对齐; 仅改文案, 代码层标识不动。
This commit is contained in:
@@ -83,7 +83,7 @@
|
||||
<text x="560" y="699" text-anchor="middle" font-size="10" fill="#64748B">供策略 / 回测 / 监控取用</text>
|
||||
<rect x="660" y="664" width="176" height="44" rx="6" fill="#F8FAFC" stroke="#E2E8F0"/>
|
||||
<text x="748" y="682" text-anchor="middle" font-size="11.5" font-weight="600" fill="#334155">页面扩展</text>
|
||||
<text x="748" y="699" text-anchor="middle" font-size="10" fill="#64748B">11 官方页 + 页面包</text>
|
||||
<text x="748" y="699" text-anchor="middle" font-size="10" fill="#64748B">11 官方页 + 页面插件</text>
|
||||
<rect x="848" y="664" width="176" height="44" rx="6" fill="#F8FAFC" stroke="#E2E8F0"/>
|
||||
<text x="936" y="682" text-anchor="middle" font-size="11.5" font-weight="600" fill="#334155">custom 后端</text>
|
||||
<text x="936" y="699" text-anchor="middle" font-size="10" fill="#64748B">AI 助手参考实现</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
@@ -1,4 +1,4 @@
|
||||
"""运行时页面包 API — 上传/清单回填/启停/删除/bundle 分发。
|
||||
"""运行时页面插件 API — 上传/清单回填/启停/删除/bundle 分发。
|
||||
|
||||
分发端点位于 /api 前缀下, 自动受集中鉴权中间件保护: 浏览器同源动态
|
||||
import 会携带登录 cookie, 与页面会话同一访问边界。
|
||||
@@ -33,7 +33,7 @@ async def _write_bundle_capped(file: UploadFile, dest: Path, max_bytes: int) ->
|
||||
break
|
||||
total += len(chunk)
|
||||
if total > max_bytes:
|
||||
raise HTTPException(413, f"页面包过大(上限 {max_bytes // (1024 * 1024)}MB)")
|
||||
raise HTTPException(413, f"页面插件过大(上限 {max_bytes // (1024 * 1024)}MB)")
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
@@ -57,17 +57,17 @@ async def upload_page_pack(
|
||||
id: Annotated[str, Form()],
|
||||
file: Annotated[UploadFile, File()],
|
||||
) -> PagePackMeta:
|
||||
"""上传页面包产物(.js/.mjs), 覆盖式: 同 ID 重新上传即更新。"""
|
||||
"""上传页面插件产物(.js/.mjs), 覆盖式: 同 ID 重新上传即更新。"""
|
||||
suffix = Path(file.filename or "").suffix.lower()
|
||||
if suffix not in (".js", ".mjs"):
|
||||
raise HTTPException(400, "仅支持 .js / .mjs 页面包产物")
|
||||
raise HTTPException(400, "仅支持 .js / .mjs 页面插件产物")
|
||||
store = _store(request)
|
||||
tmp_dir = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
tmp_path = tmp_dir / "bundle.js"
|
||||
await _write_bundle_capped(file, tmp_path, _MAX_BUNDLE_BYTES)
|
||||
if tmp_path.stat().st_size == 0:
|
||||
raise HTTPException(400, "页面包为空文件")
|
||||
raise HTTPException(400, "页面插件为空文件")
|
||||
try:
|
||||
return store.save_upload(id, tmp_path)
|
||||
except ValueError as e:
|
||||
@@ -91,7 +91,7 @@ def get_page_pack_bundle(pack_id: str, request: Request) -> FileResponse:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
bundle = store.bundle_path(pack_id) if meta else None
|
||||
if meta is None or bundle is None:
|
||||
raise HTTPException(404, f"页面包 '{pack_id}' 不存在")
|
||||
raise HTTPException(404, f"页面插件 '{pack_id}' 不存在")
|
||||
return FileResponse(
|
||||
bundle,
|
||||
media_type="text/javascript",
|
||||
@@ -109,7 +109,7 @@ def put_page_pack_manifest(pack_id: str, body: ManifestBody, request: Request) -
|
||||
try:
|
||||
return store.update_manifest(pack_id, label=body.label, path=body.path, error=body.error)
|
||||
except KeyError:
|
||||
raise HTTPException(404, f"页面包 '{pack_id}' 不存在") from None
|
||||
raise HTTPException(404, f"页面插件 '{pack_id}' 不存在") from None
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
|
||||
@@ -120,7 +120,7 @@ def put_page_pack_enabled(pack_id: str, body: EnabledBody, request: Request) ->
|
||||
try:
|
||||
return store.set_enabled(pack_id, body.enabled)
|
||||
except KeyError:
|
||||
raise HTTPException(404, f"页面包 '{pack_id}' 不存在") from None
|
||||
raise HTTPException(404, f"页面插件 '{pack_id}' 不存在") from None
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
|
||||
@@ -130,7 +130,7 @@ def delete_page_pack(pack_id: str, request: Request) -> dict:
|
||||
store = _store(request)
|
||||
try:
|
||||
if not store.delete(pack_id):
|
||||
raise HTTPException(404, f"页面包 '{pack_id}' 不存在")
|
||||
raise HTTPException(404, f"页面插件 '{pack_id}' 不存在")
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
return {"ok": True}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""运行时页面包存储 — data/ext_pages/{id}/ 目录式持久化。
|
||||
"""运行时页面插件存储 — data/ext_pages/{id}/ 目录式持久化。
|
||||
|
||||
页面包是前端扩展的打包产物(单文件 ES 模块): 上传后由前端在启动引导窗口
|
||||
页面插件是前端扩展的打包产物(单文件 ES 模块): 上传后由前端在启动引导窗口
|
||||
动态 import 挂载(见 frontend/src/extensions/runtime.ts)。后端只负责存储、
|
||||
元数据与分发, 不解析 JS 内容 —— 清单契约(能否 import、apiVersion、路由
|
||||
冲突)由前端注册表校验, 校验结果经 manifest 接口回填到 page.json。
|
||||
@@ -41,7 +41,7 @@ def _sha256_file(path: Path) -> str:
|
||||
|
||||
|
||||
class PagePackStore:
|
||||
"""每个页面包一个目录: page.json(元数据) + bundle.js(产物)。
|
||||
"""每个页面插件一个目录: page.json(元数据) + bundle.js(产物)。
|
||||
|
||||
元数据写入走 临时文件+rename, 与 ext_data 的原子写同语义, 避免半写
|
||||
状态被启动扫描读到。
|
||||
@@ -53,7 +53,7 @@ class PagePackStore:
|
||||
|
||||
def _dir(self, pack_id: str) -> Path:
|
||||
if not _VALID_ID.match(pack_id or ""):
|
||||
raise ValueError("页面包 ID 仅允许字母/数字/下划线, 长度 1-64")
|
||||
raise ValueError("页面插件 ID 仅允许字母/数字/下划线, 长度 1-64")
|
||||
return self.data_dir / pack_id
|
||||
|
||||
def _write_meta(self, meta: PagePackMeta) -> None:
|
||||
@@ -86,7 +86,7 @@ class PagePackStore:
|
||||
try:
|
||||
return PagePackMeta(**json.loads(p.read_text(encoding="utf-8")))
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# 半写/损坏的元数据按损坏页处理, 不阻断其他页面包
|
||||
# 半写/损坏的元数据按损坏页处理, 不阻断其他页面插件
|
||||
return PagePackMeta(id=pack_id, error="page.json 损坏, 请重新上传")
|
||||
|
||||
def bundle_path(self, pack_id: str) -> Path | None:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""运行时页面包 API 测试 — 上传校验/生命周期/bundle 分发。
|
||||
"""运行时页面插件 API 测试 — 上传校验/生命周期/bundle 分发。
|
||||
|
||||
TestClient 最小 app 风格(同 test_ext_market_level): 不起完整后端,
|
||||
app.state.repo 只提供 store.data_dir。
|
||||
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
# 页面包开发指南 (Page Pack SDK v1)
|
||||
# 页面插件开发指南 (Page Pack SDK v1)
|
||||
|
||||
页面包是**运行时可挂载的页面产物**:把页面打包成单文件 ES 模块(`extension.js`),在「设置 → 页面包」上传,刷新后即成为完整页面——自动注册路由、侧栏入口,可在菜单设置里排序/隐藏。本地 dev、生产构建、桌面端三种运行形态一致生效,宿主无需重新构建。
|
||||
页面插件是**运行时可挂载的页面产物**:把页面打包成单文件 ES 模块(`extension.js`),在「设置 → 页面插件」上传,刷新后即成为完整页面——自动注册路由、侧栏入口,可在菜单设置里排序/隐藏。本地 dev、生产构建、桌面端三种运行形态一致生效,宿主无需重新构建。
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -9,7 +9,7 @@ cp -r frontend/page-pack-template my-pack && cd my-pack
|
||||
pnpm install && pnpm build # → dist/extension.js
|
||||
```
|
||||
|
||||
打开宿主「设置 → 页面包」→ 填 ID(字母/数字/下划线)→ 选择 `dist/extension.js` → 上传并校验 → 刷新页面。
|
||||
打开宿主「设置 → 页面插件」→ 填 ID(字母/数字/下划线)→ 选择 `dist/extension.js` → 上传并校验 → 刷新页面。
|
||||
|
||||
## 产物契约 (apiVersion 1)
|
||||
|
||||
@@ -29,7 +29,7 @@ const extension = {
|
||||
},
|
||||
}],
|
||||
navigation: [{
|
||||
id: 'hello', routeId: 'hello', label: '页面包演示', icon: 'Sparkles',
|
||||
id: 'hello', routeId: 'hello', label: '页面插件演示', icon: 'Sparkles',
|
||||
}],
|
||||
}
|
||||
export default extension
|
||||
@@ -39,7 +39,7 @@ export default extension
|
||||
|
||||
## 能力桥 ctx
|
||||
|
||||
页面包**自包含**(React 等依赖由模板打进产物,与宿主不共享模块实例),宿主能力经 `ctx` 暴露:
|
||||
页面插件**自包含**(React 等依赖由模板打进产物,与宿主不共享模块实例),宿主能力经 `ctx` 暴露:
|
||||
|
||||
| 能力 | 说明 |
|
||||
| --- | --- |
|
||||
@@ -60,13 +60,13 @@ export default extension
|
||||
- **路由集成**:页面挂在宿主路由容器内(`/你的路径` 可直达、可刷新),但包内不共享宿主 Router 上下文——需要跳转宿主页面时用 `window.location` 或自行包 `MemoryRouter`。
|
||||
- **生效方式**:上传/启停后**刷新页面生效**(挂载发生在启动引导窗口),不做热插拔。
|
||||
- **大小**:上限 5MB/包;自包含 React 后典型产物 60–150KB(minify + 可自行加 brotli/gzip 由后端协商)。
|
||||
- **安全**:页面包是任意代码,与登录会话同权运行——只上传自己或可信来源的产物;这是单用户产品的既定边界,第三方插件沙箱不在 v1 范围。
|
||||
- **安全**:页面插件是任意代码,与登录会话同权运行——只上传自己或可信来源的产物;这是单用户产品的既定边界,第三方插件沙箱不在 v1 范围。
|
||||
- **版本**:宿主升级破坏契约时提升 `apiVersion` 并保留旧版兼容期;产物内 `apiVersion` 不匹配会被明确拒绝。
|
||||
|
||||
## 与其他扩展方式的取舍
|
||||
|
||||
| 方式 | 适用 | 见 |
|
||||
| --- | --- | --- |
|
||||
| 页面包(运行时产物) | 免重建挂载、独立分发 | 本文档 |
|
||||
| 页面插件(运行时产物) | 免重建挂载、独立分发 | 本文档 |
|
||||
| 源码级扩展 `frontend/src/custom/<ns>/extension.tsx` | 随宿主一起构建维护、可用全部核心模块 | `docs/secondary-development.md` §3 |
|
||||
| 声明式分析页面(零代码) | 扩展数据 + 模板化列表/分组页 | 设置 → 扩展页面 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 页面包模板 (Page Pack Template)
|
||||
# 页面插件模板 (Page Pack Template)
|
||||
|
||||
把一个页面打包成**单文件 ES 模块产物**(`dist/extension.js`),上传到运行中的面板(设置 → 页面包),刷新后即成为完整页面——自动注册路由与侧栏入口,可在菜单设置里排序/隐藏。本地 dev、Docker、桌面端一致生效,**宿主无需重新构建**。
|
||||
把一个页面打包成**单文件 ES 模块产物**(`dist/extension.js`),上传到运行中的面板(设置 → 页面插件),刷新后即成为完整页面——自动注册路由与侧栏入口,可在菜单设置里排序/隐藏。本地 dev、Docker、桌面端一致生效,**宿主无需重新构建**。
|
||||
|
||||
## 打包
|
||||
|
||||
@@ -13,7 +13,7 @@ pnpm build # → dist/extension.js
|
||||
|
||||
## 上传
|
||||
|
||||
面板 **设置 → 页面包**:填 ID(字母/数字/下划线)→ 选择 `dist/extension.js` → 上传并校验(即时试载,清单不合法或路由冲突会被拒绝并显示原因)→ 启用 → 刷新页面。
|
||||
面板 **设置 → 页面插件**:填 ID(字母/数字/下划线)→ 选择 `dist/extension.js` → 上传并校验(即时试载,清单不合法或路由冲突会被拒绝并显示原因)→ 启用 → 刷新页面。
|
||||
|
||||
## 开发要点
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 页面包示例 — 复制本模板改造你的页面。
|
||||
* 页面插件示例 — 复制本模板改造你的页面。
|
||||
*
|
||||
* 产物契约(v1):
|
||||
* - default 导出清单对象; apiVersion 固定 1
|
||||
@@ -51,9 +51,9 @@ function HelloPage() {
|
||||
return (
|
||||
<div style={{ minHeight: '100%' }}>
|
||||
<div style={card}>
|
||||
<h1 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>页面包演示页</h1>
|
||||
<h1 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}>页面插件演示页</h1>
|
||||
<p style={{ fontSize: 13, opacity: 0.75, lineHeight: 1.7 }}>
|
||||
这是一个自包含的运行时页面: 打包成单文件 ES 模块后上传到「设置 → 页面包」,
|
||||
这是一个自包含的运行时页面: 打包成单文件 ES 模块后上传到「设置 → 页面插件」,
|
||||
刷新即出现在侧栏。{serverTime}
|
||||
</p>
|
||||
<button style={button} onClick={() => setCount(n => n + 1)}>
|
||||
@@ -79,7 +79,7 @@ const extension = {
|
||||
},
|
||||
],
|
||||
navigation: [
|
||||
{ id: 'hello', routeId: 'hello', label: '页面包演示', icon: 'Sparkles' },
|
||||
{ id: 'hello', routeId: 'hello', label: '页面插件演示', icon: 'Sparkles' },
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* tsp.ts — 宿主能力桥读取。
|
||||
*
|
||||
* window.__TSP_EXT__ 由宿主 main.tsx 在引导时安装; 页面包经 mount(el, ctx)
|
||||
* window.__TSP_EXT__ 由宿主 main.tsx 在引导时安装; 页面插件经 mount(el, ctx)
|
||||
* 收到的 ctx 即本对象。api 为宿主完整 API 封装(同源 fetch, 自带登录态),
|
||||
* QK 为集中查询键, cn 为样式合并工具, format 为格式化工具集。
|
||||
*
|
||||
* 自包含说明: 页面包产物自带 React/ReactDOM (由本模板的 vite 构建打入),
|
||||
* 自包含说明: 页面插件产物自带 React/ReactDOM (由本模板的 vite 构建打入),
|
||||
* 与宿主不共享模块实例 —— 请勿 import 宿主内部模块, 只经由 ctx 取能力。
|
||||
*/
|
||||
type Bridge = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// 页面包构建: 单文件自包含 ES 模块 (react 等全部打入), 产物 dist/extension.js
|
||||
// 直接上传到宿主「设置 → 页面包」。不做代码分割 — 宿主按整个文件加载挂载。
|
||||
// 页面插件构建: 单文件自包含 ES 模块 (react 等全部打入), 产物 dist/extension.js
|
||||
// 直接上传到宿主「设置 → 页面插件」。不做代码分割 — 宿主按整个文件加载挂载。
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
// lib 模式不会自动替换依赖里的 process.env.NODE_ENV, 浏览器无 process 全局
|
||||
|
||||
@@ -34,6 +34,6 @@ frontend/src/custom/<namespace>/extension.tsx
|
||||
|
||||
以 [`_template/extension.tsx.example`](_template/extension.tsx.example) 为起点(模板文件不参与构建),并遵循 [`docs/secondary-development.md`](../../../docs/secondary-development.md)。
|
||||
|
||||
## 不想重建宿主?用页面包
|
||||
## 不想重建宿主?用页面插件
|
||||
|
||||
若目标是把页面打包成产物、在运行中的面板上直接上传使用(本地 / Docker / 桌面端一致),走**页面包**:用 [`frontend/page-pack-template`](../../page-pack-template) 打包单文件 ES 模块,在 设置 → 页面包 上传,刷新生效。契约与边界见 [`docs/page-sdk.md`](../../../docs/page-sdk.md)。
|
||||
若目标是把页面打包成产物、在运行中的面板上直接上传使用(本地 / Docker / 桌面端一致),走**页面插件**:用 [`frontend/page-pack-template`](../../page-pack-template) 打包单文件 ES 模块,在 设置 → 页面插件 上传,刷新生效。契约与边界见 [`docs/page-sdk.md`](../../../docs/page-sdk.md)。
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('packToExtension: 契约校验与转换', () => {
|
||||
})
|
||||
|
||||
describe('loadRuntimePageExtensions', () => {
|
||||
it('只加载已启用页面包, loader 键带 runtime: 前缀且完成契约转换', async () => {
|
||||
it('只加载已启用页面插件, loader 键带 runtime: 前缀且完成契约转换', async () => {
|
||||
const rec = recorder()
|
||||
await loadRuntimePageExtensions({
|
||||
fetchList: async () => [
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/**
|
||||
* 运行时页面包加载 — 从后端拉取已启用清单, 在启动引导窗口动态 import。
|
||||
* 运行时页面插件加载 — 从后端拉取已启用清单, 在启动引导窗口动态 import。
|
||||
*
|
||||
* 时机关键: 必须在 initializeFrontendExtensions() 之后、import('./router')
|
||||
* 之前调用(main.tsx 的 bootstrap)。router 模块求值时会 finalize 注册表
|
||||
* 并以冻结快照建路由表, 错过窗口的页面包只能等下次刷新生效。
|
||||
* 并以冻结快照建路由表, 错过窗口的页面插件只能等下次刷新生效。
|
||||
*
|
||||
* fail-open: 后端不可达 / 未登录(401) / 超时一律静默跳过 —— 页面包是
|
||||
* fail-open: 后端不可达 / 未登录(401) / 超时一律静默跳过 —— 页面插件是
|
||||
* 增强能力, 不得拖慢或阻断宿主启动; 单个包 import 失败由注册表记入
|
||||
* loadErrors, 不影响其他包与静态扩展。
|
||||
*
|
||||
* 契约(v1 自包含包): 页面包自带 React/路由(由 SDK 模板封装打包), 宿主不
|
||||
* 契约(v1 自包含包): 页面插件自带 React/路由(由 SDK 模板封装打包), 宿主不
|
||||
* 与其共享模块实例 —— 页面通过 mount(el, ctx) 挂到宿主提供的容器里,
|
||||
* ctx 暴露宿主 api/QK/cn/format 能力桥。这与源码级扩展的 component 契约
|
||||
* 不同: 此处由 packToExtension 把 mount 包装成宿主 React 组件再交给注册
|
||||
* 表, 因此路由注册、冲突检测、错误隔离全部复用源码扩展的既有机制。
|
||||
* 后续若升级为 import map 共享实例, 只需改 SDK 与本文件, 已发布页面包
|
||||
* 后续若升级为 import map 共享实例, 只需改 SDK 与本文件, 已发布页面插件
|
||||
* 源码不受影响。
|
||||
*/
|
||||
import { useEffect, useRef, type ComponentType } from 'react'
|
||||
@@ -46,7 +46,7 @@ export interface PagePackNavigation {
|
||||
icon?: string
|
||||
}
|
||||
|
||||
/** 页面包产物 default 导出契约 — apiVersion 与源码扩展同源治理 */
|
||||
/** 页面插件产物 default 导出契约 — apiVersion 与源码扩展同源治理 */
|
||||
export interface PagePackExtension {
|
||||
id: string
|
||||
apiVersion: 1
|
||||
@@ -94,12 +94,12 @@ function createMountComponent(mount: PagePackRoute['mount']): ComponentType {
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面包清单 → 注册表扩展。形态问题(缺字段/版本不符/路径非法)在此抛出,
|
||||
* 页面插件清单 → 注册表扩展。形态问题(缺字段/版本不符/路径非法)在此抛出,
|
||||
* 经 loader 传给 loadFrontendExtensions 记入 loadErrors —— 与源码扩展
|
||||
* 的失败隔离同一出口。
|
||||
*/
|
||||
export function packToExtension(pack: unknown): FrontendExtension {
|
||||
if (!pack || typeof pack !== 'object') throw new Error('页面包必须 default 导出清单对象')
|
||||
if (!pack || typeof pack !== 'object') throw new Error('页面插件必须 default 导出清单对象')
|
||||
const p = pack as Partial<PagePackExtension>
|
||||
if (p.apiVersion !== 1) throw new Error(`apiVersion 必须为 1, 收到 ${String(p.apiVersion)}`)
|
||||
if (typeof p.id !== 'string' || !p.id.includes('.')) throw new Error('id 必须是带命名空间的字符串 (如 demo.hello)')
|
||||
@@ -179,7 +179,7 @@ export async function loadRuntimePageExtensions(opts: LoadRuntimeOptions = {}):
|
||||
}
|
||||
|
||||
/**
|
||||
* 宿主应用层能力桥: 页面包经 ctx 读取宿主 api/QK/cn/format。
|
||||
* 宿主应用层能力桥: 页面插件经 ctx 读取宿主 api/QK/cn/format。
|
||||
* 安装在 main.tsx 引导早期, 生命周期与页面一致。
|
||||
*/
|
||||
export function installTspExtBridge(bridge: PagePackCtx): void {
|
||||
|
||||
@@ -63,9 +63,9 @@ const queryClient = new QueryClient({
|
||||
|
||||
async function bootstrap() {
|
||||
await initializeFrontendExtensions()
|
||||
// 页面包能力桥: 运行时页面包经 shim 读取宿主 api/QK/cn/format
|
||||
// 页面插件能力桥: 运行时页面插件经 shim 读取宿主 api/QK/cn/format
|
||||
installTspExtBridge({ api, QK, cn, format })
|
||||
// 运行时页面包: 必须在 router 模块求值(finalize + 建路由表)之前挂载
|
||||
// 运行时页面插件: 必须在 router 模块求值(finalize + 建路由表)之前挂载
|
||||
await loadRuntimePageExtensions()
|
||||
const { router } = await import('./router')
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
|
||||
@@ -36,7 +36,7 @@ const TABS: readonly TabDef[] = [
|
||||
{ key: 'ai', label: 'AI 设置', icon: Sparkles, panel: SettingsAIPanel },
|
||||
{ key: 'monitoring', label: '实时监控', icon: Radio, panel: SettingsMonitoringPanel },
|
||||
{ key: 'ext-pages', label: '扩展页面', icon: BarChart3, panel: SettingsExtPagesPanel },
|
||||
{ key: 'page-packs', label: '页面包', icon: Package, panel: SettingsPagePacksPanel },
|
||||
{ key: 'page-packs', label: '页面插件', icon: Package, panel: SettingsPagePacksPanel },
|
||||
{ key: 'timeout', label: '网络设置', icon: Clock3, panel: SettingsTimeoutPanel },
|
||||
{ key: 'menus', label: '菜单设置', icon: SlidersHorizontal, panel: SettingsMenuSettingsPanel },
|
||||
{ key: 'api-tokens', label: '开放接口', icon: KeyRound, panel: SettingsApiTokensPanel },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 设置 → 页面包: 运行时扩展页面的上传/试载/启停/删除。
|
||||
* 设置 → 页面插件: 运行时扩展页面的上传/试载/启停/删除。
|
||||
*
|
||||
* 生命周期: 上传(后端存 pending) → 前端立即动态 import 试载并做契约校验
|
||||
* (packToExtension) → 校验结果回填 manifest(成功即启用) → 刷新页面后在
|
||||
@@ -47,7 +47,7 @@ export function SettingsPagePacksPanel() {
|
||||
async function handleUpload() {
|
||||
const file = fileRef.current?.files?.[0]
|
||||
const id = idInput.trim()
|
||||
if (!file) { toast('请选择页面包产物 (.js / .mjs)', 'error'); return }
|
||||
if (!file) { toast('请选择页面插件产物 (.js / .mjs)', 'error'); return }
|
||||
if (!/^[a-zA-Z0-9_]{1,64}$/.test(id)) { toast('ID 仅允许字母/数字/下划线 (1-64 位)', 'error'); return }
|
||||
setBusy(true)
|
||||
try {
|
||||
@@ -64,8 +64,8 @@ export function SettingsPagePacksPanel() {
|
||||
}
|
||||
await api.extPagesPutManifest(id, { label, path, error })
|
||||
await invalidate()
|
||||
if (error) toast(`页面包校验失败: ${error}`, 'error')
|
||||
else { toast('页面包已启用, 刷新页面后生效', 'success'); setIdInput(''); if (fileRef.current) fileRef.current.value = '' }
|
||||
if (error) toast(`页面插件校验失败: ${error}`, 'error')
|
||||
else { toast('页面插件已启用, 刷新页面后生效', 'success'); setIdInput(''); if (fileRef.current) fileRef.current.value = '' }
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export function SettingsPagePacksPanel() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<section className="rounded-2xl border border-border bg-surface p-6">
|
||||
<div className="text-[11px] uppercase tracking-[0.2em] text-accent/80">页面包</div>
|
||||
<div className="text-[11px] uppercase tracking-[0.2em] text-accent/80">页面插件</div>
|
||||
<h2 className="mt-2 text-2xl font-semibold tracking-tight text-foreground">上传打包页面产物, 免重建挂载</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-secondary">
|
||||
用 <span className="font-mono text-xs text-accent">frontend/page-pack-template</span> 打包页面
|
||||
@@ -89,7 +89,7 @@ export function SettingsPagePacksPanel() {
|
||||
<input
|
||||
value={idInput}
|
||||
onChange={(e) => setIdInput(e.target.value)}
|
||||
placeholder="页面包 ID (字母/数字/下划线)"
|
||||
placeholder="页面插件 ID (字母/数字/下划线)"
|
||||
className="h-9 w-56 rounded-btn border border-border bg-elevated px-3 text-sm text-foreground placeholder:text-muted/60 focus:border-accent/50 focus:outline-none"
|
||||
/>
|
||||
<input
|
||||
@@ -121,7 +121,7 @@ export function SettingsPagePacksPanel() {
|
||||
{bootErrors.length > 0 && (
|
||||
<section className="rounded-card border border-danger/30 bg-danger/5 p-4 text-xs leading-5 text-secondary">
|
||||
<div className="mb-1 flex items-center gap-1.5 font-medium text-danger">
|
||||
<AlertTriangle className="h-3.5 w-3.5" /> 本次启动有页面包挂载失败
|
||||
<AlertTriangle className="h-3.5 w-3.5" /> 本次启动有页面插件挂载失败
|
||||
</div>
|
||||
{bootErrors.map((e, i) => (
|
||||
<div key={i} className="font-mono">{e.source}: {e.error}</div>
|
||||
@@ -135,7 +135,7 @@ export function SettingsPagePacksPanel() {
|
||||
<div>页面 / ID</div><div>路径</div><div>大小 / 时间</div><div className="text-center">启用</div><div className="text-center">操作</div>
|
||||
</div>
|
||||
{packs.length === 0 && (
|
||||
<div className="px-4 py-10 text-center text-sm text-muted">暂无页面包,用上方表单上传第一个。</div>
|
||||
<div className="px-4 py-10 text-center text-sm text-muted">暂无页面插件,用上方表单上传第一个。</div>
|
||||
)}
|
||||
{packs.map(p => (
|
||||
<div key={p.id} className={cn(
|
||||
@@ -176,7 +176,7 @@ export function SettingsPagePacksPanel() {
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { if (confirm(`删除页面包 "${p.label || p.id}"?`)) deleteMut.mutate(p.id) }}
|
||||
onClick={() => { if (confirm(`删除页面插件 "${p.label || p.id}"?`)) deleteMut.mutate(p.id) }}
|
||||
className="rounded p-1.5 text-muted hover:bg-danger/10 hover:text-danger transition-colors"
|
||||
title="删除"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user