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 0000000..4386f15 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ 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, () => {