# 曦寒基础应用 · 前端手册 共 11 页。索引见 https://basicapp.docs.xihanfun.com/llms.txt --- 来源:https://basicapp.docs.xihanfun.com/frontend/components # 常用组件 除 [Schema 页面引擎](./schema-page) 之外的公共组件与全局挂件,按用途分组速查。组件源码在 `packages/components/`、`packages/layouts/` 与 `packages/iconify/`。 ## 编辑器 | 组件 | 底层 | 用途 | | --- | --- | --- | | `RichTextEditor.vue` | Tiptap | 富文本。含 Link / Image / Highlight / TextAlign / Underline 等扩展 | | `MdEditor.vue` | md-editor-v3 | Markdown | | `JsonEditor.vue` | vue3-ts-jsoneditor | JSON 编辑与查看 | 通知公告正文用 Markdown 编辑器(文件库的 Markdown 预览也复用它);富文本与 JSON 编辑器目前只在编辑器演示页 `packages/views/_core/editor-demo/` 使用。三者都以 `defineAsyncComponent` 懒加载,不进主依赖图。 ## 表单增强 | 组件 | 用途 | | --- | --- | | `CronExpression.vue` | Cron 表达式:输入框 + 可视化弹窗,不必手写表达式。[任务调度](../backend/scheduling)页在用 | | `XEditModal` + `xh-edit-form-grid` | **统一的新增/编辑弹窗外壳**:两列布局、10px 间距、11px 标签、无反馈占位,跨列用 `xh-span-2` | | `IconPicker` | 图标选择器,可按需懒加载图标集 | | `XLogoUpload`(`src/components/LogoUpload.vue`) / `XUserAvatar` | 图片上传与头像展示(内置 URL 解析与首字母兜底) | ::: danger 新增/编辑弹窗一律用 `XEditModal` 全仓已统一,**不要再用 `XhDialog*` 部件手拼外壳**。统一外壳保证了弹窗尺寸、间距、标签宽度、按钮位置在所有页面一致,也让后续调整只改一处。 ::: ## 消息中心 UI 三个层次(`~/layouts/basic/` 与 `src/views/message/`): | 层 | 组件 | 行为 | | --- | --- | --- | | **顶部横幅** | `NotificationBanner.vue` + `use-banner-notices.ts` | 数据由服务端按有效期、角色/部门定向过滤后下发;按严重度 + 优先级取前 3 条轮播(5s,悬停暂停)。关闭记忆按公告 id 存 localStorage(30 天清理),后台重发即新 id、自然重现 | | **强制阅读 + 登录弹窗** | `NotificationGate.vue` | 未读必读公告以遮罩拦截、逐条「我已阅读」(最高优先级);清空后再逐条弹出普通登录后公告 | | **通知中心页** | `src/views/message/notification/` | 发布、定向、统计等运营闭环 | 业务设计见 [后端手册 · 消息通知](../backend/messaging)。 ## 全局挂件 挂在 `App.vue` 上、全应用生效: | 组件 | 说明 | | --- | --- | | `LockScreen.vue` | 锁屏遮罩。后端返回 **`423`** 时拉起——身份仍有效,解锁而非重新登录 | | `AppWatermark.vue` | 水印 | | `DynamicIsland.vue` | 灵动岛:全局操作反馈(进行中/成功/失败、进度环、岛内按钮、服务端任务进度) | 全局搜索(`AppGlobalSearch.vue`)挂在布局顶栏工具条上,`Ctrl/Cmd + K` 唤起。 ## 文件与图片 | 工具 | 说明 | | --- | --- | | `useAvatarUrl` | 头像 URL 解析(fileId → 预签名 URL,直链原样;空值/失败返回空串,由 `XUserAvatar` 首字母兜底) | | `toAbsoluteFileUrl` | 文件 URL 绝对化 | | `SchemaImportDialog` | CSV 导入对话框:模板下载 → 解析 → 预校验 → 逐行创建 | ::: warning 本地存储的 URL 是根相对路径 本地存储返回 `/uploads/...`。前后端同源没问题,**线上不同源时必须拼上 `VITE_API_BASE_URL` 的 origin**,否则浏览器拿前端域名去请求而 404。上面两个工具已处理,自己拼 URL 时别忘了。 ::: ## 图编辑 `packages/diagram` 基于 AntV X6 封装通用图编辑能力(工作流设计器在用)。应用侧只依赖本包导出的 `XDiagram` / `DiagramApi` / `DiagramData` / `registerVueShape`,不直接依赖 `@antv/x6`。 ## 布局 `packages/layouts/` 提供整套布局外壳:侧边栏、顶栏、标签页、内容区、页脚。 ::: tip 整屏页用 `h-full` 或 `XPageShell` 布局根是定高的、内容容器内部滚动(滚动源不是 `window`)。需要吸顶/吸底的整屏页直接用 `h-full` 或 `XPageShell` 即可,**不要再写 per-page 的 JS 定高 hack**。 ::: ## 相关页面 - [Schema 驱动页面](./schema-page):列表页引擎 - [布局与主题](./theme):布局配置与主题定制 - [实时通信](./realtime):消息中心的推送通道 - [字体图标](./icon):图标用法与离线模式限制 --- 来源:https://basicapp.docs.xihanfun.com/frontend/development # 开发流程 从零加一个前端页面的完整流程。**前端页面的事实源在后端**——菜单、路由、组件路径、权限码、i18n 键都在后端 `PageRegistry` 登记,前端只补落盘文件。所以流程是「后端先行、前端跟进」。 ## 五步 ```text ① 后端 PageRegistry 登记页面与按钮(建菜单即绑权限) ② 后端加分页/详情/写入接口(分页记得标 [HttpPost]) ③ 前端加 API 模块(src/api/modules/{域}/) ④ 前端加视图文件(src/views/{域}/{页}/index.vue) ⑤ 补 i18n 文案(menu.ts + 模块文案) ``` 前两步属于后端,见 [后端手册 · 二次开发](../backend/development)。下面只讲前端三步。 ## ③ API 模块 按域建两个文件: ```text src/api/modules/organization/ ├── position.ts # API 定义 └── position.types.ts # DTO 类型 ``` 标准 CRUD 用资源工厂: ```ts import { defineResource } from '../../factory' export const positionApi = defineResource< PositionListItemDto, PositionDetailDto, PositionCreateDto, PositionUpdateDto, PositionPageQueryDto >({ query: 'PositionQuery', command: 'Position', }) ``` 有自定义动作时改用低层客户端逐个拼(`position.ts` 就是这种写法): ```ts // position.ts import { createDynamicApiClient, createReadApi } from '../../base' const positionQueryApi = createDynamicApiClient('PositionQuery') const positionCommandApi = createDynamicApiClient('Position') const positionReadApi = createReadApi( 'PositionQuery', 'Position', ) export const positionApi = { detail: (id: ApiId) => positionReadApi.detail(id), page: (input: PositionPageQueryDto) => positionQueryApi.post('PositionPage', input), updateStatus: (input: PositionStatusUpdateDto) => positionCommandApi.put('PositionStatus', input), } ``` 别忘了在该域的 `index.ts` 里导出。动作名推导规则与分页协议见 [服务端交互](./request)。 ## ④ 视图文件 **路径由后端 `PageDescriptor.Component` 决定**,约定是 `Path` 去前导斜杠 + `/index`: | 后端 `Path` | 后端 `Component` | 前端文件 | | --- | --- | --- | | `/identity/position` | `identity/position/index` | `src/views/identity/position/index.vue` | 列表页直接照 [Schema 驱动页面](./schema-page) 的骨架写,三段:字段声明 → 查询构建 → 页面 Schema。 ::: warning `_core` 页面要额外登记 不落在 `src/views` 的页面(个人中心、关于页等,源码在 `packages/views/_core/`),`Component` 写 `_core/xxx/index`,**必须同时在 `packages/router/dynamic.ts` 的 `coreComponentMap` 登记**,否则菜单有、点进去 404。 ::: ## ⑤ i18n 文案 两处: | 文件 | 键 | 说明 | | --- | --- | --- | | `packages/locales/langs/{zh-CN,en-US}/menu.ts` | `identity_position` | 菜单标题。键 = 后端 `I18nKey` 去掉 `menu.` 前缀 | | `src/locales/langs/{zh-CN,en-US}/{模块}.ts` | `identity.position.position_name` 等 | 页面内文案,约定 `模块.实体.字段或动作`。新增模块文件要在同级 `zh-CN.ts` / `en-US.ts` 里 import 并导出 | ::: danger 裸 `@` 会白屏 文案里出现裸 `@`(如 `联系 @admin`)会触发 vue-i18n 的 linked message 语法而抛错、整页白屏。必须转义成 `{'@'}`。见 [国际化](./i18n)。 ::: ## 自检清单 写完对照一遍,漏哪项就是哪种表现: | 项 | 漏了会怎样 | | --- | --- | | 后端 `PageRegistry` 登记 | 侧边栏没有这个菜单 | | 后端权限定义落库 | 菜单种子 fail-closed 跳过,菜单还是不出现 | | 后端分页方法标 `[HttpPost]` | 前端发的 body 后端收不到 | | 视图文件路径与 `Component` 一致 | 菜单有、点进去 404 | | `_core` 页面登记 `coreComponentMap` | 同上 | | `menu.ts` 文案 | 侧边栏显示 `menu.xxx` 原文 | | 字段 `permission` / `exportPermission` | 无权用户也看得到列/按钮(**服务端仍会拦,但体验差**) | ## 质量门禁 ```bash pnpm type-check ``` ```bash npx eslint src/views/identity/position/index.vue --fix ``` ::: warning 别跑全仓 lint `pnpm run lint:fix` 展开是 `cross-env CI=true oxlint --fix && cross-env CI=true eslint . --fix`,会扫全仓并改动与你无关的文件。**只对本次改动的文件跑**;误跑了用 `git checkout HEAD -- <非本次任务的文件>` 还原。 ::: 前端改动由开发者自行在浏览器验证,不走自动化预览。 ## 相关页面 - [后端手册 · 二次开发](../backend/development):后端纵切片的完整接线清单 - [路由与菜单](./routing):路由怎么从后端菜单生成 - [Schema 驱动页面](./schema-page):列表页开发手册 - [目录结构与代码地图](../project-structure#前端):我要改 X 去哪个文件 --- 来源:https://basicapp.docs.xihanfun.com/frontend/i18n # 国际化 前端文案用 vue-i18n,**枚举标签的事实源在后端**,时间显示由请求头驱动。三块各有各的规则。 ## 语言包 vue-i18n(`legacy: false`),中英双包按模块拆文件,分住两处:`packages/locales/` 只放 shell 自身的命名空间,业务命名空间在 `src/locales/`,启动时由 `registerLocaleMessages()` 合并进同一个 i18n 实例(`setupI18n()` 之后、`app.mount()` 之前调用)。 ```text packages/locales/langs/zh-CN/ # shell 命名空间 ├── common.ts # 通用(按钮、字段、提示) ├── component.ts # 公共组件文案 ├── menu.ts # 菜单标题 ├── header.ts # 顶栏 ├── page.ts # 页面壳 └── … src/locales/langs/zh-CN/ # 业务命名空间 ├── identity.ts # 身份权限模块 ├── log.ts # 日志模块 ├── message.ts # 消息模块 └── … ``` 键名约定 **`模块.实体.字段或动作`**: ```ts // src/locales/langs/zh-CN/identity.ts export default { user: { page_name: '用户管理', col_status: '状态', action_reset_password: '重置密码', }, } // 使用:t('identity.user.col_status') ``` 切换语言:`useLocale().setLocale('en-US')`。 ## 菜单文案的键怎么来 后端 `PageDescriptor.I18nKey` 规则是 **`menu.{页面码中的 . 与 - 换成 _}`**: | 页面码 | 后端 `I18nKey` | `menu.ts` 里的键 | | --- | --- | --- | | `identity.position` | `menu.identity_position` | `identity_position` | | `log.permission-change` | `menu.log_permission_change` | `log_permission_change` | 缺文案时侧边栏会**直接显示键名**(如 `menu.identity_position`)——看到这个就是漏了这一步。 ## 组件库内置文案 日期选择器的星期、分页与下拉的读屏名这类 XiHan.UI 内置文案随应用 locale 切换:`App.vue` 里 `provideXhConfig(useXhUiConfig())` 把语言标记与内建文案交给组件库。文案直接取组件库的内建语言包(`@xihan-ui/vue/locale`),应用语言与语言包的对应表在 `packages/locales/xihan-ui.ts` 的 `xhLocales`;组件库没有语言包的应用语言(目前是 `hi-IN`)取英文语言包,语言标记仍是应用语言,日期时间照常按该语言排。 要带业务语义的个别文案(如穿梭框箭头钮要念出目标栏标题)在组件实例上经 `translations` 覆盖,不改全局表。 加新语言时**别忘了在 `xhLocales` 登记对应的语言包**,否则业务文案切了、组件文案还停在英文;`xh-config.test.ts` 会把漏登记的语言报出来。 ## 裸 `@` 会白屏 ::: danger 语言包文案里出现**裸 `@`**(如 `联系 @admin`)会触发 vue-i18n 的 linked message 语法,抛 `Invalid linked format` 导致**整页白屏**。 必须转义成 `{'@'}`: ```ts // ❌ 白屏 contact: '联系 @admin' // ✅ contact: "联系 {'@'}admin" ``` 邮箱、社交账号、装饰性符号都是高发区。新增文案前扫一遍。 ::: ## 枚举标签:后端单一事实源 枚举标签**不在前端语言包里维护**——事实源是后端枚举元数据(`Enums.{culture}.json` 全量),按请求头 `X-Language` 返回当前语言的标签。 前端有三条取值路径: | 场景 | 用法 | | --- | --- | | **Schema 页字段** | 字段声明 `dictionaryCode`(枚举名或字典码),`useSchemaDictionaries` 批量拉取注入 `field.options`;单元格按值映射 label,搜索区自动渲染下拉 | | **非 Schema 的下拉/标签** | `useEnumOptions(enumName, fallback)`(`~/hooks`),返回随语言/数据响应式更新的 `computed` | | **静态兜底** | 元数据为空(未加载/未部署)时回退传入的 `fallback` 静态选项,**保证绝不出现空下拉** | ### 切语言要响应式 拉取由全局 `useEnumService` **并发去重**,切 locale 时整库重取一次。`useSchemaDictionaries` / `useEnumOptions` 只读响应式状态——**免刷新即随语言更新**,各下拉不各自监听 locale,避免重复请求。 ::: warning 两个易错点 1. **`business.ts` 里的 `*_OPTIONS` 常量是写死中文的,仅作兜底**,不要当成事实源使用。 2. **选项的 `value` 要用枚举成员名,不是整数**。后端实例数据经 `JsonStringEnumConverter` 序列化为成员名字符串(如 `"Enabled"`),只有用成员名才能与表格行数据匹配;整数值放在 `valueText` 备用。 ::: ## 时区 - 前端在请求拦截器里发 **`X-Timezone`** 头(用户已选时区优先,否则跟随浏览器 `Intl`)。 - 后端**存储恒 UTC**,输出时按该头换算。 - 用户可在偏好里选时区。 「时间显示差几小时」的排查顺序:请求有没有带这个头 → 用户选的时区对不对 → 后端存的是不是 UTC。 ## 加一门新语言 | 步骤 | 做什么 | | --- | --- | | 1 | `packages/locales/langs/` 与 `src/locales/langs/` 下各加一套语言目录,按模块补齐文案;插件模块在 `src/modules/<模块>/locales/.ts` 各补一份 | | 2 | 在 `packages/constants/app.ts` 的 `SUPPORTED_LOCALES`、`packages/locales/index.ts` 的 `messages` 与 `src/locales/index.ts` 里登记新语言,并在 `LocaleSwitcher.vue` 的 `LOCALES`、`LocaleFlag.vue` 的国旗、`packages/locales/xihan-ui.ts` 的 `xhLocales`(组件库有该语言包时)各加一项 | | 3 | **后端**补一份 `Enums.{culture}.json` 与业务资源文件(`Localization/ApiResponse|Enums|Errors.{culture}.json`) | | 4 | **后端每个环境**的 `XiHan:Localization:SupportedCultures` 加上该文化——文化中间件只接受列表内的 `X-Language`,不在列内一律回退默认文化;`appsettings.Production.json` 不入库,要在服务器上单独改 | 第 3、4 步最容易漏——前端切了语言,接口返回的 `message` 和枚举标签却没变,要么是后端资源没补,要么是生产环境的 `SupportedCultures` 没登记(de-DE 上线时就漏过一次)。 ## 相关页面 - [布局与主题](./theme):外观定制 - [Schema 驱动页面](./schema-page#字段-schema-listfieldschema):`dictionaryCode` 用法 - [接口对接指南](../api-guide#json-序列化约定):枚举与时间的序列化规则 - [常见问题](../faq):白屏排查 --- 来源:https://basicapp.docs.xihanfun.com/frontend/icon # 字体图标 图标走 Iconify,**运行在离线模式**——这带来一条必须记住的限制。 ## 用法 ```vue ``` `Icon` 不是全局组件,用前须从 `~/iconify`(或转出它的 `~/components`)导入。 图标名格式是 `{集合}:{图标}`,可在 [Iconify 官网](https://icon-sets.iconify.design/) 搜索。 菜单图标在**后端** `PageRegistry` 的 `PageDescriptor.Icon` 字段里写同样的格式: ```csharp new("identity.position", "岗位管理", "menu.identity_position", MenuType.Menu, "/identity/position", "IdentityPosition", "identity/position/index", "identity", SaasPermissionCodes.Position.Read, "lucide:briefcase", 135), // ↑ 图标 ``` ## 离线模式的限制 ::: danger 只有预加载的图标集能直接渲染 配置在 `~/iconify/offline.ts`。运行期直接渲染**只保证这四个已预加载集**: - **`lucide`** —— 主力图标集,优先用它 - **`tabler`** - **`mdi`** - **`simple-icons`** —— 第三方品牌 logo(如 Gitee 用 `simple-icons:gitee`) 未预加载的集(`carbon` / `ep` / `heroicons` 等)在页面里直接用会**渲染为空白**,且不报错——因为离线 `Icon` 对已挂载组件不会因后加载而重渲染。 以仓库的 `PRELOAD_ICON_PACKAGES` 配置为准。 ::: 品牌图标优先用 `simple-icons:*` 或 `tabler:brand-*`。 ## `IconPicker` 是例外 图标选择器组件可以**按需懒加载** carbon / ep / heroicons 供用户挑选——那是「用户在弹窗里选图标」的场景,与「页面里直接渲染」不同,不受上面限制。 但要注意:**用户选了未预加载集的图标存进菜单后,页面渲染时仍然会空白**。如果允许用户自选图标,要么把可选集限制在预加载的四个里,要么把用户可能选的集也加入预加载。 ## 为什么用离线模式 在线模式下 Iconify 会在运行时向 CDN 请求图标数据。离线模式把图标数据打进产物,代价是体积和「只能用预加载集」,换来的是: - **内网/断网环境可用**——这是企业中后台的常见部署场景; - 不依赖第三方 CDN 可用性; - 首屏图标不闪烁。 ## 加一个图标集 1. 确认对应的 `@iconify-json/{集合}` 依赖已安装; 2. 在 `~/iconify/offline.ts` 的 `ICON_SET_META` 与 `PACKAGE_LOADERS` 里登记该集(carbon / ep / heroicons 已登记,可跳过); 3. 把该集加入同文件的 `PRELOAD_ICON_PACKAGES`; 4. 重新构建。 ::: warning 加集会增大产物 每个图标集都是完整数据包。只在确实需要时加,优先从已有四个集里找替代图标。 ::: ## 排查 | 现象 | 原因 | | --- | --- | | 图标位置空白 | 用了未预加载的图标集 | | 菜单图标空白 | 后端 `Icon` 字段写了未预加载集的图标名 | | 图标名写对了还是不显示 | 图标名拼错(大小写、连字符);去 Iconify 官网核对 | | `IconPicker` 里能看到、放到页面就没了 | 正是上面那条限制——选择器懒加载,页面渲染不懒加载 | ## 相关页面 - [布局与主题](./theme):外观定制 - [路由与菜单](./routing):菜单图标从哪来 - [常用组件](./components):`IconPicker` 等公共组件 --- 来源:https://basicapp.docs.xihanfun.com/frontend/introduction # 前端架构 前端不是「一个 Vue 工程」,而是**分层的单仓库多包结构**。本页讲分层与依赖方向、启动引导链路、请求链路的完整时序,以及第三方依赖的归属规则。具体怎么写页面见 [开发流程](./development)。 ## 技术栈 | 领域 | 选型 | | --- | --- | | 框架 | Vue(` ``` 三条要点: 1. **操作不在 Schema 里实现逻辑**,只声明 `key` / `scope` / `icon` / `permission`;点击时经 `@action` 上抛 `{ key, scope, row?, rows? }`,由页面处理。 2. `resource.page` 收到框架**归一化**的 `SchemaQueryParams`(`page` / `pageSize` / `sorts` / `filters` / `conditionFilters`),映射成后端 DTO 的适配器**是页面自己写的**——框架只依赖归一化契约,不关心你的 DTO 长什么样。 3. **导出复用同一个 `buildQuery`**,保证导出内容与当前筛选一致。 ### 插槽 | 插槽 | 用途 | | --- | --- | | 默认 | 页面自有弹窗/抽屉 | | `#toolbar` | 追加工具栏项 | | `#expand` | 行展开内容(如任务调度展开触发器信息) | ## 多字段排序 排序**前端驱动、后端应用**: - 列头点击累加为 `sorts: SchemaSortRule[]`,**数组顺序即优先级**(下标 0 为主排序)。 - `querySortsFromSchema(params.sorts)` 转成后端 `conditions.sorts`,每条带 `priority = 下标`。 - 后端 `ApplySorts` 应用排序,并受 **FLS 门控**:只有「可读且未脱敏」的字段才允许作为排序键,其余被静默剔除;剔完没有有效排序时回退该接口的默认排序。 - 列设置里可为每列设默认排序(单图标循环 无→升→降),优先级按列顺序。树表/子表不参与。 ::: warning 点了排序没反应 先查该字段的字段级权限与脱敏规则——被 FLS 剔掉时后端不会报错,只是排序不生效。见 [权限与脱敏](./permission)。 ::: ## 搜索:区间与多选 `searchRange` / `searchMultiple` 字段的当前值由框架的 `queryFiltersFromSchema` 派生成后端通用过滤 `conditions.filters`: | 声明 | 下发的操作符 | 细节 | | --- | --- | --- | | `searchRange` | `Between`(`4000`) | 值为 ISO 字符串;`date` 粒度末值补到当天 `23:59:59.999` 以含整日 | | `searchMultiple` | `In`(`3000`) | 值走 `values` 数组 | 这些统一放进 `params.conditionFilters`,页面适配器并入 `conditions.filters` 即可。其余普通搜索字段仍由页面的 `buildXxxQuery` 映射为 DTO 顶层字段。 相关封装组件:`SchemaSearchField` / `SchemaSearchDateRange` / `SchemaSearchMultiSelect`。 ## 树形模式 `PageSchema.tree` 存在即启用:走 `resource.tree`、**不分页**、按 `childrenKey` 展开。字段里要指定**有且仅有一个** `treeColumn`(承载展开箭头的列)。 组织机构、菜单管理这类天然层级的页面用它。 ## 导入导出 ### 导出 ```ts resource: { export: { businessType: 'log.access', buildQuery: buildAccessQuery }, } ``` 声明后走[导出中心](../backend/file#导出中心-file-export-center)的异步链路(提交任务 → 队列 → 后台生成 → 下载);未声明则退化为本地 CSV。 **按钮显隐由 `exportPermission` 精准门控**——未声明该字段的页面不显示导出按钮。 ### 导入 `SchemaImportDialog` 内置:模板下载 → 选文件 → CSV 解析 → 预校验 → 批量创建。模板字段由 `importable: true` 的字段派生,**不需要单独维护模板文件**。 ## 偏好与视图 | 能力 | 说明 | | --- | --- | | **列设置 / 搜索设置** | 按 `pageCode` 同步后端(`useUserSettingSync`),多端一致 | | **个人视图 / 搜索方案** | 保存当前筛选 + 排序 + 每页数量为命名方案(`useViewManager`);无内置 UI,经模板 ref 调用 `saveView` / `applyView` | 同步策略:**localStorage 仍是事实源**,后端加载成功则覆盖本地,保存失败静默忽略(尽力而为)。其它设备保存后经 SignalR `UserSettingChanged` 实时推送并应用到已打开的页面。 ::: tip `pageCode` 要稳定 偏好、视图、列设置与导入历史全按 `pageCode` 存储。**只改 `pageCode` 等于用户的所有个性化配置丢失**,页面上线后不要再动它;确需改名时,随升级脚本把 `sys_user_setting`(`scene = 1`)与 `sys_import_history` 里的旧码改成新码(参考 `UpdateScripts/5.4.0/5.4.0-page-codes.sql`)。 ::: ## 加一个列表页的清单 | 步骤 | 做什么 | | --- | --- | | 1 | 后端 `PageRegistry` 登记页面与按钮(**建菜单即绑权限**) | | 2 | 后端加分页查询方法,**记得标 `[HttpPost]`** | | 3 | 前端 `src/api/modules/{域}/xxx.ts` 加 API(标准 CRUD 直接用 `defineResource`) | | 4 | 前端 `src/views/{域}/{页}/index.vue` 按上面的骨架写 | | 5 | `packages/locales/langs/{zh-CN,en-US}/` 补文案 | 后端侧的完整接线见 [二次开发](../backend/development)。 ## 相关页面 - [权限与脱敏](./permission):字段级权限与 FLS 如何影响渲染 - [路由与菜单](./routing):页面怎么被路由到 - [主题与国际化](./theme):枚举标签、文案、时区 - [接口对接指南](../api-guide#分页与查询协议):`conditions` / `page` 的完整协议 --- 来源:https://basicapp.docs.xihanfun.com/frontend/theme # 布局与主题 外观相关的一切:布局结构、Tailwind CSS 的用法与陷阱、主题系统、偏好同步。 ## 布局 `packages/layouts/` 提供整套外壳:侧边栏、顶栏、标签页(tabbar)、内容区、页脚。 ### 滚动模型 ::: warning 滚动源不是 `window` 布局根是**定高**的(`h-full`),滚动发生在**内容容器内部**。这意味着: - 监听 `window` 的 scroll 事件拿不到滚动; - 需要吸顶/吸底的整屏页直接用 `h-full` 或 `XPageShell` 即可; - **不要写 per-page 的 JS 定高 hack**。 回到顶部、路由切换重置滚动都已由布局适配器统一处理。 ::: ## 样式:Tailwind CSS(CSS-first) 用 `@tailwindcss/vite` + **CSS-first `@theme`**(入口 `src/styles/index.css`),**没有 JS config 文件**。 三条关键约定: | 约定 | 说明 | | --- | --- | | **只引入 theme + utilities,不引入 preflight** | 基础重置由 XiHan.UI 的 `xihan.reset` 层与 `design/global.css` 的最小重置负责,避免两套重置互相打架 | | 颜色令牌用运行时 HSL CSS 变量 | `--color-primary: hsl(var(--primary))` 等,保证明暗与主题色能动态切换 | | 暗色走 class 策略 | `@custom-variant dark (&:where(.dark, .dark *))` | ### 嵌套 `.dark` 的坑 ::: danger 深色侧栏里 `bg-*` / `text-*` 不变暗 `@theme` 里声明的 `--color-*` **只在 `:root` 解析**。深色侧栏、深色子栏、深色顶栏这类**嵌套 `.dark`** 的容器里,Tailwind 的颜色工具类拿不到暗色值,表现是「局部深色区域里的元素还是亮色」。 修法:在 `variables.css` 的 **`:root, .dark`** 块里**重新声明全部 `--color-*`**,让嵌套作用域也能解析到。 ::: ## 主题系统 `useTheme()`(`~/hooks/useTheme.ts`)管理外观: | 维度 | 可调 | | --- | --- | | 明暗 | 亮 / 暗 / 跟随系统 | | 主题色 | 任意品牌色 | | 形状 | 圆角 | | 排版 | 字号 | ### Material You 动态取色 **从单个品牌色派生整套和谐色阶**——辅色、容器色、前景色、聚焦环、带品牌色相的中性色,明暗自适应,以内联 CSS 变量写到根元素。 所以**换主题色不需要改任何 CSS**:改一个色值,整套配色跟着重算。 主色、明暗、圆角、字号变化都会同步到 CSS 变量,页面里的自定义元素也能直接用这些变量。 ### 切换动画 `startViewTransition` 从点击处圆形扩散——明暗切换时的视觉过渡。 ## 偏好同步 UI 偏好走 `UserSettingAppService`,按**场景 + key** 存取: ```text localStorage(事实源) ↕ 后端 SysUserSetting(用户 × 场景 × 设置键) ↕ SignalR UserSettingChanged 其它设备已打开的页面即时应用 ``` 三条策略: 1. **localStorage 是事实源**——后端加载成功则覆盖本地,保存失败静默忽略(尽力而为,不阻断交互)。 2. **服务端不解释 value**,只作跨端状态载体。 3. 其它设备保存后经 SignalR 广播实时应用。 Schema 页的列设置、搜索设置、个人视图按 `pageCode` 走同一套机制,见 [Schema 驱动页面](./schema-page#偏好与视图)。 ::: tip `pageCode` 要稳定 偏好按 `pageCode` 存储,**只改它等于用户的所有个性化配置丢失**。页面上线后不要再动它;确需改名见 [Schema 驱动页面](./schema-page#偏好与视图)。 ::: ## 排查 | 现象 | 原因 | | --- | --- | | 深色区域里的元素还是亮色 | 嵌套 `.dark` 令牌问题,见上面的 danger 块 | | 改了主题色但某些地方没变 | 那些地方写死了颜色值,没用 CSS 变量 | | 页面滚不动 / 吸顶失效 | 用了 `window` 滚动的假设;改用 `h-full` 或 `XPageShell` | | 打开 preflight 后组件样式错乱 | preflight 是**故意关闭**的,别去打开——基础重置由 XiHan.UI 的 `xihan.reset` 层负责 | | 偏好换台设备就没了 | 后端保存失败被静默忽略了,查接口是否可用 | ## 相关页面 - [国际化](./i18n):文案与枚举标签 - [字体图标](./icon):图标用法与离线限制 - [常用组件](./components):布局相关组件 - [框架简介](./introduction):前端分层与启动引导