通用组件与 hooks
适用端:Web | 类型:参考
src/components(63 个目录)与 src/hooks(24 个文件)是所有页面的公共底座。本页讲清「组件是怎么变成可以直接写的标签的」、「哪些是本项目定制、哪些是上游遗留」、「新写页面该用哪个」,以及几个已经被证明是死代码的能力——避免照着上游文档去用根本不生效的东西。
前置阅读:前端工程结构(自动导入机制)。
它解决什么问题
关键结论:自动注册的粒度是「.vue 文件名」,不是「目录名」。Components 插件的 glob 是 src/components/**/**.{vue, md},所以 63 个目录里的每一个 .vue 都会成为一个全局标签——包括本来只想内部使用的子组件。实测生成的 src/types/auto-components.d.ts 里有 509 个 GlobalComponents 条目。
代码在哪
| 路径 | 说明 |
|---|---|
src/components/<Name>/index.ts | 目录出口,export { Name } + 导出类型/工具函数 |
src/components/<Name>/src/<Name>.vue | 组件实现(上游约定的一层 src/) |
src/components/index.ts | setupGlobCom(app),被 src/main.ts:72 调用 |
src/hooks/web/*.ts | 23 个业务 hook |
src/hooks/event/useScrollTo.ts | 唯一一个不在 web/ 下的 hook |
src/utils/dict.ts | 字典取值函数 + DICT_TYPE 枚举(实测 215 条) |
src/utils/permission.ts | checkPermi / checkRole,供 JS 逻辑里判权限 |
src/utils/table.ts | indexMethod,跨页连续序号 |
src/utils/download.ts | Blob → 文件下载 |
src/types/auto-components.d.ts | 自动生成的全局组件声明(gitignore,只在 pnpm dev 时产出) |
核心机制
全局组件从哪里来
src/components/index.ts 全文只有这么点东西:
import type { App } from 'vue'
import { Icon } from './Icon'
import { LottieAnimation } from './LottieAnimation'
export const setupGlobCom = (app: App<Element>): void => {
app.component('Icon', Icon)
app.component('LottieAnimation', LottieAnimation)
}其余 507 个全局名都来自 unplugin-vue-components。509 个名字按来源目录的分布(实测 top):
| 来源目录 | 注册的全局名数量 | 性质 |
|---|---|---|
SimpleProcessDesignerV2 | 58 | 上游,流程设计器内部节点 |
bpmnProcessDesigner | 56 | 上游,BPMN 面板内部组件 |
FormCreate | 52 | 上游 + 定制控件混在一起 |
Portal | 28 | ★ 定制,门户设计器/运行时 |
Verifition | 8 | 上游,滑块验证码 |
DiyEditor | 8 | 上游(components/mobile/** 已被 glob 显式排除) |
UploadFile / FormLogic / Cropper | 各 6 | — |
| 其余 50 余个目录 | 各 1–4 | — |
全局命名空间是被污染的,起组件名要避开
流程设计器、表单设计器的内部子组件(CallActivity、ChildProcessNode、AreaSelect、CollapseToggle……)全都成了全局标签。新写组件时如果取了个通用名字(Node、Panel、Item),很可能与已注册名冲突——ElementPlusResolver 与本地扫描的优先级会让结果难以预测。取名前先 grep 一遍 src/types/auto-components.d.ts。
标签写法:PascalCase 与 kebab-case 并存
同一套组件在不同页面里有两种写法,都是有效的(Vue 会自动解析)。实测使用量(src/views + src/layout):
| 组件 | PascalCase | kebab-case | 说明 |
|---|---|---|---|
Icon | 856 | 0 | 全站最常用 |
ContentWrap | 335 | 0 | 页面外层卡片容器 |
Pagination | 133 | 0 | 分页 |
Dialog | 127 | 0 | 弹窗(新增/编辑表单的标准外壳) |
DocAlert | 0 | 106 | 只写 <doc-alert> |
DictTag | 2 | 227 | 只写 <dict-tag>,另有 1 处显式 import |
Echart | 40 | 0 | 图表 |
TableActions | 29 | 0 | ★ 定制,行操作按钮组 |
StatusSwitch | 21 | 0 | ★ 定制,状态开关 |
Editor | 9 | 0 | 富文本 |
CodeEditor | 8 | 0 | ★ 定制,代码编辑器 |
OperateLogV2 | 8 | 0 | 操作日志 |
新写页面统一用 PascalCase,与 ContentWrap / Dialog / Pagination 这批高频组件一致;dict-tag 与 doc-alert 保持现状不要批量改(改动量大、收益为零)。
常用组件选型
| 需求 | 用什么 | 备注 |
|---|---|---|
| 页面外层白卡片 | ContentWrap | title / message(标题右侧问号 tooltip)/ bodyStyle,另有 header 插槽;带自动高度逻辑,见下 |
| 列表分页 | Pagination | :total(必填)+ v-model:page / v-model:limit + @pagination="getList";page-sizes 固定 [20,50,80,100],total 为 0 时整个组件 v-show 隐藏 |
| 新增/编辑弹窗 | Dialog + el-form | 独立成 XxxForm.vue,v-model 控开关;props:title / width(默认 '40%')/ scroll + maxHeight(默认 400px)/ loading / fullscreen(默认 true,指显示全屏切换按钮) |
| 状态字典渲染 | <dict-tag :type="DICT_TYPE.XXX" :value="row.status" /> | 只认字典值,未匹配显示原值 |
| 行操作按钮 | TableActions | ★ 定制,见下 |
| 行内状态开关 | StatusSwitch | ★ 定制,见下 |
| 序号列 | <el-table-column type="index" :index="indexMethod(queryParams)" /> | indexMethod 免 import,跨页连续编号:(pageNo - 1) * pageSize + index + 1 |
| 图表 | Echart | :options + :height |
| 图标 | Icon | 三种前缀,见下方「Icon 的三种来源」 |
| 文件上传 | UploadFile / UploadImg | 在 src/components/UploadFile/ 下 |
| 用户选择 | UserSelectForm | 部门选择 DeptSelectForm |
| 页面顶部提示条 | <doc-alert /> | 上游文档链接,本项目内容多为遗留 |
ContentWrap 不只是个卡片壳子
定制功能
src/components/ContentWrap/src/ContentWrap.vue(253 行)被本项目大幅改造过——实测 git 历史里有 8 次以上本项目提交(0fe8f3a33 2026-09-12 起,到 29cfe0dab 2026-10-11「UI 质感统一第 4 步」止),其中 8d91e041b 的提交信息写着「121 页经 ContentWrap/AppView 全站生效」。上游的 ContentWrap 只是一个带标题的 ElCard。
它多了一套列表页自动撑满高度的逻辑:onMounted 后挂 ResizeObserver(观察卡片、父元素、最近的 section、滚动视口)与 MutationObserver(观察卡片 body 的子树),在 requestAnimationFrame 里判断——
- 卡片 body 里同时存在
.el-table与.el-pagination; - 且表格没有自定义高度(
height/max-height属性、内联 style、h-/min-h-/max-h-开头的 UnoCSS 类,正则^(?:!+)?(?:h|min-h|max-h)-); - 且卡片自身也没有自定义高度;
三条都满足时给卡片加 --list 变体类,并把卡片高度钉成 Math.floor(视口底或下一张卡片顶 - 卡片顶 - section 的 padding-bottom - 卡片 margin-bottom),让表格在卡片内部滚动而不是整页滚动。算出来小于 200px 时放弃(窄屏下搜索区可能已占满首屏,压缩成几像素不如让外层页面滚动)。
对写页面的影响:
- 想要自动高度,就别给
el-table设height;想自己控高度,就设了它——--list会自动让路; - 一个卡片里放多个表格、或表格不在卡片 body 的直接子层,可能不触发;
- 底部留白由
:not(:has(~ .prefix-content-wrap)):not(:has(~ * .prefix-content-wrap))这条 SCSS 决定(末位面板不再叠加下外边距)。注释里写明了为什么不能用:last-child:页面末尾总有Dialog渲染出的零高度.el-overlay兄弟节点。
Icon 的三种来源
<!-- src/components/Icon/src/Icon.vue 的判定就一行 -->
const isLocal = computed(() => props.icon?.startsWith('svg-icon:'))| 写法 | 走哪条路 | 离线可用 |
|---|---|---|
icon="svg-icon:xxx" | 本地 SVG symbol(vite-plugin-svg-icons-ng 扫 src/assets/svgs,virtual:svg-icons-register 注册)。实测 src/views + src/layout 里只有 5 处 | ✅ |
icon="ep:xxx" / fa:xxx" / fa-solid:xxx | @iconify/vue,但这三个集合被 src/plugins/svgIcon/index.ts 用 addCollection() 离线内置了(@iconify/json/json/{ep,fa,fa-solid}.json)。ep: 实测 806 处 | ✅ |
其他前缀(fa6-solid:、mdi: …) | @iconify/vue 现拉 Iconify 公共 API | ❌ 内网/离线会空白 |
菜单图标只能用 ep: / fa: / fa-solid: / svg-icon:
后台「系统管理 → 菜单管理」里填的图标名会直接进 <Icon :icon="...">。填了别的集合前缀,在离线部署环境里菜单图标会全部空白——而且开发机联着网,看不出来。
★ 定制组件:TableActions 与 StatusSwitch
两者都由提交 1dee10e09(2026-10-11,「为列表页统一改造铺底座」)引入,上游没有。它们的 API 设计里有几处刻意的取舍,值得单独讲,因为照着 Element Plus 的直觉用会写错。
TableActions:把一行的操作按钮统一成「配置数组 + 自动折叠 + 权限过滤」。
// src/components/TableActions/src/actions.ts(原文)
export interface TableActionButton {
key: string // v-for 的 key,折叠后必须稳定,不要用数组下标
label: RowValue<string> // 给函数是为了「同一位置按行改文案」
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' // 只作用于行内按钮
permission?: string[] // 任一命中即显示;不传/空数组 = 不门控
show?: (row: any) => boolean // 行级显示条件,等价于模板里的 v-if
disabled?: RowValue<boolean>
loading?: RowValue<boolean>
tooltip?: RowValue<string> // 空串/不传 = 不挂;行内与「更多」下拉项都会挂
divided?: boolean // 收进「更多」后在该项之前画分隔线
onClick: (row: any) => void
}用法(src/views/agent/def/index.vue):<TableActions :row="scope.row" :buttons="rowButtons" />。
:buttons 必须是 setup 层的 const,不能是每行调一次的工厂函数
写成 :buttons="makeButtons(scope.row)" 会每行每次渲染都新建数组,computed 的依赖追踪会退化。行级差异靠配置里的 show 与 RowValue 函数表达——它们在渲染时被调用,读到的 ref 自然被追踪。这条约束写在组件 props 的注释里。
另一处刻意:组件不定义任何 emits。动作走配置里的 onClick 回调,这样各页不用再维护一套「command 字符串 → handler」的 handleCommand switch。
StatusSwitch:列表页的状态开关。
// src/components/StatusSwitch/src/StatusSwitch.vue(props 节选)
modelValue: boolean // 单向:只表达「服务端已生效的状态」,组件永远不会自己改它
permission?: string[] // 不传/空数组 = 不门控(与 TableActions 同一口径)
disabled?: boolean // 额外禁用条件,与权限门控取或
loading?: boolean // 按行 loading:页面传 statusLoadingId === row.id
activeText?: string // 默认「启用」
inactiveText?: string // 默认「禁用」
width?: number // 默认 52;EP 默认的 40 会把两个汉字截掉为什么它只有 change、没有 update:modelValue
组件刻意不 emit update:modelValue,这样页面写不出 v-model——「乐观更新」这个误用被 API 本身挡住,而不是靠注释劝阻。点击后 EP 的开关视觉由 checked = modelValue 驱动,prop 不变 → 动画回弹原位,这正是「服务端确认前不改本地状态」的可见表达;取消二次确认时也不需要回滚任何东西。
二次确认也刻意留在页面里:确认文案是领域事实(要引用具体的服务端行为才核得动),而「只在往禁用方向确认」这种非对称策略塞进组件就得长出 confirmOn / confirmMessage 回调 DSL。
为什么权限做成 prop 而不是让页面挂 v-hasPermi
v-hasPermi 会把整个开关隐藏掉,用户连当前状态都读不到。语义差别是:操作列按钮无权限 = 隐藏;状态开关无权限 = 可见但不可操作(状态是要读的信息,不是动作)。
上游遗留:Table / Search 组件是死代码
| 组件 | 实测引用情况 |
|---|---|
src/components/Table | 只被 src/hooks/web/useTable.ts import;src/views / src/layout 中零使用 |
src/components/Search | 全仓零引用 |
useTable | src/views 里只有 ai/model/{model,apiKey}/index.vue 的注释提到它,没有真实调用点 |
useCrudSchemas | 同上 |
也就是说上游那套「useCrudSchemas 定义 schema → useTable + Table + Search 组件自动渲染列表页」的链路在本项目完全没被采用。现有页面一律是手写 el-table + getList() + Pagination。
不要为了「用上 hook」去重构现有列表页
它们仍在 build/vite/index.ts 的 AutoImport.imports 清单里(所以看起来像是一等公民),但零调用点意味着零实战检验。新写页面请跟随现有手写风格。
同理,以下上游组件在 src/views 中零使用:ContentDetailWrap、Descriptions、Card、Infotip、SummaryCard、Crontab、JsonEditor、Map、Sticky、ShortcutDateRangePicker、ImageViewer(仅被其他组件内部引用)。它们在文档/教程里出现频率很高,别当成常用件。
hooks 全清单(24 个)
| hook | 行数 | 引用文件数 | 来源 | 用途 |
|---|---|---|---|---|
useMessage | 95 | 345 | 上游 | 16 个方法:info/error/success/warning、alert/alertError/alertSuccess/alertWarning、notify/notifyError/notifySuccess/notifyWarning、confirm/delConfirm/exportConfirm/prompt |
useI18n | 53 | 204 | 上游(自研包装) | 见下方说明 |
useForm | 94 | 38 | 上游 | 配 components/Form,几乎只在流程设计器里用 |
useDesign | 18 | 38 | 上游 | 读 global.module.scss 变量 + getPrefixCls(scope) |
useTagsView | 63 | 23 | 上游 | 标签页 closeAll/closeLeft/closeRight/closeOther/closeCurrent |
useCache | 54 | 13 | 上游 | wsCache(localStorage/sessionStorage)+ CACHE_KEY + deleteUserCache() |
useLocale | 35 | 7 | 上游 | 切换语言 |
useIcon | 9 | 6 | 上游 | h(Icon, props),给 render 函数用 |
useUserLabel | 70 | 5 | ★ 定制(2026-10-15) | 用户 id → 「昵称(用户名)」 |
useCrudSchemas | 326 | 3(其中 2 是注释) | 上游 | 未采用 |
useTable | 223 | 4(其中 2 是注释) | 上游 | 未采用 |
useFieldPermission | 48 | 2 | ★ 定制(2026-09-19) | 字段级隐藏/只读 |
useTitle | 24 | 2 | 上游 | 改浏览器标题(permission.ts 里用) |
useWatermark | 72 | 2 | 上游 | 页面水印 |
useConfigGlobal | 9 | 1 | 上游 | 读 app store 的全局配置 |
useNProgress | 33 | 1 | 上游 | 顶部进度条 |
usePageLoading | 18 | 1 | 上游 | 全局 loading |
useNow | 60 | 1 | 上游 | 当前时间响应式 |
useValidator | 58 | 1 | 上游 | required 等表单规则工厂 |
useScrollTo | 60 | 1 | 上游 | 滚动动画(唯一在 hooks/event/) |
useEmitt | 22 | 0 | 上游 | 零引用 |
useGuide | 49 | 0 | 上游 | 零引用(driver.js 引导) |
useNetwork | 21 | 0 | 上游 | 零引用,且它没有 export(写的是 const useNetwork = () => {}) |
useTimeAgo | 49 | 0 | 上游 | 零引用 |
「来源」一列由 git log --diff-filter=A 实测:除 useFieldPermission(868c48504,2026-09-19)与 useUserLabel(557013da5,2026-10-15)外,其余 22 个全部来自上游初始化提交(56f3017ba,2023-02-11)或上游后续提交(2023-10-31)。
useI18n 是本项目的包装,不是 vue-i18n 的那个
// src/hooks/web/useI18n.ts(节选)
const tFn = (key: string, ...arg: any[]) => {
if (!key) return ''
if (!key.includes('.') && !namespace) return key // ← 关键:没有「.」就直接原样返回
return t(getKey(namespace, key), ...arg)
}所以 t('删除成功') 返回 '删除成功'——中文字面量可以直接当 key 用,不需要在语言包里登记。这就是为什么全仓大量提示文案是硬编码中文而不是 t('xxx.yyy')。同时文件末尾还导出了一个恒等函数 export const t = (key: string) => key,供模块顶层(非 setup 上下文)使用,例如 src/directives/permission/hasPermi.ts。
别从 vue-i18n 直接 import useI18n
自动导入登记的是 @/hooks/web/useI18n。直接 import { useI18n } from 'vue-i18n' 会拿到语义不同的实现(必须在 setup 里调用、key 不存在时返回 key 本身但会打警告),行为与全站不一致。
★ 定制 hook 一:useFieldPermission
字段级权限(隐藏 / 只读)。它补的缺口是:后端 @PermissionFieldCrop 只把判成 HIDDEN 的字段置空(VO 是强类型的,删不掉属性),于是前端表格照样渲染出一列空白——对「值不能出去」是够了,但和「看不到这一列」不是一回事。所以隐藏列必须由前端做。
// src/views/system/user/UserForm.vue / index.vue(实测仅有的两个调用点)
const { isVisible, isReadonly } = useFieldPermission('system:user') // 详情字段(:fld:)
const { isVisible } = useFieldPermission('system:user', true) // 列表列(:col:)
// 模板里:v-if="isVisible('mobile')"返回 { modes, loaded, load, isVisible, isReadonly, hiddenFields }。取不到模式时按「不受限」处理(modes = {}),与后端 PermissionFieldHelper.resolve 的降级口径一致——反过来(取不到就全隐藏)会让一次偶发故障表现成整页的列都消失。
★ 定制 hook 二:useUserLabel
把「存着 system_users.id 的列」翻译成「昵称(用户名)」。库里 creator / updater 是 BaseDO 的字符串字段、业务表的 user_id 是 bigint,装的都是用户编号,直接展示是一串数字。
// src/views/infra/dataInterface/LogList.vue:直接用 el-table-column 的 formatter
const { userFormatter } = useUserLabel('调用人') // 参数只用于失败提示里的列名
// <el-table-column label="调用人" :formatter="userFormatter" />
// src/views/infra/billTemplate/BillTemplateLogList.vue:要保住空值那个灰色的 `-`,改用 userLabel
const { userLabel } = useUserLabel('取号人')
// <span v-if="row.userId != null">{{ userLabel(row.userId) }}</span>
// src/views/agent/workflow/index.vue:一个页面多处用,重命名解构
const { userFormatter: ownerFormatter } = useUserLabel('负责人')返回 { userList, userMap, loadUsers, userLabel, userFormatter },onMounted 里自动 loadUsers()。userFormatter 只能返回字符串,需要给空值单独上色时用 <template #default> 配 userLabel——这条写在 hook 的注释里,两个真实调用点正好各用一种。
两个设计点:走 /system/user/simple-list(它没有 @PreAuthorize,任何登录用户都能调,不会因为某个角色没被授予「用户查询」就让整个列表页 403),代价是只返回启用状态的用户;查不到就原样显示编号而不是空白(用户被删除或被停用都会查不到,显示数字至少能拿去库里查出是谁)。失败只降级不抛出,并 message.warning 提示该列将显示编号。
字典没有 hook
不存在 useDict
取字典走 src/utils/dict.ts 的函数(底层是 dict store):
| 函数 | 返回 |
|---|---|
getIntDictOptions(dictType) | NumberDictDataType[],值是 number |
getStrDictOptions(dictType) | 值是 string |
getBoolDictOptions(dictType) | 值是 boolean |
getDictOptions(dictType) | 原始数组 |
getDictLabel(dictType, value) | 单个值 → 标签 |
getDictObj(dictType, value) | 单个值 → 完整对象 |
getDictTree / getDictPathLabels / getDictChildren | 树形字典(如部门、地区) |
只有 DICT_TYPE 枚举(215 条)被登记进了自动导入,函数本身要 import { getIntDictOptions } from '@/utils/dict'。
数据模型
组件目录的主流形状(上游约定,定制组件也遵守):
src/components/
TableActions/
index.ts # export { TableActions } + export * from './src/actions'
src/
TableActions.vue # 实现,defineOptions({ name: 'TableActions' })
actions.ts # 类型与纯函数
actions.spec.ts # 纯函数的单测(vitest,node 环境)但这不是硬性约定
实测 63 个目录里,38 个是「index.ts + src/」的标准形状,25 个是扁平的(.vue 直接放在目录根,如 Pagination/index.vue、DocAlert/、SummaryCard/)。有意思的是:0fe8f3a33 那批定制的 Form* 与 Portal 全部是扁平形状。自动注册按 .vue 文件名走,两种都能用。新建组件时建议照 TableActions 的标准形状写——它把类型与纯函数单独放 actions.ts,因此可以被 actions.spec.ts 测到。
把纯逻辑抽到同目录 .ts 并配 .spec.ts 是本项目定制部分的固定做法——vitest.config.ts 的 environment 是 node,测不了组件渲染,所以可测的部分必须先脱离组件。src/components/FormWidgets/ 是这套做法的极致:45 个 .ts 文件里有 20 个是 .spec.ts(arrayValue / cellMerge / columnSetting / customButton / formSteps / inlineEdit / listCell / printTemplate / renderScene / rowGroup / searchWidget / stepValidate / template / uploadValue 等),承载了低代码表单的取值、合并、校验契约。
怎么扩展
新增一个全局组件
- 建
src/components/<Name>/index.ts+src/components/<Name>/src/<Name>.vue; .vue里写defineOptions({ name: '<Name>' });- 跑一次
pnpm dev——unplugin-vue-components会把它写进src/types/auto-components.d.ts(该文件被 gitignore,只在 dev 下生成); - 之后任意页面直接
<Name />,不用 import。
跳过第 3 步,pnpm ts:check 会报「找不到组件」
build/vite/index.ts 里 Components({ dts: !isBuild && ... })——构建时不生成声明文件。CI 或本地首次 clone 后必须先跑一次 dev 再做类型检查。
若组件名与 Element Plus 冲突,ElementPlusResolver 优先,改用别的名字。
新增一个 hook:放 src/hooks/web/<name>.ts。只有希望「免 import 直接用」的才去 build/vite/index.ts 的 AutoImport.imports 里登记(现有登记的只有 useI18n、useMessage、useTable、useCrudSchemas 四个),登记后同样要重跑 pnpm dev 生成 auto-imports.d.ts。
在 JS 逻辑里判权限(不是模板):import { checkPermi } from '@/utils/permission',checkPermi(['system:user:update'])。它就是 hasPermi.ts 里 hasPermission 的转手,含超管 *:*:* 短路。另有 checkRole(['super_admin']) 读 localStorage 里的 user.roles。
新增一个字典类型:后端「系统管理 → 字典管理」里建类型与数据,前端在 src/utils/dict.ts 的 DICT_TYPE 枚举里补一行(枚举值 = 后端字典类型字符串),然后 getIntDictOptions(DICT_TYPE.XXX) 即可。字典数据由 dict store 在登录后一次性拉取并缓存(CACHE_KEY.DICT_CACHE / DICT_VERSION)。
什么时候该抽组件:跟 TableActions / StatusSwitch 的判据一致——同一段模板在 3 个以上页面重复,且行为约束需要被 API 强制(例如「不许乐观更新」)时才抽;只是长得像,就留在页面里。
坑与注意
src/types/auto-components.d.ts与auto-imports.d.ts都不入库,且dts: !isBuild。新增组件/hook 后必须先pnpm dev再pnpm ts:check。setupGlobCom只注册 2 个组件,别把它当成「全局组件的总入口」去加东西——加了会与自动注册重复。Icon的非内置前缀在离线环境会空白,见「Icon 的三种来源」。菜单管理里填图标名时尤其要注意。ContentWrap的自动高度会被el-table的height关掉——这是设计好的让路机制,不是 bug。想让表格在卡片内滚动就别设高度。useTable/useCrudSchemas/Table/Search是一套未采用的上游链路,见上文 danger。useEmitt/useGuide/useNetwork/useTimeAgo零引用;其中useNetwork连export都没有,import 它会拿到undefined。useFieldPermission目前只有system/user两个页面接入。它是定制能力,不是全站默认行为——给别的页面加字段权限需要显式调用,并且后端要配好对应的:col:/:fld:规则。TableActions的permission不传或传空数组 = 不门控(永远显示),不是「谁都不能看」。组件里显式跳过了checkPermi([]),因为那对超管返回true、对普通用户返回false(*:*:*的短路在.some()之前),语义不稳定。StatusSwitch不能用v-model,只能:model-value+@change。useDesign().getPrefixCls(scope)依赖src/styles/global.module.scss,该文件被vite.config.ts的additionalData显式排除注入(否则会 Sass duplicate global variables 报错)。改 SCSS 注入逻辑时别把它一起处理。DICT_TYPE里有IM_*等 17 个字典,但后端的enlev-module-im已被整体删除——这些字典项是上游遗留,前端能用、后端没有对应业务。别按它们去找接口。
→ 本节是贴着上下文的提醒;按症状查的全域清单见坑位索引。
相关页面
- 前端工程结构:自动导入的完整配置、alias 顺序陷阱、构建分包;
- 路由与权限机制:
v-hasPermi的实现、useFieldPermission在三层权限里的位置; - axios 封装与 API 约定:
useMessage与拦截器提示的分工; - 仓库结构与改名约定:怎么判断一段代码是上游还是定制。
