ow
This commit is contained in:
1 parent
3c0d389fcd
commit
32f237c4bf
22 files changed
+3351
-24
No files matched your search
@@ -0,0 +1,132 @@
|
||||
# 管理端页面层(security 域移动端移植)
|
||||
|
||||
适用范围:`src/components/admin-list|admin-form|admin-filter-drawer`、`src/composables/useAdminList|useAdminPick|useAdminMenu`、`src/admin/*`、`src/pages/security/*`、`src/pages/dashboard/dashboard.vue`,以及 `src/router/routes.ts` 里 security 域的 `PAGE_PATHS` / `PAGE_PERMISSIONS` 登记。
|
||||
|
||||
判定层(`can`/`canVisitPage`/`usePagePermission` 的语义)不在本文,见 `src/md/permission.md`;本文只讲"页面怎么把判定结果用起来"。
|
||||
|
||||
---
|
||||
|
||||
## 1. 移植策略:移植模式,不移植框架
|
||||
|
||||
旧 WebVue `src/views/security` 是桌面形态:`SearchCard`(近 900 行,多列网格 + 列配置 + 打印 + 排序表头 + 行选择)、`ft-page/ft-hero/ft-card/ft-field` 一套 CSS 布局、"选角色"用 `<dragModal>` 内嵌 Enquiry + `provide/inject` 双向通信。
|
||||
|
||||
手机上这三样都成立不了:屏幕放不下多列网格,页面之间没有 `provide/inject`(uni 每个页面是独立实例)。所以本层的取舍是——
|
||||
|
||||
- **保留** `bindOption` 数据模型(后端列名 / `Type` / `Operator` / `SelectOptions`)与查询语义(分页、条件、`orderby`),因为后端 `FindBySearchCriteria` 吃的就是这个形状;
|
||||
- **砍掉** 多列网格、列配置、打印、排序表头——手机没有对应交互;
|
||||
- **重写** 列表为卡片流、详情为单列卡、选择为"真跳一页再回传"。
|
||||
|
||||
复用分三层,边界清楚,换域移植(Group/Function/Dms/Publish…)时机械部分零改动:
|
||||
|
||||
| 层 | 文件 | 认识什么 | 不认识什么 |
|
||||
| --- | --- | --- | --- |
|
||||
| 纯函数 | `admin/admin-filter.ts`、`admin-date.ts`、`admin-column.ts`、`admin-types.ts` | SearchFilter / BindOption / 日期口径 | vue、uni、service |
|
||||
| composable | `useAdminList`、`useAdminPick`、`useAdminMenu` | 上面那层 + vue 响应式 | z-paging、service、权限名 |
|
||||
| 组件 | `admin-list`、`admin-form`、`admin-filter-drawer` | z-paging + uview-pro | service、**权限** |
|
||||
|
||||
**组件不判权限**:按钮由页面按 `canCreate/canAmend/canDelete` 组好数组(详情)或用 `v-if`(列表工具条)传进来。这样公共组件不需要知道任何一个域的权限名。
|
||||
|
||||
---
|
||||
|
||||
## 2. 三个页面 + 入口
|
||||
|
||||
| 页面名 | 路径 | 旧对应 | 作用 |
|
||||
| --- | --- | --- | --- |
|
||||
| `userEnquiry` | `pages/security/user-enquiry` | `UserEnquiry.vue` | 用户列表:三条筛选(用户名/中文名/创建日期区间)、每页 20、按创建日期倒序、行点进详情、多选批量作废 |
|
||||
| `userOperation` | `pages/security/user-operation` | `UserOperation.vue` | 用户详情:查看/新增/修改,`?id=&mode=`;角色明细在 `#extra` 插槽自绘 |
|
||||
| `actorEnquiry` | `pages/security/actor-enquiry` | `ActorEnquiry.vue` | 角色查询,**双用途**(见 §3.3) |
|
||||
| `dashboard` | `pages/dashboard/dashboard` | `layout` 菜单 | 入口九宫格,`FindMenuByUsername` 驱动(见 §5) |
|
||||
|
||||
列表页写法(`user-enquiry.vue` 为准):`reactive(bindOptions)` + 一个只碰具体 service 的 `fetchData(query)` 交给 `useAdminList`;解构出的 `filterCount/filterVisible/draft/searchToken` 是 ref/computed,模板自动 unwrap,方法仍走 `state.xxx`。取数排在 `useAuthGuard` 之后(`admin-list` 的 `auto` 默认 false,首屏手动 `listRef.refresh()` 一次)。
|
||||
|
||||
**为什么 `bindOptions` 必须 `reactive`**:`useAdminList.filters` 是 `computed`,源数组非响应式就永不失效,改了条件却仍是旧结果。
|
||||
|
||||
---
|
||||
|
||||
## 3. 详情页与"选角色"
|
||||
|
||||
### 3.1 admin-form 的形状
|
||||
`sections: AdminFormSection[]`(每张卡 `title` + `fields: AdminFormField[]`),字段用 `type`(text/password/number/digit/textarea/switch/date)、`lock`(主键编辑态也锁死)、`date`(只读态按日期格式化)、`required`。编辑控件的值改写通过 `@field-change(prop, value)` 回抛给页面,组件**不 mutate** `props.model`(`vue/no-mutating-props`)。
|
||||
|
||||
底部按钮是 `actions: AdminFormAction[]`:`when: "view" | "edit"` 按当前态过滤显示,`confirm: true` 走 `uni.showModal` 二次确认。
|
||||
|
||||
### 3.2 user-operation 的取数与保存
|
||||
`onLoad` 读 `id`(=`UserName`)与 `mode`;`FindByKey` 取 `SecurityUserData`,取不到提示"记录不存在"。保存按有无 `CreatedBy` 决定 `Update`/`Insert`,成功后用后端回写的档案刷新、回填主键、退回只读。密码编辑的是 `SecurityUserDisplayData.OrgUserPassword`(留空即不改),只在编辑态出现。
|
||||
|
||||
### 3.3 选择页回传协议(useAdminPick / useAdminPickTarget)
|
||||
旧系统的"选角色"是 Operation 用 `<dragModal>` 内嵌 Enquiry + `provide/inject`。移动端改成**真跳一页再回传**:
|
||||
|
||||
- 调用方 `useAdminPick<T>(channel).open({ page, params }, onPicked)`——先退订再订阅,`onUnmounted` 兜底清理;
|
||||
- 目标页(`actor-enquiry`)`useAdminPickTarget()` 在 `onLoad` 里 `readOptions(options)` 存下 `channel`/`multiple`,确定时 `deliver(rows)`(`uni.$emit('admin:pick:'+channel, rows)` + `navigateBack`);
|
||||
- `actor-enquiry` 因此**同一个列表两种用法**:菜单进入(无 `channel`)是普通查询;被带 `?channel=&multiple=1` 打开就是选择器,单选点行即回传、多选底部"完成"批量回传。
|
||||
|
||||
**协议参数一律不做 `encodeURIComponent`**:H5 侧 uni/vue-router 拼 URL 用 `encodeURI`,预编码会把 `%` 再编成 `%25`。`channel` 与 `params` 的值只允许字母/数字/下划线/连字符/点/斜杠。
|
||||
|
||||
> 命名订正:这个页面就叫 `actor-enquiry`(旧系统本是 `ActorEnquiry.vue`),早期草稿里的 `actor-pick` 是造错的概念——"选择"只是它带 `channel` 打开时的一种模式,不是另一张页面。
|
||||
|
||||
---
|
||||
|
||||
## 4. 权限登记与命名(改这条最容易踩坑)
|
||||
|
||||
`PAGE_PERMISSIONS` 登记(`routes.ts`):
|
||||
|
||||
| 页面 | permission_name | 来源说明 |
|
||||
| --- | --- | --- |
|
||||
| `userEnquiry` | `UserEnquiry` | 功能名与权限名**同名** |
|
||||
| `userOperation` | `UserOperation` | 功能名与权限名**同名** |
|
||||
| `actorEnquiry` | `SecurityActor` | 功能名是 `ActorEnquiry`,但权限名是**实体名** |
|
||||
|
||||
**security 域命名不统一,逐字照抄,别"顺手归一"**:多数功能(如 ActorEnquiry)的 `permission_name` 是实体名(`SecurityActor`/`SecurityGroup`),而 `UserEnquiry`/`UserOperation` 的 `permission_name` 与功能名本身同名。写错等于该页永远进不去。
|
||||
|
||||
反查方法:
|
||||
- 菜单渲染用 `POST /SecurityUser/FindMenuByUsername`——**按当前用户授权过滤**,`UserId` 为空直接返回 `null`;
|
||||
- 校对权限名用 `POST /SecurityFunction/FindMenus`——`[AllowAnonymous]` 的**全量表**(实测 37 行),只读它的 `permission_name` 列来核对登记,**绝不用它做菜单兜底**(会把没授权的功能也列出来)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 入口菜单(useAdminMenu + menu-map 白名单)
|
||||
|
||||
数据链路三层过滤,缺一不可:
|
||||
|
||||
```
|
||||
FindMenuByUsername(后端按用户过滤)
|
||||
-> mapMenuEntries(menu-map.ts 白名单:只认已移植的功能)
|
||||
-> canVisitPage(本地再判一次路由权限)
|
||||
-> 剩下的才是能点的入口
|
||||
```
|
||||
|
||||
- `menu-map.ts` 的 `MENU_PAGE_MAP` 把后端功能名映射到本地页名(现登记 `UserEnquiry`/`ActorEnquiry`),`MENU_ICON_MAP` 给图标(键含特殊字符故全部加引号,守 `quote-props: consistent`);没进白名单的功能不显示入口。
|
||||
- **菜单缓存按账号**(模块级 `cached`/`cachedFor`):一次登录会话里首页反复 `onShow` 不重复打接口;换登录人或退出(`clearCache`)必须重取,否则会把上一个人的入口给下一个人。
|
||||
- `canVisitPage` 再判一次的理由:菜单接口有缓存、档案可能比菜单新,入口点下去才 403 是最难排查的那种 bug。
|
||||
|
||||
---
|
||||
|
||||
## 6. 本批已知边界
|
||||
|
||||
- **只做了 User 域 + 入口菜单**。Group/Function/Dms/Publish 等域未移植;它们在 `menu-map` 白名单里没有映射,所以后端即便授权也不会显示入口——这是有意的,不是漏判。
|
||||
- `actor-enquiry` 从菜单进入时"点行看详情"尚未接(本批它只作查询/回传),详情依赖后续 Actor 域。
|
||||
- `PAGE_PERMISSIONS` 这三个权限名后端存在,但**当前账号是否被授予 `ViewIndc=Y` 尚未端到端验证**——入口可能点出 403。要验证就在真机上点,或查 `security_group_permission` 是否含对应授权。
|
||||
|
||||
---
|
||||
|
||||
## 7. 环境与代码约定(务必遵守,改一次撞一次)
|
||||
|
||||
- **i18n 走 uview-pro 的 `useLocale()`,项目没装 vue-i18n**:`import { useI18n } from "vue-i18n"` 会 TS2307。正确写法 `import { useLocale } from "uview-pro"; const { t, currentLocale } = useLocale();`。`currentLocale` 是 ref(`{ name }` 形态),语言切换监听用 `watch(currentLocale, ...)`,没有 `locale`。
|
||||
- **ESLint CLI 在本机崩溃**:`eslint-plugin-jsdoc` 在 `join(import.meta.dirname, ...)` 处因当前 Node 版本 `import.meta.dirname` 为 `undefined` 抛 `ERR_INVALID_ARG_TYPE`(需 Node ≥ 20.11)。所以 lint 门禁靠 **IDE 诊断**(`GetProblems`),类型门禁靠 `npx vue-tsc --noEmit`(权威,能解析 `@/`)。IDE 报的"找不到模块 `@/…`"是别名解析未就绪的**误报**,以 vue-tsc 裁决。
|
||||
- **`.vue` 模板**:`indent: 2`;多属性标签一律 tag 名单独一行、每个属性各占一行、缩进 = 元素缩进 + 2、闭合 `>` / `/>` 单独一行;带属性或有内容的 `<text>`/`<view>` 其内容单独成行(`vue/singleline-html-element-content-newline`);属性顺序 `ref` → `v-model` → `:props`/静态属性 → `@events`(`vue/attributes-order`)。
|
||||
- **脚本/`.ts` 约定**:Stroustrup(`}` 换行后再 `catch`/`else`/`finally`);`arrow-parens: as-needed`(单参数无类型注解时不加括号);`quote-props: consistent`;`<script setup>` 内不 `export`;perfectionist 导入排序——type 块按路径字母序,value 块"包(`@dcloudio` → `uview-pro` → `vue`)先、后 alias 字母序"(`@/admin/*` < `@/composables` < `@/data/*` < `@/permission/*` < `@/policy/*` < `@/router/*` < `@/service/*`),同模块路径不重复 import。
|
||||
- **uview-pro 组件契约**(0.6.x):`u-input` 的 `border` 是**布尔**,不是旧 uview 的 `'surround'` 字符串;`u-switch` 只吃布尔且只发 `change`;`u-popup` 的 `length`/`border-radius` 吃字符串。
|
||||
- **`u-popup` 只认 `modelValue`,不消费 `show`**:其内部只 `watch(() => props.modelValue)` 调 `open()/close()`,`show` 仅在 types 里声明无人用。所以 `admin-filter-drawer` 必须 `:model-value="props.show"` 绑定,写 `:show` 抽屉永远弹不出来(表现为点了没反应、DOM 里只有一个注释占位节点)。图标名同理:uview-pro 没有 `filter` 图标(会渲染成空白方块),筛选入口用已存在的 `list`。
|
||||
- **视觉语言**:管理页统一走 iOS/微信「设置」式,沿用登录页的翠绿主色(`green` 主题,primary `#059669`)。列表卡片 = 加粗主标题左上 + 右箭头 + 「左固定标签列(132rpx)+ 紧跟其左对齐的值」(不用两端对齐,避免中间拉大空白);详情 = 渐变英雄卡 + 分组白卡内「标签左 / 值右」带细线的行;空值统一显示 `AdminPage.notSetTip`("未设置")而非刺眼的 `-`(列表 `EMPTY_CELL_TEXT` 仍为 `-`,纯逻辑层不引 i18n);次级动作(新增)走靠右的翠绿实心胶囊。
|
||||
|
||||
## 8. 校验清单
|
||||
|
||||
改完这一层,按顺序跑(全部已在本批通过):
|
||||
|
||||
```
|
||||
npx vue-tsc --noEmit # 类型 + 模板表达式,EXIT=0
|
||||
node + esbuild 跑 permission-kernel.spec # 内核判定 59 断言(见 permission.md §7)
|
||||
npm run build:h5 ; npm run build:app # 两端构建 DONE
|
||||
```
|
||||
|
||||
lint(属性顺序 / 缩进 / singleline-content)当前只能靠 IDE `GetProblems` 逐个文件确认——CLI 不可用时,新写的每个 `.vue` 落盘后都单独查一遍。
|
||||
@@ -301,19 +301,18 @@ Get-ChildItem 'dist\build\mp-weixin' -Recurse -File -Include *.js | Select-Strin
|
||||
|
||||
## 8. 已知缺口
|
||||
|
||||
- **入口可见性这一层还没做(菜单/首页九宫格)**。权限的可见性分两层,本层只实现了第二层:
|
||||
- **入口可见性(菜单/首页九宫格)已实现**——这一层不再是缺口。权限可见性分两层,两层现在都落地了:
|
||||
|
||||
| 层 | 管什么 | 数据源 | 现状 |
|
||||
| --- | --- | --- | --- |
|
||||
| ① 入口可见性 | 首页列不列得出这个入口 | `POST /SecurityUser/FindMenuByUsername`(后端按用户权限过滤) | **未实现**:`dashboard.vue` 是空白占位页 |
|
||||
| ② 页面可进入性 | 手输地址/深链跳过去能不能进 | `PAGE_PERMISSIONS` + `canVisitPage()` | 判定层就绪,登记表为空 |
|
||||
| ① 入口可见性 | 首页列不列得出这个入口 | `POST /SecurityUser/FindMenuByUsername`(后端按用户权限过滤) | **已实现**:`dashboard.vue` + `useAdminMenu` + `admin/menu-map.ts` 白名单,详见 `src/md/admin-pages.md` §5 |
|
||||
| ② 页面可进入性 | 手输地址/深链跳过去能不能进 | `PAGE_PERMISSIONS` + `canVisitPage()` | 判定层就绪,security 域三页已登记(见下条) |
|
||||
|
||||
旧 WebVue 主要靠 ①:看不见的入口自然不会点,②只是补"手敲 URL"的漏洞。①的后端链路(`SecurityUserService.cs:232-248`)是 `用户名 -> security_user_actor -> security_actor_group -> security_group_permission -> security_permission_function_map -> security_function`,只返回本人被授权的功能,`UserId` 为空直接返回 null。
|
||||
两个接口都已随 service 层移植进来且**零调用者**:`securityUserService.FindMenuByUsername()`(返回 `SecurityFunctionResultData`,含 `SecurityFunctionDisplayData[]` 的 `FunctionLocalName/FunctionEngName/Icon/Orderby/PageUrl/PermissionName` 与 `SecurityFunctionMaps` 层级,正是旧 `layout/index.vue` 的 `initMenu()` 消费的形态)和 `securityFunctionService.FindMenus()`(`[AllowAnonymous]`、**不区分用户的全量表**,旧项目只用来全量注册路由,别拿它做过滤)。
|
||||
所以这一条缺口的真实含义是:等做首页入口时调 `FindMenuByUsername` 渲染,并用返回的 `PermissionName` 顺手校对 `PAGE_PERMISSIONS` 的登记;`stores/tabbar.ts` 的默认列表目前也是空的,等同一个数据源。
|
||||
①的后端链路(`SecurityUserService.cs:232-248`)是 `用户名 -> security_user_actor -> security_actor_group -> security_group_permission -> security_permission_function_map -> security_function`,只返回本人被授权的功能,`UserId` 为空直接返回 null。`securityUserService.FindMenuByUsername()` 返回 `SecurityFunctionResultData`(含 `SecurityFunctionDisplayData[]` 的 `FunctionLocalName/FunctionEngName/Icon/Orderby/PermissionName`),正是旧 `layout/index.vue` 的 `initMenu()` 消费的形态;`useAdminMenu` 拿它渲染,再过 `menu-map` 白名单与 `canVisitPage` 两道本地过滤。`securityFunctionService.FindMenus()`(`[AllowAnonymous]`、**不区分用户的全量表**)仍只用来全量核对 `permission_name`,别拿它做菜单兜底。
|
||||
遗留:`stores/tabbar.ts` 的默认列表仍是空的(tabbar 与本批九宫格是两套东西,暂未统一数据源)。
|
||||
- **数据级(dependent)判定尚无真实页面在用**:旧项目所有调用点都是单参形式,口子为 `Submit`/`Approval` 与将来的目录级授权保留。
|
||||
- **`PAGE_PERMISSIONS` 目前是空表**:现在只有 login / dashboard / 三个错误页,除 dashboard 有意不绑(§3.1)之外,其余页面还没迁进来。空表意味着路由级 403 实际上还没被任何页面触发,只能靠请求层的 HTTP 403。
|
||||
- **403 页的"返回首页"在首页本身被拦时会原地弹回一次**:`page403.vue` 的返回按钮走 `reLaunch(pageUrl("dashboard"))`,而 reLaunch 同样过 `nav-guard`;若 dashboard 恰好被绑了拿不到的权限,这次跳转会被再拦一次、弹回 403(实测是一次性弹回,不是死循环,因为 403 页自身在 `AUTH_FREE_PAGES` 里)。等真有页面登记权限时,这里应该改成"按 `PAGE_PERMISSIONS` 找第一个可达页"或直接隐藏该按钮。
|
||||
- **`PAGE_PERMISSIONS` 登记仍很稀疏**:security 域已登记 `userEnquiry=UserEnquiry`、`userOperation=UserOperation`、`actorEnquiry=SecurityActor` 三条(命名不统一、别"顺手归一"的坑见 `src/md/admin-pages.md` §4);其余旧 WebVue 域(Group/Function/Dms/Publish…)尚未迁进来。已登记页面会真正触发路由级 403,未登记页面仍只靠请求层 HTTP 403 兜底。
|
||||
- **403 页的"返回首页"在首页本身被拦时会原地弹回一次**:`page403.vue` 的返回按钮走 `reLaunch(pageUrl("dashboard"))`,而 reLaunch 同样过 `nav-guard`;若 dashboard 恰好被绑了拿不到的权限,这次跳转会被再拦一次、弹回 403(实测是一次性弹回,不是死循环,因为 403 页自身在 `AUTH_FREE_PAGES` 里)。dashboard 目前有意不绑(§3.1),所以这条还触发不到;等哪天给 dashboard 也登记了权限,这里就该改成"按 `PAGE_PERMISSIONS` 找第一个可达页"或直接隐藏该按钮。
|
||||
- **前端判定滞后于后端**:后端每次请求实时查库,前端读登录时的快照。管理员改了授权后,用户重新登录前看到的仍是旧按钮状态(可能点了才 403)。这是"权限随令牌下发"这个既有模型的代价,不是本层引入的。
|
||||
- **`SecurityUserPermissionInfo.UserGroups` 未参与判定**:后端也不用它做短路(只用于 join 出授权行),前端同样只用 `Permissions` / `PermissionDependents`。
|
||||
- **孤儿授权数据**:种子库里 `security_group_permission` 有 `abc` 组以及 `acc`/`PhotoPublish`/`AudioPublish`/`VideoPublish` 等权限名,而 `security_group` / `security_permission` 里查不到它们。判定层对此的表现是"多几行永远匹配不上的索引项",无害;但登记权限名时**不能以授权表的并集为准**,要以 `security_permission` 为准,否则会照抄到这些历史残留。
|
||||
Reference in new issue
Block a user