refactor(ui): 页面包更名为页面插件

消除断句歧义, 与数据源插件命名对齐; 仅改文案, 代码层标识不动。
This commit is contained in:
shy3130
2026-10-15 12:27:59 +08:00
parent 2a1c076831
commit db300cad45
15 changed files with 58 additions and 58 deletions
+1 -1
View File
@@ -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

+9 -9
View File
@@ -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}
+5 -5
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
"""运行时页面包 API 测试 — 上传校验/生命周期/bundle 分发。
"""运行时页面插件 API 测试 — 上传校验/生命周期/bundle 分发。
TestClient 最小 app 风格(同 test_ext_market_level): 不起完整后端,
app.state.repo 只提供 store.data_dir。
+7 -7
View File
@@ -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 |
| 声明式分析页面(零代码) | 扩展数据 + 模板化列表/分组页 | 设置 → 扩展页面 |
+3 -3
View File
@@ -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' },
],
}
+2 -2
View File
@@ -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 = {
+2 -2
View File
@@ -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 全局
+2 -2
View File
@@ -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)。
+1 -1
View File
@@ -42,7 +42,7 @@ describe('packToExtension: 契约校验与转换', () => {
})
describe('loadRuntimePageExtensions', () => {
it('只加载已启用页面包, loader 键带 runtime: 前缀且完成契约转换', async () => {
it('只加载已启用页面插件, loader 键带 runtime: 前缀且完成契约转换', async () => {
const rec = recorder()
await loadRuntimePageExtensions({
fetchList: async () => [
+9 -9
View File
@@ -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 {
+2 -2
View File
@@ -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(
+1 -1
View File
@@ -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 },
+9 -9
View File
@@ -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="删除"
>