Merge remote-tracking branch 'origin/main'

This commit is contained in:
Vic 2025-07-31 22:20:22 +08:00
commit b8ffc08513
32 changed files with 19110 additions and 538 deletions

View File

@ -3,7 +3,9 @@
VITE_API_PREFIX = '/dev-api'
# 接口地址
VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
# VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
# VITE_API_BASE_URL = 'http://localhost:8888/'
VITE_API_BASE_URL = 'http://10.18.34.213:8888/'
# 接口地址 (WebSocket)
VITE_API_WS_URL = 'ws://localhost:8000'

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -2,6 +2,5 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/Industrial-image-management-system---web" vcs="Git" />
</component>
</project>

16854
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -143,6 +143,9 @@ importers:
xgplayer:
specifier: ^2.31.6
version: 2.32.6
xlsx:
specifier: ^0.18.5
version: 0.18.5
devDependencies:
'@antfu/eslint-config':
specifier: ^2.16.3
@ -1481,6 +1484,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
aieditor@1.0.13:
resolution: {integrity: sha512-A1NIydCJgno3VvEKWPyHZlS7IF5FwBO1X4QO3GEKNcs8wMmmVGbcoVDPHON3uo9bTKaxuuIiONyfLCGHLBpW2Q==}
@ -1688,6 +1695,10 @@ packages:
capital-case@1.0.4:
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chalk@1.1.3:
resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
engines: {node: '>=0.10.0'}
@ -1759,6 +1770,10 @@ packages:
codemirror@6.0.1:
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
collection-visit@1.0.0:
resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==}
engines: {node: '>=0.10.0'}
@ -1849,6 +1864,11 @@ packages:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
@ -2655,6 +2675,10 @@ packages:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fragment-cache@0.2.1:
resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
engines: {node: '>=0.10.0'}
@ -4106,6 +4130,10 @@ packages:
resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
engines: {node: '>=0.10.0'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
stable@0.1.8:
resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
@ -4687,10 +4715,18 @@ packages:
resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==}
engines: {node: '>=12'}
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@ -4713,6 +4749,11 @@ packages:
resolution: {integrity: sha512-ESwYYcG8SQciPaN43tZkN3r0dS/jQ5RtyxyGbxn2+qcKgZQ861M899xq8Cab/z6qVVX+/4eIsxDbm3lfYGYzvA==}
hasBin: true
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
xml-name-validator@4.0.0:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
@ -6166,6 +6207,8 @@ snapshots:
acorn@8.11.3: {}
adler-32@1.3.1: {}
aieditor@1.0.13(@tiptap/extension-code-block@2.5.8(@tiptap/core@2.5.8(@tiptap/pm@2.5.8))(@tiptap/pm@2.5.8)):
dependencies:
'@tiptap/core': 2.5.8(@tiptap/pm@2.5.8)
@ -6421,6 +6464,11 @@ snapshots:
tslib: 2.6.2
upper-case-first: 2.0.2
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chalk@1.1.3:
dependencies:
ansi-styles: 2.2.1
@ -6521,6 +6569,8 @@ snapshots:
transitivePeerDependencies:
- '@lezer/common'
codepage@1.15.0: {}
collection-visit@1.0.0:
dependencies:
map-visit: 1.0.0
@ -6608,6 +6658,8 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
crc-32@1.2.2: {}
crelt@1.0.6: {}
cron-parser@4.9.0:
@ -7548,6 +7600,8 @@ snapshots:
combined-stream: 1.0.8
mime-types: 2.1.35
frac@1.1.2: {}
fragment-cache@0.2.1:
dependencies:
map-cache: 0.2.2
@ -9049,6 +9103,10 @@ snapshots:
dependencies:
extend-shallow: 3.0.2
ssf@0.11.2:
dependencies:
frac: 1.1.2
stable@0.1.8: {}
static-extend@0.1.2:
@ -9717,8 +9775,12 @@ snapshots:
dependencies:
string-width: 5.1.2
wmf@1.0.2: {}
word-wrap@1.2.5: {}
word@0.3.0: {}
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@ -9755,6 +9817,16 @@ snapshots:
fs-extra: 5.0.0
xgplayer-subtitles: 1.0.19
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
xml-name-validator@4.0.0: {}
y18n@5.0.8: {}

View File

@ -17,14 +17,13 @@
<script setup lang="ts">
import { useAppStore, useUserStore } from '@/stores'
// 1
defineOptions({ name: 'App' })
const userStore = useUserStore()
const appStore = useAppStore()
appStore.initTheme()
appStore.initSiteConfig()
</script>
<style scoped lang="scss">
.loading-icon {
animation: arco-loading-circle 1s infinite cubic-bezier(0,0,1,1);

View File

@ -6,6 +6,7 @@ import type { AttachInfoData, BusinessTypeResult } from './type'
*
* @param businessType
* @param files
* @returns
*/
export function batchAddAttachment(businessType: string, formData: FormData) {
return request<AttachInfoData[]>({

View File

@ -1,64 +1,64 @@
/** 用户信息响应类型 */
export interface UserInfoResponse {
code: number;
status: number;
success: boolean;
msg: string;
code: number
status: number
success: boolean
msg: string
data: {
user: UserDetail;
dept: DeptDetail;
roles: RoleDetail[];
posts: any[];
};
user: UserDetail
dept: DeptDetail
roles: RoleDetail[]
posts: any[]
}
}
/** 用户详细信息 */
export interface UserDetail {
userId: string;
account: string;
name: string;
status: number;
userCode: string;
userStatus: string;
userType: string;
mobile: string;
createTime: string;
avatar?: string;
userId: string
account: string
name: string
status: number
userCode: string
userStatus: string
userType: string
mobile: string
createTime: string
avatar?: string
}
/** 部门详细信息 */
export interface DeptDetail {
deptId: string;
deptName: string;
parentId: string;
orderNum: number;
leaderId: string;
status: string;
deptId: string
deptName: string
parentId: string
orderNum: number
leaderId: string
status: string
}
/** 角色详细信息 */
export interface RoleDetail {
roleId: string;
roleName: string;
roleCode: string | null;
roleKey: string;
roleId: string
roleName: string
roleCode: string | null
roleKey: string
}
/** 用户类型 - 兼容旧版本 */
export interface UserInfo {
id: string;
username: string;
nickname: string;
gender: 0 | 1 | 2;
email: string;
phone: string;
avatar: string;
pwdResetTime: string;
pwdExpired: boolean;
registrationDate: string;
deptName: string;
roles: string[];
permissions: string[];
id: string
username: string
nickname: string
gender: 0 | 1 | 2
email: string
phone: string
avatar: string
pwdResetTime: string
pwdExpired: boolean
registrationDate: string
deptName: string
roles: string[]
permissions: string[]
}
/** 路由类型 */

View File

@ -16,6 +16,7 @@ export * as InsuranceTypeAPI from './insurance-type'
export * as HealthRecordAPI from './health-record'
export * as InsuranceFileAPI from './insurance-file'
export * as EmployeeAPI from './employee'
export * as RegulationAPI from './regulation'
export * from './area/type'
export * from './auth/type'

View File

@ -23,9 +23,9 @@ export function deleteTaskGroup(id: number) {
return http.del(`${BASE_URL}/group/${id}`)
}
/** @desc 查询任务列表 */
export function listTask(query: T.TaskPageQuery) {
return http.get<PageRes<T.TaskResp[]>>(`${BASE_URL}`, query)
/** @desc 查询任务列表(标准导出) */
export const listTask = (params: any) => {
return http.get('/project-task/list', params)
}
/** @desc 获取任务详情 */

View File

@ -0,0 +1,58 @@
import http from '@/utils/http'
// 制度管理API接口
export const regulationApi = {
// 获取制度列表
getRegulationList: (params: {
page: number
size: number
}) => {
return http.get('/regulation', params)
},
// 获取制度详情
getRegulationDetail: (regulationId: string) => {
return http.get(`/regulation/${regulationId}`)
},
// 创建制度提案
createProposal: (data: {
title: string
content: string
regulationType: string
scope: string
level: string
remark?: string
}) => {
return http.post('/regulation/proposal', data)
},
// 更新制度提案
updateProposal: (regulationId: string, data: any) => {
return http.put(`/regulation/proposal/${regulationId}`, data)
},
// 删除制度提案
deleteProposal: (regulationId: string) => {
return http.del(`/regulation/proposal/${regulationId}`)
},
// 发布制度
publishRegulation: (regulationId: string) => {
return http.post(`/regulation/${regulationId}/publish`)
},
// 获取已发布制度列表
getPublishedRegulationList: (params: {
page: number
size: number
status: string
}) => {
return http.get('/regulation', params)
},
// 确认制度知晓
confirmRegulation: (regulationId: string) => {
return http.post(`/regulation/${regulationId}/confirm`)
}
}

View File

@ -0,0 +1,57 @@
// 制度状态枚举
export enum RegulationStatus {
DRAFT = 'DRAFT', // 草稿
VOTING = 'VOTING', // 投票中
APPROVED = 'APPROVED', // 已通过
REJECTED = 'REJECTED', // 已否决
PUBLISHED = 'PUBLISHED', // 已发布
ARCHIVED = 'ARCHIVED' // 已归档
}
// 制度级别枚举
export enum RegulationLevel {
LOW = 'LOW', // 低
MEDIUM = 'MEDIUM', // 中
HIGH = 'HIGH' // 高
}
// 制度信息接口
export interface Regulation {
regulationId: string
title: string
content: string
regulationType: string
status: RegulationStatus
publisherId: string
publisherName: string
publishTime: string
effectiveTime: string
expireTime: string
scope: string
level: RegulationLevel
version: string
remark?: string
createBy: string
updateBy: string
createTime: string
updateTime: string
page: number
pageSize: number
delFlag: string
}
// 创建提案请求接口
export interface CreateProposalRequest {
title: string
content: string
regulationType: string
scope: string
level: RegulationLevel
remark?: string
}
// 分页参数接口
export interface PaginationParams {
page: number
size: number
}

View File

@ -4,6 +4,7 @@ import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/system/role'
const BASE_URL_NEW = '/role'
/** @desc 查询角色列表(已废弃) */
export function listRole(query: T.RoleQuery) {
@ -72,7 +73,7 @@ export function updateRolePermission(id: string, data: any) {
/** @desc 查询角色关联用户 */
export function listRoleUser(id: string, query: T.RoleUserPageQuery) {
return http.get<PageRes<T.RoleUserResp[]>>(`${BASE_URL}/${id}/user`, query)
return http.get<PageRes<T.RoleUserResp[]>>(`${BASE_URL_NEW}/${id}/user`, query)
}
/** @desc 分配角色给用户 */
@ -87,5 +88,5 @@ export function unassignFromUsers(userRoleIds: Array<string | number>) {
/** @desc 查询角色关联用户 ID */
export function listRoleUserId(id: string) {
return http.get(`${BASE_URL}/${id}/user/id`)
return http.get(`${BASE_URL_NEW}/${id}/user`)
}

View File

@ -27,202 +27,258 @@ export const systemRoutes: RouteRecordRaw[] = [
// ],
// },
{
path: '/organization',
name: 'Organization',
path: '/regulation',
name: 'Regulation',
component: Layout,
redirect: '/organization/hr/member',
meta: { title: '组织架构', icon: 'user-group', hidden: false, sort: 2 },
redirect: '/regulation/system-regulation',
meta: { title: '制度管理', icon: 'file-text', hidden: false, sort: 1.5 },
children: [
{
path: '/organization/hr',
name: 'HRManagement',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/organization/hr/member',
meta: { title: '人员管理', icon: 'user', hidden: false },
children: [
{
path: '/organization/hr/member',
name: 'HRMember',
component: () => import('@/views/system/user/index.vue'),
meta: { title: '成员', icon: 'user', hidden: false },
},
{
path: '/organization/hr/dept',
name: 'HRDept',
component: () => import('@/views/system/dept/index.vue'),
meta: { title: '部门', icon: 'dept', hidden: false },
},
{
path: '/organization/hr/workload',
name: 'HRWorkload',
component: () => import('@/views/hr/workload/index.vue'),
meta: { title: '工作量', icon: 'workload', hidden: false },
},
{
path: '/organization/hr/attendance',
name: 'HRAttendance',
component: () => import('@/views/hr/attendance/index.vue'),
meta: { title: '考勤', icon: 'attendance', hidden: false },
},
{
path: '/organization/hr/performance',
name: 'HRPerformance',
component: () => import('@/components/ParentView/index.vue'),
meta: { title: '绩效', icon: 'performance', hidden: false },
children: [
{
path: '/organization/hr/performance/dimention',
name: 'Dimention',
component: () => import('@/views/performance/setting/index.vue'),
meta: { title: '绩效维度', icon: 'performance', hidden: false },
},
{
path: '/organization/hr/performance/rule',
name: 'Rule',
component: () => import('@/views/performance/rule.vue'),
meta: { title: '绩效细则', icon: 'performance', hidden: false },
},
{
path: '/organization/hr/performance/my',
name: 'MyPerformance',
component: () => import('@/views/performance/my.vue'),
meta: { title: '我的绩效', icon: 'performance', hidden: false },
},
],
},
{
path: '/organization/hr/salary',
name: 'HRSalary',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/organization/hr/salary/overview',
meta: { title: '工资', icon: 'salary', hidden: false },
children: [
{
path: '/organization/hr/salary/overview',
name: 'HRSalaryOverview',
component: () => import('@/components/ParentView/index.vue'),
meta: { title: '工资概览', icon: 'salary', hidden: false },
children: [
{
path: '/organization/hr/salary/payroll',
name: 'Payroll',
component: () => import('@/views/salary-management/index.vue'),
meta: { title: '工资单', icon: 'salary', hidden: false },
},
],
},
],
},
// {
// path: '/organization/hr/salary/insurance',
// name: 'HRInsurance',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/insurance/overview',
// meta: { title: '保险', icon: 'safety', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/insurance/overview',
// name: 'HRInsuranceOverview',
// component: () => import('@/views/hr/salary/insurance/overview/index.vue'),
// meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/my-insurance',
// name: 'HRMyInsurance',
// component: () => import('@/views/hr/salary/insurance/my-insurance/index.vue'),
// meta: { title: '我的保险', icon: 'shield', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/health-records',
// name: 'HRHealthRecords',
// component: () => import('@/views/hr/salary/insurance/health-records/index.vue'),
// meta: { title: '健康档案', icon: 'heart', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/policy-files',
// name: 'HRPolicyFiles',
// component: () => import('@/views/hr/salary/insurance/policy-files/index.vue'),
// meta: { title: '保单文件', icon: 'file', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/personal-info',
// name: 'HRPersonalInfo',
// component: () => import('@/views/hr/salary/insurance/personal-info/index.vue'),
// meta: { title: '个人信息', icon: 'user', hidden: false },
// }
// ]
// },
{
path: '/organization/hr/salary/system-insurance/health-management',
name: 'HRSystemHealthManagement',
component: () => import('@/views/hr/salary/system-insurance/health-management/index.vue'),
meta: { title: '健康档案管理', icon: 'heart', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance',
name: 'HRSystemInsurance',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/organization/hr/salary/system-insurance/overview',
meta: { title: '人员保险', icon: 'settings', hidden: false },
children: [
{
path: '/organization/hr/salary/system-insurance/overview',
name: 'HRSystemInsuranceOverview',
component: () => import('@/views/hr/salary/system-insurance/overview/index.vue'),
meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/management',
name: 'HRSystemInsuranceManagement',
component: () => import('@/views/hr/salary/system-insurance/management/index.vue'),
meta: { title: '保险管理', icon: 'shield', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/file-management',
name: 'HRSystemFileManagement',
component: () => import('@/views/hr/salary/system-insurance/file-management/index.vue'),
meta: { title: '保单文件管理', icon: 'file', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/company-management',
name: 'HRSystemCompanyManagement',
component: () => import('@/views/hr/salary/system-insurance/company-management/index.vue'),
meta: { title: '保险公司管理', icon: 'building', hidden: false },
},
{
path: '/organization/hr/salary/system-insurance/type-management',
name: 'HRSystemTypeManagement',
component: () => import('@/views/hr/salary/system-insurance/type-management/index.vue'),
meta: { title: '保险类型管理', icon: 'category', hidden: false },
},
],
},
{
path: '/organization/hr/salary/certification',
name: 'HRCertification',
component: () => import('@/views/hr/salary/certification/index.vue'),
meta: { title: '人员资质管理', icon: 'idcard', hidden: false },
},
{
path: '/organization/hr/contribution',
name: 'HRContribution',
component: () => import('@/views/hr/contribution/index.vue'),
meta: { title: '责献积分制度', icon: 'contribution', hidden: false },
},
],
path: '/regulation/system-regulation',
name: 'SystemRegulation',
component: () => import('@/views/regulation/repository.vue'),
meta: { title: '制度确认', icon: 'file-text', hidden: false },
},
{
path: '/organization/role',
name: 'OrganizationRole',
component: () => import('@/views/system/role/index.vue'),
meta: { title: '角色管理', icon: 'role', hidden: false },
path: '/regulation/process-management',
name: 'ProcessManagement',
component: () => import('@/views/regulation/confirm.vue'),
meta: { title: '流程管理', icon: 'workflow', hidden: false },
},
],
},
{
path: '/organization',
name: 'Organization',
component: Layout,
redirect: '/organization/dept',
meta: { title: '组织架构', icon: 'user-group', hidden: false, sort: 2 },
children: [
{
path: '/organization/user',
name: 'OrganizationUser',
component: () => import('@/views/system/user/index.vue'),
meta: { title: '用户管理', icon: 'user', hidden: false, sort: 2.25 },
},
{
path: '/organization/dept',
name: 'OrganizationDept',
component: () => import('@/views/system/dept/index.vue'),
meta: { title: '部门管理', icon: 'mind-mapping', hidden: false, sort: 2.5 },
},
{
path: '/organization/post',
name: 'OrganizationPost',
component: () => import('@/views/system/post/index.vue'),
meta: { title: '岗位管理', icon: 'nav', hidden: false, sort: 2.75 },
},
{
path: '/organization/hr/workload',
name: 'HRWorkload',
component: () => import('@/views/hr/workload/index.vue'),
meta: { title: '任务管理', icon: 'bookmark', hidden: false },
},
],
},
// {
// path: '/organization',
// name: 'Organization',
// component: Layout,
// redirect: '/organization/hr/member',
// meta: { title: '组织架构', icon: 'user-group', hidden: false, sort: 2 },
// children: [
// {
// path: '/organization/hr',
// name: 'HRManagement',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/member',
// meta: { title: '人员管理', icon: 'user', hidden: false },
// children: [
// {
// path: '/organization/hr/member',
// name: 'HRMember',
// component: () => import('@/views/system/user/index.vue'),
// meta: { title: '成员', icon: 'user', hidden: false },
// },
// {
// path: '/organization/hr/dept',
// name: 'HRDept',
// component: () => import('@/views/system/dept/index.vue'),
// meta: { title: '部门', icon: 'dept', hidden: false },
// },
// {
// path: '/organization/hr/workload',
// name: 'HRWorkload',
// component: () => import('@/views/hr/workload/index.vue'),
// meta: { title: '任务管理', icon: 'workload', hidden: false },
// },
// {
// path: '/organization/hr/attendance',
// name: 'HRAttendance',
// component: () => import('@/views/hr/attendance/index.vue'),
// meta: { title: '考勤', icon: 'attendance', hidden: false },
// },
// {
// path: '/organization/hr/performance',
// name: 'HRPerformance',
// component: () => import('@/components/ParentView/index.vue'),
// meta: { title: '绩效', icon: 'performance', hidden: false },
// children: [
// {
// path: '/organization/hr/performance/dimention',
// name: 'Dimention',
// component: () => import('@/views/performance/setting/index.vue'),
// meta: { title: '绩效维度', icon: 'performance', hidden: false },
//
// },
// {
// path: '/organization/hr/performance/rule',
// name: 'Rule',
// component: () => import('@/views/performance/rule.vue'),
// meta: { title: '绩效细则', icon: 'performance', hidden: false },
//
// },
// {
// path: '/organization/hr/performance/my',
// name: 'MyPerformance',
// component: () => import('@/views/performance/my.vue'),
// meta: { title: '我的绩效', icon: 'performance', hidden: false },
//
// },
// ],
// },
// {
// path: '/organization/hr/salary',
// name: 'HRSalary',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/overview',
// meta: { title: '工资', icon: 'salary', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/overview',
// name: 'HRSalaryOverview',
// component: () => import('@/components/ParentView/index.vue'),
// meta: { title: '工资概览', icon: 'salary', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/payroll',
// name: 'Payroll',
// component: () => import('@/views/salary-management/index.vue'),
// meta: { title: '工资单', icon: 'salary', hidden: false },
//
// },
// ],
// },
// ],
// },
//
// {
// path: '/organization/hr/salary/insurance',
// name: 'HRInsurance',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/insurance/overview',
// meta: { title: '保险', icon: 'safety', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/insurance/overview',
// name: 'HRInsuranceOverview',
// component: () => import('@/views/hr/salary/insurance/overview/index.vue'),
// meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/my-insurance',
// name: 'HRMyInsurance',
// component: () => import('@/views/hr/salary/insurance/my-insurance/index.vue'),
// meta: { title: '我的保险', icon: 'shield', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/health-records',
// name: 'HRHealthRecords',
// component: () => import('@/views/hr/salary/insurance/health-records/index.vue'),
// meta: { title: '健康档案', icon: 'heart', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/policy-files',
// name: 'HRPolicyFiles',
// component: () => import('@/views/hr/salary/insurance/policy-files/index.vue'),
// meta: { title: '保单文件', icon: 'file', hidden: false },
// },
// {
// path: '/organization/hr/salary/insurance/personal-info',
// name: 'HRPersonalInfo',
// component: () => import('@/views/hr/salary/insurance/personal-info/index.vue'),
// meta: { title: '个人信息', icon: 'user', hidden: false },
// },
// ],
// },
//
// {
// path: '/organization/hr/salary/system-insurance/health-management',
// name: 'HRSystemHealthManagement',
// component: () => import('@/views/hr/salary/system-insurance/health-management/index.vue'),
// meta: { title: '健康档案管理', icon: 'heart', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance',
// name: 'HRSystemInsurance',
// component: () => import('@/components/ParentView/index.vue'),
// redirect: '/organization/hr/salary/system-insurance/overview',
// meta: { title: '人员保险', icon: 'settings', hidden: false },
// children: [
// {
// path: '/organization/hr/salary/system-insurance/overview',
// name: 'HRSystemInsuranceOverview',
// component: () => import('@/views/hr/salary/system-insurance/overview/index.vue'),
// meta: { title: '工作台概览', icon: 'dashboard', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/management',
// name: 'HRSystemInsuranceManagement',
// component: () => import('@/views/hr/salary/system-insurance/management/index.vue'),
// meta: { title: '保险管理', icon: 'shield', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/file-management',
// name: 'HRSystemFileManagement',
// component: () => import('@/views/hr/salary/system-insurance/file-management/index.vue'),
// meta: { title: '保单文件管理', icon: 'file', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/company-management',
// name: 'HRSystemCompanyManagement',
// component: () => import('@/views/hr/salary/system-insurance/company-management/index.vue'),
// meta: { title: '保险公司管理', icon: 'building', hidden: false },
// },
// {
// path: '/organization/hr/salary/system-insurance/type-management',
// name: 'HRSystemTypeManagement',
// component: () => import('@/views/hr/salary/system-insurance/type-management/index.vue'),
// meta: { title: '保险类型管理', icon: 'category', hidden: false },
// },
// ],
// },
// {
// path: '/organization/hr/salary/certification',
// name: 'HRCertification',
// component: () => import('@/views/hr/salary/certification/index.vue'),
// meta: { title: '人员资质管理', icon: 'idcard', hidden: false },
// },
// {
// path: '/organization/hr/contribution',
// name: 'HRContribution',
// component: () => import('@/views/hr/contribution/index.vue'),
// meta: { title: '责献积分制度', icon: 'contribution', hidden: false },
// },
// ],
// },
// {
// path: '/organization/role',
// name: 'OrganizationRole',
// component: () => import('@/views/system/role/index.vue'),
// meta: { title: '角色管理', icon: 'role', hidden: false },
// },
// ],
// },
{
path: '/asset-management',
name: 'AssetManagement',
@ -947,6 +1003,30 @@ export const systemRoutes: RouteRecordRaw[] = [
// }
],
},
{
path: '/user/profile',
name: 'UserProfile',
component: Layout,
redirect: '/user/profile',
meta: {
title: '个人中心',
icon: 'user',
hidden: false,
sort: 100,
},
children: [
{
path: '/user/profile',
name: 'UserProfile',
component: () => import('@/views/user/profile/index.vue'),
meta: {
title: '个人中心',
icon: 'user',
hidden: false,
},
},
],
},
{
path: '/enterprise-settings',
name: 'EnterpriseSettings',
@ -1150,7 +1230,6 @@ export const systemRoutes: RouteRecordRaw[] = [
},
],
},
{
path: '/',
redirect: '/project-management/project-template/project-aproval',
@ -1176,6 +1255,11 @@ export const constantRoutes: RouteRecordRaw[] = [
},
],
},
// {
// path: '/user/profile',
// component: () => import('@/views/user/profile/index.vue'),
// meta: { hidden: true },
// },
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/default/error/404.vue'),

View File

@ -0,0 +1,49 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export interface Regulation {
id: string
name: string
type: string
typeName: string
publisher: string
publishTime: string
effectiveDate: string
confirmStatus: 'pending' | 'confirmed'
content: string
scope: string
requirements: string
notes: string
}
export const useRegulationStore = defineStore('regulation', () => {
// 已发布的制度列表
const publishedRegulations = ref<Regulation[]>([])
// 添加新发布的制度
const addPublishedRegulation = (regulation: Regulation) => {
publishedRegulations.value.unshift(regulation)
}
// 更新制度确认状态
const updateRegulationConfirmStatus = (id: string, status: 'pending' | 'confirmed') => {
const regulation = publishedRegulations.value.find(item => item.id === id)
if (regulation) {
regulation.confirmStatus = status
}
}
// 批量确认所有制度
const confirmAllRegulations = () => {
publishedRegulations.value.forEach(regulation => {
regulation.confirmStatus = 'confirmed'
})
}
return {
publishedRegulations,
addPublishedRegulation,
updateRegulationConfirmStatus,
confirmAllRegulations
}
})

View File

@ -4,10 +4,9 @@ import type { RouteRecordRaw } from 'vue-router'
import { mapTree, toTreeArray } from 'xe-utils'
import { cloneDeep, omit } from 'lodash-es'
import { constantRoutes, systemRoutes } from '@/router/route'
import { type RouteItem, getUserRouteWithAdapter } from '@/apis'
import type { RouteItem } from '@/apis'
import { transformPathToName } from '@/utils'
import { asyncRouteModules } from '@/router/asyncModules'
import { convertMenuData, type ApiMenuItem } from '@/utils/menuConverter'
const layoutComponentMap = {
Layout: () => import('@/layout/index.vue'),
@ -94,91 +93,63 @@ const storeSetup = () => {
// 获取路由数据并已通过适配器转换
// const { data } = await getUserRouteWithAdapter()
const data = [{
"id": 1000,
"parentId": 0,
"title": "系统管理",
"type": 1,
"path": "/system",
"name": "System",
"component": "Layout",
"redirect": "/system/user",
"icon": "settings",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 1,
"children": [
{
"id": 1010,
"parentId": 1000,
"title": "用户管理",
"type": 2,
"path": "/system/user",
"name": "SystemUser",
"component": "system/user/index",
"icon": "user",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 1
},
{
"id": 1030,
"parentId": 1000,
"title": "角色管理",
"type": 2,
"path": "/system/role",
"name": "SystemRole",
"component": "system/role/index",
"icon": "user-group",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 2
},
{
"id": 1050,
"parentId": 1000,
"title": "菜单管理",
"type": 2,
"path": "/system/menu",
"name": "SystemMenu",
"component": "system/menu/index",
"icon": "menu",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 3
},
{
"id": 1070,
"parentId": 1000,
"title": "部门管理",
"type": 2,
"path": "/system/dept",
"name": "SystemDept",
"component": "system/dept/index",
"icon": "mind-mapping",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 4
},
{
"id": 1090,
"parentId": 1000,
"title": "岗位管理",
"type": 2,
"path": "/system/post",
"name": "SystemPost",
"component": "system/post/index",
"icon": "settings",
"isExternal": false,
"isCache": false,
"isHidden": false,
"sort": 5
}
]
id: 1000,
parentId: 0,
title: '系统管理',
type: 1,
path: '/system',
name: 'System',
component: 'Layout',
redirect: '/system/user',
icon: 'settings',
isExternal: false,
isCache: false,
isHidden: false,
sort: 1,
children: [
{
id: 1010,
parentId: 1000,
title: '用户管理',
type: 2,
path: '/system/user',
name: 'SystemUser',
component: 'system/user/index',
icon: 'user',
isExternal: false,
isCache: false,
isHidden: false,
sort: 1,
},
{
id: 1030,
parentId: 1000,
title: '角色管理',
type: 2,
path: '/system/role',
name: 'SystemRole',
component: 'system/role/index',
icon: 'user-group',
isExternal: false,
isCache: false,
isHidden: false,
sort: 2,
},
{
id: 1050,
parentId: 1000,
title: '菜单管理',
type: 2,
path: '/system/menu',
name: 'SystemMenu',
component: 'system/menu/index',
icon: 'menu',
isExternal: false,
isCache: false,
isHidden: false,
sort: 3,
},
],
}]
// 使用已转换的数据生成路由
const asyncRoutes = formatAsyncRoutes(data as unknown as RouteItem[])

View File

@ -1,3 +1,4 @@
<!-- 考勤统计 -->
<template>
<GiPageLayout>
<GiTable

View File

@ -1,8 +1,10 @@
<!--任务管理-->
<template>
<GiPageLayout>
<a-button type="primary" style="margin-bottom: 16px" @click="openAddModal">发布任务</a-button>
<GiTable
row-key="id"
title="工作量管理"
row-key="taskId"
title="任务记录"
:data="dataList"
:columns="tableColumns"
:loading="loading"
@ -11,38 +13,74 @@
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
@row-click="onRowClick"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增工作量记录</template>
</a-button>
<template #status="{ record }">
<a-tag :color="getTaskStatusColor(record.status)">{{ getTaskStatusText(record.status) }}</a-tag>
</template>
<!-- 工作量显示 -->
<template #workload="{ record }">
<span class="font-medium text-blue-600">{{ record.workload }}小时</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link status="danger" @click="deleteRecord(record)">删除</a-link>
<a-link @click.stop="onRowClick(record)">详情</a-link>
<a-link status="danger" @click.stop="deleteRecord(record)">删除</a-link>
</a-space>
</template>
</GiTable>
<!-- 发布任务弹窗 -->
<a-modal v-model:visible="showAddModal" title="发布任务" @ok="handleAddTask" @cancel="showAddModal = false">
<a-form :model="addForm" label-width="90px">
<a-form-item label="任务ID">
<a-input v-model="addForm.taskId" disabled />
</a-form-item>
<a-form-item label="任务描述">
<a-input v-model="addForm.remark" placeholder="请输入任务描述" />
</a-form-item>
<a-form-item label="创建时间">
<a-input v-model="addForm.createTime" disabled />
</a-form-item>
</a-form>
</a-modal>
<!-- 任务详情弹窗 -->
<a-modal v-model:visible="showDetailModal" title="任务详情" @ok="handleUpdateTask" @cancel="showDetailModal = false" :footer="false">
<a-form :model="detailForm" label-width="90px">
<a-form-item label="任务ID">
<a-input v-model="detailForm.taskId" disabled />
</a-form-item>
<a-form-item label="任务描述">
<a-input v-model="detailForm.remark" />
</a-form-item>
<a-form-item label="分配岗位">
<a-select v-model="detailForm.deptName" :options="deptOptions" allow-clear placeholder="请选择岗位" />
</a-form-item>
<a-form-item label="分配人员">
<a-select v-model="detailForm.mainUserId" :options="userOptions" allow-clear placeholder="请选择人员" />
</a-form-item>
<a-form-item label="任务状态">
<a-input :value="getTaskStatusText(detailForm.status)" disabled />
</a-form-item>
<a-form-item label="创建时间">
<a-input v-model="detailForm.createTime" disabled />
</a-form-item>
<a-form-item label="完成时间">
<a-input v-model="detailForm.finishTime" disabled />
</a-form-item>
</a-form>
<template #footer>
<a-space style="float: right">
<a-button status="danger" @click="handleDeleteTask">删除</a-button>
<a-button type="primary" @click="handleUpdateTask">确定</a-button>
</a-space>
</template>
</a-modal>
</GiPageLayout>
</template>
@ -50,13 +88,52 @@
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
// import { listTask } from '@/apis/project/task' //
//
// 0123
const statusOptions = [
{ label: '未分配', value: 0 },
{ label: '已分配', value: 1 },
{ label: '已完成', value: 2 },
{ label: '已取消', value: 3 }
]
const getTaskStatusColor = (status: number) => {
const colorMap: Record<number, string> = {
0: 'gray',
1: 'blue',
2: 'green',
3: 'red'
}
return colorMap[status] || 'gray'
}
const getTaskStatusText = (status: number) => {
const textMap: Record<number, string> = {
0: '未分配',
1: '已分配',
2: '已完成',
3: '已取消'
}
return textMap[status] || status
}
//
const deptOptions = [
{ label: '开发', value: '开发' },
{ label: '测试', value: '测试' },
{ label: '运维', value: '运维' }
]
const userOptions = [
{ label: '张三', value: '张三' },
{ label: '李四', value: '李四' },
{ label: '王五', value: '王五' }
]
//
let searchForm = reactive({
userName: '',
projectName: '',
startDate: '',
endDate: '',
taskName: '',
responsiblePerson: '', // mainUserId
status: '',
page: 1,
size: 10
})
@ -64,119 +141,241 @@ let searchForm = reactive({
//
const queryFormColumns = [
{
field: 'userName',
label: '员工姓名',
field: 'taskName',
label: '任务标题',
type: 'input' as const,
props: {
placeholder: '请输入员工姓名'
placeholder: '请输入任务标题',
style: { width: '200px' }
}
},
{
field: 'projectName',
label: '项目名称',
field: 'responsiblePerson',
label: '分配用户',
type: 'input' as const,
props: {
placeholder: '请输入项目名称'
placeholder: '请输入分配用户',
style: { width: '200px' }
}
},
{
field: 'status',
label: '任务状态',
type: 'select' as const,
props: {
placeholder: '请选择任务状态',
options: statusOptions,
style: { width: '200px' }
}
}
]
//
const tableColumns: TableColumnData[] = [
{ title: '员工姓名', dataIndex: 'userName', width: 120 },
{ title: '部门', dataIndex: 'deptName', width: 120 },
{ title: '项目名称', dataIndex: 'projectName', width: 200, ellipsis: true, tooltip: true },
{ title: '工作内容', dataIndex: 'workContent', width: 250, ellipsis: true, tooltip: true },
{ title: '工作量(小时)', dataIndex: 'workload', slotName: 'workload', width: 120 },
{ title: '工作日期', dataIndex: 'workDate', width: 120 },
{ title: '任务ID', dataIndex: 'taskId', width: 80 },
{ title: '任务标题', dataIndex: 'taskName', width: 200, ellipsis: true, tooltip: true },
{ title: '分配用户', dataIndex: 'mainUserId', width: 120 },
{ title: '分配部门', dataIndex: 'deptName', width: 120 },
{ title: '任务内容', dataIndex: 'remark', width: 250, ellipsis: true, tooltip: true },
{ title: '任务状态', dataIndex: 'status', slotName: 'status', width: 120 },
{ title: '创建时间', dataIndex: 'createTime', width: 160 },
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
userName: '张三',
deptName: '技术部',
projectName: '企业管理系统',
workContent: '前端开发',
workload: 8,
workDate: '2024-01-15',
createTime: '2024-01-15 10:30:00'
},
{
id: 2,
userName: '李四',
deptName: '技术部',
projectName: '移动端应用',
workContent: '后端接口开发',
workload: 6,
workDate: '2024-01-15',
createTime: '2024-01-15 11:20:00'
}
])
const dataList = ref<any[]>([])
const allData = ref<any[]>([])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 2,
total: 0,
showTotal: true,
showPageSize: true
})
//
const search = async () => {
loading.value = true
// API
setTimeout(() => {
loading.value = false
}, 1000)
//
const showAddModal = ref(false)
const addForm = reactive({
taskId: '',
remark: '',
createTime: ''
})
const openAddModal = () => {
addForm.taskId = Date.now().toString()
addForm.remark = ''
addForm.createTime = new Date().toLocaleString()
showAddModal.value = true
}
const handleAddTask = () => {
if (!addForm.remark) {
Message.warning('请输入任务描述')
return
}
const newTask = {
taskId: addForm.taskId,
taskName: addForm.remark,
mainUserId: '',
deptName: '',
remark: addForm.remark,
status: 0, //
createTime: addForm.createTime,
finishTime: ''
}
allData.value.unshift(newTask)
filterAndPaginate()
showAddModal.value = false
Message.success('任务发布成功')
}
const reset = () => {
//
const showDetailModal = ref(false)
const detailForm = reactive({
taskId: '',
taskName: '',
mainUserId: '',
deptName: '',
remark: '',
status: 0,
createTime: '',
finishTime: ''
})
let detailIndex = -1
const onRowClick = (record: any) => {
Object.assign(detailForm, record)
detailIndex = allData.value.findIndex(item => item.taskId === record.taskId)
showDetailModal.value = true
}
const handleUpdateTask = () => {
if (detailIndex !== -1) {
allData.value[detailIndex] = { ...detailForm }
filterAndPaginate()
showDetailModal.value = false
Message.success('任务更新成功')
}
}
const handleDeleteTask = () => {
if (detailIndex !== -1) {
allData.value.splice(detailIndex, 1)
filterAndPaginate()
showDetailModal.value = false
Message.success('任务已删除')
}
}
//
const fetchTaskList = async () => {
loading.value = true
try {
// const res = await listTask(params)
// const rows = res?.rows || res?.data?.rows || []
// allData.value = rows
//
allData.value = [
{
taskId: '1710000000000',
taskName: '示例任务A',
mainUserId: '张三',
deptName: '开发',
remark: '开发新功能',
status: 1,
createTime: '2024-05-01 10:00:00',
finishTime: ''
},
{
taskId: '1710000000001',
taskName: '示例任务B',
mainUserId: '',
deptName: '',
remark: '待分配任务',
status: 0,
createTime: '2024-05-02 11:00:00',
finishTime: ''
},
{
taskId: '1710000000002',
taskName: '示例任务C',
mainUserId: '李四',
deptName: '测试',
remark: '测试任务',
status: 2,
createTime: '2024-05-03 09:00:00',
finishTime: '2024-05-05 18:00:00'
}
]
filterAndPaginate()
pagination.total = allData.value.length
} catch (e) {
Message.error('获取任务数据失败')
} finally {
loading.value = false
}
}
//
const filterAndPaginate = () => {
let filtered = allData.value
if (searchForm.taskName) {
filtered = filtered.filter((item: any) =>
item.taskName?.toLowerCase().includes(searchForm.taskName.toLowerCase())
)
}
if (searchForm.responsiblePerson) {
filtered = filtered.filter((item: any) =>
item.mainUserId?.toLowerCase().includes(searchForm.responsiblePerson.toLowerCase())
)
}
if (searchForm.status !== '' && searchForm.status !== undefined) {
filtered = filtered.filter((item: any) => String(item.status) === String(searchForm.status))
}
//
const start = (pagination.current - 1) * pagination.pageSize
const end = start + pagination.pageSize
dataList.value = filtered.slice(start, end)
pagination.total = filtered.length
}
//
const search = async () => {
pagination.current = 1
filterAndPaginate()
}
const reset = async () => {
Object.assign(searchForm, {
userName: '',
projectName: '',
startDate: '',
endDate: '',
taskName: '',
responsiblePerson: '',
status: '',
page: 1,
size: 10
})
pagination.current = 1
search()
filterAndPaginate()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
filterAndPaginate()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
//
const openAddModal = () => {
Message.info('新增工作量记录功能开发中...')
}
const editRecord = (record: any) => {
Message.info(`编辑工作量记录: ${record.userName}`)
filterAndPaginate()
}
//
const deleteRecord = (record: any) => {
Message.info(`删除工作量记录: ${record.userName}`)
const idx = allData.value.findIndex(item => item.taskId === record.taskId)
if (idx !== -1) {
allData.value.splice(idx, 1)
filterAndPaginate()
Message.success('任务已删除')
}
}
onMounted(() => {
search()
fetchTaskList()
})
</script>

View File

@ -0,0 +1,509 @@
<template>
<div class="process-management">
<a-card title="制度提案管理" :bordered="false">
<template #extra>
<a-button type="primary" @click="handleAdd">
<template #icon>
<GiSvgIcon name="plus" />
</template>
提交制度提案
</a-button>
</template>
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template #level="{ record }">
<a-tag :color="getLevelColor(record.level)">
{{ getLevelText(record.level) }}
</a-tag>
</template>
<template #operations="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleView(record)">
查看详情
</a-button>
<a-button
v-if="record.status === RegulationStatus.PUBLISHED"
type="text"
size="small"
disabled
>
已发布
</a-button>
<a-button
v-if="record.publisherId === currentUser && record.status === RegulationStatus.DRAFT"
type="text"
size="small"
@click="handleEdit(record)"
>
编辑
</a-button>
<a-popconfirm
v-if="record.publisherId === currentUser && record.status === RegulationStatus.DRAFT"
content="确定要删除这个提案吗?"
@ok="handleDelete(record)"
>
<a-button type="text" size="small" status="danger">
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-card>
<!-- 提案表单弹窗 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
width="800px"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="提案标题" field="title">
<a-input v-model="formData.title" placeholder="请输入提案标题" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="提案类型" field="regulationType">
<a-select v-model="formData.regulationType" placeholder="请选择提案类型">
<a-option value="人事制度">人事制度</a-option>
<a-option value="财务制度">财务制度</a-option>
<a-option value="安全制度">安全制度</a-option>
<a-option value="设备制度">设备制度</a-option>
<a-option value="工作流程">工作流程</a-option>
<a-option value="其他制度">其他制度</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="适用范围" field="scope">
<a-input v-model="formData.scope" placeholder="请输入适用范围" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="制度级别" field="level">
<a-select v-model="formData.level" placeholder="请选择制度级别">
<a-option :value="RegulationLevel.LOW"></a-option>
<a-option :value="RegulationLevel.MEDIUM"></a-option>
<a-option :value="RegulationLevel.HIGH"></a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="提案内容" field="content">
<a-textarea
v-model="formData.content"
placeholder="请详细描述提案的具体内容"
:rows="6"
/>
</a-form-item>
<a-form-item label="备注" field="remark">
<a-textarea
v-model="formData.remark"
placeholder="请输入其他补充说明"
:rows="2"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 提案详情弹窗 -->
<a-modal
v-model:visible="detailModalVisible"
title="提案详情"
width="800px"
:footer="false"
>
<div class="proposal-detail" v-if="currentProposal">
<div class="detail-header">
<h3>{{ currentProposal.title }}</h3>
<div class="detail-meta">
<span>提案人: {{ currentProposal.publisherName }}</span>
<span>提案类型: {{ currentProposal.regulationType }}</span>
<span>适用范围: {{ currentProposal.scope }}</span>
<span>级别: <a-tag :color="getLevelColor(currentProposal.level)">{{ getLevelText(currentProposal.level) }}</a-tag></span>
<span>创建时间: {{ formatDate(currentProposal.createTime) }}</span>
<span>状态: <a-tag :color="getStatusColor(currentProposal.status)">{{ getStatusText(currentProposal.status) }}</a-tag></span>
</div>
</div>
<a-divider />
<div class="detail-content">
<h4>提案内容</h4>
<div class="content-text">{{ currentProposal.content }}</div>
<h4>备注</h4>
<div class="content-text">{{ currentProposal.remark || '暂无备注' }}</div>
</div>
<a-divider />
<div class="detail-footer">
<a-button
v-if="currentProposal.status === RegulationStatus.PUBLISHED"
disabled
>
已自动发布
</a-button>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { useRegulationStore } from '@/stores/modules/regulation'
import { regulationApi } from '@/apis/regulation'
import {
RegulationStatus,
RegulationLevel,
type Regulation
} from '@/apis/regulation/type'
defineOptions({ name: 'ProcessManagement' })
//
const columns = [
{ title: '提案标题', dataIndex: 'title', key: 'title' },
{ title: '提案人', dataIndex: 'publisherName', key: 'publisherName' },
{ title: '提案类型', dataIndex: 'regulationType', key: 'regulationType' },
{ title: '状态', dataIndex: 'status', key: 'status', slotName: 'status' },
{ title: '级别', dataIndex: 'level', key: 'level', slotName: 'level' },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime' },
{ title: '操作', key: 'operations', slotName: 'operations', width: 280 }
]
//
const tableData = ref<Regulation[]>([])
const loading = ref(false)
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true
})
//
const modalVisible = ref(false)
const modalTitle = ref('提交制度提案')
const formRef = ref()
const formData = reactive({
regulationId: '',
title: '',
regulationType: '',
content: '',
scope: '',
level: RegulationLevel.MEDIUM,
remark: ''
})
const currentProposal = ref<Regulation | null>(null)
//
const detailModalVisible = ref(false)
// -
const currentUser = ref('') // TODO: ID
// store
const regulationStore = useRegulationStore()
//
const rules = {
title: [{ required: true, message: '请输入提案标题' }],
regulationType: [{ required: true, message: '请选择提案类型' }],
content: [{ required: true, message: '请输入提案内容' }],
scope: [{ required: true, message: '请输入适用范围' }]
}
//
const getStatusColor = (status: RegulationStatus) => {
const colors = {
[RegulationStatus.DRAFT]: 'blue',
[RegulationStatus.VOTING]: 'orange',
[RegulationStatus.REJECTED]: 'red',
[RegulationStatus.PUBLISHED]: 'purple',
[RegulationStatus.APPROVED]: 'green',
[RegulationStatus.ARCHIVED]: 'gray'
}
return colors[status] || 'blue'
}
//
const getStatusText = (status: RegulationStatus) => {
const texts = {
[RegulationStatus.DRAFT]: '草稿',
[RegulationStatus.VOTING]: '投票中',
[RegulationStatus.REJECTED]: '已否决',
[RegulationStatus.PUBLISHED]: '已发布',
[RegulationStatus.APPROVED]: '已通过',
[RegulationStatus.ARCHIVED]: '已归档'
}
return texts[status] || '草稿'
}
//
const formatDate = (dateString: string) => {
if (!dateString) return '-'
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
//
const getLevelColor = (level: RegulationLevel) => {
const colors = {
[RegulationLevel.LOW]: 'blue',
[RegulationLevel.MEDIUM]: 'orange',
[RegulationLevel.HIGH]: 'red'
}
return colors[level] || 'blue'
}
//
const getLevelText = (level: RegulationLevel) => {
const texts = {
[RegulationLevel.LOW]: '低',
[RegulationLevel.MEDIUM]: '中',
[RegulationLevel.HIGH]: '高'
}
return texts[level] || '中'
}
//
const getTableData = async () => {
loading.value = true
try {
const response = await regulationApi.getRegulationList({
page: pagination.current,
size: pagination.pageSize
})
if (response.status === 200) {
tableData.value = response.data.records
pagination.total = response.data.total
pagination.current = response.data.current
} else {
Message.error('获取数据失败')
}
} catch (error) {
console.error('获取制度列表失败:', error)
Message.error('获取数据失败')
} finally {
loading.value = false
}
}
//
const handleAdd = () => {
modalTitle.value = '提交制度提案'
modalVisible.value = true
resetForm()
}
//
const handleEdit = (record: Regulation) => {
modalTitle.value = '编辑制度提案'
modalVisible.value = true
Object.assign(formData, {
regulationId: record.regulationId,
title: record.title,
regulationType: record.regulationType,
content: record.content,
scope: record.scope,
level: record.level,
remark: record.remark
})
}
//
const handleView = (record: Regulation) => {
currentProposal.value = record
detailModalVisible.value = true
}
//
const handleDelete = async (record: Regulation) => {
try {
await regulationApi.deleteProposal(record.regulationId)
Message.success('删除成功')
getTableData()
} catch (error) {
console.error('删除失败:', error)
Message.error('删除失败')
}
}
//
const handleSubmit = async () => {
try {
await formRef.value.validate()
if (formData.regulationId) {
//
await regulationApi.updateProposal(formData.regulationId, {
title: formData.title,
regulationType: formData.regulationType,
content: formData.content,
scope: formData.scope,
level: formData.level,
remark: formData.remark
})
Message.success('更新成功')
} else {
//
await regulationApi.createProposal({
title: formData.title,
regulationType: formData.regulationType,
content: formData.content,
scope: formData.scope,
level: formData.level,
remark: formData.remark
})
Message.success('提案提交成功')
}
modalVisible.value = false
getTableData()
} catch (error) {
console.error('操作失败:', error)
Message.error('操作失败')
}
}
//
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
//
const resetForm = () => {
Object.assign(formData, {
regulationId: '',
title: '',
regulationType: '',
content: '',
scope: '',
level: RegulationLevel.MEDIUM,
remark: ''
})
formRef.value?.resetFields()
}
//
const handlePageChange = (page: number) => {
pagination.current = page
getTableData()
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
getTableData()
}
onMounted(() => {
getTableData()
})
</script>
<style scoped lang="scss">
.process-management {
.arco-card {
margin-bottom: 16px;
}
}
.proposal-detail {
.detail-header {
margin-bottom: 16px;
h3 {
margin-bottom: 12px;
color: var(--color-text-1);
}
.detail-meta {
display: flex;
gap: 16px;
color: var(--color-text-3);
font-size: 14px;
flex-wrap: wrap;
}
}
.detail-content {
h4 {
margin: 16px 0 8px 0;
color: var(--color-text-1);
font-weight: 500;
}
.content-text {
color: var(--color-text-2);
line-height: 1.6;
margin-bottom: 16px;
padding: 12px;
background: var(--color-fill-1);
border-radius: 6px;
}
}
.detail-footer {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<GiPageLayout
:margin="true"
:default-collapsed="false"
:header-style="isDesktop ? { padding: 0, borderBottomWidth: 0 } : { borderBottomWidth: '1px' } "
>
<template v-if="isDesktop" #left>
<a-tabs v-model:active-key="activeKey" type="rounded" position="left" hide-content size="large" @change="change">
<a-tab-pane v-for="(item) in menuList" :key="item.key">
<template #title>
<div style="display: flex; align-items: center">
<GiSvgIcon :name="item.icon" :size="18" style="margin-right: 4px" />
{{ item.name }}
</div>
</template>
</a-tab-pane>
</a-tabs>
</template>
<template #header>
<a-tabs v-if="!isDesktop" v-model:active-key="activeKey" type="rounded" position="top" size="large" @change="change">
<a-tab-pane v-for="(item) in menuList" :key="item.key" :title="item.name">
<template #title>
<div style="display: flex; align-items: center">
<GiSvgIcon :name="item.icon" :size="18" style="margin-right: 4px" />
{{ item.name }}
</div>
</template>
</a-tab-pane>
</a-tabs>
</template>
<transition name="fade-slide" mode="out-in" appear>
<component :is="menuList.find((item) => item.key === activeKey)?.value"></component>
</transition>
</GiPageLayout>
</template>
<script setup lang="tsx">
import { useRoute, useRouter } from 'vue-router'
import SystemRegulation from './system-regulation/index.vue'
import ProcessManagement from './process-management/index.vue'
import { useDevice } from '@/hooks'
defineOptions({ name: 'ZhiduManagement' })
const { isDesktop } = useDevice()
const data = [
{ name: '制度规范', key: 'system-regulation', icon: 'file-text', value: SystemRegulation, path: '/zhidu/system-regulation' },
{ name: '流程管理', key: 'process-management', icon: 'workflow', value: ProcessManagement, path: '/zhidu/process-management' },
]
const menuList = computed(() => {
return data
})
const route = useRoute()
const router = useRouter()
//
const activeKey = computed(() => {
const currentPath = route.path
const menuItem = menuList.value.find(item => item.path === currentPath)
return menuItem ? menuItem.key : menuList.value[0].key
})
watch(
() => route.path,
() => {
// activeKey
},
{ immediate: true },
)
const change = (key: string | number) => {
const menuItem = menuList.value.find(item => item.key === key)
if (menuItem) {
router.push(menuItem.path)
}
}
</script>
<style scoped lang="scss">
.gi_page {
padding-top: 0;
}
:deep(.arco-tabs-nav-vertical.arco-tabs-nav-type-line .arco-tabs-tab) {
margin: 0;
padding: 8px 16px;
&:hover {
background: var(--color-fill-1);
.arco-tabs-tab-title {
&::before {
display: none !important;
}
}
}
&.arco-tabs-tab-active {
background: rgba(var(--primary-6), 0.08);
}
}
</style>

View File

@ -0,0 +1,350 @@
<template>
<div class="system-regulation">
<a-card title="已发布制度确认" :bordered="false">
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
>
<template #confirmStatus="{ record }">
<a-tag :color="record.confirmStatus === 'confirmed' ? 'green' : 'orange'">
{{ record.confirmStatus === 'confirmed' ? '已确认' : '待确认' }}
</a-tag>
</template>
<template #operations="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleView(record)">
查看详情
</a-button>
<a-button
v-if="record.confirmStatus !== 'confirmed'"
type="text"
size="small"
@click="handleConfirm(record)"
>
确认知晓
</a-button>
<a-button type="text" size="small" @click="handleDownload(record)">
下载
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 制度详情弹窗 -->
<a-modal
v-model:visible="detailModalVisible"
title="制度详情"
width="800px"
:footer="false"
>
<div class="regulation-detail" v-if="currentRegulation">
<div class="detail-header">
<h3>{{ currentRegulation.title }}</h3>
<div class="detail-meta">
<span>发布人: {{ currentRegulation.publisherName }}</span>
<span>发布时间: {{ currentRegulation.publishTime }}</span>
<span>生效日期: {{ currentRegulation.effectiveTime }}</span>
</div>
</div>
<a-divider />
<div class="detail-content">
<h4>制度内容</h4>
<div class="content-text">{{ currentRegulation.content }}</div>
<h4>适用范围</h4>
<div class="content-text">{{ currentRegulation.scope }}</div>
<h4>实施要求</h4>
<div class="content-text">{{ currentRegulation.requirements }}</div>
<h4>注意事项</h4>
<div class="content-text">{{ currentRegulation.notes }}</div>
</div>
<a-divider />
<div class="detail-footer">
<a-button type="primary" @click="handleConfirm(currentRegulation)">
确认知晓并遵守
</a-button>
<a-button @click="handleDownload(currentRegulation)">
下载制度文件
</a-button>
</div>
</div>
</a-modal>
<!-- 确认承诺弹窗 -->
<a-modal
v-model:visible="confirmModalVisible"
title="制度确认承诺"
width="600px"
@ok="submitConfirm"
@cancel="confirmModalVisible = false"
>
<div class="confirm-content">
<p>我确认已仔细阅读并理解{{ currentRegulation?.title }}的全部内容承诺</p>
<ul>
<li>严格遵守该制度的各项规定</li>
<li>认真履行相关职责和义务</li>
<li>如有违反愿意承担相应责任</li>
</ul>
<a-checkbox v-model="agreeTerms">
我已阅读并同意上述承诺
</a-checkbox>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { Regulation } from '@/apis/regulation/type'
import { Message } from '@arco-design/web-vue'
import { useRegulationStore } from '@/stores/modules/regulation'
import { regulationApi } from '@/apis/regulation'
defineOptions({ name: 'SystemRegulation' })
//
const columns = [
{ title: '制度名称', dataIndex: 'title', key: 'title' },
{ title: '制度类型', dataIndex: 'regulationType', key: 'regulationType' },
{ title: '发布人', dataIndex: 'publisherName', key: 'publisherName' },
{ title: '发布时间', dataIndex: 'publishTime', key: 'publishTime' },
{ title: '生效日期', dataIndex: 'effectiveTime', key: 'effectiveTime' },
{ title: '确认状态', dataIndex: 'confirmStatus', key: 'confirmStatus', slotName: 'confirmStatus' },
{ title: '操作', key: 'operations', slotName: 'operations', width: 250 }
]
//
const tableData = ref<Regulation[]>([])
const loading = ref(false)
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true
})
//
const detailModalVisible = ref(false)
const confirmModalVisible = ref(false)
const currentRegulation = ref<Regulation | null>(null)
const agreeTerms = ref(false)
// store
const regulationStore = useRegulationStore()
//
const getTableData = async () => {
loading.value = true
try {
const response = await regulationApi.getPublishedRegulationList({
page: pagination.current,
size: pagination.pageSize,
status: "PUBLISHED"
})
if (response.status === 200) {
tableData.value = response.data.records
pagination.pageSize = response.data.size
pagination.current = response.data.current
pagination.total = response.data.total
} else {
Message.error('获取数据失败')
}
} catch (error) {
console.error('获取已发布制度列表失败:', error)
Message.error('获取数据失败')
} finally {
loading.value = false
}
}
//
const onPageChange = (page: number) => {
pagination.current = page
getTableData()
}
const onPageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
getTableData()
}
//
const handleView = (record: any) => {
currentRegulation.value = record
detailModalVisible.value = true
}
//
const handleConfirm = (record: any) => {
currentRegulation.value = record
confirmModalVisible.value = true
agreeTerms.value = false
}
//
const handleDownload = (record: any) => {
try {
//
const content = `
制度名称${record.title}
制度类型${record.regulationType}
发布人${record.publisherName}
发布时间${record.publishTime}
生效日期${record.effectiveTime}
制度内容
${record.content}
适用范围
${record.scope}
实施要求
${record.requirements || '请按照制度要求执行'}
注意事项
${record.notes || '请严格遵守制度规定'}
`.trim()
// Blob
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
//
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${record.title}.txt`
//
document.body.appendChild(link)
link.click()
//
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
Message.success('制度文件下载成功')
} catch (error) {
Message.error('下载失败')
console.error('下载错误:', error)
}
}
//
const submitConfirm = async () => {
if (!agreeTerms.value) {
Message.warning('请先同意承诺条款')
return
}
try {
if (currentRegulation.value) {
await regulationApi.confirmRegulation(currentRegulation.value.regulationId)
Message.success('确认成功,您已承诺遵守该制度')
confirmModalVisible.value = false
//
const index = tableData.value.findIndex(item => item.regulationId === currentRegulation.value.regulationId)
if (index !== -1) {
tableData.value[index].confirmStatus = 'confirmed'
}
}
} catch (error) {
console.error('确认失败:', error)
Message.error('确认失败')
}
}
onMounted(() => {
getTableData()
})
</script>
<style scoped lang="scss">
.system-regulation {
.arco-card {
margin-bottom: 16px;
}
}
.regulation-detail {
.detail-header {
margin-bottom: 16px;
h3 {
margin-bottom: 12px;
color: var(--color-text-1);
}
.detail-meta {
display: flex;
gap: 16px;
color: var(--color-text-3);
font-size: 14px;
}
}
.detail-content {
h4 {
margin: 16px 0 8px 0;
color: var(--color-text-1);
font-weight: 500;
}
.content-text {
color: var(--color-text-2);
line-height: 1.6;
margin-bottom: 16px;
padding: 12px;
background: var(--color-fill-1);
border-radius: 6px;
}
}
.detail-footer {
display: flex;
gap: 12px;
justify-content: center;
}
}
.confirm-content {
p {
margin-bottom: 16px;
color: var(--color-text-1);
line-height: 1.6;
}
ul {
margin-bottom: 20px;
padding-left: 20px;
li {
margin-bottom: 8px;
color: var(--color-text-2);
line-height: 1.5;
}
}
}
</style>

View File

@ -1,4 +1,5 @@
<template>
<GiPageLayout>
<div>
<a-radio-group v-model="viewType" type="button" size="small" style="margin-bottom: 16px;">
@ -9,13 +10,15 @@
<GiTable
v-show="viewType === 'table'"
ref="tableRef"
row-key="id"
row-key="deptId"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="false"
:disabled-column-keys="['name']"
:expanded-keys="expandedKeys"
@expand="onExpand"
@refresh="search"
>
<template #expand-icon="{ expanded }">
@ -49,6 +52,7 @@
<a-link v-permission="['system:dept:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['system:dept:delete']"
:disabled="record.children && record.children.length > 0"
status="danger"
title="删除"
@click="onDelete(record)"
@ -69,14 +73,14 @@
:collapsable="true"
:horizontal="false"
:define-menus="menus"
:expand-all="true"
:default-expand-level="999"
:props="{ id: 'deptId', parentId: 'parentId', label: 'deptName', children: 'children' }"
center
:node-add="handleAdd"
:node-delete="onDelete"
:node-edit="onUpdate"
@on-expand-all="bool => nodeExpandAll = bool"
:expanded-keys="nodeExpandedKeys"
@node-click="(node) => toggleExpand(node.deptId)"
>
</Vue3TreeOrg>
</a-dropdown>
@ -90,6 +94,7 @@
import 'vue3-tree-org/lib/vue3-tree-org.css'
import { Vue3TreeOrg } from 'vue3-tree-org'
import type { TableInstance } from '@arco-design/web-vue'
import { Message } from '@arco-design/web-vue'
import DeptAddModal from './DeptAddModal.vue'
import { type DeptQuery, type DeptResp, deleteDept, getDeptTree } from '@/apis/system/dept'
import type GiTable from '@/components/GiTable/index.vue'
@ -97,6 +102,8 @@ import { useDownload, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
const $message = Message
defineOptions({ name: 'SystemDept' })
const queryForm = reactive<DeptQuery>({})
@ -109,9 +116,21 @@ const {
} = useTable<DeptResp>(() => getDeptTree(queryForm), {
immediate: true,
onSuccess: () => {
nextTick(() => {
tableRef.value?.tableRef?.expandAll(true)
})
if (isFirstLoad.value) {
// id
const collectKeys = (nodes: DeptResp[]): string[] => {
let keys: string[] = []
nodes.forEach(node => {
keys.push(node.deptId)
if (node.children && node.children.length) {
keys = keys.concat(collectKeys(node.children))
}
})
return keys
}
expandedKeys.value = collectKeys(tableData.value)
isFirstLoad.value = false
}
},
})
//
@ -123,7 +142,9 @@ const menus = [
{ name: '删除部门', command: 'delete' },
]
//
const nodeExpandAll = ref<boolean>(true)
//
const expandedKeys = ref<string[]>([])
const isFirstLoad = ref(true)
//
const searchData = (name: string) => {
const loop = (data: DeptResp[]) => {
@ -180,6 +201,13 @@ const reset = () => {
//
const onDelete = (record: DeptResp) => {
//
if (record.children && record.children.length > 0) {
//
$message.warning('该部门存在子部门,无法直接删除')
return Promise.reject()
}
return handleDelete(() => deleteDept(record.deptId), {
content: `是否确定删除部门「${record.deptName}」?`,
showModal: true,
@ -204,6 +232,31 @@ const handleAdd = (record: DeptResp) => {
const onUpdate = (record: DeptResp) => {
DeptAddModalRef.value?.onUpdate(record.deptId)
}
const onExpand = (expanded: boolean, record: DeptResp) => {
const key = record.deptId
if (expanded) {
if (!expandedKeys.value.includes(key)) {
expandedKeys.value.push(key)
}
} else {
expandedKeys.value = expandedKeys.value.filter(k => k !== key)
}
}
// /
const toggleExpandAll = () => {
nodeExpandAll.value = !nodeExpandAll.value
}
// :
const nodeExpandedKeys = ref<string[]>([])
const toggleExpand = (deptId: string) => {
const index = nodeExpandedKeys.value.indexOf(deptId)
index > -1
? nodeExpandedKeys.value.splice(index, 1)
: nodeExpandedKeys.value.push(deptId)
}
</script>
<style scoped lang="scss">

View File

@ -27,8 +27,8 @@
<a-radio :value="0">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="remark" label="备注">
<a-textarea v-model="formData.remark" placeholder="请输入备注" allow-clear />
<a-form-item field="remark" label="说明">
<a-textarea v-model="formData.remark" placeholder="请输入岗位说明" allow-clear />
</a-form-item>
</a-form>
</a-modal>

View File

@ -2,65 +2,276 @@
<a-drawer
v-model:visible="visible"
title="岗位详情"
width="500px"
:footer="false"
width="620px"
unmount-on-close
class="post-detail-drawer"
@close="() => visible = false"
>
<a-descriptions
:data="detailData"
:column="1"
:align="{ label: 'right' }"
label-style="width: 120px"
size="medium"
:loading="loading"
border
>
<template #label="{ label }">{{ label }}</template>
<template #value="{ value }">
<span v-if="value !== undefined && value !== null">{{ value }}</span>
<span v-else>-</span>
</template>
</a-descriptions>
<a-spin :loading="loading" class="detail-container">
<div class="detail-card">
<div class="detail-header">
<div class="post-name">{{ primaryInfo?.name || '-' }}</div>
<a-tag class="status-tag" :color="getStatusColor(primaryInfo?.status)">
{{ getStatusText(primaryInfo?.status) }}
</a-tag>
</div>
<div class="detail-group">
<div class="group-title">基本信息</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">岗位ID</div>
<div class="info-value">{{ primaryInfo?.id || '-' }}</div>
</div>
<div class="info-item">
<div class="info-label">岗位排序</div>
<div class="info-value">{{ primaryInfo?.sort || '-' }}</div>
</div>
</div>
</div>
<div v-if="primaryInfo?.remark" class="detail-group">
<div class="group-title">岗位说明</div>
<div class="remark-container">
<div class="remark-content">{{ primaryInfo.remark }}</div>
</div>
</div>
<div class="detail-group">
<div class="group-title">时间信息</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">创建时间</div>
<div class="info-value">{{ primaryInfo?.createTime || '-' }}</div>
</div>
<div class="info-item">
<div class="info-label">更新时间</div>
<div class="info-value">{{primaryInfo?.updateTime || '-' }}</div>
</div>
</div>
</div>
</div>
</a-spin>
<template #footer>
<div class="footer-actions">
<a-button type="primary" @click="handleClose">关闭详情</a-button>
</div>
</template>
</a-drawer>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { getPostDetail } from '@/apis/system/post'
import { useLoading } from '@/hooks'
//import { formatDate } from '@/utils/date'
defineOptions({ name: 'PostDetailDrawer' })
const visible = ref(false)
const { loading, setLoading } = useLoading()
const detailData = ref<Array<{ label: string; value: any }>>([])
const primaryInfo = ref<any>(null)
//
const getDetail = async (id: string) => {
try {
setLoading(true)
const { data } = await getPostDetail(id)
if (data) {
detailData.value = [
{ label: '岗位名称', value: data.postName },
{ label: '岗位排序', value: data.postSort },
{ label: '状态', value: Number(data.status) === 1 ? '正常' : '停用' },
{ label: '备注', value: data.remark },
{ label: '创建时间', value: data.createTime },
{ label: '更新时间', value: data.updateTime },
]
primaryInfo.value = null
const response = await getPostDetail(id)
const data = response?.data || response
if (data && typeof data === 'object') {
primaryInfo.value = {
id: data.postId ?? data.id,
name: data.postName ?? data.name,
sort: data.postSort,
status: data.status,
remark: data.remark,
createTime: data.createTime,
updateTime: data.updateTime,
}
}
} catch (error: any) {
console.error('获取岗位详情失败:', error)
} finally {
setLoading(false)
}
}
//
const getStatusText = (status: number | string) => {
if (status === 1 || status === '1') return '正常'
if (status === 0 || status === '0') return '停用'
return '未知状态'
}
//
const getStatusColor = (status: number | string) => {
if (status === 1 || status === '1') return 'rgb(82, 196, 26)'
if (status === 0 || status === '0') return 'rgb(245, 34, 45)'
return 'rgb(150, 150, 150)'
}
//
const handleClose = () => {
visible.value = false
}
//
const onDetail = async (id: string) => {
if (!id) return
visible.value = true
await nextTick()
await getDetail(id)
}
defineExpose({
onDetail,
})
</script>
</script>
<style scoped>
.post-detail-drawer {
--primary-color: #3498db;
--light-bg: #f9fafc;
--border-color: #eaeaea;
--label-color: #666;
--value-color: #333;
--group-title-color: #555;
--group-bg: #f5f7fa;
}
.detail-container {
padding: 16px 24px;
}
.detail-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
padding: 20px;
transition: all 0.3s ease;
}
.detail-header {
display: flex;
align-items: center;
margin-bottom: 28px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.post-name {
font-size: 22px;
font-weight: 600;
color: var(--value-color);
margin-right: 15px;
letter-spacing: 0.5px;
}
.status-tag {
padding: 4px 12px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
border: none;
color: white !important;
}
.detail-group {
margin-bottom: 25px;
position: relative;
background-color: var(--group-bg);
border-radius: 8px;
padding: 12px 16px;
}
.detail-group:last-child {
margin-bottom: 0;
}
.group-title {
font-size: 16px;
font-weight: 600;
color: var(--group-title-color);
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 15px;
display: flex;
align-items: center;
}
.group-title::before {
content: '';
display: inline-block;
width: 4px;
height: 16px;
background-color: var(--primary-color);
margin-right: 8px;
border-radius: 2px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 18px;
}
.info-item {
display: flex;
flex-direction: column;
background: white;
padding: 12px 15px;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
border: 1px solid var(--border-color);
transition: transform 0.2s, box-shadow 0.2s;
}
.info-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
}
.info-label {
font-size: 14px;
color: var(--label-color);
margin-bottom: 6px;
font-weight: 500;
display: flex;
align-items: center;
}
.info-label::after {
content: ':';
margin-right: 4px;
}
.info-value {
font-size: 16px;
color: var(--value-color);
font-weight: 500;
word-break: break-word;
}
.remark-container {
padding: 16px;
background: white;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.remark-content {
color: var(--value-color);
line-height: 1.7;
font-size: 15px;
}
.footer-actions {
display: flex;
justify-content: flex-end;
padding: 16px 24px;
}
</style>

View File

@ -26,6 +26,7 @@
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['system:post:query']" title="详情" @click="onDetail(record)">详情</a-link>
<a-link v-permission="['system:post:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['system:post:delete']"
@ -45,11 +46,11 @@
</template>
<script setup lang="ts">
import type { TableColumnData, TableInstance } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import PostAddModal from './PostAddModal.vue'
import PostDetailDrawer from './PostDetailDrawer.vue'
import { deletePost, pagePost,listPost } from '@/apis/system/post'
import type { PostVO, PostPageQuery } from '@/apis/system/type'
import { deletePost, listPost } from '@/apis/system/post'
import type { PostVO } from '@/apis/system/type'
import { useResetReactive, useTable } from '@/hooks'
import { isMobile } from '@/utils'
import has from '@/utils/has'
@ -80,7 +81,7 @@ const queryFormColumns: ColumnItem[] = reactive([
props: {
options: [
{ label: '正常', value: 1 },
{ label: '停用', value: 0 }
{ label: '停用', value: 0 },
],
placeholder: '请选择状态',
},
@ -93,8 +94,8 @@ const {
pagination,
search,
handleDelete,
} = useTable((params) => listPost({
...queryForm,
} = useTable((params) => listPost({
...queryForm,
}), { immediate: true })
const tableColumns = ref<TableColumnData[]>([
@ -105,50 +106,50 @@ const tableColumns = ref<TableColumnData[]>([
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '岗位名称',
dataIndex: 'postName',
minWidth: 140,
ellipsis: true,
{
title: '岗位名称',
dataIndex: 'postName',
minWidth: 140,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '岗位排序',
dataIndex: 'postSort',
minWidth: 100,
ellipsis: true,
tooltip: true
{
title: '岗位排序',
dataIndex: 'postSort',
minWidth: 100,
ellipsis: true,
tooltip: true,
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100,
},
{
title: '备注',
dataIndex: 'remark',
minWidth: 180,
ellipsis: true,
tooltip: true
{
title: '说明',
dataIndex: 'remark',
minWidth: 180,
ellipsis: true,
tooltip: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
minWidth: 180,
ellipsis: true,
{
title: '创建时间',
dataIndex: 'createTime',
minWidth: 180,
ellipsis: true,
tooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
}
},
},
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 160,
width: 200,
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['system:post:update', 'system:post:delete']),
},
@ -187,4 +188,4 @@ const onDetail = (record: PostVO) => {
}
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss"></style>

View File

@ -1,13 +1,13 @@
<template>
<a-modal
v-model:visible="visible"
title="分配角色"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 1100 ? 1100 : '100%'"
draggable
@before-ok="save"
@close="reset"
v-model:visible="visible"
title="分配角色"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 1100 ? 1100 : '100%'"
draggable
@before-ok="save"
@close="reset"
>
<UserSelect v-if="visible" ref="UserSelectRef" v-model:value="selectedUsers" :role-id="dataId" @select-user="onSelectUser" />
</a-modal>
@ -17,6 +17,7 @@
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { assignToUsers } from '@/apis/system/role'
//
const emit = defineEmits<{
(e: 'save-success'): void

View File

@ -98,21 +98,11 @@ const columns: TableInstance['columns'] = [
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '昵称',
dataIndex: 'nickname',
slotName: 'nickname',
minWidth: 130,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{ title: '用户名', dataIndex: 'username', slotName: 'username', minWidth: 120, ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' },
{ title: '用户名', dataIndex: 'name', slotName: 'name', minWidth: 120, ellipsis: true, tooltip: true },
{ title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center' },
{ title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' },
{ title: '所属部门', dataIndex: 'deptName', minWidth: 140, ellipsis: true, tooltip: true },
{ title: '角色', dataIndex: 'roleNames', slotName: 'roleNames', minWidth: 165 },
{ title: '描述', dataIndex: 'description', minWidth: 130, ellipsis: true, tooltip: true },
{
title: '操作',
dataIndex: 'action',
@ -142,7 +132,7 @@ const onMulDelete = () => {
content: `是否确定取消分配角色给所选的${selectedKeys.value.length}个用户?`,
hideCancel: false,
onOk: async () => {
await unassignFromUsers(selectedKeys.value)
await unassignFromUsers(selectedKeys.value.map(id => String(id)))
Message.success('取消成功')
search()
},
@ -151,7 +141,7 @@ const onMulDelete = () => {
//
const onDelete = (record: RoleUserResp) => {
return handleDelete(() => unassignFromUsers([record.id]), {
return handleDelete(() => unassignFromUsers([String(record.id)]), {
content: `是否确定取消分配角色给用户「${record.nickname}(${record.username})」?`,
successTip: '取消成功',
showModal: true,

View File

@ -18,6 +18,7 @@
import RoleTree from './tree/index.vue'
import Permission from './components/Permission.vue'
import RoleUser from './components/RoleUser.vue'
import {listRoleUserId} from "@/apis";
defineOptions({ name: 'SystemRole' })

View File

@ -60,7 +60,7 @@ const save = async () => {
try {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
await updateUserRole({ roleIds: form.roleIds }, dataId.value)
await updateUserRole({ roleIds: form.roleIds.map(id => String(id)) }, dataId.value)
Message.success('分配成功')
emit('save-success')
return true

View File

@ -93,7 +93,7 @@ const handleSubmit = async () => {
await bindUserRole({
userId: props.userData.userId,
roleIds: selectedRoles.value
roleIds: selectedRoles.value.map(id => String(id))
})
Message.success('角色分配成功')

View File

@ -313,17 +313,17 @@ const columns: TableInstance['columns'] = [
},
{ title: '账号', dataIndex: 'account', minWidth: 140, ellipsis: true, tooltip: true },
{ title: '员工编码', dataIndex: 'userCode', minWidth: 120, ellipsis: true, tooltip: true },
{ title: '在职状态', dataIndex: 'userStatus', slotName: 'userStatus', align: 'center', width: 100 },
{ title: '员工性质', dataIndex: 'userType', slotName: 'userType', align: 'center', width: 100 },
{ title: '性别', dataIndex: 'gender', slotName: 'gender', align: 'center', width: 80 },
{ title: '所属部门', dataIndex: 'deptName', minWidth: 180, ellipsis: true, tooltip: true },
{ title: '角色', dataIndex: 'roleIds', slotName: 'roleIds', minWidth: 165 },
{ title: '手机号', dataIndex: 'mobile', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '邮箱', dataIndex: 'email', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '入职日期', dataIndex: 'hiredate', width: 120, ellipsis: true, tooltip: true },
{ title: '出生日期', dataIndex: 'birthdate', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '所属部门', dataIndex: 'deptName', minWidth: 180, ellipsis: true, tooltip: true },
{ title: '学历', dataIndex: 'educationLabel', width: 100, ellipsis: true, tooltip: true, show: false },
{ title: '专业', dataIndex: 'majorField', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '在职状态', dataIndex: 'userStatus', slotName: 'userStatus', align: 'center', width: 100 },
{ title: '员工性质', dataIndex: 'userType', slotName: 'userType', align: 'center', width: 100 },
{ title: '角色', dataIndex: 'roleName', slotName: 'roleName', minWidth: 185 },
{ title: '邮箱', dataIndex: 'email', slotName: 'email', minWidth: 170, ellipsis: true, tooltip: true },
{ title: '入职日期', dataIndex: 'hiredate', width: 120, ellipsis: true, tooltip: true },
{ title: '出生日期', dataIndex: 'birthdate', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '工作方向', dataIndex: 'workField', width: 120, ellipsis: true, tooltip: true, show: false },
{ title: '身份证', dataIndex: 'identityCard', width: 180, ellipsis: true, tooltip: true, show: false },
{

View File

@ -17,25 +17,25 @@
</template>
</a-upload>
<div class="name">
<span style="margin-right: 10px">{{ userInfo.nickname }}</span>
<span style="margin-right: 10px">{{ userInfo.name }}</span>
<icon-edit :size="16" class="btn" @click="onUpdate" />
</div>
<div class="id">
<GiSvgIcon name="id" :size="16" />
<span>{{ userInfo.id }}</span>
<span>{{ userInfo.userId }}</span>
</div>
</section>
<footer>
<a-descriptions :column="4" size="large">
<a-descriptions-item :span="4">
<template #label> <icon-user /><span style="margin-left: 5px">用户名</span></template>
{{ userInfo.username }}
{{ userInfo.account }}
<icon-man v-if="userInfo.gender === 1" style="color: #19bbf1" />
<icon-woman v-else-if="userInfo.gender === 2" style="color: #fa7fa9" />
</a-descriptions-item>
<a-descriptions-item :span="4">
<template #label> <icon-phone /><span style="margin-left: 5px">手机</span></template>
{{ userInfo.phone || '暂无' }}
{{ userInfo.mobile || '暂无' }}
</a-descriptions-item>
<a-descriptions-item :span="4">
<template #label> <icon-email /><span style="margin-left: 5px">邮箱</span></template>
@ -47,12 +47,12 @@
</a-descriptions-item>
<a-descriptions-item :span="4">
<template #label> <icon-user-group /><span style="margin-left: 5px">角色</span></template>
{{ userInfo.roles.join('') }}
{{ userInfo.roles?.map(role => role.roleName).join(', ') || '暂无' }}
</a-descriptions-item>
</a-descriptions>
</footer>
</div>
<div class="footer">注册于 {{ userInfo.registrationDate }}</div>
<div class="footer">注册于 {{ userInfo.createTime }}</div>
</a-card>
<a-modal v-model:visible="visible" title="上传头像" :width="width >= 400 ? 400 : '100%'" :footer="false" draggable @close="reset">
@ -102,19 +102,22 @@ import { uploadAvatar } from '@/apis/system'
import 'vue-cropper/dist/index.css'
import { useUserStore } from '@/stores'
import getAvatar from '@/utils/avatar'
import XG_DEBUG from 'xgplayer/es/utils/debug'
import config = XG_DEBUG.config
const { width } = useWindowSize()
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const avatar = {
const avatar = computed(() => ({
uid: '-2',
name: 'avatar.png',
url: userInfo.value.avatar,
}
const avatarList = ref<FileItem[]>([avatar])
const fileRef = ref(reactive({ name: 'avatar.png' }))
const options: cropperOptions = reactive({
url: userInfo.value.avatar || getAvatar(userInfo.value.avatar, undefined),
}))
const avatarList = computed<FileItem[]>(() => [avatar.value])
const fileRef = ref<File | null>(null)
const options = reactive<cropperOptions>({
img: '',
autoCrop: true,
autoCropWidth: 160,
@ -128,13 +131,14 @@ const options: cropperOptions = reactive({
outputType: 'png',
})
const visible = ref(false)
//
const onBeforeUpload = (file: File): boolean => {
fileRef.value = file
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
options.img = reader.result
options.img = reader.result as string
}
visible.value = true
return false