From a1765209ddbe3e54ded31e2b05fe4cd5dcc3c8f0 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 30 Aug 2026 13:23:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(gerp-ui):=20=E6=89=B9=E9=87=8F=E8=81=8C?= =?UTF-8?q?=E5=8A=A1=E8=B0=83=E6=95=B4=E6=A0=91=E5=BD=A2=E9=80=89=E6=8B=A9?= =?UTF-8?q?=20+=20OA=20=E5=AE=A1=E6=89=B9=E9=A1=B5=20+=20=E8=99=9A?= =?UTF-8?q?=E6=8B=9F=E8=81=8C=E5=8A=A1=E4=B8=80=E9=94=AE=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BatchPositionDialog:部门列表页以部门树组织员工、工作组列表页以工作组树组织成员, 均可勾选具体的人;选中行成员自动预勾选,可追加/取消;提交映射 UserRole/UserRoleWorkgroup - Department/Workgroup 列表页 toolbar 新增"批量调整职务"入口(CompanyUUID 兜底当前用户公司) - CrudTable 新增 #toolbar 插槽(暴露 selectedRows/refresh) - OA 审批页 Approval.vue(请假/出差待审批,30s 轮询,通过/驳回)+ api/oa.ts - AppConfigPage 虚拟职务"一键绑定"按钮 + api/app.ts autoBindVirtualPositions - 附带:OA/财务/eHRM 应用页、MultiEntityPicker、菜单/布局优化等系列改动 --- index.html | 2 +- public/favicon.ico | Bin 0 -> 39387 bytes public/favicon.svg | 1 - src/api/app.ts | 117 ++++ src/api/auth.ts | 26 +- src/api/form.ts | 15 + src/api/oa.ts | 31 ++ src/apps/index.ts | 32 ++ src/components/BatchPositionDialog.vue | 477 ++++++++++++++++ src/components/CrudFormPanel.vue | 80 ++- src/components/CrudTable.vue | 137 ++++- src/components/MultiEntityPicker.vue | 724 +++++++++++++++++++++++++ src/components/RoleSwitcher.vue | 26 +- src/layouts/DefaultLayout.vue | 153 +++++- src/main.ts | 28 + src/router/index.ts | 129 ++++- src/styles/global.scss | 5 + src/types/api.ts | 2 + src/views/Login.vue | 2 +- src/views/Profile.vue | 442 +++++++++++++-- src/views/app/AppConfigPage.vue | 481 +++++++++++++++- src/views/app/AppMarket.vue | 142 ++++- src/views/app/AppMine.vue | 5 + src/views/company/CompanyDetail.vue | 76 ++- src/views/company/Department.vue | 50 +- src/views/company/DepartmentDetail.vue | 38 +- src/views/company/EmployeeDetail.vue | 448 --------------- src/views/company/Role.vue | 8 + src/views/company/TenantDetail.vue | 11 +- src/views/company/Workgroup.vue | 43 +- src/views/company/WorkgroupDetail.vue | 35 +- src/views/ehrm/Assessment.vue | 42 ++ src/views/ehrm/Performance.vue | 42 ++ src/views/ehrm/Profile.vue | 42 ++ src/views/finance/Arap.vue | 42 ++ src/views/finance/Budget.vue | 42 ++ src/views/finance/Cashier.vue | 42 ++ src/views/finance/Ledger.vue | 42 ++ src/views/finance/Reimburse.vue | 42 ++ src/views/finance/Report.vue | 155 ++++++ src/views/finance/Voucher.vue | 346 ++++++++++++ src/views/hr/Position.vue | 214 ++++++++ src/views/hr/PositionCategory.vue | 159 ++++++ src/views/oa/Announcement.vue | 42 ++ src/views/oa/Approval.vue | 206 +++++++ src/views/oa/Leave.vue | 42 ++ src/views/oa/Meeting.vue | 42 ++ src/views/oa/Report.vue | 42 ++ src/views/oa/Travel.vue | 42 ++ src/views/system/UserDetail.vue | 198 +++++-- 50 files changed, 4969 insertions(+), 621 deletions(-) create mode 100644 public/favicon.ico delete mode 100644 public/favicon.svg create mode 100644 src/api/oa.ts create mode 100644 src/components/BatchPositionDialog.vue create mode 100644 src/components/MultiEntityPicker.vue delete mode 100644 src/views/company/EmployeeDetail.vue create mode 100644 src/views/ehrm/Assessment.vue create mode 100644 src/views/ehrm/Performance.vue create mode 100644 src/views/ehrm/Profile.vue create mode 100644 src/views/finance/Arap.vue create mode 100644 src/views/finance/Budget.vue create mode 100644 src/views/finance/Cashier.vue create mode 100644 src/views/finance/Ledger.vue create mode 100644 src/views/finance/Reimburse.vue create mode 100644 src/views/finance/Report.vue create mode 100644 src/views/finance/Voucher.vue create mode 100644 src/views/hr/Position.vue create mode 100644 src/views/hr/PositionCategory.vue create mode 100644 src/views/oa/Announcement.vue create mode 100644 src/views/oa/Approval.vue create mode 100644 src/views/oa/Leave.vue create mode 100644 src/views/oa/Meeting.vue create mode 100644 src/views/oa/Report.vue create mode 100644 src/views/oa/Travel.vue diff --git a/index.html b/index.html index 061fd9d..e7a93c0 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + GERP 管理系统 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4386f15bfe512abbf87796d0786fcc54b2b2fe58 GIT binary patch literal 39387 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 \ No newline at end of file diff --git a/src/api/app.ts b/src/api/app.ts index 24a73d9..b2ef3d2 100644 --- a/src/api/app.ts +++ b/src/api/app.ts @@ -24,6 +24,10 @@ export interface AppItem { InstalledCompanies?: string[] UserEnabled?: number ApplyStatus?: number + /** 严格职务可见开关:1=无职务绑定时全员不可见(必须绑定职务);0=宽松(无绑定全员可见) */ + StrictPositionVisible?: number + /** 强制职务绑定(应用清单声明,硬性):1=无绑定时必不可见,不受管理员开关影响 */ + RequirePositionBinding?: number /** 远端注入(MSYNC):来源/提供方服务/在线状态 */ Source?: string ServiceName?: string @@ -68,6 +72,64 @@ export interface AppConfigItem { Updated: boolean } +/** 应用虚拟职务(含已绑定真实职务) */ +export interface VirtualMappingItem { + AppVirtualPositionUUID: string + Code: string + Name: string + MenuNames: string[] + PositionUUIDs: string[] + /** 与 PositionUUIDs 对齐的职务名(回显 tag 用) */ + PositionNames?: string[] +} + +/** 真实职务选项(配置页选择用) */ +export interface PositionOption { + PositionUUID: string + PositionName: string +} + +/** 通用部门与职务模板同步——缺失项(按名称匹配,同名已有=复用) */ +export interface PositionTemplateDiffItem { + CategoryName: string + Exists: boolean + Status: 'new' | 'exists' + Positions: { + PositionName: string + Exists: boolean + Status: 'new' | 'exists' + }[] +} + +/** 通用部门与职务同步结果统计 */ +export interface PositionTemplateSyncStats { + CreatedCategories: string[] + CreatedPositions: string[] + ReusedCategories: number + ReusedPositions: number +} + +/** 通用部门与职务反向移除——可移除性预览项 */ +export interface PositionTemplateRemoveItem { + CategoryName: string + Exists: boolean + Removable: boolean + BlockedReason: string + Positions: { + PositionName: string + Exists: boolean + Removable: boolean + BlockedReason: string + }[] +} + +/** 通用部门与职务移除结果统计 */ +export interface PositionTemplateRemoveStats { + RemovedCategories: string[] + RemovedPositions: string[] + BlockedPositions: string[] +} + /** 应用/功能 API 客户端 */ export const appApi = { /** 应用市场列表(附租户/公司安装/个人启用/申请状态) */ @@ -109,6 +171,61 @@ export const appApi = { /** 租户管理员:待审批申请列表 */ pending: (): Promise<{ list: PendingItem[]; total: number }> => get<{ list: PendingItem[]; total: number }>('/API/App/Pending'), + /** 管理员:设置应用严格职务可见开关(1=严格,必须绑定职务才可见;0=宽松) */ + setStrictVisible: (AppUUID: string, StrictPositionVisible: number): Promise<{ msg: string }> => + post<{ msg: string }>('/API/App/SetStrictVisible', { AppUUID, StrictPositionVisible }), + + /** 管理员:应用虚拟职务↔真实职务映射(配置页读取;CompanyUUID 指定公司范围,空=当前公司) */ + getVirtualMapping: (AppUUID: string, CompanyUUID?: string): Promise<{ VirtualPositions: VirtualMappingItem[]; AllPositions: PositionOption[] }> => + get<{ VirtualPositions: VirtualMappingItem[]; AllPositions: PositionOption[] }>( + '/API/App/VirtualMapping', + CompanyUUID ? { AppUUID, CompanyUUID } : { AppUUID }, + ), + + /** 管理员:重写虚拟职务的真实职务绑定(配置页保存;CompanyUUID 指定公司范围,空=当前公司) */ + setVirtualMapping: ( + AppUUID: string, + Mappings: { AppVirtualPositionUUID: string; PositionUUIDs: string[] }[], + CompanyUUID?: string, + ): Promise<{ msg: string }> => + post<{ msg: string }>('/API/App/VirtualMapping', { + AppUUID, + Mappings, + ...(CompanyUUID ? { CompanyUUID } : {}), + }), + + /** 管理员:一键绑定虚拟职务↔真实职务(按名称自动匹配;前提:职务模板已同步) */ + autoBindVirtualPositions: ( + AppUUID: string, + CompanyUUID?: string, + ): Promise<{ msg: string; Bound: string[]; Unmatched: string[] }> => + post<{ msg: string; Bound: string[]; Unmatched: string[] }>('/API/App/VirtualAutoBind', { + AppUUID, + ...(CompanyUUID ? { CompanyUUID } : {}), + }), + + /** 管理员:应用通用部门与职务模板缺失项(配置页同步前预览) */ + getPositionTemplateDiff: (AppUUID: string): Promise<{ Categories: PositionTemplateDiffItem[]; NeedSync: boolean }> => + get<{ Categories: PositionTemplateDiffItem[]; NeedSync: boolean }>( + '/API/App/PositionTemplateDiff', + { AppUUID }, + ), + + /** 管理员:同步应用声明的通用部门与职务到当前租户(按名称,已有复用) */ + syncPositionTemplates: (AppUUID: string): Promise<{ msg: string } & PositionTemplateSyncStats> => + post<{ msg: string } & PositionTemplateSyncStats>('/API/App/PositionTemplateSync', { AppUUID }), + + /** 管理员:应用通用部门与职务可移除项预览(员工均未持有才可移除) */ + getPositionTemplateRemovePreview: (AppUUID: string): Promise<{ Categories: PositionTemplateRemoveItem[]; AnyRemovable: boolean }> => + get<{ Categories: PositionTemplateRemoveItem[]; AnyRemovable: boolean }>( + '/API/App/PositionTemplateRemovePreview', + { AppUUID }, + ), + + /** 管理员:移除应用声明的通用部门与职务(校验员工持有后删除) */ + removePositionTemplates: (AppUUID: string): Promise<{ msg: string } & PositionTemplateRemoveStats> => + post<{ msg: string } & PositionTemplateRemoveStats>('/API/App/PositionTemplateRemove', { AppUUID }), + /** 应用配置:读取(GET,CompanyUUID 可选——空=租户默认配置)/保存(POST,可指定公司) */ config: (AppUUID: string, CompanyUUID?: string): Promise<{ Configs: AppConfigItem[]; Total: number; Scope: string; CompanyUUID: string }> => get<{ Configs: AppConfigItem[]; Total: number; Scope: string; CompanyUUID: string }>( diff --git a/src/api/auth.ts b/src/api/auth.ts index e3d9682..cfd8697 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,4 +1,4 @@ -import { get, post } from './request' +import { get, post, upload } from './request' import type { UserInfo } from '@/types/api' /** 登录参数(对齐后端 LoginSerializer 字段:UserName / UserPassword) */ @@ -40,6 +40,8 @@ export const refreshToken = (): Promise<{ AccessToken: string }> => { export interface RoleItem { RoleUUID?: string RoleName: string + /** 角色原名(Role.RoleName):覆盖名 RoleName 可能被 UserRole 定制,原名供对照展示 */ + RoleNameOriginal?: string RoleStatus?: string | number AssignScope?: string RoleDesc?: string @@ -72,5 +74,23 @@ export const getCurrentUser = (silent = false): Promise => { return get('/Query/GetELTUserInfo', undefined, silent ? { silent: true } : undefined) } -/** 获取用户头像 URL(直接作为图片 src 使用) */ -export const getAvatarUrl = (): string => '/Query/GetELTUserAvatar' +/** 获取用户头像 URL(直接作为图片 src 使用;version 时间戳用于上传后强制刷新缓存) */ +export const getAvatarUrl = (version = 0): string => + version ? `/Query/GetELTUserAvatar?t=${version}` : '/Query/GetELTUserAvatar' + +/** 上传头像(multipart,字段 avatar;后端 /Modify/ModifyELTUserAvatar) */ +export const uploadAvatar = (file: File): Promise => { + const formData = new FormData() + formData.append('avatar', file) + return upload('/Modify/ModifyELTUserAvatar', formData) +} + +/** 修改个人配置(名字/邮箱/电话/性别/签名等,后端 /API/User/ModifyPersonalConfig) */ +export const modifyPersonalConfig = (data: Record): Promise<{ reload?: boolean }> => { + return post<{ reload?: boolean }>('/API/User/ModifyPersonalConfig', data) +} + +/** 修改密码(旧密码 + 新密码,后端 /API/User/ModifyPassword) */ +export const modifyPassword = (data: { UserOldPassword: string; UserNewPassword: string }): Promise => { + return post('/API/User/ModifyPassword', data) +} diff --git a/src/api/form.ts b/src/api/form.ts index 98a064e..50a6076 100644 --- a/src/api/form.ts +++ b/src/api/form.ts @@ -75,4 +75,19 @@ export const formApi = { search: >(entity: string, keyword: string): Promise => { return get(`/API/FormAPI/${entity}Search`, { keyword }) }, + + /** + * 批量调整员工职务(多员工 × 多职务 批量增加/移除) + * 后端 /API/FormAPI/BatchPositions,职务统一落 UserRoleDepartmentPosition 子表 + */ + batchPositions: >(data: { + Table: 'UserRole' | 'UserRoleDepartment' | 'UserRoleWorkgroup' | 'Department' | 'Workgroup' + Mode: 'add' | 'remove' + PKs: string[] + PositionUUIDs: string[] + /** 公司上下文:后端按该公司解析主部门关联(超管跨租户也限定) */ + CompanyUUID?: string + }): Promise => { + return post('/API/FormAPI/BatchPositions', data) + }, } diff --git a/src/api/oa.ts b/src/api/oa.ts new file mode 100644 index 0000000..ca0067c --- /dev/null +++ b/src/api/oa.ts @@ -0,0 +1,31 @@ +import { get, post } from './request' + +/** OA 待审批记录(请假/出差) */ +export interface OaApprovalItem { + UUID: string + UserUUID: string + UserName: string + StartTime: string + EndTime: string + Days: string + Reason: string + Remark: string + CreateTime: string +} + +/** OA 待审批列表响应 */ +export interface OaPendingApprovals { + leave: OaApprovalItem[] + travel: OaApprovalItem[] +} + +/** OA 审批 API:当前用户作为审批人的待审批列表与审批操作 */ +export const oaApi = { + /** 待审批列表(Type: all | leave | travel) */ + pendingApprovals: (Type: 'all' | 'leave' | 'travel' = 'all'): Promise => + get('/API/Oa/PendingApprovals', { Type }), + + /** 审批操作:通过 / 驳回(仅审批人本人) */ + approve: (Type: 'leave' | 'travel', UUID: string, Approved: boolean, Remark = ''): Promise<{ msg: string }> => + post<{ msg: string }>('/API/Oa/Approve', { Type, UUID, Approved, Remark }), +} diff --git a/src/apps/index.ts b/src/apps/index.ts index bf686a9..9ef18c3 100644 --- a/src/apps/index.ts +++ b/src/apps/index.ts @@ -90,6 +90,38 @@ const builtin: Record = { FrontendType: 'legacy', LegacySrc: 'pages/private/CreateE/enterprise.html', }, + 'dingcalc-finance': { + AppCode: 'dingcalc-finance', + Title: '鼎算财务', + Icon: 'Money', + FrontendRoute: '/finance/voucher', + FrontendType: 'vue', + LegacySrc: '', + }, + 'product-order': { + AppCode: 'product-order', + Title: '产品与订单', + Icon: 'Goods', + FrontendRoute: '/inventory/products', + FrontendType: 'vue', + LegacySrc: '', + }, + ehrm: { + AppCode: 'ehrm', + Title: '人力资源管理扩展', + Icon: 'UserFilled', + FrontendRoute: '/ehrm/profile', + FrontendType: 'vue', + LegacySrc: '', + }, + oa: { + AppCode: 'oa', + Title: '办公自动化流程', + Icon: 'Files', + FrontendRoute: '/oa/leave', + FrontendType: 'vue', + LegacySrc: '', + }, } /** 远端注册表(MSYNC 上报的应用,本地内置同名优先) */ diff --git a/src/components/BatchPositionDialog.vue b/src/components/BatchPositionDialog.vue new file mode 100644 index 0000000..b8edf03 --- /dev/null +++ b/src/components/BatchPositionDialog.vue @@ -0,0 +1,477 @@ + + + + + diff --git a/src/components/CrudFormPanel.vue b/src/components/CrudFormPanel.vue index 6037d1e..ab2c2f8 100644 --- a/src/components/CrudFormPanel.vue +++ b/src/components/CrudFormPanel.vue @@ -1,9 +1,11 @@ @@ -294,6 +336,9 @@ onMounted(loadMarket)

{{ a.AppCode }}
{{ typeLabel(a.AppType).text }} + + 强制职务 + 严格可见
{{ a.AppDesc || '暂无描述' }}
@@ -308,11 +353,36 @@ onMounted(loadMarket) class="app-installed-tag" >{{ n }} + +
+ {{ statusRowText(a) }} +
- {{ marketActionDesc(a) }} - 已授权 - 已拒绝
+ + + + {{ a.RequirePositionBinding === 1 ? '强制职务' : (a.StrictPositionVisible === 1 ? '关闭严格' : '开启严格') }} + + 打开 @@ -485,6 +555,49 @@ onMounted(loadMarket) 保存公司状态 + + + +

+ 绑定后,仅拥有绑定职务的用户可见/使用该应用(未绑定的应用默认全员可见; + 标记"强制职务/严格可见"的应用在无绑定时全员不可见)。 +

+ + + + + + + +

+ 虚拟职务由应用清单声明(每个虚拟职务声明可见的应用菜单)。此处把系统实际职务绑定到虚拟职务: + 拥有被绑定实际职务的用户,将看到对应虚拟职务声明的应用菜单。 +

+ +
+ + + +
@@ -545,6 +658,11 @@ onMounted(loadMarket) background: linear-gradient(135deg, var(--el-color-primary-light-8), var(--el-color-primary-light-7)); color: var(--el-color-primary); } +// dark 主题:浅色渐变块刺眼,改用主色低透明度暗底 + 主色图标 +:global(html.dark) .app-icon { + background: rgba(64, 158, 255, 0.14); + color: var(--el-color-primary); +} .app-titles { flex: 1; min-width: 0; @@ -591,6 +709,12 @@ onMounted(loadMarket) align-items: center; gap: 8px; } +/* 状态文本独立一行(不与操作按钮同行,避免"已安装于 X 家公司"等长文本折叠) */ +.app-status-row { + display: flex; + align-items: center; + min-height: 18px; +} .spacer { flex: 1; } @@ -633,6 +757,16 @@ onMounted(loadMarket) color: var(--el-text-color-secondary); margin: 10px 0 0; } +.gs-bind-tip { + font-size: 12px; + color: var(--el-text-color-secondary); + margin: 0 0 10px; + line-height: 1.6; +} +/* 虚拟职务 Tab 内两个子表的分隔 */ +.gs-vp-sep { + height: 14px; +} /* RAM 角色授权区 */ .gs-role-block { margin-top: 16px; diff --git a/src/views/app/AppMine.vue b/src/views/app/AppMine.vue index f992e55..d6a8c98 100644 --- a/src/views/app/AppMine.vue +++ b/src/views/app/AppMine.vue @@ -163,6 +163,11 @@ onMounted(loadMyApps) background: linear-gradient(135deg, var(--el-color-primary-light-8), var(--el-color-primary-light-7)); color: var(--el-color-primary); } +// dark 主题:浅色渐变块刺眼,改用主色低透明度暗底 + 主色图标 +:global(html.dark) .app-icon { + background: rgba(64, 158, 255, 0.14); + color: var(--el-color-primary); +} .app-titles { flex: 1; min-width: 0; diff --git a/src/views/company/CompanyDetail.vue b/src/views/company/CompanyDetail.vue index 1670b55..40d93c7 100644 --- a/src/views/company/CompanyDetail.vue +++ b/src/views/company/CompanyDetail.vue @@ -9,7 +9,7 @@ * - 子表 1:Department(部门),按 CompanyUUID 过滤,点击行跳转部门详情。 * - 子表 2:Workgroup(工作组),按 CompanyUUID 过滤,点击行跳转工作组详情。 * - 子表 3:UserRole(员工关联,实际表名 UserRole),按 CompanyUUID 过滤, - * 点击行跳转员工详情(employee-detail)。 + * 点击行跳转员工详情(/system/user-detail 统一用户详情页)。 * - 管理员特权模式:URL 携带 admin_verify=1 时跨公司隔离关闭,由 DetailPage 内置 * AdminBadge 横幅统一提示,无需本页处理。 * - server / admin_verify 参数由 axios 请求拦截器统一注入,子表 CrudTable 与主表 @@ -27,6 +27,8 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import DetailPage from '@/components/DetailPage.vue' import CrudTable from '@/components/CrudTable.vue' +import EntityPicker from '@/components/EntityPicker.vue' +import BatchPositionDialog from '@/components/BatchPositionDialog.vue' import { formApi } from '@/api/form' import { upload } from '@/api/request' import { useResolveUUIDs } from '@/composables/useResolveUUIDs' @@ -171,9 +173,26 @@ function goDeptDetail(row: RowData): void { function goWorkgroupDetail(row: RowData): void { router.push({ path: '/company/workgroup-detail', query: { WorkgroupUUID: asStr(row.WorkgroupUUID) } }) } -/** 跳转员工详情(UserRole 行携带 UserUUID) */ +/** 跳转员工详情(UserRole 行携带 UserUUID;统一走系统用户详情页) */ function goEmployeeDetail(row: RowData): void { - router.push({ path: '/company/employee-detail', query: { UserUUID: asStr(row.UserUUID) } }) + router.push({ path: '/system/user-detail', query: { UserUUID: asStr(row.UserUUID) } }) +} + +// ===== 员工批量调整职务 ===== +const empTableRef = ref() +const batchDialogVisible = ref(false) +const batchPreselected = ref>([]) + +/** 打开批量职务弹窗:有选中行 → 应用到选中行;无选中 → 弹窗内按部门树选员工 */ +function openBatchDialog(): void { + const rows: Array = empTableRef.value?.selectedRows ?? [] + batchPreselected.value = rows.map((r) => ({ + pk: String(r.UserRoleUUID ?? ''), + label: + `${String((r.RoleName || r.RoleUUID_label) ?? '')}${r.UserUUID_label ? `(${String(r.UserUUID_label)})` : ''}` || + String(r.UserRoleUUID ?? ''), + })) + batchDialogVisible.value = true } // ===== 主表编辑弹窗(对齐原生 showEditDialog)===== @@ -208,6 +227,10 @@ async function openEdit(): Promise { } Object.keys(editForm).forEach((k) => delete editForm[k]) Object.assign(editForm, companyData.value) + // 实体选择回显:TenantUUID 名称(entityPicker 依赖 *_label 展示名称而非 UUID) + if (companyData.value.TenantUUID && resolvedNames.value.tenant?.[companyData.value.TenantUUID]) { + editForm.TenantUUID_label = resolvedNames.value.tenant[companyData.value.TenantUUID] + } editVisible.value = true } @@ -315,10 +338,15 @@ watch(companyUUID, () => { table="Department" title="部门" :query="{ CompanyUUID: companyUUID }" + card-mode + card-title-field="DeptName" + card-tree-parent-field="ParentDeptUUID" + :card-fields="['DeptStatus', 'PositionCategoryUUID', 'CreateTime']" + :card-value-maps="{ + DeptStatus: { 0: '禁用', 1: '启用' }, + }" + :default-page-size="1000" :selectable="true" - :pageable="true" - :max-height="400" - pk-field="DeptUUID" :on-view="goDeptDetail" /> @@ -336,14 +364,15 @@ watch(companyUUID, () => { title="工作组" :query="{ CompanyUUID: companyUUID }" :selectable="true" - :pageable="true" - :max-height="400" pk-field="WorkgroupUUID" + card-mode + card-title-field="WorkgroupName" + :card-fields="['WorkgroupEnglishName', 'WorkgroupIntroduction']" :on-view="goWorkgroupDetail" /> - + { :max-height="400" pk-field="UserRoleUUID" :on-view="goEmployeeDetail" - /> + > + + @@ -397,9 +441,19 @@ watch(companyUUID, () => { :label="col.title" :required="col.required" > + + >([]) +const batchCompany = ref('') + +/** 打开批量职务弹窗:把选中部门作为目标(后端解析其全部成员) */ +function openBatchDialog(): void { + const rows: Array = deptListRef.value?.selectedRows ?? [] + batchPreselected.value = rows.map((r) => ({ + pk: String(r.DeptUUID ?? ''), + label: String(r.DeptName ?? r.DeptUUID ?? ''), + })) + // 未勾选行时兜底当前登录用户公司,保证部门树只显示本公司(超管也不混入其它公司) + batchCompany.value = String(rows[0]?.CompanyUUID ?? userStore.adminCheck?.current_company_uuid ?? '') + batchDialogVisible.value = true +} + diff --git a/src/views/company/DepartmentDetail.vue b/src/views/company/DepartmentDetail.vue index d652930..ed0a412 100644 --- a/src/views/company/DepartmentDetail.vue +++ b/src/views/company/DepartmentDetail.vue @@ -24,6 +24,7 @@ import DetailPage from '@/components/DetailPage.vue' import InfoGrid from '@/components/InfoGrid.vue' import CrudTable from '@/components/CrudTable.vue' import EntityPicker from '@/components/EntityPicker.vue' +import BatchPositionDialog from '@/components/BatchPositionDialog.vue' import { formApi } from '@/api/form' import { useResolveUUIDs } from '@/composables/useResolveUUIDs' import { setPageTitle } from '@/utils/serverScope' @@ -154,6 +155,23 @@ function goUserDetail(row: Record): void { router.push({ path: '/system/user-detail', query: { UserUUID: row.UserUUID || '' } }) } +// === 批量调整职务 === +const deptTableRef = ref() +const batchDialogVisible = ref(false) +const batchPreselected = ref>([]) + +/** 打开批量职务弹窗:有选中行 → 应用到选中行;无选中 → 弹窗内选部门员工 */ +function openBatchDialog(): void { + const rows: Array> = deptTableRef.value?.selectedRows ?? [] + batchPreselected.value = rows.map((r) => ({ + pk: String(r.UserRoleDepartmentUUID ?? ''), + label: + `${String((r.RoleNameCover || r.UserRoleUUID_label) ?? '')}${r.UserUUID_label ? `(${String(r.UserUUID_label)})` : ''}` || + String(r.UserRoleUUID ?? ''), + })) + batchDialogVisible.value = true +} + onMounted(async () => { await Promise.all([loadData(), loadColumns()]) }) @@ -193,6 +211,9 @@ watch(deptUUID, () => { {{ parentDeptName || data?.ParentDeptUUID || '-' }} + + {{ data?.PositionCategoryUUID_label || data?.PositionCategoryUUID || '-' }} + {{ data?.DeptSort ?? '-' }} {{ formatTime(data?.DeptCreateTime, false) }} @@ -207,6 +228,7 @@ watch(deptUUID, () => { { pageable :max-height="400" :on-view="goUserDetail" - /> + > + + diff --git a/src/views/company/EmployeeDetail.vue b/src/views/company/EmployeeDetail.vue deleted file mode 100644 index 5af11ab..0000000 --- a/src/views/company/EmployeeDetail.vue +++ /dev/null @@ -1,448 +0,0 @@ - - - - - diff --git a/src/views/company/Role.vue b/src/views/company/Role.vue index e2a0fec..3f4c1ff 100644 --- a/src/views/company/Role.vue +++ b/src/views/company/Role.vue @@ -62,6 +62,14 @@ function handleView(row: RoleRow): void { pageable stretch selectable + card-mode + card-title-field="RoleName" + :card-fields="['RoleType', 'RoleStatus', 'AssignScope', 'RoleDesc', 'CreateTime']" + :card-value-maps="{ + RoleType: { 0: '组员', 1: '组长', 2: '其他' }, + RoleStatus: { 0: '禁用', 1: '启用' }, + AssignScope: { 0: '全租户', 1: '公司', 2: '部门' }, + }" :on-view="handleView" />
diff --git a/src/views/company/TenantDetail.vue b/src/views/company/TenantDetail.vue index 7ed0893..39ba0b5 100644 --- a/src/views/company/TenantDetail.vue +++ b/src/views/company/TenantDetail.vue @@ -350,8 +350,15 @@ watch(tenantUUID, () => { :query="{ TenantUUID: tenantUUID }" :pk-field="'CompanyUUID'" selectable - pageable - :max-height="400" + card-mode + card-title-field="CompanyName" + card-icon-field="CompanyIcon" + card-tree-parent-field="ParentCompanyUUID" + :default-page-size="1000" + :card-fields="['CompanyShort', 'CompanyStatus', 'ParentCompanyUUID', 'CreateTime']" + :card-value-maps="{ + CompanyStatus: { 0: '禁用', 1: '启用' }, + }" :on-view="goCompanyDetail" /> diff --git a/src/views/company/Workgroup.vue b/src/views/company/Workgroup.vue index e84fe8d..e713f7c 100644 --- a/src/views/company/Workgroup.vue +++ b/src/views/company/Workgroup.vue @@ -24,18 +24,23 @@ * - role_scope 细粒度判断暂用 userStore.isAdmin 二分(TODO 待 checkAdmin 接入) * - onView 跳 Vue 路由 /company/workgroup-detail?WorkgroupUUID=xxx(替代 GERP.Navigate) */ +import { ref } from 'vue' import { useRouter } from 'vue-router' import CrudTable from '@/components/CrudTable.vue' +import BatchPositionDialog from '@/components/BatchPositionDialog.vue' +import { useUserStore } from '@/stores/user' import { setPageTitle } from '@/utils/serverScope' setPageTitle('工作组管理') const router = useRouter() +const userStore = useUserStore() // 工作组记录类型(仅声明本页用到的字段) interface WorkgroupRow { WorkgroupUUID?: string WorkgroupName?: string + CompanyUUID?: string [k: string]: unknown } @@ -46,11 +51,30 @@ function handleView(row: WorkgroupRow): void { query: { WorkgroupUUID: row.WorkgroupUUID || '' }, }) } + +// ===== 批量调整职务:选中工作组 → 应用到其全部成员 ===== +const wgListRef = ref() +const batchDialogVisible = ref(false) +const batchPreselected = ref>([]) +const batchCompany = ref('') + +/** 打开批量职务弹窗:把选中工作组作为目标(后端解析其全部成员) */ +function openBatchDialog(): void { + const rows: Array = wgListRef.value?.selectedRows ?? [] + batchPreselected.value = rows.map((r) => ({ + pk: String(r.WorkgroupUUID ?? ''), + label: String(r.WorkgroupName ?? r.WorkgroupUUID ?? ''), + })) + // 未勾选行时兜底当前登录用户公司,保证工作组树只显示本公司(超管也不混入其它公司) + batchCompany.value = String(rows[0]?.CompanyUUID ?? userStore.adminCheck?.current_company_uuid ?? '') + batchDialogVisible.value = true +} diff --git a/src/views/company/WorkgroupDetail.vue b/src/views/company/WorkgroupDetail.vue index 811ffd8..48c7275 100644 --- a/src/views/company/WorkgroupDetail.vue +++ b/src/views/company/WorkgroupDetail.vue @@ -23,6 +23,7 @@ import DetailPage from '@/components/DetailPage.vue' import InfoGrid from '@/components/InfoGrid.vue' import CrudTable from '@/components/CrudTable.vue' import EntityPicker from '@/components/EntityPicker.vue' +import BatchPositionDialog from '@/components/BatchPositionDialog.vue' import { formApi } from '@/api/form' import { useResolveUUIDs } from '@/composables/useResolveUUIDs' import { setPageTitle } from '@/utils/serverScope' @@ -151,6 +152,23 @@ function goUserDetail(row: Record): void { router.push({ path: '/system/user-detail', query: { UserUUID: row.UserUUID || '' } }) } +// === 批量调整职务 === +const wgTableRef = ref() +const batchDialogVisible = ref(false) +const batchPreselected = ref>([]) + +/** 打开批量职务弹窗:有选中行 → 应用到选中行;无选中 → 弹窗内选工作组成员 */ +function openBatchDialog(): void { + const rows: Array> = wgTableRef.value?.selectedRows ?? [] + batchPreselected.value = rows.map((r) => ({ + pk: String(r.UserRoleWorkgroupUUID ?? ''), + label: + `${String((r.RoleNameCover || r.UserRoleUUID_label) ?? '')}${r.UserUUID_label ? `(${String(r.UserUUID_label)})` : ''}` || + String(r.UserRoleUUID ?? ''), + })) + batchDialogVisible.value = true +} + onMounted(async () => { await Promise.all([loadData(), loadColumns()]) }) @@ -199,6 +217,7 @@ watch(workgroupUUID, () => { { pageable :max-height="400" :on-view="goUserDetail" - /> + > + + diff --git a/src/views/ehrm/Assessment.vue b/src/views/ehrm/Assessment.vue new file mode 100644 index 0000000..b4162c4 --- /dev/null +++ b/src/views/ehrm/Assessment.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/ehrm/Performance.vue b/src/views/ehrm/Performance.vue new file mode 100644 index 0000000..21855f9 --- /dev/null +++ b/src/views/ehrm/Performance.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/ehrm/Profile.vue b/src/views/ehrm/Profile.vue new file mode 100644 index 0000000..4e1bcbf --- /dev/null +++ b/src/views/ehrm/Profile.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/finance/Arap.vue b/src/views/finance/Arap.vue new file mode 100644 index 0000000..9d11283 --- /dev/null +++ b/src/views/finance/Arap.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/finance/Budget.vue b/src/views/finance/Budget.vue new file mode 100644 index 0000000..cc26cc4 --- /dev/null +++ b/src/views/finance/Budget.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/finance/Cashier.vue b/src/views/finance/Cashier.vue new file mode 100644 index 0000000..5ef096b --- /dev/null +++ b/src/views/finance/Cashier.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/finance/Ledger.vue b/src/views/finance/Ledger.vue new file mode 100644 index 0000000..7471a65 --- /dev/null +++ b/src/views/finance/Ledger.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/finance/Reimburse.vue b/src/views/finance/Reimburse.vue new file mode 100644 index 0000000..930f2ec --- /dev/null +++ b/src/views/finance/Reimburse.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/finance/Report.vue b/src/views/finance/Report.vue new file mode 100644 index 0000000..2b7a4b2 --- /dev/null +++ b/src/views/finance/Report.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/src/views/finance/Voucher.vue b/src/views/finance/Voucher.vue new file mode 100644 index 0000000..39f8e72 --- /dev/null +++ b/src/views/finance/Voucher.vue @@ -0,0 +1,346 @@ + + + + + diff --git a/src/views/hr/Position.vue b/src/views/hr/Position.vue new file mode 100644 index 0000000..843f88f --- /dev/null +++ b/src/views/hr/Position.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/src/views/hr/PositionCategory.vue b/src/views/hr/PositionCategory.vue new file mode 100644 index 0000000..90f07ca --- /dev/null +++ b/src/views/hr/PositionCategory.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/src/views/oa/Announcement.vue b/src/views/oa/Announcement.vue new file mode 100644 index 0000000..8c8bd61 --- /dev/null +++ b/src/views/oa/Announcement.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/oa/Approval.vue b/src/views/oa/Approval.vue new file mode 100644 index 0000000..e42e460 --- /dev/null +++ b/src/views/oa/Approval.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/src/views/oa/Leave.vue b/src/views/oa/Leave.vue new file mode 100644 index 0000000..389c4ad --- /dev/null +++ b/src/views/oa/Leave.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/oa/Meeting.vue b/src/views/oa/Meeting.vue new file mode 100644 index 0000000..57f9236 --- /dev/null +++ b/src/views/oa/Meeting.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/oa/Report.vue b/src/views/oa/Report.vue new file mode 100644 index 0000000..5c57a5f --- /dev/null +++ b/src/views/oa/Report.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/oa/Travel.vue b/src/views/oa/Travel.vue new file mode 100644 index 0000000..4863bd1 --- /dev/null +++ b/src/views/oa/Travel.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/views/system/UserDetail.vue b/src/views/system/UserDetail.vue index a664163..701d4c8 100644 --- a/src/views/system/UserDetail.vue +++ b/src/views/system/UserDetail.vue @@ -5,13 +5,16 @@ * 迁移自 TempLate/GERP/pages/system/user-detail.html * * 业务结构: - * 1. 基本信息(InfoGrid):用户名 / 邮箱 / 邮箱验证 / 电话 / 性别 / 账号状态 / 职位状态 / 是否员工 / 是否超级用户 / 创建时间 / 最后登录 - * 2. 2FA 双因素信息卡(TODO:GetUser2FAInfo 接口契约确认后补全字段渲染) + * 1. 基本信息(头像 + InfoGrid):用户名 / 真实姓名 / 邮箱 / 邮箱验证 / 电话 / 性别 / 账号状态 / 职位状态 / 是否员工 / 是否超级用户 / 创建时间 / 最后登录 + * 2. 2FA 双因素信息卡(IsEnabled / Verified / LastUsedAt / CreateTime) * 3. 角色与组织子表(CrudTable: UserRole,按 UserUUID 过滤) - * 4. 部门关联子表(CrudTable: UserDept,按 UserUUID 过滤) + * 4. 部门职务子表(CrudTable: UserRoleDepartment,按 UserUUID 反查该用户部门挂靠并设置职务) * 5. 隔离提示横幅:管理员特权(跨公司)vs 仅当前公司 * 6. 编辑按钮:基于列定义的动态编辑弹窗(formApi.update) * + * 注:本页为公司"员工详情"(employee-detail) 与系统"用户详情"的合并页 + * (统一用户详情入口,原 /company/employee-detail 路由重定向至此)。 + * * 迁移要点(对齐工程迁移规则): * - server / admin_verify 由 request.ts 拦截器统一注入,不再手动拼 buildApiUrl * - 主表单条加载:formApi.list('User', { UserUUID, pageSize: 1 }) 取首条 @@ -30,7 +33,7 @@ import EntityPicker from '@/components/EntityPicker.vue' import { formApi } from '@/api/form' import { get } from '@/api/request' import { useUserStore } from '@/stores/user' -import { setPageTitle } from '@/utils/serverScope' +import { getServerScopeParams, setPageTitle } from '@/utils/serverScope' import { formatUtcTime as formatTime } from '@/utils/dateTime' import type { ColumnSchema } from '@/types/api' @@ -58,6 +61,16 @@ const PK_FIELD = 'UserUUID' // 页面标题:有数据时显示用户名 const title = computed(() => data.value?.UserName || '用户详情') +// 头像 URL:手动附加 server / admin_verify 作用域参数(el-avatar 直接以 img 请求,不走 axios 拦截器) +const avatarUrl = computed(() => { + const params = new URLSearchParams() + params.set('UserUUID', userUUID.value) + const scope = getServerScopeParams() + if (scope.server) params.set('server', scope.server) + if (scope.admin_verify) params.set('admin_verify', scope.admin_verify) + return `/Query/GetELTUserAvatar?${params.toString()}` +}) + // 隔离提示:管理员跨公司 vs 仅当前公司 // TODO: 接入 checkAdmin → role_scope,补 dept_manager / workgroup_lead 分支 const isolationHint = computed(() => @@ -193,11 +206,6 @@ function goRoleDetail(row: Record): void { router.push({ path: '/system/role-detail', query: { RoleUUID: row.RoleUUID || '' } }) } -/** UserDept 子表查看 → 部门详情 */ -function goDeptDetail(row: Record): void { - router.push({ path: '/company/department-detail', query: { DeptUUID: row.DeptUUID || '' } }) -} - onMounted(async () => { await Promise.all([loadData(), loadColumns()]) void loadTwoFactor() @@ -232,42 +240,59 @@ watch(userUUID, () => { /> @@ -404,4 +486,22 @@ watch(userUUID, () => {