前端规范(Vue 3 + Naive UI)
写页面、调接口前对着这份清单核一遍。栈是 <script setup> + Naive UI + Pinia(持久化)+ vue-router + vue-i18n + VueUse。应用里路径别名 @ → src,内核的组件、composables、stores、API 原语一律 import { … } from 'smart-admin-web';内核包内部用 #/ → src。整体架构翻 核心概念;组件怎么用、设计系统长什么样,去仓库的 web/COMPONENTS.md 和 web/DESIGN.md 找。
目录落点
- 页面按模块/实体分:
views/<模块>/<实体>/index.vue,应用里就是src/views/,由createSmartAdmin({ views })收进页面表;完整 CRUD 范例照内核包的views/system/menu/index.vue(SmartTable+FormContainer弹窗表单 +useConfirm二次确认)。 composables/(use*)放逻辑单源,默认与 UI 库解耦,错误与消息回调由视图注入。确需 Naive Provider 的交互样板是明确例外:useConfirm内部直接用useDialog/useMessage,useTheme直接用darkTheme,这类只能在 setup 里调用。- 应用的
api/:client.ts(createApiClient<paths>())+ 按域分的<域>.ts+ 生成的schema.d.ts;内核包的api/是client.ts+index.ts(内置端点按域分组)+schema.d.ts。其余目录职责见 项目结构。
API 契约
schema.d.ts 是生成产物,禁手改
schema.d.ts 由后端 OpenAPI 生成,命令是 npm run gen:api。生成时后端必须正在运行,不然拉不到 /openapi/v1.json。手改它,下次一生成就被覆盖。要调类型,只能改后端接口或 DTO,再重新生成。生产环境为什么不挂这个端点,见 常见问题。
- API 调用集中在
api/层按域分组(内置的在内核包api/index.ts:authApi/userApi/moduleApi/menuApi…;你自己的模块新建src/api/<域>.ts,client用应用的./client,unwrap/pageParams/toPage从smart-admin-web导入),每个方法形如client.X(...).then(r => unwrap<T>(r)),不在视图里裸调client。 unwrap统一解信封,失败(code≠0或非 2xx)都归一到ApiError(带code/msgKey);视图catch后用translateError(e)出文案。- 分页在 api 层归一为
{ items, total }以适配 SmartTable 的fetcher(后端是PagedList<T>{current,size,total,items})。 - 查询参数名用 PascalCase(ASP.NET 模型绑定要求)。
- 只有前后端真不同源(CDN / 独立域名)才构建期给
VITE_API_BASE,模板的main.ts把它作为apiBase交给内核,且后端要显式配SmartAdmin:Api:Cors:AllowedOrigins(默认 deny-all)。鉴权与 401 刷新中间件在 HTTP 请求层,解信封细节在 对接后端响应。
路由
router/routes.ts只放静态路由(login、error、shell/layout);真实菜单树登录后从后端拉取,注入为动态路由(只活内存,不落盘)。- 菜单节点的
component串(如system/user/index)是页面表的键,也就是views/之后去掉.vue的路径;应用src/views/下同名的页面覆盖内置页。路由name = menu-${id},挂在layout下。 - 登出 / 切应用用
registerDynamic/resetRouter精确增删动态路由,不整体重置整棵路由树。 - 外链菜单(
path填 URL、component留空)与内嵌 iframe 菜单(component填 URL)复用现有字段,不新增菜单类型;views/**/detail.vue是约定式详情路由(/<模块>/:id/detail),配DetailPage组件与useTabTitle()。两条约定的机制见 路由与动态菜单。
不要持久化 routesReady / menuTree
持久化会跳过刷新重建流程,刷新后直接导向 404。这两个状态必须只活在内存里。重建机制见 路由与动态菜单。
状态(Pinia)
defineStore+actions;持久化按需pick,别把整个 store 无脑全量存:auth只存currentModuleId,tabs只存tabs且落 sessionStorage。user的令牌和用户信息少一个刷新就掉登录,走一份自定义serializer整份落localStorage;Cookie 会话模式下令牌字段序列化时强制清空,只留cookieSession标记。- 现有 store:
auth(模块/菜单/权限码/routesReady)、user(令牌/登录态)、app(主题/偏好)、tabs(标签页)、dict(字典缓存,会话级内存、不持久化,增删改后invalidate失效)。登出走reset()清授权态并清标签。
组合式函数
use*命名,返回响应式引用与方法。- 列表页统一用
smart-naive-table的SmartTable远程模式:传:fetcher,签名是(p: { page, pageSize, ...params }) => Promise<{ items, total }>,分页与 loading 由 SmartTable 自己管。 - 已有
useConfirm(二次确认)、useTabTitle(详情页动态标签标题)、useRealtime(SignalR 实时推送客户端,鉴权外壳挂载时start)等,用法见各自源码头注释与web/COMPONENTS.md。
按钮级权限
vue
<n-button v-auth="'POST:/api/v1/sys/user'">新增</n-button>- 单权限码传字符串;数组默认 OR,
.and修饰符做 AND;不命中靠display: none隐藏(响应式,权限刷新后自动跟着显隐),元素本身还在 DOM 里。 - 权限码取值就是后端的规范化路由(与
[RolePermission]同源),不自造权限字符串。更细的取值规则在 前端权限。
共享组件
- 后台不设组件演示菜单,组件用法统一沉在
web/COMPONENTS.md;写页面前先看一遍避免重复造轮子,加了新的通用组件也同步更新它。 - 已有 SmartTable / FormContainer /
useConfirm/ StatusSwitch / 字典组件(DictSelect、DictTag)/ OrgTreeSelect / FileUpload(chunked走分片续传)/ ApiSelect(派生 UserSelect)/ UserPicker / PasswordStrength / Chart / CodeBlock / MarkdownEditor / DetailPage(详情页外壳,配useTabTitle)/ IconPicker 等,完整清单以web/COMPONENTS.md为准,每个组件的详细 API 见内核包components/<组件>/README.md。SmartTable 从smart-naive-table导入,其余从smart-admin-web导入。
i18n
- 视图内所有可见文本走
t('...'),禁止硬编码中文 / 英文字面量。 - 错误文案不出后端:后端给
code+msgKey,translateError优先按msgKey取字,没有msgKey才退到内置的数字码白名单(utils/error.ts的CODE_MSG_KEY,只覆盖内核自己的码)兜底。所以 locale 里的键必须和后端[MsgKey]字符串逐字对上,比如后端标error.dict.typeNotFound,前端词典里就要有同名的键;自定义错误码不在那份白名单里,写成error: { 60001: '...' }这样的数字键没人读,一样要走[MsgKey]。内置文案在内核包的locales/zh-CN.ts/en-US.ts;你自己的放应用的src/locales/ext/<locale>/<模块>.ts,经createSmartAdmin({ locales })并入。机制见 国际化。
设计系统
- 业务代码只消费角色令牌层(如
--color-text-primary),不直接引原语层(如--color-gray-500);tokens 单源是内核包的styles/tokens.css,随smart-admin-web/style.css发给应用。 - 组件样式用
scoped+ CSS 变量(var(--gap-card)等),不写死颜色 / 间距。 - 明暗切换靠
<html data-theme="dark">,不打即亮色;角色令牌 / 主色 / 语义色 / 阴影在其下整体翻转。完整规范在web/DESIGN.md与 主题与图标。
提交前
bash
npm run lint # oxlint(lint:fix 自动修)
npm run typecheck # vue-tsc --noEmit
npm run build # vue-tsc --noEmit && vite build三者都通过才算完成,不要只跑其中一个就认为没问题。模板拉出来的应用没配 lint,另两条照样要过。在内核仓的 web/ 下改动,还要加跑 npm run format:check 和 npm test,CI 的前端检查这几样都跑。