diff --git a/src/components/CrudTable.vue b/src/components/CrudTable.vue index f2062e5..f5ba46e 100644 --- a/src/components/CrudTable.vue +++ b/src/components/CrudTable.vue @@ -80,8 +80,17 @@ const props = defineProps<{ defaultPageSize?: number /** 新增前拦截回调:返回 false 阻止打开新增弹窗(如依赖父级筛选条件的页面需先选条件) */ beforeCreate?: () => boolean | void + /** + * 是否允许新增/编辑/删除(默认 true)。 + * 传 false 时隐藏新增、批量删除、行内编辑/删除按钮,实现只读视图 + * (用于 role_scope 非管理员场景,如 Products/Orders 仅管理员可改)。 + */ + editable?: boolean }>() +// 是否可写:editable 未传视为可写;传 false 时隐藏新增/编辑/删除入口(只读视图) +const canEdit = computed(() => props.editable !== false) + // 表格实际使用的最大高度:显式 maxHeight 优先; // stretch(整页列表)场景默认取视口高度 55%,保证表格与卡片一体、 // 不产生页面级滚动栏(行多时表格内部滚动) @@ -488,11 +497,11 @@ defineExpose({
{{ title || table }}
- + 新增 - - + + @@ -695,14 +704,14 @@ defineExpose({
-
+
查看 - + 编辑 - + 删除
@@ -774,14 +783,14 @@ defineExpose({
-
+
查看 - + 编辑 - + 删除
diff --git a/src/stores/user.ts b/src/stores/user.ts index 8fa8330..b1cc59a 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -17,15 +17,30 @@ export interface AdminCheckResult { current_tenant_uuid?: string has_current_company?: boolean role_scope?: { - Type?: string + Type?: RoleScopeType TypeLabel?: string IsAdmin?: boolean IsDeptManager?: boolean IsWorkgroupLead?: boolean IsMember?: boolean + /** 所属部门 UUID(多个用逗号拼接,负责人可挂多个) */ + DeptUUID?: string | null + /** 所属工作组 UUID(多个用逗号拼接,组长可挂多个) */ + WorkgroupUUID?: string | null } | null } +/** 角色作用域类型(对齐后端 CheckAdminView.role_scope.Type) */ +export type RoleScopeType = 'admin' | 'dept_manager' | 'workgroup_lead' | 'member' + +/** 作用域默认中文标签(后端 TypeLabel 缺失时回退) */ +const ROLE_SCOPE_LABELS: Record = { + admin: '管理员', + dept_manager: '部门负责人', + workgroup_lead: '工作组长', + member: '普通组员', +} + /** 用户状态管理:当前登录用户、角色、菜单、权限 */ export const useUserStore = defineStore('user', () => { // === 状态 === @@ -80,6 +95,21 @@ export const useUserStore = defineStore('user', () => { // 管理员判定优先用 CheckAdmin(后端按当前角色 admin:super/admin:tenant 权限码判定); // 未加载 CheckAdmin 时回退 userInfo.IsAdmin(旧缓存数据) const isAdmin = computed(() => adminCheck.value?.is_admin === true || userInfo.value?.IsAdmin === true) + + // === 角色作用域(role_scope,CheckAdmin 下发) === + // 原生 GERP 语义:admin 看全公司 / dept_manager 看本部门 / workgroup_lead 看本工作组 / member 只看自己 + // 未加载完成时按 isAdmin 回退(刷新瞬间不闪烁,与后端计算逻辑一致) + const roleScopeType = computed(() => { + const t = adminCheck.value?.role_scope?.Type + if (t === 'admin' || t === 'dept_manager' || t === 'workgroup_lead' || t === 'member') return t + return isAdmin.value ? 'admin' : 'member' + }) + const roleScopeLabel = computed(() => adminCheck.value?.role_scope?.TypeLabel || ROLE_SCOPE_LABELS[roleScopeType.value]) + const isScopeAdmin = computed(() => roleScopeType.value === 'admin') + const isDeptManager = computed(() => roleScopeType.value === 'dept_manager') + const isWorkgroupLead = computed(() => roleScopeType.value === 'workgroup_lead') + const isScopeMember = computed(() => roleScopeType.value === 'member') + const currentRole = computed(() => userInfo.value?.CurrentRole || '') const avatar = computed(() => userInfo.value?.Avatar || '') const displayName = computed(() => userInfo.value?.DisplayName || userInfo.value?.UserName || '未登录') @@ -262,6 +292,12 @@ export const useUserStore = defineStore('user', () => { // 计算属性 isLoggedIn, isAdmin, + roleScopeType, + roleScopeLabel, + isScopeAdmin, + isDeptManager, + isWorkgroupLead, + isScopeMember, currentRole, avatar, displayName, diff --git a/src/types/api.ts b/src/types/api.ts index e9f0945..f69bf9e 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -346,5 +346,9 @@ export interface AdminCheckResult { IsDeptManager?: boolean IsWorkgroupLead?: boolean IsMember?: boolean + /** 所属部门 UUID(多个用逗号拼接) */ + DeptUUID?: string | null + /** 所属工作组 UUID(多个用逗号拼接) */ + WorkgroupUUID?: string | null } | null } diff --git a/src/views/company/Company.vue b/src/views/company/Company.vue index a8fe7cf..c71c28a 100644 --- a/src/views/company/Company.vue +++ b/src/views/company/Company.vue @@ -21,7 +21,7 @@ * - 列定义优先走后端 /API/FormAPI/Columns/Company(CrudTable 自动处理) * 非主实例的 getDefaultColumns fallback 暂不实现(TODO) * - AdminBadge 自动根据 server/admin_verify 显示特权横幅(对齐原生 adminBadge:true) - * - role_scope 细粒度判断暂用 userStore.isAdmin 二分(TODO 待 checkAdmin 接入) + * - 作用域隔离由后端表单服务按公司/租户强制,前端不做 role_scope 细粒度判定 * - onView 跳 Vue 路由 /company/company-detail?CompanyUUID=xxx(替代 GERP.Navigate) */ import { ref } from 'vue' diff --git a/src/views/company/Department.vue b/src/views/company/Department.vue index 162ba7a..6b48fa2 100644 --- a/src/views/company/Department.vue +++ b/src/views/company/Department.vue @@ -23,7 +23,7 @@ * - 列定义优先走后端 /API/FormAPI/Columns/Department(CrudTable 自动处理) * 非主实例的 getDefaultColumns fallback 暂不实现(TODO) * - 无 adminBadge(原生未启用特权徽章) - * - role_scope 细粒度判断暂用 userStore.isAdmin 二分(TODO 待 checkAdmin 接入) + * - 作用域隔离由后端表单服务按公司/租户强制,前端不做 role_scope 细粒度判定 * - 树形展示暂未实现(TODO:待 vxe-table tree-config 接入) * - onView 跳 Vue 路由 /company/department-detail?DeptUUID=xxx(替代 GERP.Navigate) */ diff --git a/src/views/company/Role.vue b/src/views/company/Role.vue index 3f4c1ff..e19b0bb 100644 --- a/src/views/company/Role.vue +++ b/src/views/company/Role.vue @@ -21,7 +21,7 @@ * - 列定义优先走后端 /API/FormAPI/Columns/Role(CrudTable 自动处理) * 非主实例的 getDefaultColumns fallback 暂不实现(TODO) * - AdminBadge 自动根据 server/admin_verify 显示特权横幅(对齐原生 adminBadge:true) - * - role_scope 细粒度判断暂用 userStore.isAdmin 二分(TODO 待 checkAdmin 接入) + * - 作用域隔离由后端表单服务按公司/租户强制,前端不做 role_scope 细粒度判定 * - onView 跳 Vue 路由 /company/role-detail?RoleUUID=xxx(替代 GERP.Navigate) * - 注意:与 system/Roles.vue(system 模块角色管理)区分,两者迁移来源与路由不同 */ diff --git a/src/views/company/Tenant.vue b/src/views/company/Tenant.vue index 58b6bcb..9006bb2 100644 --- a/src/views/company/Tenant.vue +++ b/src/views/company/Tenant.vue @@ -16,7 +16,7 @@ * - 列定义优先走后端 /API/FormAPI/Columns/Tenant(CrudTable 自动处理) * 非主实例的 getDefaultColumns fallback 暂不实现(TODO) * - AdminBadge 自动根据 server/admin_verify 显示特权横幅(对齐原生 adminBadge:true) - * - role_scope 细粒度判断暂用 userStore.isAdmin 二分(TODO 待 checkAdmin 接入) + * - 作用域隔离由后端表单服务按公司/租户强制,前端不做 role_scope 细粒度判定 * - onView 跳 Vue 路由 /company/tenant-detail?TenantUUID=xxx(替代 GERP.Navigate) * - 页面标题随 server 变化由 setPageTitle 统一处理 */ diff --git a/src/views/company/Workgroup.vue b/src/views/company/Workgroup.vue index e713f7c..35f8d0f 100644 --- a/src/views/company/Workgroup.vue +++ b/src/views/company/Workgroup.vue @@ -21,7 +21,7 @@ * - 列定义优先走后端 /API/FormAPI/Columns/Workgroup(CrudTable 自动处理) * 非主实例的 getDefaultColumns fallback 暂不实现(TODO) * - 无 adminBadge(原生未启用特权徽章) - * - role_scope 细粒度判断暂用 userStore.isAdmin 二分(TODO 待 checkAdmin 接入) + * - 作用域隔离由后端表单服务按公司/租户强制,前端不做 role_scope 细粒度判定 * - onView 跳 Vue 路由 /company/workgroup-detail?WorkgroupUUID=xxx(替代 GERP.Navigate) */ import { ref } from 'vue' diff --git a/src/views/inventory/Products.vue b/src/views/inventory/Products.vue index ed1311e..d3ceeac 100644 --- a/src/views/inventory/Products.vue +++ b/src/views/inventory/Products.vue @@ -8,29 +8,47 @@ * * Vue 版(阶段 1 简化): * - server / admin_verify 由 request.ts 拦截器统一注入 - * - 作用域判断暂用 userStore.isAdmin 二分(admin / normal) - * - 细粒度 role_scope(dept_manager / workgroup_lead)待 checkAdmin 接入(TODO) - * - 管理员开启行选择;普通组员只读(editable 待 CrudTable 暴露 editable prop 后接入,TODO) + * - role_scope 细粒度(admin / dept_manager / workgroup_lead / member)取自 CheckAdmin: + * 标题与提示文案四态;横幅色与图标两态(admin 蓝 / 其余灰) + * - 行选择与编辑仅特权模式开放(对齐原生 showCheckbox/editable = isAdminMode) * - onView 跳 /inventory/product-detail?TemplateUUID=xxx */ -import { computed } from 'vue' +import { computed, watch } from 'vue' import { useRouter } from 'vue-router' import CrudTable from '@/components/CrudTable.vue' import AdminBadge from '@/components/AdminBadge.vue' -import { useUserStore } from '@/stores/user' +import { useUserStore, type RoleScopeType } from '@/stores/user' import { setPageTitle } from '@/utils/serverScope' const userStore = useUserStore() const router = useRouter() -setPageTitle('产品管理') +// === role_scope 作用域(对齐原生 pages/inventory/products.html 的 applyScopeUI) === +/** 页面标题:admin=产品管理 / dept_manager=部门产品 / workgroup_lead=工作组产品 / member=产品列表 */ +const titleMap: Record = { + admin: '产品管理', + dept_manager: '部门产品', + workgroup_lead: '工作组产品', + member: '产品列表', +} +/** 作用域提示文案 */ +const hintMap: Record = { + admin: '管理员视图:可查看和管理全公司产品模板,含跨公司特权模式。', + dept_manager: '部门负责人视图:可查看本部门可用产品模板。', + workgroup_lead: '工作组组长视图:可查看本工作组可用产品模板。', + member: '普通组员视图:可查看可用产品列表。', +} + +const pageTitle = computed(() => titleMap[userStore.roleScopeType]) +const scopeHint = computed(() => hintMap[userStore.roleScopeType]) +// 横幅两态色与图标(对齐原生:scopeType === 'admin' 为蓝,其余为灰) +const bannerClass = computed(() => `scope-banner ${userStore.isScopeAdmin ? 'admin' : 'normal'}`) +const scopeIcon = computed(() => (userStore.isScopeAdmin ? 'fa-shield-alt' : 'fa-info-circle')) +// 行选择与编辑:仅特权模式开放(对齐原生 showCheckbox/editable = isAdminMode) const isAdmin = computed(() => userStore.isAdmin) -const bannerClass = computed(() => `scope-banner ${isAdmin.value ? 'admin' : 'normal'}`) -const scopeHint = computed(() => - isAdmin.value - ? '管理员视图:可查看和管理全公司产品模板,含跨公司特权模式。' - : '普通组员视图:可查看可用产品列表。', -) + +// 标题随作用域变化(setPageTitle 自动处理非主实例的 server 前缀) +watch(pageTitle, (t) => setPageTitle(t), { immediate: true }) interface TemplateRow { TemplateUUID?: string @@ -50,17 +68,18 @@ function handleView(row: TemplateRow): void {
- + {{ scopeHint }}
diff --git a/src/views/sales/Orders.vue b/src/views/sales/Orders.vue index 4c76480..d01cc8a 100644 --- a/src/views/sales/Orders.vue +++ b/src/views/sales/Orders.vue @@ -8,29 +8,47 @@ * * Vue 版(阶段 1 简化): * - server / admin_verify 由 request.ts 拦截器统一注入 - * - 作用域判断暂用 userStore.isAdmin 二分 - * - 行选择始终开启(对齐原生 showCheckbox: true) - * - editable 待 CrudTable 暴露 editable prop 后按 isAdmin 控制(TODO) + * - role_scope 细粒度(admin / dept_manager / workgroup_lead / member)取自 CheckAdmin: + * 标题与提示文案四态;横幅色与图标两态(admin 蓝 / 其余灰) + * - 行选择始终开启(对齐原生 showCheckbox: true);编辑仅特权模式开放(editable = isAdminMode) * - onView 跳 /sales/order-detail?OrderUUID=xxx */ -import { computed } from 'vue' +import { computed, watch } from 'vue' import { useRouter } from 'vue-router' import CrudTable from '@/components/CrudTable.vue' import AdminBadge from '@/components/AdminBadge.vue' -import { useUserStore } from '@/stores/user' +import { useUserStore, type RoleScopeType } from '@/stores/user' import { setPageTitle } from '@/utils/serverScope' const userStore = useUserStore() const router = useRouter() -setPageTitle('销售订单') +// === role_scope 作用域(对齐原生 pages/sales/orders.html 的 applyScopeUI) === +/** 页面标题:admin=销售订单 / dept_manager=部门订单 / workgroup_lead=工作组订单 / member=我的订单 */ +const titleMap: Record = { + admin: '销售订单', + dept_manager: '部门订单', + workgroup_lead: '工作组订单', + member: '我的订单', +} +/** 作用域提示文案 */ +const hintMap: Record = { + admin: '管理员视图:可查看全公司订单,含跨公司特权模式。', + dept_manager: '部门负责人视图:仅可查看本部门相关订单。', + workgroup_lead: '工作组组长视图:仅可查看本工作组相关订单。', + member: '普通组员视图:仅可查看自己的订单。', +} + +const pageTitle = computed(() => titleMap[userStore.roleScopeType]) +const scopeHint = computed(() => hintMap[userStore.roleScopeType]) +// 横幅两态色与图标(对齐原生:scopeType === 'admin' 为蓝,其余为灰) +const bannerClass = computed(() => `scope-banner ${userStore.isScopeAdmin ? 'admin' : 'normal'}`) +const scopeIcon = computed(() => (userStore.isScopeAdmin ? 'fa-shield-alt' : 'fa-info-circle')) +// 编辑权限:仅特权模式开放(对齐原生 editable = isAdminMode;行选择恒开) const isAdmin = computed(() => userStore.isAdmin) -const bannerClass = computed(() => `scope-banner ${isAdmin.value ? 'admin' : 'normal'}`) -const scopeHint = computed(() => - isAdmin.value - ? '管理员视图:可查看全公司订单,含跨公司特权模式。' - : '普通组员视图:仅可查看自己的订单。', -) + +// 标题随作用域变化(setPageTitle 自动处理非主实例的 server 前缀) +watch(pageTitle, (t) => setPageTitle(t), { immediate: true }) interface OrderRow { OrderUUID?: string @@ -50,17 +68,18 @@ function handleView(row: OrderRow): void {
- + {{ scopeHint }}
diff --git a/src/views/system/RoleDetail.vue b/src/views/system/RoleDetail.vue index 1551105..1b6968a 100644 --- a/src/views/system/RoleDetail.vue +++ b/src/views/system/RoleDetail.vue @@ -16,7 +16,7 @@ * - server / admin_verify 由 request.ts 拦截器统一注入 * - 主表单条加载:formApi.list('Role', { RoleUUID, pageSize: 1 }) * - 权限树分配:跳 /system/role-permissions?RoleUUID=xxx(RolePermissions 页用 el-tree 实现) - * - role_scope 细粒度判断暂用 userStore.isAdmin 二分(TODO) + * - 隔离提示由特权模式(admin_verify)决定,与原生 isAdminMode 一致 */ import { ref, reactive, computed, onMounted, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' @@ -54,8 +54,7 @@ const PK_FIELD = 'RoleUUID' // 页面标题 const title = computed(() => data.value?.RoleName || '角色详情') -// 隔离提示 -// TODO: 接入 checkAdmin → role_scope +// 隔离提示(特权模式显示全公司记录,否则仅当前公司) const isolationHint = computed(() => userStore.isAdmin ? '特权模式:显示该角色在所有公司的关联记录(无公司隔离)' diff --git a/src/views/system/RolePermissions.vue b/src/views/system/RolePermissions.vue index 91f395f..788cb71 100644 --- a/src/views/system/RolePermissions.vue +++ b/src/views/system/RolePermissions.vue @@ -13,8 +13,8 @@ * 迁移要点(对齐工程迁移规则): * - server / admin_verify 由 request.ts 拦截器统一注入 * - 权限树用 el-tree 实现(TODO:待 PermissionTreePicker 组件封装后替换) - * - role_scope 细粒度判断暂用 userStore.isAdmin 二分(TODO) - * - 仅管理员可访问:非管理员显示拒绝页 + * - 仅管理员可访问(原生语义如此,不涉及 role_scope 细粒度分支) + * - 非管理员显示拒绝页 * - 离开页面前提示未保存(beforeunload + 路由守卫) * * 接口: @@ -75,8 +75,7 @@ interface PermGroup { Permissions: PermItem[] } -// 仅管理员可访问 -// TODO: 接入 checkAdmin → role_scope,细化 dept_manager / workgroup_lead 分支 +// 仅管理员可访问(原生语义一致) const isAdmin = computed(() => userStore.isAdmin) // el-tree 数据:组 → 权限,统一 _nodeKey 作为 node-key diff --git a/src/views/system/UserDetail.vue b/src/views/system/UserDetail.vue index 701d4c8..1f5052b 100644 --- a/src/views/system/UserDetail.vue +++ b/src/views/system/UserDetail.vue @@ -20,7 +20,7 @@ * - 主表单条加载:formApi.list('User', { UserUUID, pageSize: 1 }) 取首条 * - 子表 CrudTable 通过 query 透传父级 UUID,无需手动建表 * - UUID 解析使用 useResolveUUIDs composable(本页主表暂无 UUID 字段需解析) - * - role_scope 细粒度判断暂用 userStore.isAdmin 二分(TODO:接入 checkAdmin 后补 dept_manager / workgroup_lead) + * - 隔离提示由特权模式(admin_verify)决定,与原生 isAdminMode 一致 * - 2FA 接口:get('/API/SiteUserManagement/GetUser2FAInfo', { UserUUID })(TODO:契约确认) */ import { ref, reactive, computed, onMounted, watch } from 'vue' diff --git a/src/views/system/Users.vue b/src/views/system/Users.vue index 02a5f64..f19a73a 100644 --- a/src/views/system/Users.vue +++ b/src/views/system/Users.vue @@ -8,45 +8,55 @@ * * Vue 版(阶段 1 简化): * - server / admin_verify 由 request.ts 拦截器统一注入 - * - 作用域判断暂用 userStore.isAdmin 二分(管理员 / 普通组员) - * - 细粒度 role_scope(dept_manager / workgroup_lead)待 checkAdmin 接口确认后接入(TODO) + * - role_scope 细粒度(admin / dept_manager / workgroup_lead / member)取自 CheckAdmin, + * 按作用域差异化标题、横幅色、图标、提示文案;仅 member 关闭行选择与编辑 * - 列 fallback 暂未实现,主实例走后端 Columns 接口(TODO:非主实例 getDefaultColumns) * - AdminBadge 自动根据 server/admin_verify 显示特权横幅 * - onView 跳 /system/user-detail?UserUUID=xxx */ -import { computed } from 'vue' +import { computed, watch } from 'vue' import { useRouter } from 'vue-router' import CrudTable from '@/components/CrudTable.vue' import AdminBadge from '@/components/AdminBadge.vue' -import { useUserStore } from '@/stores/user' +import { useUserStore, type RoleScopeType } from '@/stores/user' import { setPageTitle } from '@/utils/serverScope' const userStore = useUserStore() const router = useRouter() -setPageTitle('用户管理') -// 阶段 1 简化:仅区分管理员 / 普通组员 -// TODO: 接入 checkAdmin → role_scope,补 dept_manager / workgroup_lead 分支 -const scopeType = computed<'admin' | 'member'>(() => - userStore.isAdmin ? 'admin' : 'member', -) +// === role_scope 作用域(对齐原生 pages/system/users.html 的 applyScopeUI) === +/** 页面标题:admin=用户管理 / dept_manager=部门成员 / workgroup_lead=工作组成员 / member=我的资料 */ +const titleMap: Record = { + admin: '用户管理', + dept_manager: '部门成员', + workgroup_lead: '工作组成员', + member: '我的资料', +} +/** 横幅图标 */ +const iconMap: Record = { + admin: 'fa-shield-alt', + dept_manager: 'fa-sitemap', + workgroup_lead: 'fa-users-cog', + member: 'fa-user', +} +/** 作用域提示文案 */ +const hintMap: Record = { + admin: '管理员视图:可查看和管理全公司用户,含跨公司特权模式。', + dept_manager: '部门负责人视图:仅可查看和管理本部门成员,新增/编辑/删除受部门隔离约束。', + workgroup_lead: '工作组组长视图:仅可查看和管理本工作组成员,新增/编辑/删除受工作组隔离约束。', + member: '普通组员视图:仅可查看和编辑自己的资料,无法查看他人数据。', +} -// 作用域提示文本(对齐原生 hintMap) -const scopeHint = computed(() => { - switch (scopeType.value) { - case 'admin': - return '管理员视图:可查看和管理全公司用户,含跨公司特权模式。' - case 'member': - default: - return '普通组员视图:仅可查看和编辑自己的资料,无法查看他人数据。' - } -}) +const pageTitle = computed(() => titleMap[userStore.roleScopeType]) +const scopeIcon = computed(() => iconMap[userStore.roleScopeType]) +const scopeHint = computed(() => hintMap[userStore.roleScopeType]) +// 作用域横幅样式类(对齐原生 user-scope-banner 四态色) +const scopeBannerClass = computed(() => `user-scope-banner ${userStore.roleScopeType}`) +// 普通组员:只读(对齐原生 showCheckbox/editable = !IsMember) +const isMember = computed(() => userStore.isScopeMember) -// 作用域横幅样式类(对齐原生 user-scope-banner 样式分支) -const scopeBannerClass = computed(() => `user-scope-banner ${scopeType.value}`) - -// 普通组员:禁用行选择与编辑 -const isMember = computed(() => scopeType.value === 'member') +// 标题随作用域变化(setPageTitle 自动处理非主实例的 server 前缀) +watch(pageTitle, (t) => setPageTitle(t), { immediate: true }) // 用户记录类型 interface UserRow { @@ -71,18 +81,19 @@ function handleView(row: UserRow): void {
- + {{ scopeHint }}
@@ -119,6 +130,20 @@ function handleView(row: UserRow): void { border: 1px solid rgba(30, 64, 175, 0.2); } + // 部门负责人:绿色 + &.dept_manager { + background: rgba(16, 185, 129, 0.08); + color: #047857; + border: 1px solid rgba(16, 185, 129, 0.2); + } + + // 工作组组长:琥珀色 + &.workgroup_lead { + background: rgba(245, 158, 11, 0.08); + color: #b45309; + border: 1px solid rgba(245, 158, 11, 0.2); + } + // 普通组员:灰色 &.member { background: rgba(100, 116, 139, 0.08);