Compare commits

..

1 Commits
main ... master

Author SHA1 Message Date
wxy 6e92ac705c 侧边栏结构修改 2025-07-21 08:25:03 +08:00
63 changed files with 1865 additions and 5721 deletions

View File

@ -4,8 +4,8 @@
VITE_BUILD_MOCK = false
# 接口地址
VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
VITE_API_WS_URL = 'ws://localhost:8000'
VITE_API_BASE_URL = 'https://api.continew.top'
VITE_API_WS_URL = 'wss://api.continew.top'
# 地址前缀
VITE_BASE = '/'

View File

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

View File

@ -58,8 +58,7 @@
"vue-router": "^4.3.3",
"vue3-tree-org": "^4.2.2",
"xe-utils": "^3.5.7",
"xgplayer": "^2.31.6",
"xlsx": "^0.18.5"
"xgplayer": "^2.31.6"
},
"devDependencies": {
"@antfu/eslint-config": "^2.16.3",

View File

@ -143,9 +143,6 @@ 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
@ -1484,10 +1481,6 @@ 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==}
@ -1695,10 +1688,6 @@ 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'}
@ -1770,10 +1759,6 @@ 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'}
@ -1864,11 +1849,6 @@ 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==}
@ -2675,10 +2655,6 @@ 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'}
@ -4130,10 +4106,6 @@ 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'
@ -4715,18 +4687,10 @@ 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'}
@ -4749,11 +4713,6 @@ 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'}
@ -6207,8 +6166,6 @@ 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)
@ -6464,11 +6421,6 @@ 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
@ -6569,8 +6521,6 @@ snapshots:
transitivePeerDependencies:
- '@lezer/common'
codepage@1.15.0: {}
collection-visit@1.0.0:
dependencies:
map-visit: 1.0.0
@ -6658,8 +6608,6 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
crc-32@1.2.2: {}
crelt@1.0.6: {}
cron-parser@4.9.0:
@ -7600,8 +7548,6 @@ 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
@ -9103,10 +9049,6 @@ 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:
@ -9775,12 +9717,8 @@ 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
@ -9817,16 +9755,6 @@ 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: {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,20 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- 风机塔筒 -->
<rect x="47" y="40" width="6" height="45" fill="#4a5568" rx="3"/>
<!-- 风机机舱 -->
<ellipse cx="50" cy="40" rx="8" ry="5" fill="#2d3748"/>
<!-- 风机叶片 -->
<g transform="translate(50, 40)">
<!-- 叶片1 -->
<path d="M0,0 L-25,-15 Q-35,-20 -40,-30" stroke="#4a5568" stroke-width="3" fill="none"/>
<!-- 叶片2 -->
<path d="M0,0 L15,-30 Q20,-40 30,-45" stroke="#4a5568" stroke-width="3" fill="none"/>
<!-- 叶片3 -->
<path d="M0,0 L30,10 Q40,15 45,25" stroke="#4a5568" stroke-width="3" fill="none"/>
</g>
<!-- 风机底座 -->
<ellipse cx="50" cy="85" rx="12" ry="3" fill="#2d3748"/>
</svg>

Before

Width:  |  Height:  |  Size: 761 B

View File

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

View File

@ -1,13 +0,0 @@
import http from '@/utils/http'
import type { AttendanceRecordReq, AttendanceRecordResp } from './type'
const BASE_URL = '/attendance-record'
/** 新增考勤记录 */
export function addAttendanceRecord(data: AttendanceRecordReq) {
return http.post<AttendanceRecordResp>(BASE_URL, data, {
headers: {
'Content-Type': 'application/json'
}
})
}

View File

@ -1,15 +0,0 @@
/** 新增考勤记录请求体 */
export interface AttendanceRecordReq {
recordImage?: string
recordPosition?: string
recordPositionLabel?: string
}
/** 新增考勤记录响应体 */
export interface AttendanceRecordResp {
code: number
data: object
msg: string
status: number
success: boolean
}

View File

@ -1,5 +1,5 @@
import http from '@/utils/http'
import type { CertificationInfo, CertificationListParams, CertificationListResponse, SimpleUserInfo,CertificationPageResponse, CertificationReq } from './type'
import type { CertificationInfo, CertificationListParams, CertificationListResponse, SimpleUserInfo } from './type'
const { request } = http
@ -7,7 +7,7 @@ const { request } = http
export type { CertificationInfo, CertificationListParams, CertificationListResponse, SimpleUserInfo }
// 新增人员资质
export function createCertification(data: CertificationReq) {
export function createCertification(data: CertificationInfo) {
return request({
url: '/certification',
method: 'post',
@ -74,12 +74,4 @@ export function getUserList() {
url: '/user/list',
method: 'get'
})
}
// 查询人员资质信息分页列表(新接口)
export function getCertificationPage(params: CertificationListParams) {
return request<CertificationPageResponse>({
url: '/certification/page',
method: 'get',
params
})
}
}

View File

@ -11,25 +11,19 @@ export interface CertificationInfo {
createTime?: string
updateTime?: string
}
export interface CertificationReq {
certificationCode: string
certificationImage: string
certificationName: string
certificationType: string
userId: string
validityDateBegin: string
validityDateEnd: string
}
/** 人员资质列表查询参数 */
export interface CertificationListParams {
certificationName?: string
certificationType?: string
userName?: string
current?: number
size?: number
}
/** 人员资质列表响应 */
export interface CertificationListResponse {
data: CertificationInfo[]
records: CertificationInfo[]
total: number
current: number
size: number
@ -38,22 +32,7 @@ export interface CertificationListResponse {
/** 用户信息简化版 */
export interface SimpleUserInfo {
userId: string
name: string
userName: string
account: string
}
export interface CertificationInfo {
certificationId?: string
certificationCode: string
certificationName: string
userId: string
userName?: string // 新增,后端会返回
}
// 分页响应
export interface CertificationPageResponse {
rows: CertificationInfo[]
total: number
code?: number
msg?: string
[key: string]: any
}
name: string
}

View File

@ -1,40 +0,0 @@
import http from '@/utils/http'
import type { PerformanceDimension, PerformanceRule, DimensionQuery, RuleQuery } from './type'
/** 维度相关 */
export function getDimensionList(params?: DimensionQuery) {
return http.get<PerformanceDimension[]>('/performance-dimension/list', params)
}
export function getDimensionDetail(id: string) {
return http.get<PerformanceDimension>(`/performance-dimension/${id}`)
}
export function addDimension(data: Partial<PerformanceDimension>) {
return http.post('/performance-dimension', data)
}
export function updateDimension(id: string, data: Partial<PerformanceDimension>) {
return http.put(`/performance-dimension/${id}`, data)
}
export function deleteDimension(id: string) {
return http.del(`/performance-dimension/${id}`)
}
/** 细则相关 */
export function getRuleList(params?: RuleQuery) {
return http.get<PerformanceRule[]>('/performance-rule/list', params)
}
export function getRuleDetail(id: string) {
return http.get<PerformanceRule>(`/performance-rule/${id}`)
}
export function addRule(data: Partial<PerformanceRule>) {
return http.post('/performance-rule', data)
}
export function updateRule(id: string, data: Partial<PerformanceRule>) {
return http.put(`/performance-rule/${id}`, data)
}
export function deleteRule(id: string) {
return http.del(`/performance-rule/${id}`)
}
// 我的绩效
export function getMyEvaluation() {
return http.get('/performance-evaluation/my')
}

View File

@ -1,39 +0,0 @@
/** 绩效维度 */
export interface PerformanceDimension {
dimensionId: string
dimensionName: string
description?: string
deptName: string
status: 0 | 1
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/** 绩效细则 */
export interface PerformanceRule {
ruleId: string
ruleName: string
description?: string
dimensionName: string
bonus?: string
score?: number
weight?: number
status: 0 | 1
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/** 查询参数 */
export interface DimensionQuery {
dimensionName?: string
status?: 0 | 1
}
export interface RuleQuery {
dimensionName?: string
ruleName?: string
status?: 0 | 1
}

View File

@ -1,59 +0,0 @@
import type * as T from './type'
import http from '@/utils/http'
const BASE_URL = '/performance'
/* ===== 维度 ===== */
export const getDimensionList = () => http.get<T.DimensionResp[]>(`${BASE_URL}/dimension`)
export const addDimension = (data: T.DimensionAddReq) =>
http.post<boolean>(`${BASE_URL}/dimension`, data)
export const updateDimension = (id: string, data: T.DimensionUpdateReq) =>
http.put<boolean>(`${BASE_URL}/dimension/${id}`, data)
export const deleteDimension = (id: string) =>
http.del<boolean>(`${BASE_URL}/dimension/${id}`)
/** 维度详情RuleDrawer.vue 需要) */
export const getDimensionDetail = (id: string) =>
http.get<T.DimensionResp>(`${BASE_URL}/dimension/${id}`)
/* ===== 细则 ===== */
export const getRuleList = (dimensionId: string) =>
http.get<T.RuleResp[]>(`${BASE_URL}/rule`, { dimensionId })
export const addRule = (data: T.RuleAddReq) =>
http.post<boolean>(`${BASE_URL}/rule`, data)
export const updateRule = (id: string, data: T.RuleUpdateReq) =>
http.put<boolean>(`${BASE_URL}/rule/${id}`, data)
export const deleteRule = (id: string) =>
http.del<boolean>(`${BASE_URL}/rule/${id}`)
/** 细则详情RuleDrawer.vue 需要) */
export const getRuleDetail = (id: string) =>
http.get<T.RuleResp>(`${BASE_URL}/rule/${id}`)
/* ===== 周期 ===== */
export const getPeriodList = () => http.get<T.PeriodResp[]>(`${BASE_URL}/period`)
export const addPeriod = (data: T.PeriodResp) => http.post<boolean>(`${BASE_URL}/period`, data)
export const deletePeriod = (id: string) => http.del<boolean>(`${BASE_URL}/period/${id}`)
export const updataPeriod = (id: string, data: T.PeriodResp) => http.post<boolean>(`${BASE_URL}/period/${id}`, data)
/* ===== 评估 ===== */
export const startEvaluate = (data: T.EvaluateReq) =>
http.post<boolean>(`${BASE_URL}/evaluate`, data)
export const getEvaluatePage = (query?: T.EvaluateQuery) =>
http.get<PageRes<T.EvaluateResp[]>>(`${BASE_URL}/evaluate/page`, query)
// 同样的修改和删除评估接口
/** 员工查看自己的绩效 */
export const getMyEvaluate = (query?: T.EvaluateQuery) =>
http.get<PageRes<T.EvaluateResp[]>>(`${BASE_URL}/evaluate/my`, query)
/* ===== 反馈 ===== */
export const submitFeedback = (data: T.FeedbackReq) =>
http.post<boolean>(`${BASE_URL}/feedback`, data)

View File

@ -1,87 +0,0 @@
// ========== 维度 ==========
export interface DimensionResp {
id: string
name: string
desc: string
weight: number
status: 0 | 1
ruleCount: number
deptId: string
}
export interface DimensionAddReq {
name: string
desc: string
weight: number
deptId: string
}
export interface DimensionUpdateReq extends DimensionAddReq {
status: 0 | 1
}
// ========== 细则 ==========
export interface RuleResp {
id: string
dimensionId: string
name: string
ruleDesc: string
score: number
weight: number
isExtra: boolean
}
export interface RuleAddReq {
dimensionId: string
name: string
ruleDesc: string
score: number
weight: number
isExtra: boolean
}
export interface RuleUpdateReq extends RuleAddReq {}
// ========== 绩效周期 ==========
export interface PeriodResp {
id: string
name: string
startDate: string
endDate: string
}
// ========== 评估 ==========
export interface EvaluateReq {
userId: string
dimensionId: string
ruleId: string
periodId: string
}
export interface EvaluateResp {
id: string // 细则id
userId: string
userName: string
dimensionName: string
ruleName: string
periodName: string
score: number
aiComment: string
status: 0 | 1 // 0待反馈 1已反馈
}
// ========== 查询 ==========
export interface EvaluateQuery {
keyword?: string
periodId?: string
dimensionId?: string
status?: 0 | 1
}
// ========== 反馈 ==========
export interface FeedbackReq {
evaluateId: string
level: 0 | 1 | 2
content: string
userId: string
}

View File

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

View File

@ -1,64 +0,0 @@
import type { SalaryRecord, SalaryQuery, SalaryCreateRequest } from '@/views/salary-management/types'
import http from '@/utils/http'
const BASE_URL = '/salary'
// 获取工资单列表
export const getSalaryList = (query?: SalaryQuery) => {
return http.get<PageRes<SalaryRecord[]>>(`${BASE_URL}/list`, query)
}
// 获取工资单详情
export const getSalaryDetail = (id: string) => {
return http.get<SalaryRecord>(`${BASE_URL}/${id}`)
}
// 创建工资单
export const createSalary = (data: SalaryCreateRequest) => {
return http.post<SalaryRecord>(`${BASE_URL}`, data)
}
// 更新工资单
export const updateSalary = (id: string, data: Partial<SalaryRecord>) => {
return http.put<SalaryRecord>(`${BASE_URL}/${id}`, data)
}
// 删除工资单
export const deleteSalary = (id: string) => {
return http.del<boolean>(`${BASE_URL}/${id}`)
}
// 提交审批
export const submitApproval = (id: string) => {
return http.post<boolean>(`${BASE_URL}/${id}/submit`)
}
// 审批工资单
export const approveSalary = (id: string, data: { status: string; comment?: string }) => {
return http.put<boolean>(`${BASE_URL}/${id}/approve`, data)
}
// 导出工资单
export const exportSalary = (id: string, format: 'excel' | 'pdf' = 'excel') => {
return http.download(`${BASE_URL}/${id}/export`, { format })
}
// 批量导出
export const exportSalaryBatch = (ids: string[], format: 'excel' | 'pdf' = 'excel') => {
return http.download(`${BASE_URL}/export/batch`, { ids, format })
}
// 获取实习生配置
export const getInternConfig = () => {
return http.get<any>(`${BASE_URL}/config/intern`)
}
// 获取员工列表
export const getEmployeeList = (type?: string) => {
return http.get<any[]>(`${BASE_URL}/employees`, { type })
}
// 获取项目列表
export const getProjectList = () => {
return http.get<any[]>(`${BASE_URL}/projects`)
}

View File

@ -1,234 +0,0 @@
<template>
<div class="turbine-grid-container">
<div class="turbine-grid">
<div v-for="turbine in turbines" :key="turbine.id" class="turbine-card"
:class="getStatusClass(turbine.status)">
<div class="turbine-status-badge" :class="`status-${turbine.status}`">
{{ getStatusText(turbine.status) }}
</div>
<div class="turbine-icon">
<img src="/static/images/wind-turbine-icon.svg" alt="风机图标" class="turbine-image" />
</div>
<div class="turbine-info">
<div class="turbine-number">
<a-input v-model="turbine.turbineNo" size="small" class="turbine-input" placeholder="请输入机组编号"
@change="handleTurbineNoChange(turbine)" />
</div>
</div>
<div class="turbine-actions">
<a-button type="text" size="mini" @click="openMapModal(turbine)" title="地图选点">
<template #icon><icon-location /></template>
</a-button>
<a-button type="text" size="mini" @click="editTurbine(turbine)" title="编辑">
<template #icon><icon-edit /></template>
</a-button>
</div>
</div>
</div>
<!-- 添加新机组按钮 -->
<div v-if="showAddButton" class="turbine-card add-turbine-card" @click="addTurbine">
<div class="add-icon">
<icon-plus />
</div>
<div class="add-text">添加机组</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
interface Turbine {
id: number
turbineNo: string
status: 0 | 1 | 2 // 0: , 1: , 2:
lat?: number
lng?: number
}
interface Props {
turbines: Turbine[]
showAddButton?: boolean
}
interface Emits {
(e: 'update:turbines', turbines: Turbine[]): void
(e: 'turbine-change', turbine: Turbine): void
(e: 'add-turbine'): void
}
const props = withDefaults(defineProps<Props>(), {
showAddButton: false
})
const emit = defineEmits<Emits>()
const getStatusText = (status: number) => {
const statusMap = {
0: '待施工',
1: '施工中',
2: '已完成'
}
return statusMap[status] || '未知状态'
}
const getStatusClass = (status: number) => {
return `status-${status}`
}
const handleTurbineNoChange = (turbine: Turbine) => {
emit('turbine-change', turbine)
emit('update:turbines', props.turbines)
}
const openMapModal = (turbine: Turbine) => {
Message.info(`地图选点功能待开发,当前机组编号:${turbine.turbineNo}`)
}
const editTurbine = (turbine: Turbine) => {
//
Message.info(`编辑机组:${turbine.turbineNo}`)
}
const addTurbine = () => {
emit('add-turbine')
}
</script>
<style scoped>
.turbine-grid-container {
padding: 16px;
}
.turbine-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 16px;
}
.turbine-card {
position: relative;
background: white;
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 16px;
transition: all 0.3s ease;
cursor: pointer;
}
.turbine-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.turbine-card.status-0 {
border-left: 4px solid #ff7d00;
}
.turbine-card.status-1 {
border-left: 4px solid #165dff;
}
.turbine-card.status-2 {
border-left: 4px solid #00b42a;
}
.turbine-status-badge {
position: absolute;
top: 8px;
left: 8px;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
color: white;
}
.turbine-status-badge.status-0 {
background-color: #ff7d00;
}
.turbine-status-badge.status-1 {
background-color: #165dff;
}
.turbine-status-badge.status-2 {
background-color: #00b42a;
}
.turbine-icon {
display: flex;
justify-content: center;
margin: 20px 0;
}
.turbine-image {
width: 80px;
height: 80px;
object-fit: contain;
}
.turbine-info {
text-align: center;
margin-bottom: 12px;
}
.turbine-input {
text-align: center;
font-weight: 500;
}
.turbine-actions {
display: flex;
justify-content: center;
gap: 8px;
}
.add-turbine-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2px dashed #d9d9d9;
background-color: #fafafa;
color: #666;
min-height: 200px;
}
.add-turbine-card:hover {
border-color: #165dff;
color: #165dff;
background-color: #f0f7ff;
}
.add-icon {
font-size: 32px;
margin-bottom: 8px;
}
.add-text {
font-size: 14px;
}
@media (max-width: 768px) {
.turbine-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.turbine-card {
padding: 12px;
}
.turbine-image {
width: 60px;
height: 60px;
}
}
</style>

View File

@ -4,7 +4,7 @@
:style="appStore.menuDark ? appStore.themeCSSVar : undefined"
>
<a-layout-sider
class="menu" collapsible breakpoint="xl" hide-trigger :width="250"
class="menu" collapsible breakpoint="xl" hide-trigger :width="300"
:collapsed="appStore.menuCollapse" @collapse="handleCollapse"
>
<Logo :collapsed="appStore.menuCollapse"></Logo>

View File

@ -1,12 +1,8 @@
<template>
<section class="system-logo" :class="{ collapsed: props.collapsed }" @click="toHome">
<row>
<img v-if="logo" class="logo" :src="logo" alt="logo" />
<img v-else class="logo" src="/logo.svg" alt="logo" />
</row>
<row>
<span class="system-name gi_line_1">{{ title }}</span>
</row>
<img v-if="logo" class="logo" :src="logo" alt="logo" />
<img v-else class="logo" src="/logo.svg" alt="logo" />
<span class="system-name gi_line_1">{{ title }}</span>
</section>
</template>
@ -18,10 +14,8 @@ const props = withDefaults(defineProps<Props>(), {
})
const appStore = useAppStore()
// const title = computed(() => appStore.getTitle())
const title = "数智平台"
const logo = "/logo.png"
//computed(() => appStore.getLogo())
const title = "武汉迪特聚能有限公司管理平台"
const logo = computed(() => appStore.getLogo())
interface Props {
collapsed?: boolean
@ -35,46 +29,50 @@ const toHome = () => {
<style scoped lang="scss">
.system-logo {
height: 64px; //
padding: 8px 12px;
height: 56px;
padding: 0 12px;
color: var(--color-text-1);
font-size: 16px;
line-height: 1;
display: flex;
flex-direction: column;
justify-content: center;
font-weight: 600;
align-items: center;
flex-shrink: 0;
cursor: pointer;
user-select: none;
box-sizing: border-box;
&.collapsed {
height: 56px;
flex-direction: row;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
.system-name {
display: none;
}
}
.logo {
margin-bottom: 0;
.logo {
width: 32px;
height: 32px;
border-radius: 6px;
transition: all 0.2s;
overflow: hidden;
flex-shrink: 0;
}
.system-name {
padding-left: 6px;
white-space: nowrap;
transition: color 0.3s;
line-height: 1.5;
display: inline-flex;
align-items: center;
&:hover {
color: $color-theme !important;
cursor: pointer;
}
}
}
.logo {
height: 32px;
width: auto;
max-width: 140px; // logo
object-fit: contain;
margin-bottom: 4px;
}
.system-name {
font-size: 13px;
font-weight: 600;
color: var(--color-text-1);
text-align: center;
line-height: 1.2;
white-space: nowrap;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -57,7 +57,6 @@ declare global {
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']

View File

@ -61,7 +61,6 @@ declare module 'vue' {
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
TextCopy: typeof import('./../components/TextCopy/index.vue')['default']
TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default']
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
Verify: typeof import('./../components/Verify/index.vue')['default']
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']

View File

@ -0,0 +1,431 @@
<template>
<div class="airport-management">
<div class="page-header">
<a-card class="general-card" title="机场管理">
<template #extra>
<a-button type="primary" @click="handleAdd">
<template #icon>
<icon-plus />
</template>
新建机场
</a-button>
</template>
<a-row :gutter="16" style="margin-bottom: 16px">
<a-col :span="6">
<a-input-search
v-model="searchForm.name"
placeholder="请输入机场名称"
@search="search"
allow-clear
/>
</a-col>
<a-col :span="6">
<a-select
v-model="searchForm.status"
placeholder="请选择状态"
allow-clear
style="width: 100%"
>
<a-option value="1">运营中</a-option>
<a-option value="0">停运</a-option>
</a-select>
</a-col>
<a-col :span="6">
<a-button type="primary" @click="search">
<template #icon>
<icon-search />
</template>
查询
</a-button>
<a-button style="margin-left: 8px" @click="reset">
<template #icon>
<icon-refresh />
</template>
重置
</a-button>
</a-col>
</a-row>
</a-card>
</div>
<div class="page-content">
<a-card class="general-card">
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #status="{ record }">
<a-tag :color="record.status === '1' ? 'green' : 'red'">
{{ record.status === '1' ? '运营中' : '停运' }}
</a-tag>
</template>
<template #actions="{ record }">
<a-button type="text" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button type="text" size="small" @click="handleView(record)">
查看详情
</a-button>
<a-popconfirm
content="确定要删除这个机场吗?"
@ok="handleDelete(record)"
>
<a-button type="text" size="small" status="danger">
删除
</a-button>
</a-popconfirm>
</template>
</a-table>
</a-card>
</div>
<!-- 新增/编辑机场弹窗 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
@ok="handleSave"
@cancel="handleCancel"
width="800px"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="机场名称" field="name">
<a-input v-model="form.name" placeholder="请输入机场名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="机场代码" field="code">
<a-input v-model="form.code" placeholder="请输入机场代码" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="所在省份" field="province">
<a-input v-model="form.province" placeholder="请输入所在省份" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="所在城市" field="city">
<a-input v-model="form.city" placeholder="请输入所在城市" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="经度" field="longitude">
<a-input-number
v-model="form.longitude"
placeholder="请输入经度"
:precision="6"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="纬度" field="latitude">
<a-input-number
v-model="form.latitude"
placeholder="请输入纬度"
:precision="6"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="海拔高度(米)" field="altitude">
<a-input-number
v-model="form.altitude"
placeholder="请输入海拔高度"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" field="status">
<a-select v-model="form.status" placeholder="请选择状态">
<a-option value="1">运营中</a-option>
<a-option value="0">停运</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="备注" field="remark">
<a-textarea
v-model="form.remark"
placeholder="请输入备注信息"
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconPlus, IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon'
//
const searchForm = reactive({
name: '',
status: ''
})
//
const columns = [
{
title: '机场名称',
dataIndex: 'name',
width: 150
},
{
title: '机场代码',
dataIndex: 'code',
width: 120
},
{
title: '所在地区',
dataIndex: 'location',
width: 200
},
{
title: '经纬度',
dataIndex: 'coordinates',
width: 180
},
{
title: '海拔高度(米)',
dataIndex: 'altitude',
width: 120
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180
},
{
title: '操作',
slotName: 'actions',
width: 200,
fixed: 'right'
}
]
//
const tableData = ref([
{
id: 1,
name: '北京首都国际机场',
code: 'PEK',
province: '北京市',
city: '北京市',
location: '北京市朝阳区',
longitude: 116.584556,
latitude: 40.080111,
coordinates: '116.584556, 40.080111',
altitude: 35,
status: '1',
remark: '中国最大的国际机场之一',
createTime: '2024-01-01 10:00:00'
},
{
id: 2,
name: '上海浦东国际机场',
code: 'PVG',
province: '上海市',
city: '上海市',
location: '上海市浦东新区',
longitude: 121.805214,
latitude: 31.143378,
coordinates: '121.805214, 31.143378',
altitude: 4,
status: '1',
remark: '华东地区重要的国际航空枢纽',
createTime: '2024-01-02 14:30:00'
},
{
id: 3,
name: '广州白云国际机场',
code: 'CAN',
province: '广东省',
city: '广州市',
location: '广州市白云区',
longitude: 113.298889,
latitude: 23.392436,
coordinates: '113.298889, 23.392436',
altitude: 15,
status: '1',
remark: '华南地区最大的民用机场',
createTime: '2024-01-03 09:15:00'
}
])
const loading = ref(false)
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showTotal: true,
showPageSize: true
})
//
const modalVisible = ref(false)
const modalTitle = ref('新增机场')
const formRef = ref()
//
const form = reactive({
id: null,
name: '',
code: '',
province: '',
city: '',
longitude: null,
latitude: null,
altitude: null,
status: '1',
remark: ''
})
//
const rules = {
name: [{ required: true, message: '请输入机场名称' }],
code: [{ required: true, message: '请输入机场代码' }],
province: [{ required: true, message: '请输入所在省份' }],
city: [{ required: true, message: '请输入所在城市' }],
longitude: [{ required: true, message: '请输入经度' }],
latitude: [{ required: true, message: '请输入纬度' }],
status: [{ required: true, message: '请选择状态' }]
}
//
const search = () => {
loading.value = true
// API
setTimeout(() => {
loading.value = false
Message.success('查询成功')
}, 1000)
}
//
const reset = () => {
searchForm.name = ''
searchForm.status = ''
search()
}
//
const handleAdd = () => {
modalTitle.value = '新增机场'
modalVisible.value = true
resetForm()
}
//
const handleEdit = (record: any) => {
modalTitle.value = '编辑机场'
modalVisible.value = true
Object.assign(form, record)
}
//
const handleView = (record: any) => {
Message.info(`查看机场详情: ${record.name}`)
//
}
//
const handleDelete = (record: any) => {
Message.success(`删除机场: ${record.name}`)
// API
}
//
const handleSave = async () => {
const valid = await formRef.value?.validate()
if (valid) return
try {
loading.value = true
// API
await new Promise(resolve => setTimeout(resolve, 1000))
Message.success(form.id ? '更新成功' : '新增成功')
modalVisible.value = false
search()
} catch (error) {
Message.error('操作失败')
} finally {
loading.value = false
}
}
//
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
//
const resetForm = () => {
Object.keys(form).forEach(key => {
if (key === 'status') {
form[key] = '1'
} else {
form[key] = null
}
})
formRef.value?.clearValidate()
}
//
const handlePageChange = (page: number) => {
pagination.current = page
search()
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
search()
}
onMounted(() => {
search()
})
</script>
<style scoped lang="scss">
.airport-management {
.page-header {
margin-bottom: 16px;
}
.general-card {
border-radius: 8px;
}
}</style>

View File

@ -12,14 +12,6 @@
<a-form-item label="缺陷名称" field="defectName">
<a-input v-model="form.defectName" placeholder="请输入缺陷名称" />
</a-form-item>
<!-- 缺陷位置 -->
<a-form-item
field="defectPosition"
label="缺陷位置"
>
<a-input v-model="form.defectPosition" placeholder="请输入缺陷位置" />
</a-form-item>
<!-- 缺陷类型 -->
<a-form-item
@ -35,47 +27,19 @@
<a-option v-for="type in defectTypes" :key="type.code" :value="type.code">{{ type.label }}</a-option>
</a-select>
</a-form-item>
<!--轴向尺寸-->
<div class="dimension-fields">
<a-form-item
field="axialDimension"
label="轴向尺寸 (mm)"
:rules="[{ type: 'number', min: 0, message: '必须大于等于0' }]"
>
<a-input-number
v-model="form.axialDimension"
:min="0"
:step="1"
placeholder="请输入轴向尺寸"
mode="button"
size="large"
>
</a-input-number>
</a-form-item>
<!--弦向尺寸 -->
<a-form-item
field="chordDimension"
label="弦向尺寸 (mm)"
:rules="[{ type: 'number', min: 0, message: '必须大于等于0' }]"
>
<a-input-number
v-model="form.chordDimension"
:min="0"
:step="1"
placeholder="请输入弦向尺寸"
mode="button"
size="large"
>
</a-input-number>
</a-form-item>
</div>
<!-- 缺陷位置 -->
<a-form-item
field="defectPosition"
label="缺陷位置"
>
<a-input v-model="form.defectPosition" placeholder="请输入缺陷位置" />
</a-form-item>
<!-- 缺陷等级 -->
<a-form-item
field="defectLevel"
label="严重程度"
label="缺陷等级"
:rules="[{ required: true, message: '请选择缺陷等级' }]"
>
<a-select
@ -140,8 +104,6 @@ interface DefectFormData {
defectPosition: string
description: string
repairIdea: string
axialDimension: number | 0
chordDimension: number | 0
}
const props = defineProps<{
@ -173,9 +135,7 @@ const form = reactive<DefectFormData>({
defectLevelLabel: '',
defectPosition: '',
description: '',
repairIdea: '建议进行进一步检查',
axialDimension: 0, // null
chordDimension: 0 // null
repairIdea: '建议进行进一步检查'
})
//
@ -366,7 +326,6 @@ const handleSubmit = async () => {
}
//
console.log("form:",form);
emit('submit', form, props.annotation)
} catch (error) {
@ -475,25 +434,6 @@ onMounted(() => {
}
}
.dimension-fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
:deep(.arco-input-number) {
width: 100%;
.arco-input-number-step {
width: 32px;
}
.arco-input-number-input {
text-align: center;
}
}
}
:deep(.arco-form-item) {
margin-bottom: 16px;
}

View File

@ -1,242 +0,0 @@
<template>
<a-modal
:visible="previewModalVisible"
title="图像详情"
width="80%"
:footer="footerButtons"
:mask-closable="true"
@update:visible="emit('update:previewModalVisible', $event)"
:confirm-loading="loading"
>
<div v-if="previewImage" class="modal-image-viewer">
<img :src="getImageUrl(previewImage.imagePath)" :alt="editingData.imageName" class="preview-image" />
<div class="image-details">
<a-form layout="vertical" :model="editingData">
<a-form-item label="图片名称" field="imageName">
<a-input v-model="editingData.imageName" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="大小" field="size">
<a-input :model-value="formatFileSize(editingData.size || 0)" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="尺寸" field="imageResolution">
<a-input v-model="editingData.imageResolution" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="采集员ID" field="collectId">
<a-input v-model="editingData.collectId" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="采集员姓名" field="collectorName">
<a-input v-model="editingData.collectorName" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="来源" field="imageSource">
<a-input v-model="editingData.imageSource" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="图片类型" field="imageType">
<a-select v-model="editingData.imageType">
<a-option value="DEFECT">缺陷影像</a-option>
<a-option value="TYPICAL">典型影像</a-option>
<a-option value="OTHER">其他影像</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="图片类型描述" field="imageTypeLabel">
<a-textarea
v-model="editingData.imageTypeLabel"
:auto-size="{ minRows: 2, maxRows: 4 }"
/>
</a-form-item>
</a-form>
</div>
</div>
<template #footer>
<a-button @click="emit('update:previewModalVisible', false)">取消</a-button>
<a-button type="primary" :loading="loading" @click="handleSave">保存</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import axios from 'axios'
const props = defineProps<{
previewModalVisible: boolean
previewImage: {
imagePath: string
imageName: string
size?: number
imageResolution?: string
collectId?: string
collectorName?: string
imageSource?: string
imageType?: string
imageTypeLabel?: string
partId?: string
} | null
selectedImage: any | null
}>()
const emit = defineEmits<{
'update:previewModalVisible': [visible: boolean]
'save': [data: any]
}>()
const loading = ref(false)
const editingData = ref<{
imagePath: string
imageName: string
size?: number
imageResolution?: string
collectId?: string
collectorName?: string
imageSource?: string
imageType?: string
imageTypeLabel?: string
partId?: string
imageId?:string
}>({
imagePath: '',
imageName: '',
size: 0,
imageResolution: '',
collectId: '',
collectorName: '',
imageSource: '',
imageType: '',
imageTypeLabel: '',
partId: '',
imageId:''
})
//
watch(() => props.previewImage, (newVal) => {
if (newVal) {
editingData.value = { ...newVal }
}
}, { immediate: true, deep: true })
const getImageUrl = (imagePath: string): string => {
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
const baseUrl = 'http://pms.dtyx.net:9158'
return `${baseUrl}${imagePath}`
}
const formatFileSize = (size: number): string => {
if (size < 1024) return `${size} B`
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`
}
const handleSave = async () => {
if (!editingData.value.imageName?.trim()) {
Message.error('图片名称不能为空')
return
}
if (!editingData.value.partId) {
Message.error('缺少部件ID无法保存')
return
}
try {
loading.value = true
//
const requestData = {
collectId: editingData.value.collectId,
collectorName: editingData.value.collectorName,
imageSource: editingData.value.imageSource,
imageType: editingData.value.imageType,
imageTypeLabel: editingData.value.imageTypeLabel,
imageList: [
{
imageId: editingData.value.imageId,
imageName: editingData.value.imageName,
imageResolution: editingData.value.imageResolution,
}
],
}
console.log("requestData:",requestData);
//
const response = await axios.post(
`http://pms.dtyx.net:9158/image/setting-info/${editingData.value.partId}`,
requestData
)
if (response.data && response.data.code === 200 && response.data.success) {
Message.success('图片信息保存成功')
emit('save', editingData.value)
emit('update:previewModalVisible', false)
} else {
Message.error(response.data?.message || '保存失败')
}
} catch (error) {
console.error('保存图片信息失败:', error)
Message.error('保存失败,请稍后重试')
} finally {
loading.value = false
}
}
</script>
<style scoped lang="scss">
.modal-image-viewer {
display: flex;
gap: 24px;
align-items: flex-start;
.preview-image {
flex: 1;
max-width: 60%;
max-height: 500px;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.image-details {
flex: 1;
padding: 16px;
:deep(.arco-form) {
.arco-form-item {
margin-bottom: 16px;
}
}
}
}
@media (max-width: 768px) {
.modal-image-viewer {
flex-direction: column;
.preview-image {
max-width: 100%;
}
}
}
</style>

View File

@ -349,7 +349,6 @@ export function useIndustrialImage() {
}
const handleImagePreview = (image: any) => {
console.log("image:",image);
previewImage.value = image
previewModalVisible.value = true
}

View File

@ -145,14 +145,7 @@
</div>
<!-- 模态框 -->
<ImageModals
:preview-modal-visible="previewModalVisible"
:preview-image="previewImage"
:process-image="processImage"
:selected-image="selectedImage"
@update:preview-modal-visible="previewModalVisible = $event"
@save="handleSaveImageSuccess"
/>
</div>
</template>
@ -165,7 +158,6 @@ import ProjectTree from './components/ProjectTree.vue'
import ImagePreview from './components/ImagePreview.vue'
import ImageCanvas from '@/views/project-operation-platform/data-processing/industrial-image/components/ImageCanvas.vue'
import IndustrialImageList from '@/components/IndustrialImageList/index.vue'
import ImageModals from './components/ImageModals.vue'
import RecognitionResults from './components/RecognitionResults.vue'
import DefectListPanel from './components/DefectListPanel.vue'
import DefectDetailsForm from './components/DefectDetailsForm.vue'
@ -220,19 +212,6 @@ const loadDefectLevels = async () => {
}
}
//
const handleSaveImageSuccess = (updatedData: any) => {
//
Message.success('图片信息已更新')
// selectedImagepreviewImage
if (selectedImage.value && selectedImage.value.id === updatedData.id) {
selectedImage.value = { ...selectedImage.value, ...updatedData }
}
if (previewImage.value && previewImage.value.id === updatedData.id) {
previewImage.value = { ...previewImage.value, ...updatedData }
}
}
// 使
const {
//
@ -615,8 +594,8 @@ const handleDefectFormSubmit = async (formData: any, annotation: Annotation) =>
const defectData = {
attachId: attachId,
attachPath: '',
axial: formData.axialDimension,
chordwise: formData.chordDimension,
// axial: 0,
// chordwise: 0,
// defectCode: `MANUAL_${Date.now()}`,
defectId: '',
defectLevel: formData.defectLevel,

View File

@ -38,7 +38,7 @@ const IconMap: Record<number, Component> = {
const router = useRouter()
//
const back = () => {
router.replace({ path: '/asset-management/intellectual-property' })
router.replace({ path: '/project-management/bidding/tender-documents' })
}
</script>
@ -58,7 +58,6 @@ const back = () => {
flex-direction: column;
align-items: center;
}
&__img {
width: 100%;
position: relative;
@ -67,17 +66,14 @@ const back = () => {
justify-content: center;
align-items: center;
}
&__icon {
max-width: 90%;
height: 50vh;
}
&__tip {
display: flex;
flex-direction: column;
align-items: center;
&--a {
margin-bottom: 20px;
font-size: 32px;
@ -89,7 +85,6 @@ const back = () => {
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&--b {
margin-bottom: 10px;
font-size: 20px;
@ -102,7 +97,6 @@ const back = () => {
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&--c {
padding: 0 30px;
margin-bottom: 20px;
@ -118,13 +112,11 @@ const back = () => {
}
}
}
@keyframes slideUp {
0% {
opacity: 0;
transform: translateY(60px);
}
100% {
opacity: 1;
transform: translateY(0);

View File

@ -1,73 +0,0 @@
<template>
<GiPageLayout :margin="true">
<template #header>
<h2>新增考勤记录</h2>
</template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical"
@submit="onSubmit"
>
<a-form-item label="打卡照片" name="recordImage">
<a-input v-model="form.recordImage" placeholder="请输入打卡照片URL" />
<!-- 你可以替换为图片上传组件 -->
</a-form-item>
<a-form-item label="打卡地点(经纬度)" name="recordPosition">
<a-input v-model="form.recordPosition" placeholder="请输入经纬度" />
</a-form-item>
<a-form-item label="打卡地点(中文描述)" name="recordPositionLabel">
<a-input v-model="form.recordPositionLabel" placeholder="请输入地点描述" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="loading">提交</a-button>
</a-form-item>
</a-form>
<a-alert v-if="resultMsg" :type="resultType" :show-icon="true" style="margin-top: 16px;">
{{ resultMsg }}
</a-alert>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { addAttendanceRecord } from '@/apis/attendance-record/index'
import type { AttendanceRecordReq } from '@/apis/attendance-record/type'
const formRef = ref()
const form = ref<AttendanceRecordReq>({
recordImage: '',
recordPosition: '',
recordPositionLabel: '',
})
const rules = {
//
}
const loading = ref(false)
const resultMsg = ref('')
const resultType = ref<'success' | 'error'>('success')
const onSubmit = async () => {
loading.value = true
resultMsg.value = ''
try {
const res = await addAttendanceRecord(form.value)
if (res.success) {
resultMsg.value = '考勤记录提交成功'
resultType.value = 'success'
form.value = { recordImage: '', recordPosition: '', recordPositionLabel: '' }
} else {
resultMsg.value = res.msg || '提交失败'
resultType.value = 'error'
}
} catch (e: any) {
resultMsg.value = e?.message || '提交异常'
resultType.value = 'error'
} finally {
loading.value = false
}
}
</script>

View File

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

View File

@ -15,15 +15,30 @@
<a-card class="search-card" :bordered="false">
<a-form :model="searchForm" layout="inline">
<a-form-item label="证书名称" field="certificationName">
<a-input v-model="searchForm.certificationName" placeholder="请输入证书名称" allow-clear style="width: 200px" />
<a-input
v-model="searchForm.certificationName"
placeholder="请输入证书名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="证书类型" field="certificationType">
<a-input v-model="searchForm.certificationType" placeholder="请输入证书类型" allow-clear style="width: 200px" />
<a-input
v-model="searchForm.certificationType"
placeholder="请输入证书类型"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="用户姓名" field="userName">
<a-input v-model="searchForm.userName" placeholder="请输入用户姓名" allow-clear style="width: 200px" />
<a-input
v-model="searchForm.userName"
placeholder="请输入用户姓名"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item>
@ -47,15 +62,28 @@
<!-- 人员资质表格 -->
<a-card class="table-card" :bordered="false">
<a-table :columns="columns" :data="certificationList" :pagination="paginationConfig" :loading="loading"
row-key="certificationId" @page-change="handlePageChange">
<a-table
:columns="columns"
:data="certificationList"
:pagination="paginationConfig"
:loading="loading"
row-key="certificationId"
@page-change="handlePageChange"
>
<template #userName="{ record }">
<span>{{ getUserName(record.userId) }}</span>
</template>
<template #certificationImage="{ record }">
<a-image v-if="record.certificationImage" :src="record.certificationImage" width="60" height="40"
fit="cover" show-loader preview />
<a-image
v-if="record.certificationImage"
:src="record.certificationImage"
width="60"
height="40"
fit="cover"
show-loader
preview
/>
<span v-else>-</span>
</template>
@ -77,9 +105,19 @@
</a-card>
<!-- 新增/编辑资质信息模态框 -->
<a-modal v-model:visible="modalVisible" :title="isEdit ? '编辑资质信息' : '新增资质信息'" width="700px" @ok="handleSubmit"
@cancel="handleCancel">
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
<a-modal
v-model:visible="modalVisible"
:title="isEdit ? '编辑资质信息' : '新增资质信息'"
width="700px"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="证书编号" field="certificationCode">
@ -94,47 +132,75 @@
</a-col>
</a-row>
<a-form-item label="证书类型" field="certificationType">
<a-select v-model="formData.certificationType" placeholder="请选择证书类型">
<a-option v-for="item in CERTIFICATION_TYPE_OPTIONS" :key="item.value" :value="item.value"
:label="item.label">
{{ item.label }}
</a-option>
</a-select>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="证书类型" field="certificationType">
<a-input v-model="formData.certificationType" placeholder="请输入证书类型" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="持证人" field="userId">
<a-select v-model="formData.userId" placeholder="请选择持证人" :loading="loadingUsers" allow-search
:filter-option="filterUserOption" @="console.log('d', formData)">
<a-option v-for="user in userList" :key="user.userId" :value="user.userId" :label="user.name">
{{ user.name }}({{ user.account }})
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="持证人" field="userId">
<a-select
v-model="formData.userId"
placeholder="请选择持证人"
:loading="loadingUsers"
allow-search
:filter-option="filterUserOption"
>
<a-option
v-for="user in userList"
:key="user.userId"
:value="user.userId"
:label="user.name"
>
{{ user.name }}({{ user.account }})
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="有效期开始" field="validityDateBegin">
<a-date-picker v-model="formData.validityDateBegin" placeholder="请选择有效期开始日期" style="width: 100%" />
<a-date-picker
v-model="formData.validityDateBegin"
placeholder="请选择有效期开始日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="有效期结束" field="validityDateEnd">
<a-date-picker v-model="formData.validityDateEnd" placeholder="请选择有效期结束日期" style="width: 100%" />
<a-date-picker
v-model="formData.validityDateEnd"
placeholder="请选择有效期结束日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="证书图片" field="certificationImage">
<a-upload :custom-request="handleUpload" :show-file-list="false" accept="image/*"
:before-upload="beforeUpload">
<a-upload
:custom-request="handleUpload"
:show-file-list="false"
accept="image/*"
:before-upload="beforeUpload"
>
<template #upload-button>
<div class="upload-wrapper">
<a-image v-if="formData.certificationImage" :src="formData.certificationImage" width="200"
height="120" fit="cover" show-loader preview />
<a-image
v-if="formData.certificationImage"
:src="formData.certificationImage"
width="200"
height="120"
fit="cover"
show-loader
preview
/>
<div v-else class="upload-placeholder">
<icon-plus />
<div>点击上传证书图片</div>
@ -167,7 +233,7 @@ const isEdit = ref(false)
const formRef = ref()
//
const certificationList = ref<CertificationAPI.CertificationListResponse>()
const certificationList = ref<CertificationInfo[]>([])
const userList = ref<SimpleUserInfo[]>([])
//
@ -188,14 +254,14 @@ const paginationConfig = reactive({
//
const formData = reactive<CertificationInfo>({
certificationId: '',
certificationCode: '',
certificationImage: 'n',
certificationImage: '',
certificationName: '',
certificationType: '',
userId: '',
validityDateBegin: '',
validityDateEnd: '',
// certificationTypeLabel: ''
validityDateEnd: ''
})
//
@ -219,15 +285,7 @@ const formRules = {
{ required: true, message: '请选择有效期结束日期' }
]
}
const CERTIFICATION_TYPE_OPTIONS = [
{ label: '高处作业', value: 'height-operation' },
{ label: '低压电工', value: 'low-voltage-operation' },
{ label: '高压电工', value: 'high-voltage-operation' },
{ label: '海上交通安全', value: 'maritime-traffic-safety' },
{ label: '驾驶证', value: 'driving-license' },
{ label: '防雷检测', value: 'lightning-protection-detection' },
{ label: '无人机驾驶', value: 'drone-driving' }
]
//
const columns = [
{
@ -243,15 +301,11 @@ const columns = [
{
title: '证书类型',
dataIndex: 'certificationType',
width: 150,
render: ({ record }) => {
const type = CERTIFICATION_TYPE_OPTIONS.find(item => item.value === record.certificationType)
return type ? type.label : '-'
}
width: 150
},
{
title: '持证人',
dataIndex: 'userName',
dataIndex: 'userId',
slotName: 'userName',
width: 120
},
@ -285,10 +339,10 @@ const getUserName = (userId: string) => {
const filterUserOption = (inputValue: string, option: any) => {
const user = userList.value.find(u => u.userId === option.value)
if (!user) return false
const searchText = inputValue.toLowerCase()
return user.name.toLowerCase().includes(searchText) ||
user.account.toLowerCase().includes(searchText)
return user.name.toLowerCase().includes(searchText) ||
user.account.toLowerCase().includes(searchText)
}
//
@ -296,16 +350,11 @@ const getUserList = async () => {
try {
loadingUsers.value = true
const response = await CertificationAPI.getUserList()
console.log('用户列表响应:', response)
if (response.data) {
userList.value = response.data.map(item => ({
userId: item.userId,
name: item.name,
account: item.account,
})) || []
console.log('用户列表响应2:', userList.value)
userList.value = response.data || []
}
} catch (error) {
console.error('获取用户列表失败:', error)
@ -321,15 +370,20 @@ const getCertificationList = async () => {
loading.value = true
const params: CertificationListParams = {
...searchForm,
current: paginationConfig.current,
size: paginationConfig.pageSize
}
const response = await CertificationAPI.getCertificationList(params)
console.log('资质信息列表响应:', response)
if (response.data) {
certificationList.value = response.data || []
certificationList.value = response.data.records || []
paginationConfig.total = response.data.total || 0
console.log('cc', certificationList.value)
console.log('cd', response.data[0])
}
} catch (error) {
console.error('获取资质信息列表失败:', error)
Message.error('获取资质信息列表失败')
} finally {
loading.value = false
@ -343,13 +397,13 @@ const beforeUpload = (file: File) => {
Message.error('只能上传图片文件')
return false
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
Message.error('图片大小不能超过 2MB')
return false
}
return true
}
@ -358,9 +412,9 @@ const handleUpload = async (option: any) => {
try {
const uploadFormData = new FormData()
uploadFormData.append('file', option.fileItem.file)
const response = await uploadFile(uploadFormData)
if (response.data && response.data.url) {
formData.certificationImage = response.data.url
Message.success('图片上传成功')
@ -437,7 +491,7 @@ const deleteRecord = (record: CertificationInfo) => {
const handleSubmit = async () => {
try {
await formRef.value?.validate()
if (isEdit.value) {
await CertificationAPI.updateCertification(formData.certificationId!, formData)
Message.success('更新成功')
@ -445,7 +499,7 @@ const handleSubmit = async () => {
await CertificationAPI.createCertification(formData)
Message.success('创建成功')
}
modalVisible.value = false
resetForm()
await getCertificationList()
@ -544,4 +598,4 @@ onMounted(() => {
font-size: 24px;
margin-bottom: 8px;
}
</style>
</style>

View File

@ -1,10 +1,8 @@
<!--任务管理-->
<template>
<GiPageLayout>
<a-button type="primary" style="margin-bottom: 16px" @click="openAddModal">发布任务</a-button>
<GiTable
row-key="taskId"
title="任务记录"
row-key="id"
title="工作量管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
@ -13,74 +11,38 @@
@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 #status="{ record }">
<a-tag :color="getTaskStatusColor(record.status)">{{ getTaskStatusText(record.status) }}</a-tag>
<template #toolbar-left>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新增工作量记录</template>
</a-button>
</template>
<!-- 工作量显示 -->
<template #workload="{ record }">
<span class="font-medium text-blue-600">{{ record.workload }}小时</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click.stop="onRowClick(record)">详情</a-link>
<a-link status="danger" @click.stop="deleteRecord(record)">删除</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link status="danger" @click="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>
@ -88,52 +50,13 @@
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({
taskName: '',
responsiblePerson: '', // mainUserId
status: '',
userName: '',
projectName: '',
startDate: '',
endDate: '',
page: 1,
size: 10
})
@ -141,241 +64,119 @@ let searchForm = reactive({
//
const queryFormColumns = [
{
field: 'taskName',
label: '任务标题',
field: 'userName',
label: '员工姓名',
type: 'input' as const,
props: {
placeholder: '请输入任务标题',
style: { width: '200px' }
placeholder: '请输入员工姓名'
}
},
{
field: 'responsiblePerson',
label: '分配用户',
field: 'projectName',
label: '项目名称',
type: 'input' as const,
props: {
placeholder: '请输入分配用户',
style: { width: '200px' }
}
},
{
field: 'status',
label: '任务状态',
type: 'select' as const,
props: {
placeholder: '请选择任务状态',
options: statusOptions,
style: { width: '200px' }
placeholder: '请输入项目名称'
}
}
]
//
const tableColumns: TableColumnData[] = [
{ 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: '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: '创建时间', dataIndex: 'createTime', width: 160 },
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
]
//
const loading = ref(false)
const dataList = ref<any[]>([])
const allData = ref<any[]>([])
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 pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
total: 2,
showTotal: true,
showPageSize: true
})
//
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 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()
loading.value = true
// API
setTimeout(() => {
loading.value = false
}, 1000)
}
const reset = async () => {
const reset = () => {
Object.assign(searchForm, {
taskName: '',
responsiblePerson: '',
status: '',
userName: '',
projectName: '',
startDate: '',
endDate: '',
page: 1,
size: 10
})
pagination.current = 1
filterAndPaginate()
search()
}
//
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
filterAndPaginate()
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
filterAndPaginate()
search()
}
//
const openAddModal = () => {
Message.info('新增工作量记录功能开发中...')
}
const editRecord = (record: any) => {
Message.info(`编辑工作量记录: ${record.userName}`)
}
//
const deleteRecord = (record: any) => {
const idx = allData.value.findIndex(item => item.taskId === record.taskId)
if (idx !== -1) {
allData.value.splice(idx, 1)
filterAndPaginate()
Message.success('任务已删除')
}
Message.info(`删除工作量记录: ${record.userName}`)
}
onMounted(() => {
fetchTaskList()
search()
})
</script>

View File

@ -1,6 +1,13 @@
<template>
<a-form ref="formRef" :model="form" :rules="rules" :label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }" size="large" @submit="handleLogin">
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }"
size="large"
@submit="handleLogin"
>
<a-form-item field="account" hide-label>
<a-input v-model="form.account" placeholder="请输入用户名" allow-clear />
</a-form-item>
@ -101,7 +108,7 @@ const handleLogin = async () => {
const { rememberMe } = loginConfig.value
loginConfig.value.account = rememberMe ? form.account : ''
await router.push({
path: (redirect as string) || '/asset-management/intellectual-property',
path: (redirect as string) || '/project-management/projects/initiation',
query: {
...othersQuery,
},

View File

@ -1,335 +0,0 @@
<template>
<!-- 图片预览模态框 -->
<a-modal
v-model:visible="previewModalVisible"
:width="1000"
:footer="false"
title="多媒体预览"
class="preview-modal"
@close="handleModalClose"
>
<div class="preview-container">
<!-- 图片预览区域 -->
<div class="image-section">
<img
:src="currentPreviewItem?.url"
:alt="currentPreviewItem?.name"
class="preview-image"
/>
</div>
<!-- 音频列表区域 -->
<div class="audio-section">
<h3 class="audio-title">关联音频 ({{ audioList.length }})</h3>
<div class="audio-list">
<div
v-for="(audio, index) in audioList"
:key="index"
class="audio-item"
:class="{ 'active': currentAudioIndex === index }"
>
<div class="audio-info">
<span class="audio-name">{{ audio.audioId || `音频 ${index + 1}` }}</span>
<span class="audio-duration">{{ formatDuration(audio.duration) }}</span>
</div>
<div class="audio-controls">
<a-button
size="mini"
@click="toggleAudio(index)"
:loading="audioLoading && currentAudioIndex === index"
>
{{ currentAudioIndex === index && isPlaying ? '暂停' : '播放' }}
</a-button>
<a-progress
v-if="currentAudioIndex === index"
:percent="playProgress"
:show-text="false"
size="small"
class="progress-bar"
/>
</div>
<!-- 隐藏的audio元素 -->
<audio
ref="audioPlayers"
:src="audio.url"
@timeupdate="handleTimeUpdate"
@ended="handleAudioEnded"
preload="metadata"
/>
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
import { Message } from '@arco-design/web-vue'
interface AudioItem {
url: string
name?: string
duration?: number //
}
interface PreviewItem {
url: string
name: string
audios?: AudioItem[]
}
//
const previewModalVisible = ref(false)
const currentPreviewItem = ref<PreviewItem | null>(null)
//
const audioList = ref<AudioItem[]>([])
const currentAudioIndex = ref(-1)
const isPlaying = ref(false)
const audioLoading = ref(false)
const playProgress = ref(0)
const audioPlayers = ref<HTMLAudioElement[]>([])
const getAudioUrl = (filePath: string): string => {
if (!filePath) return ''
if (filePath.startsWith('http')) return filePath
const baseUrl = 'http://pms.dtyx.net:9158'
return `${baseUrl}${filePath}`
}
//
const openPreview = (item: PreviewItem) => {
currentPreviewItem.value = item
audioList.value = []
for (const audio of item.audios) {
let temp={
audioId:audio.audioId,
url:getAudioUrl(audio.filePath)
}
audioList.value.push(temp)
}
previewModalVisible.value = true
resetAudioState()
//
nextTick(() => {
loadAudioDurations()
})
}
//
const handleModalClose = () => {
stopAudio()
previewModalVisible.value = false
}
//
const resetAudioState = () => {
currentAudioIndex.value = -1
isPlaying.value = false
playProgress.value = 0
}
//
const loadAudioDurations = () => {
audioPlayers.value.forEach((player, index) => {
player.onloadedmetadata = () => {
if (!audioList.value[index].duration) {
audioList.value[index].duration = player.duration
}
}
})
}
//
const toggleAudio = (index: number) => {
if (currentAudioIndex.value === index) {
// /
if (isPlaying.value) {
pauseAudio()
} else {
playAudio(index)
}
} else {
//
stopAudio()
playAudio(index)
}
}
//
const playAudio = (index: number) => {
audioLoading.value = true
currentAudioIndex.value = index
const player = audioPlayers.value[index]
player.play()
.then(() => {
isPlaying.value = true
audioLoading.value = false
})
.catch(err => {
console.error('播放失败:', err)
Message.error('音频播放失败')
audioLoading.value = false
})
}
//
const pauseAudio = () => {
const player = audioPlayers.value[currentAudioIndex.value]
player.pause()
isPlaying.value = false
}
//
const stopAudio = () => {
if (currentAudioIndex.value !== -1) {
const player = audioPlayers.value[currentAudioIndex.value]
player.pause()
player.currentTime = 0
}
isPlaying.value = false
currentAudioIndex.value = -1
playProgress.value = 0
}
//
const handleTimeUpdate = (e: Event) => {
const player = e.target as HTMLAudioElement
if (player.duration) {
playProgress.value = (player.currentTime / player.duration) * 100
}
}
//
const handleAudioEnded = () => {
isPlaying.value = false
playProgress.value = 100
}
//
const formatDuration = (seconds?: number) => {
if (!seconds) return '--:--'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs < 10 ? '0' : ''}${secs}`
}
//
defineExpose({
openPreview
})
</script>
<style scoped lang="less">
.preview-modal {
:deep(.arco-modal-content) {
padding: 0;
}
}
.preview-container {
display: flex;
height: 70vh;
max-height: 800px;
}
.image-section {
flex: 1;
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
overflow: hidden;
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.audio-section {
width: 350px;
border-left: 1px solid #e5e5e5;
display: flex;
flex-direction: column;
.audio-title {
padding: 16px;
margin: 0;
font-size: 16px;
border-bottom: 1px solid #e5e5e5;
}
.audio-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.audio-item {
padding: 12px 16px;
transition: background-color 0.2s;
&.active {
background-color: #f0f7ff;
}
&:hover {
background-color: #f9f9f9;
}
}
.audio-info {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
.audio-name {
font-weight: 500;
}
.audio-duration {
color: #8c8c8c;
font-size: 12px;
}
}
.audio-controls {
display: flex;
align-items: center;
gap: 12px;
.progress-bar {
flex: 1;
}
}
}
@media (max-width: 1200px) {
.preview-container {
flex-direction: column;
height: 80vh;
}
.image-section {
height: 60%;
}
.audio-section {
width: 100%;
height: 40%;
border-left: none;
border-top: 1px solid #e5e5e5;
}
}
</style>

View File

@ -1,290 +1,151 @@
<template>
<GiPageLayout>
<!-- 页面标题 -->
<!-- <div class="page-header">
<h2 class="page-title">图像音频关联查看</h2>
</div>-->
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">数据入库 - 1号机组叶片A型检查</h2>
</div>
<!-- 选项卡区域 -->
<div class="tabs-section">
<a-tabs v-model:active-key="activeTab" type="line">
<a-tab-pane key="image" tab="列表" title="列表">
<div class="tab-content">
<!-- 选项卡区域 -->
<div class="tabs-section">
<a-tabs v-model:active-key="activeTab" type="line">
<a-tab-pane key="image" tab="图片" title="图片">
<div class="tab-content">
<!-- 文件上传区域 -->
<div class="upload-section">
<a-upload
:custom-request="customUpload"
:show-file-list="false"
multiple
:accept="getAcceptType()"
@change="handleFileChange"
drag
class="upload-dragger"
>
<div class="upload-content">
<div class="upload-icon">
<icon-upload :size="48" />
</div>
<div class="upload-text">
<p class="primary-text">将文件拖拽到此处<span class="link-text">点击上传</span></p>
<p class="secondary-text">支持上传任意不超过100MB格式文件</p>
</div>
</div>
</a-upload>
</div>
<!-- 筛选条件区域 -->
<div class="filter-section">
<a-space size="large">
<!-- 项目选择 -->
<div class="filter-item">
<span class="filter-label">项目</span>
<a-select
v-model="filterParams.project"
placeholder="请选择项目"
:options="projectOptions"
allow-search
allow-clear
:loading="loading.project"
style="width: 200px"
@change="handleFilterChange"
/>
</div>
<!-- 操作按钮区域 -->
<div class="action-buttons">
<a-button
type="primary"
@click="startUpload"
:loading="uploading"
:disabled="uploadQueue.length === 0"
>
开始上传
</a-button>
<a-button @click="clearFiles">清空文件</a-button>
<a-button @click="batchImport">批量导入...</a-button>
</div>
<!-- 机组选择 -->
<div class="filter-item">
<span class="filter-label">机组</span>
<a-select
v-model="filterParams.unit"
placeholder="请先选择项目"
:options="unitOptions"
allow-search
allow-clear
:disabled="!filterParams.project"
:loading="loading.unit"
style="width: 200px"
@change="handleFilterChange"
/>
</div>
<!-- 部件选择 -->
<div class="filter-item">
<span class="filter-label">部件</span>
<a-select
v-model="filterParams.component"
placeholder="请先选择机组"
:options="componentOptions"
allow-search
allow-clear
:disabled="!filterParams.unit"
:loading="loading.component"
style="width: 200px"
@change="handleFilterChange"
/>
</div>
</a-space>
</div>
<!-- 已上传数据列表 -->
<div class="uploaded-files-section">
<a-table
<!-- 已上传数据列表 -->
<div class="uploaded-files-section">
<h3 class="section-title">已上传数据</h3>
<a-table
:columns="fileColumns"
:data="imageList"
:data="uploadedFiles"
:pagination="false"
:scroll="{ x: '100%', y: 'calc(100vh - 380px)' }"
:loading="loading.image"
class="scrollable-table"
>
<!-- 文件类型 -->
<template #type="{ record }">
<a-tag :color="getFileTypeColor(record.type)" size="small">
{{ record.type }}
</a-tag>
</template>
:scroll="{ x: '100%' }"
>
<!-- 文件类型 -->
<template #type="{ record }">
<a-tag :color="getFileTypeColor(record.type)" size="small">
{{ record.type }}
</a-tag>
</template>
<!-- 文件大小 -->
<template #size="{ record }">
<span>{{ record.imageTypeLabel}}</span>
</template>
<!-- 文件大小 -->
<template #size="{ record }">
<span>{{ formatFileSize(record.size) }}</span>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.preTreatment)" size="small">
{{ record.preTreatment }}
</a-tag>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)" size="small">
{{ record.status }}
</a-tag>
</template>
<!-- 操作 -->
<template #action="{ record }">
<a-space>
<a-button size="small" @click="previewFile(record)">预览</a-button>
<a-button size="small" status="danger" @click="deleteFile(record)">删除</a-button>
</a-space>
</template>
</a-table>
<!-- 操作 -->
<template #action="{ record }">
<a-space>
<a-button size="small" @click="previewFile(record)">预览</a-button>
<a-button size="small" status="danger" @click="deleteFile(record)">删除</a-button>
</a-space>
</template>
</a-table>
</div>
</div>
</div>
</a-tab-pane>
</a-tabs>
</div>
</a-tab-pane>
<a-tab-pane key="video" tab="视频" title="视频">
<div class="tab-content">
<a-empty description="视频上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="audio" tab="语音" title="语音">
<div class="tab-content">
<a-empty description="语音上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="document" tab="文档" title="文档">
<div class="tab-content">
<a-empty description="文档上传功能开发中..." />
</div>
</a-tab-pane>
<a-tab-pane key="other" tab="其他" title="其他">
<div class="tab-content">
<a-empty description="其他文件上传功能开发中..." />
</div>
</a-tab-pane>
</a-tabs>
</div>
<!-- 文件预览模态框 -->
<PreviewModal ref="previewModal" />
<a-modal
v-model:visible="previewModalVisible"
title="文件预览"
:width="800"
:footer="false"
>
<div class="preview-container">
<div v-if="previewFileData && previewFileData.type === 'image'" class="image-preview">
<img :src="previewFileData.url" :alt="previewFileData.name" style="max-width: 100%; height: auto;" />
</div>
<div v-else-if="previewFileData && previewFileData.type === 'video'" class="video-preview">
<video :src="previewFileData.url" controls style="max-width: 100%; height: auto;" />
</div>
<div v-else class="file-info">
<p>文件名{{ previewFileData?.name }}</p>
<p>文件类型{{ previewFileData?.type }}</p>
<p>文件大小{{ formatFileSize(previewFileData?.size) }}</p>
</div>
</div>
</a-modal>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed,onMounted } from 'vue'
import { ref, reactive, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import PreviewModal from './components/PreviewModal.vue'
import { IconUpload } from '@arco-design/web-vue/es/icon'
import {
getProjectList,
getTurbineList,
getPartList,
getImageList,
deleteImage,
autoAnnotateImage,
generateReport,
batchUploadImages,
uploadImageToPartV2
} from '@/apis/industrial-image'
const previewModal = ref()
//
const activeTab = ref('image')
//
const filterParams = reactive({
project: null,
unit: null,
component: null
})
//
const projectOptions = ref<Array<{label: string, value: string}>>([])
const unitOptions = ref<Array<{label: string, value: string}>>([])
const componentOptions = ref<Array<{label: string, value: string}>>([])
//
const imageList = ref<Array<{
id: number
name: string
type: string
imageTypeLabel: string
shootingTime: string
preTreatment: string
url: string
}>>([])
//
const loading = reactive({
project: false,
unit: false,
component: false,
image: false
})
//
onMounted(() => {
fetchProjectList()
})
//
if (watch) {
watch(() => filterParams.project, (newVal) => {
//
filterParams.unit = null
filterParams.component = null
unitOptions.value = []
componentOptions.value = []
imageList.value = []
if (newVal) {
fetchTurbineList(newVal)
}
})
}
//
if (watch) {
watch(() => filterParams.unit, (newVal) => {
//
filterParams.component = null
componentOptions.value = []
imageList.value = []
if (newVal && filterParams.project) {
fetchPartList(filterParams.project, newVal)
}
})
}
//
const fetchProjectList = async () => {
loading.project = true
try {
const res = await getProjectList({ page: 1, pageSize: 1000 });
projectOptions.value = res.data.map(item => ({
label: item.projectName,
value: item.projectId
}))
} catch (error) {
Message.error('获取项目列表失败')
} finally {
loading.project = false
}
}
//
const fetchTurbineList = async (projectId: string) => {
loading.unit = true
try {
const res = await getTurbineList({ projectId })
unitOptions.value = res.data.map(item => ({
label: item.turbineName,
value: item.turbineId
}))
} catch (error) {
console.error('获取机组列表失败:', error)
Message.error('获取机组列表失败')
} finally {
loading.unit = false
}
}
//
const fetchPartList = async (projectId: string, turbineId: string) => {
loading.component = true
try {
const res = await getPartList({ projectId, turbineId })
componentOptions.value = res.data.map(item => ({
label: item.partName, //
value: item.partId //
}))
} catch (error) {
console.error('获取部件列表失败:', error)
Message.error('获取部件列表失败')
} finally {
loading.component = false
}
}
//
const handleFilterChange = async () => {
if (!filterParams.unit) return
loading.image = true
try {
let params = {
turbineId: filterParams.unit
}
if(filterParams.component){
params = {
turbineId: filterParams.unit,
partId: filterParams.component
}
}
const res = await getImageList(params)
imageList.value = res.data.map((item: any) => ({
id: item.imageId,
name: item.imageName,
type: item.imageType?item.imageType:"未指定类型",
imageTypeLabel: item.imageTypeLabel,
shootingTime: item.shootingTime,
preTreatment: item.preTreatment?"已审核":"未审核",
imagePath: item.imagePath,
audioList:item.audioList
}))
Message.success(`获取到 ${imageList.value.length} 条图像数据`)
} catch (error) {
Message.error('获取图像列表失败')
} finally {
loading.image = false
}
}
// const response = await getTurbineList({ projectId: node.id })
//
const uploading = ref(false)
const uploadQueue = ref<any[]>([])
@ -294,32 +155,65 @@ const previewModalVisible = ref(false)
const previewFileData = ref<any>(null)
//
const uploadedFiles = ref([])
const uploadedFiles = ref([
{
id: 1,
name: 'IMG_20231105_1430.jpg',
type: 'image',
size: 3355443, // 3.2MB
uploadTime: '2023-11-05 14:32',
status: '成功',
url: '/api/files/IMG_20231105_1430.jpg'
},
{
id: 2,
name: 'VID_20231106_0915.mp4',
type: 'video',
size: 47185920, // 45.6MB
uploadTime: '2023-11-06 09:18',
status: '成功',
url: '/api/files/VID_20231106_0915.mp4'
},
{
id: 3,
name: 'IMG_20231107_1645.jpg',
type: 'image',
size: 2936013, // 2.8MB
uploadTime: '2023-11-07 16:48',
status: '成功',
url: '/api/files/IMG_20231107_1645.jpg'
}
])
//
const fileColumns: TableColumnData[] = [
{ title: '文件名', dataIndex: 'name', width: 250 },
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 100 },
{ title: '类型描述', dataIndex: 'imageTypeLabel', slotName: 'imageTypeLabel', width: 100 },
{ title: '上传时间', dataIndex: 'shootingTime', width: 150 },
{ title: '状态', dataIndex: 'preTreatment', slotName: 'preTreatment', width: 100 },
{ title: '大小', dataIndex: 'size', slotName: 'size', width: 100 },
{ title: '上传时间', dataIndex: 'uploadTime', width: 150 },
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
]
//
const filteredFiles = computed(() => {
return uploadedFiles.value.filter(file =>
(filterParams.project === null || file.project === filterParams.project) &&
(filterParams.unit === null || file.unit === filterParams.unit) &&
(filterParams.component === null || file.component === filterParams.component)
)
})
//
const getAcceptType = () => {
const typeMap: Record<string, string> = {
'image': 'image/*',
'video': 'video/*',
'audio': 'audio/*',
'document': '.pdf,.doc,.docx,.txt,.xls,.xlsx,.ppt,.pptx',
'other': '*'
}
return typeMap[activeTab.value] || '*'
}
//
const getFileTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
'TYPICAL': 'blue',
'DEFECT': 'orange',
'image': 'blue',
'video': 'green',
'audio': 'orange',
'document': 'purple',
'other': 'gray'
}
return colorMap[type] || 'gray'
@ -327,10 +221,11 @@ const getFileTypeColor = (type: string) => {
//
const getStatusColor = (status: string) => {
console.log(status);
const colorMap: Record<string, string> = {
'已审核': 'green',
'未审核': 'red'
'成功': 'green',
'失败': 'red',
'上传中': 'blue',
'等待': 'orange'
}
return colorMap[status] || 'gray'
}
@ -344,30 +239,107 @@ const formatFileSize = (bytes: number) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + sizes[i]
}
const getImageUrl = (imagePath: string): string => {
if (!imagePath) return ''
if (imagePath.startsWith('http')) return imagePath
const baseUrl = 'http://pms.dtyx.net:9158'
return `${baseUrl}${imagePath}`
//
const customUpload = (option: any) => {
const { file } = option
//
if (file.size > 100 * 1024 * 1024) {
Message.error('文件大小不能超过100MB')
return
}
//
uploadQueue.value.push({
file,
name: file.name,
size: file.size,
type: getFileType(file.name),
status: '等待'
})
Message.success(`文件 ${file.name} 已添加到上传队列`)
}
//
const getFileType = (fileName: string) => {
const extension = fileName.split('.').pop()?.toLowerCase()
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(extension || '')) {
return 'image'
} else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(extension || '')) {
return 'video'
} else if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension || '')) {
return 'audio'
} else if (['pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx', 'ppt', 'pptx'].includes(extension || '')) {
return 'document'
} else {
return 'other'
}
}
//
const handleFileChange = (fileList: any[]) => {
//
}
//
const startUpload = async () => {
if (uploadQueue.value.length === 0) {
Message.warning('请先选择要上传的文件')
return
}
uploading.value = true
try {
//
for (let i = 0; i < uploadQueue.value.length; i++) {
const queueItem = uploadQueue.value[i]
queueItem.status = '上传中'
//
await new Promise(resolve => setTimeout(resolve, 1000))
//
uploadedFiles.value.push({
id: Date.now() + i,
name: queueItem.name,
type: queueItem.type,
size: queueItem.size,
uploadTime: new Date().toLocaleString(),
status: '成功',
url: `/api/files/${queueItem.name}`
})
}
uploadQueue.value = []
Message.success('所有文件上传完成')
} catch (error) {
Message.error('上传失败,请重试')
} finally {
uploading.value = false
}
}
//
const clearFiles = () => {
uploadQueue.value = []
Message.success('已清空文件队列')
}
//
const batchImport = () => {
Message.info('批量导入功能开发中...')
}
//
const previewFile = (file: any) => {
/* previewFileData.value = file
previewModalVisible.value = true*/
const fileObj ={
id: file.id,
name: file.name,
url: getImageUrl(file.imagePath),
audios: file.audioList
}
previewModal.value.openPreview(fileObj)
previewFileData.value = file
previewModalVisible.value = true
}
//
const deleteFile = (file: any) => {
console.log(index);
const index = uploadedFiles.value.findIndex(f => f.id === file.id)
if (index > -1) {
uploadedFiles.value.splice(index, 1)
@ -385,7 +357,7 @@ const deleteFile = (file: any) => {
.page-header {
margin-bottom: 16px;
.page-title {
font-size: 20px;
font-weight: 500;
@ -405,22 +377,65 @@ const deleteFile = (file: any) => {
margin-top: 16px;
}
.filter-section {
.upload-section {
margin-bottom: 16px;
padding: 12px;
background: #fafafa;
border-radius: 4px;
.filter-item {
display: inline-flex;
align-items: center;
.filter-label {
font-size: 14px;
color: #595959;
margin-right: 8px;
.upload-dragger {
:deep(.arco-upload-drag) {
border: 2px dashed #d9d9d9;
border-radius: 8px;
background: #fafafa;
padding: 40px;
text-align: center;
transition: all 0.3s ease;
&:hover {
border-color: #1890ff;
background: #f0f7ff;
}
}
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
.upload-icon {
color: #bfbfbf;
font-size: 48px;
}
.upload-text {
.primary-text {
font-size: 16px;
color: #595959;
margin: 0 0 8px 0;
.link-text {
color: #1890ff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.secondary-text {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
}
}
}
.action-buttons {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.uploaded-files-section {
@ -434,16 +449,16 @@ const deleteFile = (file: any) => {
.preview-container {
text-align: center;
.image-preview,
.video-preview {
max-height: 500px;
overflow: hidden;
}
.file-info {
text-align: left;
p {
margin: 8px 0;
font-size: 14px;
@ -484,4 +499,4 @@ const deleteFile = (file: any) => {
:deep(.arco-upload-drag:hover) {
border-color: #1890ff;
}
</style>
</style>

View File

@ -1,75 +0,0 @@
<template>
<a-drawer
v-model:visible="visible"
:title="isUpdate ? '修改维度' : '新增维度'"
@before-ok="save"
@cancel="reset"
>
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="维度名称" field="name" required>
<a-input v-model="form.name" />
</a-form-item>
<a-form-item label="权重%" field="weight" required>
<a-input-number v-model="form.weight" :min="1" :max="100" />
</a-form-item>
<a-form-item label="描述" field="desc">
<a-textarea v-model="form.desc" :rows="3" />
</a-form-item>
<a-form-item label="状态" field="status">
<a-switch v-model="form.status" :checked-value="0" :unchecked-value="1" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { addDimension, getDimensionDetail, updateDimension } from '@/apis/performance'
const emit = defineEmits<{
(e: 'success'): void
}>()
const visible = ref(false)
const isUpdate = ref(false)
const formRef = ref()
const id = ref('')
const form = reactive({
name: '',
weight: 50,
desc: '',
status: 0 as 0 | 1,
})
const reset = () => {
formRef.value?.resetFields()
Object.assign(form, { name: '', weight: 50, desc: '', status: 0 })
id.value = ''
}
const save = async () => {
if (isUpdate.value) {
await updateDimension(id.value, form)
} else {
await addDimension(form)
}
visible.value = false
emit('success')
}
const onAdd = () => {
reset()
isUpdate.value = false
visible.value = true
}
const onUpdate = async (dimensionId: string) => {
reset()
isUpdate.value = true
id.value = dimensionId
const data = await getDimensionDetail(dimensionId)
Object.assign(form, data)
visible.value = true
}
defineExpose({ onAdd, onUpdate })
</script>

View File

@ -1,100 +0,0 @@
<template>
<a-spin :loading="loading">
<a-descriptions :data="detailData" bordered>
<a-descriptions-item label="员工">
{{ data.userName }}
</a-descriptions-item>
<a-descriptions-item label="维度">
{{ data.dimensionName }}
</a-descriptions-item>
<a-descriptions-item label="周期">
{{ data.periodName }}
</a-descriptions-item>
<a-descriptions-item label="评分">
<a-tag color="blue" size="large">{{ data.score }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="AI评价" :span="2">
{{ data.aiComment }}
</a-descriptions-item>
</a-descriptions>
<!-- 反馈表单 -->
<a-divider orientation="left">评价反馈</a-divider>
<a-form ref="feedbackRef" :model="feedback" :rules="rules" layout="vertical">
<a-form-item label="是否有异议" field="level" required>
<a-radio-group v-model="feedback.level">
<a-radio :value="0">无异议</a-radio>
<a-radio :value="1">部分有异议</a-radio>
<a-radio :value="2">完全不同意</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="请说明理由" field="content" required>
<a-textarea
v-model="feedback.content"
:rows="4"
placeholder="请具体描述您对评价结果的看法..."
/>
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitting" @click="handleSubmit">
提交反馈
</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { submitFeedback } from '@/apis/performance'
import type { EvaluateResp, FeedbackReq } from '@/apis/performance/type'
interface Props {
data: EvaluateResp
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'feedback'): void
}>()
const loading = ref(false)
const submitting = ref(false)
const feedbackRef = ref()
const feedback = reactive<FeedbackReq>({
evaluateId: props.data.id,
level: 0,
content: '',
})
const rules = {
level: [{ required: true, message: '请选择异议等级' }],
content: [{ required: true, message: '请填写反馈内容' }],
}
const detailData = computed(() => [
{ label: '员工', value: props.data.userName },
{ label: '维度', value: props.data.dimensionName },
{ label: '周期', value: props.data.periodName },
{ label: '评分', value: props.data.score },
{ label: 'AI评价', value: props.data.aiComment },
])
const handleSubmit = async () => {
const ok = await feedbackRef.value?.validate()
if (ok) return
submitting.value = true
try {
await submitFeedback(feedback)
Message.success('反馈已提交')
emit('feedback')
} finally {
submitting.value = false
}
}
</script>

View File

@ -1,18 +0,0 @@
<template>
<a-menu
:default-selected-keys="['dimension']"
@menu-item-click="handleClick"
>
<a-menu-item key="dimension">绩效维度</a-menu-item>
<a-menu-item key="evaluate">员工评估</a-menu-item>
<a-menu-item key="my">我的绩效</a-menu-item>
</a-menu>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(e: 'select', key: string): void
}>()
const handleClick = (key: string) => emit('select', key)
</script>

View File

@ -1,131 +0,0 @@
<template>
<a-drawer
v-model:visible="visible"
:title="isUpdate ? '编辑细则' : '新增细则'"
:width="480"
@before-ok="handleSave"
@cancel="reset"
>
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="所属维度" field="dimensionId" required>
<a-select v-model="form.dimensionId" placeholder="请选择维度">
<a-option
v-for="item in dimensionOptions"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</a-select>
</a-form-item>
<a-form-item label="细则名称" field="name" required>
<a-input v-model="form.name" placeholder="如:持有基础资质证书" />
</a-form-item>
<a-form-item label="评分规则描述" field="ruleDesc" required>
<a-textarea
v-model="form.ruleDesc"
:rows="4"
placeholder="详细说明评分标准"
/>
</a-form-item>
<a-form-item label="满分" field="score" required>
<a-input-number v-model="form.score" :min="0" :precision="0" />
</a-form-item>
<a-form-item label="权重" field="weight" required>
<a-input-number v-model="form.weight" :min="0" :max="100" />
</a-form-item>
<a-form-item label="是否加分项" field="isExtra">
<a-switch v-model="form.isExtra" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
addRule,
getDimensionList,
getRuleDetail,
updateRule,
} from '@/apis/performance'
import type { DimensionResp, RuleAddReq, RuleUpdateReq } from '@/apis/performance/type'
const emit = defineEmits<{
(e: 'success'): void
}>()
const visible = ref(false)
const isUpdate = ref(false)
const id = ref('')
const formRef = ref()
const dimensionOptions = ref<DimensionResp[]>([])
const form = reactive<RuleAddReq>({
dimensionId: '',
name: '',
ruleDesc: '',
score: 100,
weight: 10,
isExtra: false,
})
const reset = () => {
formRef.value?.resetFields()
Object.assign(form, {
dimensionId: '',
name: '',
ruleDesc: '',
score: 100,
weight: 10,
isExtra: false,
})
id.value = ''
}
const openAdd = () => {
reset()
isUpdate.value = false
visible.value = true
}
const openUpdate = async (ruleId: string) => {
reset()
isUpdate.value = true
id.value = ruleId
const detail = await getRuleDetail(ruleId)
Object.assign(form, detail)
visible.value = true
}
const handleSave = async () => {
const ok = await formRef.value?.validate()
if (ok) return false
try {
if (isUpdate.value) {
await updateRule(id.value, form as RuleUpdateReq)
Message.success('细则已更新')
} else {
await addRule(form)
Message.success('细则已新增')
}
visible.value = false
emit('success')
return true
} catch {
return false
}
}
onMounted(async () => {
dimensionOptions.value = await getDimensionList()
})
defineExpose({ openAdd, openUpdate })
</script>

View File

@ -1,67 +0,0 @@
<template>
<a-modal
v-model:visible="visible"
title="细则管理"
:width="800"
@before-ok="handleSave"
@cancel="reset"
>
<a-table :data="rules" :columns="columns" row-key="id">
<template #action="{ record }">
<a-space>
<a-button type="text" @click="RuleDrawerRef?.openUpdate(record.id)">
编辑
</a-button>
<a-button type="text" status="danger" @click="handleDelete(record.id)">
删除
</a-button>
</a-space>
</template>
</a-table>
<a-button type="primary" @click="RuleDrawerRef?.openAdd()">
<icon-plus /> 新增细则
</a-button>
</a-modal>
<RuleDrawer ref="RuleDrawerRef" @success="load" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import RuleDrawer from './RuleDrawer.vue'
import { deleteRule, getRuleList } from '@/apis/performance'
import type { RuleResp } from '@/apis/performance/type'
import { Message } from '@arco-design/web-vue'
const visible = ref(false)
const rules = ref<RuleResp[]>([])
const RuleDrawerRef = ref()
const columns = [
{ title: '细则名称', dataIndex: 'name' },
{ title: '评分规则描述', dataIndex: 'ruleDesc' },
{ title: '满分', dataIndex: 'score' },
{ title: '权重', dataIndex: 'weight' },
{ title: '是否加分项', dataIndex: 'isExtra', render: ({ record }) => record.isExtra ? '是' : '否' },
{ title: '操作', slotName: 'action' },
]
const open = async (dimensionId: string) => {
visible.value = true
rules.value = await getRuleList(dimensionId)
}
const handleDelete = async (id: string) => {
await deleteRule(id)
Message.success('删除成功')
load()
}
const load = async () => {
//
rules.value = await getRuleList(dimensionId.value)
}
defineExpose({ open })
</script>

View File

@ -1,72 +0,0 @@
<template>
<div>
<a-card title="绩效维度管理">
<template #extra>
<a-button type="primary" @click="DimensionDrawerRef?.onAdd()">
<icon-plus /> 添加维度
</a-button>
</template>
<a-table :data="dimensions" :columns="columns" row-key="id">
<template #status="{ record }">
<a-tag :color="record.status === 0 ? 'green' : 'red'">
{{ record.status === 0 ? '启用' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="DimensionDrawerRef?.onUpdate(record.id)">
编辑
</a-button>
<a-button type="text" status="danger" @click="handleDelete(record.id)">
删除
</a-button>
<a-button type="text" @click="openRuleList(record.id)">
管理细则
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 维度抽屉 -->
<DimensionDrawer ref="DimensionDrawerRef" @success="load" />
<!-- 细则列表 -->
<RuleList ref="RuleListRef" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getDimensionList, deleteDimension } from '@/apis/performance'
import DimensionDrawer from './components/DimensionDrawer.vue'
import RuleList from './components/RuleList.vue'
import type { DimensionResp } from '@/apis/performance/type'
const dimensions = ref<DimensionResp[]>([])
const DimensionDrawerRef = ref()
const RuleListRef = ref()
const columns = [
{ title: '维度名称', dataIndex: 'name' },
{ title: '权重', dataIndex: 'weight', render: ({ record }) => `${record.weight}%` },
{ title: '细则数量', dataIndex: 'ruleCount' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action' },
]
const load = async () => {
const response=await getDimensionList()
dimensions.value = await getDimensionList().data||[]
}
onMounted(load)
const handleDelete = async (id: string) => {
await deleteDimension(id)
load()
}
const openRuleList = (dimensionId: string) => {
RuleListRef.value.open(dimensionId)
}
</script>

View File

@ -1,70 +0,0 @@
<template>
<a-card title="员工绩效评估">
<a-form layout="inline" style="margin-bottom: 16px">
<a-form-item label="绩效周期">
<a-select v-model="query.periodId" placeholder="请选择周期" allow-clear>
<a-option v-for="p in periods" :key="p.id" :value="p.id">{{ p.name }}</a-option>
</a-select>
</a-form-item>
<a-form-item label="维度">
<a-select v-model="query.dimensionId" placeholder="请选择维度" allow-clear>
<a-option v-for="d in dimensions" :key="d.id" :value="d.id">{{ d.name }}</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-input v-model="query.keyword" placeholder="姓名/工号" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="load">查询</a-button>
</a-form-item>
</a-form>
<a-table :data="data" :columns="columns" row-key="id">
<template #action="{ record }">
<a-button type="text" @click="openEvaluate(record)">
开始评估
</a-button>
</template>
</a-table>
</a-card>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { getDimensionList, getEvaluatePage, getPeriodList, startEvaluate } from '@/apis/performance'
import type { EvaluateQuery, EvaluateResp } from '@/apis/performance/type'
const periods = ref([])
const dimensions = ref([])
const query = ref<EvaluateQuery>({})
const data = ref<EvaluateResp[]>([])
const columns = [
{ title: '员工', dataIndex: 'userName' },
{ title: '维度', dataIndex: 'dimensionName' },
{ title: '周期', dataIndex: 'periodName' },
{ title: '评分', dataIndex: 'score' },
{ title: '操作', slotName: 'action' },
]
const load = async () => {
data.value = await getEvaluatePage(query.value)
}
onMounted(async () => {
periods.value = await getPeriodList()
dimensions.value = await getDimensionList()
load()
})
const openEvaluate = async (row: EvaluateResp) => {
await startEvaluate({
userId: row.userId,
dimensionId: row.dimensionId,
ruleId: row.ruleId,
periodId: row.periodId,
})
Message.success('评估已启动')
load()
}
</script>

View File

@ -1,19 +0,0 @@
<template>
<GiPageLayout>
<template #left>
<PerformanceMenu @select="handleSelectMenu" />
</template>
<router-view />
</GiPageLayout>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import PerformanceMenu from './components/PerformanceMenu.vue'
const router = useRouter()
const handleSelectMenu = (key: string) => {
router.push(`/performance/${key}`)
}
</script>

View File

@ -1,50 +0,0 @@
<template>
<a-card title="我的绩效">
<a-table :data="data" :columns="columns" row-key="evaluationId">
<template #action="{ record }">
<a-button type="text" @click="openDetail(record)">查看详情</a-button>
</template>
</a-table>
</a-card>
<a-modal v-model:visible="visible" title="绩效详情">
<div v-if="selected">
<div>维度{{ selected.dimensionName }}</div>
<div>周期{{ selected.periodName }}</div>
<div>评分{{ selected.score }}</div>
<div>奖金{{ selected.bonus }}</div>
<div>评价{{ selected.comment }}</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { getMyEvaluation } from '@/apis/performance-setting'
import type { PerformanceRule } from '@/apis/performance-setting/type'
const data = ref<any[]>([])
const visible = ref(false)
const selected = ref<any>()
const columns = [
{ title: '维度', dataIndex: 'dimensionName' },
{ title: '周期', dataIndex: 'periodName' },
{ title: '评分', dataIndex: 'score' },
{ title: '奖金', dataIndex: 'bonus' },
{ title: '评价', dataIndex: 'comment' },
{ title: '操作', slotName: 'action' },
]
const load = async () => {
const res = await getMyEvaluation()
data.value = res.data || []
}
const openDetail = (row: any) => {
selected.value = row
visible.value = true
}
onMounted(load)
</script>

View File

@ -1,61 +0,0 @@
<template>
<div>
<a-card title="绩效细则管理">
<template #extra>
<a-button type="primary" @click="RuleDrawerRef?.openAdd()">
<icon-plus /> 新增细则
</a-button>
</template>
<a-table :data="rules" :columns="columns" row-key="id">
<template #action="{ record }">
<a-space>
<a-button type="text" @click="RuleDrawerRef?.openUpdate(record.id)">
编辑
</a-button>
<a-button type="text" status="danger" @click="handleDelete(record.id)">
删除
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 细则抽屉 -->
<RuleDrawer ref="RuleDrawerRef" @success="load" />
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import RuleDrawer from './components/RuleDrawer.vue'
import { deleteRule, getRuleList } from '@/apis/performance'
import type { RuleResp } from '@/apis/performance/type'
const rules = ref<RuleResp[]>([])
const RuleDrawerRef = ref()
const columns = [
{ title: '细则名称', dataIndex: 'name' },
{ title: '评分规则描述', dataIndex: 'ruleDesc' },
{ title: '满分', dataIndex: 'score' },
{ title: '权重', dataIndex: 'weight' },
{ title: '是否加分项', dataIndex: 'isExtra', render: ({ record }) => record.isExtra ? '是' : '否' },
{ title: '操作', slotName: 'action' },
]
const load = async (dimensionId: string) => {
rules.value = await getRuleList(dimensionId)
}
onMounted(() => {
//
load('default-dimension-id')
})
const handleDelete = async (id: string) => {
await deleteRule(id)
Message.success('删除成功')
load('default-dimension-id') //
}
</script>

View File

@ -1,58 +0,0 @@
<template>
<a-drawer v-model:visible="visible" :title="isEdit ? '编辑维度' : '新增维度'" @before-ok="save">
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="维度名称" field="dimensionName" required>
<a-input v-model="form.dimensionName" />
</a-form-item>
<a-form-item label="岗位" field="deptName" required>
<a-input v-model="form.deptName" />
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea v-model="form.description" :rows="3" />
</a-form-item>
<a-form-item label="状态" field="status">
<a-switch v-model="form.status" :checked-value="0" :unchecked-value="1" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { addDimension, updateDimension } from '@/apis/performance-setting'
import type { PerformanceDimension } from '@/apis/performance-setting/type'
const emit = defineEmits(['success'])
const visible = ref(false)
const isEdit = ref(false)
const formRef = ref()
const form = ref<Partial<PerformanceDimension>>({
dimensionName: '',
deptName: '',
description: '',
status: 0,
})
const open = (record?: PerformanceDimension) => {
if (record) {
Object.assign(form.value, record)
isEdit.value = true
} else {
Object.assign(form.value, { dimensionName: '', deptName: '', description: '', status: 0 })
isEdit.value = false
}
visible.value = true
}
const save = async () => {
if (isEdit.value && form.value.dimensionId) {
await updateDimension(form.value.dimensionId, form.value)
} else {
await addDimension(form.value)
}
visible.value = false
emit('success')
}
defineExpose({ open })
</script>

View File

@ -1,86 +0,0 @@
<template>
<a-drawer v-model:visible="visible" :title="isEdit ? '编辑细则' : '新增细则'" @before-ok="save">
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="细则名称" field="ruleName" required>
<a-input v-model="form.ruleName" />
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea v-model="form.description" :rows="3" />
</a-form-item>
<a-form-item label="分数" field="score">
<a-input-number v-model="form.score" :min="0" />
</a-form-item>
<a-form-item label="权重" field="weight">
<a-input-number v-model="form.weight" :min="0" :max="100" />
</a-form-item>
<a-form-item label="奖金" field="bonus">
<a-input v-model="form.bonus" />
</a-form-item>
<a-form-item label="状态" field="status">
<a-switch v-model="form.status" :checked-value="0" :unchecked-value="1" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { addRule, updateRule } from '@/apis/performance-setting'
import type { PerformanceRule, PerformanceDimension } from '@/apis/performance-setting/type'
const props = defineProps<{ dimension?: PerformanceDimension }>()
const emit = defineEmits(['success'])
const visible = ref(false)
const isEdit = ref(false)
const formRef = ref()
const form = ref<Partial<PerformanceRule>>({
ruleName: '',
description: '',
score: 0,
weight: 0,
bonus: '',
status: 0,
dimensionName: '',
})
watch(
() => props.dimension,
(val) => {
if (val && !isEdit.value) {
form.value.dimensionName = val.dimensionName
}
},
{ immediate: true }
)
const open = (record?: PerformanceRule) => {
if (record) {
Object.assign(form.value, record)
isEdit.value = true
} else {
Object.assign(form.value, {
ruleName: '',
description: '',
score: 0,
weight: 0,
bonus: '',
status: 0,
dimensionName: props.dimension?.dimensionName || '',
})
isEdit.value = false
}
visible.value = true
}
const save = async () => {
if (isEdit.value && form.value.ruleId) {
await updateRule(form.value.ruleId, form.value)
} else {
await addRule(form.value)
}
visible.value = false
emit('success')
}
defineExpose({ open })
</script>

View File

@ -1,64 +0,0 @@
<template>
<a-drawer v-model:visible="visible" :title="`管理细则 - ${dimension?.dimensionName || ''}`">
<a-button type="primary" @click="openRuleDrawer()">新增细则</a-button>
<a-table :data="ruleList" :columns="ruleColumns" row-key="ruleId" style="margin-top: 16px;">
<template #status="{ record }">
<a-tag :color="record.status === 0 ? 'green' : 'red'">
{{ record.status === 0 ? '启用' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="openRuleDrawer(record)">编辑</a-button>
<a-button type="text" status="danger" @click="handleDeleteRule(record.ruleId)">删除</a-button>
</a-space>
</template>
</a-table>
<RuleDrawer ref="ruleDrawerRef" @success="loadRules" :dimension="dimension" />
</a-drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { getRuleList, deleteRule } from '@/apis/performance-setting'
import RuleDrawer from './RuleDrawer.vue'
import type { PerformanceRule, PerformanceDimension } from '@/apis/performance-setting/type'
const visible = ref(false)
const dimension = ref<PerformanceDimension>()
const ruleList = ref<PerformanceRule[]>([])
const ruleDrawerRef = ref()
const ruleColumns = [
{ title: '细则名称', dataIndex: 'ruleName' },
{ title: '描述', dataIndex: 'description' },
{ title: '分数', dataIndex: 'score' },
{ title: '权重', dataIndex: 'weight' },
{ title: '奖金', dataIndex: 'bonus' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action' },
]
const open = (dim: PerformanceDimension) => {
dimension.value = dim
visible.value = true
loadRules()
}
const loadRules = async () => {
if (!dimension.value) return
const res = await getRuleList({ dimensionName: dimension.value.dimensionName })
ruleList.value = res.data || []
}
const openRuleDrawer = (record?: PerformanceRule) => {
ruleDrawerRef.value.open(record)
}
const handleDeleteRule = async (id: string) => {
await deleteRule(id)
loadRules()
}
defineExpose({ open })
</script>

View File

@ -1,67 +0,0 @@
<template>
<a-card title="绩效维度管理">
<template #extra>
<a-button type="primary" @click="openDimensionDrawer()">新增维度</a-button>
</template>
<a-table :data="dimensionList" :columns="dimensionColumns" row-key="dimensionId">
<template #status="{ record }">
<a-tag :color="record.status === 0 ? 'green' : 'red'">
{{ record.status === 0 ? '启用' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="openDimensionDrawer(record)">编辑</a-button>
<a-button type="text" status="danger" @click="handleDeleteDimension(record.dimensionId)">删除</a-button>
<a-button type="text" @click="openRuleList(record)">管理细则</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 维度抽屉 -->
<DimensionDrawer ref="dimensionDrawerRef" @success="loadDimensions" />
<!-- 细则管理 -->
<RuleList ref="ruleListRef" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import DimensionDrawer from './components/DimensionDrawer.vue'
import RuleList from './components/RuleList.vue'
import { deleteDimension, getDimensionList } from '@/apis/performance-setting/index'
import type { PerformanceDimension } from '@/apis/performance-setting/type.ts'
const dimensionList = ref<PerformanceDimension[]>([])
const dimensionDrawerRef = ref()
const ruleListRef = ref()
const dimensionColumns = [
{ title: '维度名称', dataIndex: 'dimensionName' },
{ title: '岗位', dataIndex: 'deptName' },
{ title: '描述', dataIndex: 'description' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action' },
]
const loadDimensions = async () => {
const res = await getDimensionList()
dimensionList.value = res.data || []
}
const openDimensionDrawer = (record?: PerformanceDimension) => {
dimensionDrawerRef.value.open(record)
}
const handleDeleteDimension = async (id: string) => {
await deleteDimension(id)
loadDimensions()
}
const openRuleList = (dimension: PerformanceDimension) => {
ruleListRef.value.open(dimension)
}
onMounted(loadDimensions)
</script>

View File

@ -1,88 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import WindTurbine from './icons/WindTurbine.vue'
interface Turbine {
id: number
turbineNo: string
status: 0 | 1 | 2 // 0 1 2
lat?: number
lng?: number
}
const props = defineProps<{ modelValue: Turbine }>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Turbine): void
(e: 'map'): void
}>()
const turbine = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
/* 状态文字 & 颜色 */
const statusTextMap = { 0: '待施工', 1: '施工中', 2: '已完成' }
const statusColorMap = { 0: '#FF7D00', 1: '#165DFF', 2: '#00B42A' }
/* 点击循环切换 */
function toggleStatus() {
const next = ((turbine.value.status + 1) % 3) as 0 | 1 | 2
turbine.value = { ...turbine.value, status: next }
}
function updateNo(val: string) {
turbine.value = { ...turbine.value, turbineNo: val }
}
</script>
<template>
<div class="turbine-card">
<!-- 可点击的状态标签 -->
<div class="status-tag" :style="{ backgroundColor: statusColorMap[turbine.status] }" @click="toggleStatus">
{{ statusTextMap[turbine.status] }}
</div>
<!-- 风机图标 -->
<WindTurbine />
<!-- 机组编号输入框 -->
<a-input :model-value="turbine.turbineNo" @update:model-value="updateNo" size="small" class="turbine-input"
placeholder="编号" />
<!-- 地图选点按钮 -->
<a-button size="mini" @click="$emit('map')">
<template #icon><icon-location /></template>
</a-button>
</div>
</template>
<style scoped>
.turbine-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 8px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
background: var(--color-bg-2);
}
.status-tag {
position: absolute;
top: 4px;
left: 4px;
font-size: 10px;
color: #fff;
padding: 2px 4px;
border-radius: 2px;
cursor: pointer;
user-select: none;
}
.turbine-input {
margin-top: 8px;
text-align: center;
}
</style>

View File

@ -1,41 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import TurbineCard from './TurbineCard.vue'
interface Turbine {
id: number
turbineNo: string
status: 0 | 1 | 2
lat?: number
lng?: number
}
const props = defineProps<{
modelValue: Turbine[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Turbine[]): void
(e: 'map', turbine: Turbine): void
}>()
const turbines = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
function updateTurbine(index: number, t: Turbine) {
const arr = [...turbines.value]
arr.splice(index, 1, t)
turbines.value = arr
}
</script>
<template>
<a-row v-if="turbines.length" :gutter="[16, 16]" wrap>
<a-col v-for="(t, i) in turbines" :key="t.id" :xs="12" :sm="8" :md="6" :lg="4" :xl="3">
<TurbineCard v-model="turbines[i]" @update:model-value="v => updateTurbine(i, v)" @map="$emit('map', t)" />
</a-col>
</a-row>
<a-empty v-else description="请先设置项目规模" />
</template>

View File

@ -7,9 +7,7 @@
<template #icon><icon-arrow-left /></template>
</a-button>
<h2 class="ml-2">{{ projectTitle }}</h2>
<a-tag class="ml-2" :color="getStatusColor(projectData.status)" v-if="projectData.status">{{
projectData.status
}}</a-tag>
<a-tag class="ml-2" :color="getStatusColor(projectData.status)" v-if="projectData.status">{{ projectData.status }}</a-tag>
</div>
<div class="flex items-center">
<a-button v-permission="['project:update']" type="primary" class="mr-2" @click="editProject">
@ -26,18 +24,17 @@
<a-tab-pane key="1" title="详细信息">
<a-card class="general-card" title="项目基本信息">
<a-descriptions :data="projectInfos" layout="horizontal" bordered column="3" />
<a-divider />
<div class="card-header">
<div class="card-title">项目收支情况</div>
</div>
<div class="finance-cards grid grid-cols-3 gap-4 mb-6">
<a-card class="finance-card">
<div class="finance-title">利润</div>
<div class="finance-amount" :class="{ 'text-danger': finance.profit < 0 }">{{ finance.profit.toFixed(2) }}
</div>
<div class="finance-amount" :class="{ 'text-danger': finance.profit < 0 }">{{ finance.profit.toFixed(2) }}</div>
</a-card>
<a-card class="finance-card">
<div class="finance-title">收入</div>
@ -48,11 +45,11 @@
<div class="finance-amount text-warning">{{ finance.expense.toFixed(2) }}</div>
</a-card>
</div>
<a-descriptions title="合同金额" :data="contractInfos" layout="horizontal" bordered />
</a-card>
</a-tab-pane>
<a-tab-pane key="2" title="执行情况">
<a-card class="general-card" title="项目进度跟踪">
<div class="flex items-center justify-between mb-6">
@ -69,7 +66,7 @@
</a-button>
</div>
</div>
<a-card :bordered="false" class="mt-4">
<a-row :gutter="16" class="task-container">
<a-col :span="6" v-for="(column, index) in taskColumns" :key="index">
@ -78,34 +75,38 @@
<span class="column-title">{{ column.title }}</span>
<a-tag>{{ column.tasks.length }}</a-tag>
</div>
<div class="task-list">
<div v-for="task in column.tasks" :key="task.id" class="task-card" @click="openTaskDetail(task)">
<div
v-for="task in column.tasks"
:key="task.id"
class="task-card"
@click="openTaskDetail(task)"
>
<div class="task-card-title">{{ task.taskName }}</div>
<div class="task-card-desc text-gray-500 text-xs mt-1">{{ task.description || '暂无描述' }}</div>
<div class="task-card-footer flex items-center justify-between mt-2">
<div class="flex items-center">
<icon-calendar class="mr-1 text-gray-500" />
<span class="text-xs">{{ formatDate(task.taskPeriod?.[0]) }} - {{
formatDate(task.taskPeriod?.[1])
}}</span>
<span class="text-xs">{{ formatDate(task.taskPeriod?.[0]) }} - {{ formatDate(task.taskPeriod?.[1]) }}</span>
</div>
<div class="text-xs">
<a-progress :percent="task.progress" size="small" />
</div>
</div>
<div class="task-card-members mt-2">
<a-avatar :size="24" v-for="(user, idx) in task.participants?.slice(0, 3)" :key="idx">{{
user.substring(0, 1) }}</a-avatar>
<a-avatar :size="24" v-if="task.participants?.length > 3">+{{ task.participants.length - 3
}}</a-avatar>
<a-avatar :size="24" v-for="(user, idx) in task.participants?.slice(0, 3)" :key="idx">{{ user.substring(0, 1) }}</a-avatar>
<a-avatar :size="24" v-if="task.participants?.length > 3">+{{ task.participants.length - 3 }}</a-avatar>
</div>
</div>
<div class="add-task-placeholder" @click="handleAddTaskClick(column.status)">
<icon-plus />
<span>添加任务</span>
</div>
<div
class="add-task-placeholder"
@click="handleAddTaskClick(column.status)"
>
<icon-plus />
<span>添加任务</span>
</div>
</div>
</div>
</a-col>
@ -113,11 +114,15 @@
</a-card>
</a-card>
</a-tab-pane>
<a-tab-pane key="3" title="附件">
<a-card class="general-card" title="项目附件">
<div class="attachment-section">
<a-upload list-type="picture-card" :file-list="attachments" @change="handleAttachmentChange">
<a-upload
list-type="picture-card"
:file-list="attachments"
@change="handleAttachmentChange"
>
<template #upload-button>
<div class="upload-button-content">
<icon-plus />
@ -131,9 +136,13 @@
</a-tabs>
<!-- 新增任务弹窗 -->
<a-modal v-model:visible="addTaskModalVisible" title="新增任务" @cancel="resetTaskForm" @before-ok="handleTaskSubmit">
<a-form ref="taskFormRef" :model="taskForm" label-position="left" :label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }">
<a-modal
v-model:visible="addTaskModalVisible"
title="新增任务"
@cancel="resetTaskForm"
@before-ok="handleTaskSubmit"
>
<a-form ref="taskFormRef" :model="taskForm" label-position="left" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
<a-form-item field="taskName" label="任务名称" required>
<a-input v-model="taskForm.taskName" placeholder="请输入" />
</a-form-item>
@ -163,10 +172,13 @@
</a-modal>
<!-- 新增任务组弹窗 -->
<a-modal v-model:visible="addTaskGroupModalVisible" title="新增任务组" @cancel="resetTaskGroupForm"
@before-ok="handleTaskGroupSubmit">
<a-form ref="taskGroupFormRef" :model="taskGroupForm" label-position="left" :label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }">
<a-modal
v-model:visible="addTaskGroupModalVisible"
title="新增任务组"
@cancel="resetTaskGroupForm"
@before-ok="handleTaskGroupSubmit"
>
<a-form ref="taskGroupFormRef" :model="taskGroupForm" label-position="left" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
<a-form-item field="groupName" label="组名称" required>
<a-input v-model="taskGroupForm.groupName" placeholder="请输入" />
</a-form-item>
@ -174,14 +186,18 @@
</a-modal>
<!-- 任务详情弹窗 -->
<a-modal v-model:visible="taskDetailModalVisible" title="任务详情" @cancel="closeTaskDetail">
<a-modal
v-model:visible="taskDetailModalVisible"
title="任务详情"
@cancel="closeTaskDetail"
>
<div v-if="currentTask">
<a-descriptions :data="taskDetailInfos" layout="horizontal" bordered />
<div class="mt-4">
<h4>任务进度</h4>
<a-progress :percent="currentTask.progress || 0" />
<a-form class="mt-4">
<a-form-item label="更新进度">
<a-input-number v-model="updateProgress" :min="0" :max="100" style="width: 100%" />
@ -273,7 +289,7 @@ const contractInfos = computed(() => [
const taskDetailInfos = computed(() => {
if (!currentTask.value) return []
return [
{ label: '任务名称', value: currentTask.value.taskName },
{ label: '任务编号', value: currentTask.value.taskCode },
@ -334,19 +350,19 @@ const fetchProjectData = async () => {
const fetchTaskData = async () => {
try {
const res = await listTask({
const res = await listTask({
projectId: projectId.value,
page: 1,
size: 100
})
//
taskColumns.value.forEach(column => {
column.tasks = []
})
const tasks = res.data?.list || []
//
tasks.forEach((task: any) => {
const column = taskColumns.value.find(col => col.status === task.status)
@ -478,10 +494,10 @@ const submitProgressUpdate = async () => {
try {
await updateTaskProgress({ progress: updateProgress.value }, currentTask.value.id)
Message.success('更新进度成功')
//
currentTask.value.progress = updateProgress.value
//
await fetchTaskData()
} catch (error) {
@ -500,41 +516,33 @@ onMounted(() => {
.finance-card {
text-align: center;
}
.finance-title {
color: #7f7f7f;
font-size: 14px;
}
.finance-amount {
font-size: 24px;
font-weight: bold;
}
.text-danger {
color: #f53f3f;
}
.text-success {
color: #00b42a;
}
.text-warning {
color: #ff7d00;
}
.task-container {
overflow-x: auto;
min-height: 600px;
}
.task-column {
background-color: #f5f5f5;
border-radius: 4px;
height: 100%;
min-height: 600px;
}
.task-column-header {
padding: 12px;
border-radius: 4px 4px 0 0;
@ -543,11 +551,9 @@ onMounted(() => {
align-items: center;
font-weight: bold;
}
.task-list {
padding: 8px;
}
.task-card {
background-color: white;
border-radius: 4px;
@ -557,20 +563,16 @@ onMounted(() => {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s;
}
.task-card:hover {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
}
.task-card-title {
font-weight: bold;
}
.task-card-members {
display: flex;
gap: 4px;
}
.add-task-placeholder {
display: flex;
align-items: center;
@ -582,31 +584,26 @@ onMounted(() => {
cursor: pointer;
gap: 8px;
}
.add-task-placeholder:hover {
background-color: #d9d9d9;
}
.upload-button-content {
display: flex;
flex-direction: column;
align-items: center;
color: #7f7f7f;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.card-title {
font-size: 16px;
font-weight: bold;
}
.general-card {
margin-bottom: 16px;
}
</style>
</style>

View File

@ -1,358 +0,0 @@
<template>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" width="80px" height="160px" viewBox="0 0 210 234" enable-background="new 0 0 210 234"
xml:space="preserve">
<image id="image0" width="210" height="234" x="0" y="0" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANIAAADqCAMAAADgbuuwAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACc1BMVEXx8fH29vb8+/z////+
/v74+Pjz8/Py8vLv7+/39/fs7Oz6+vrw8PD19fX5+fn9/f3q6up+fn60tLTu7u7p6ekPDw9SUVLr
6+vo6OiZmZkHBwcBAQHMzMwAAAA7Ozvn5+fk5OSrq6vj4+Pl5eUkJCRvb2/c3Nx4eHgFBQWysrIQ
EBANDQ03Nzevr68MDAxHR0cXFxeQkJAKCgoDAwMYGBjFxcUJCQlxcXEICAixsbEVFRVBQUECAgIG
BgaamprV1dVbW1sUFBTJycmNjY0TExO4uLgbGxvd3d2lpaXh4eFjY2O+vr5/f38rKytmZmZFRUUt
LS23t7epqana2tqWlpbGxsaIiIiwsLB5eXmBgYGLi4uSkpKzs7PBwcEREREODg4zMzNWVlYjIyN2
dnZYWFibm5vS0tLb29vPz884ODhAQEDe3t7U1NQcHBx7e3u2trZtbW3T09MpKSlMTEykpKTZ2dks
LCxqamq7u7teXl4/Pz9sbGzNzc1kZGQWFhaPj4+hoaF9fX3IyMgdHR1iYmK9vb2AgIDi4uLCwsLX
19fExMSOjo5LS0tdXV2npqesrKwiIiKoqKg5OTmHh4coKCi/v78vLy/g4OA2NjZoaGgxMTFPT08m
JiYhISFfX19CQkI+Pj5ycnLQ0NBpaWlhYWEeHh7Hx8dwcHBzc3OXl5fAwMA9PT1VVVWUlJR6enpr
a2tERES5ublTU1NaWlqVlZUfHx8aGhpJSUmGhoaTk5Otra2ioqLOzs6dnZ26urpcXFxlZWWgoKBN
TU2FhYWJiYkyMjIwMDDW1tZQUFCEhISMjIxXV1dOTk4lJSVISEiCgoJUVFSQ4NxnAAAAAWJLR0QD
EQxM8gAAAAd0SU1FB+kHFwcmCqQ1L2MAAEkTSURBVHjazb2LnyXbVR62d712PXZVHVxnqEMfFz2a
hqvjO2gQc9CoJbqRr/VCCeAABoRB94JsBEIgE/FKiCwhIFxQYuOA4JpHHBSQkYgBB5PEIQlxiO28
H3+S1/etvetUd5/umXsRmPPTT/f0dNfj24+1vvXcxtgkTbO8cM6Z0uTyQ5pW8jG1aTL5pN4W+Bn/
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" />
</svg>
</template>
<style scoped>
.turbine-svg {
width: 100%;
height: 100%;
pointer-events: none;
}
</style>

View File

@ -12,12 +12,20 @@
-->
<template>
<GiPageLayout>
<GiTable row-key="id" :data="dataList" :columns="tableColumns" :loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }" :pagination="pagination" :disabled-tools="['size']"
@page-change="onPageChange" @page-size-change="onPageSizeChange" @refresh="search">
<GiTable
row-key="id"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
:disabled-tools="['size']"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset">
</GiForm>
<GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset"></GiForm>
</template>
<template #toolbar-left>
<a-button v-permission="['project:create']" type="primary" @click="openAddModal">
@ -63,7 +71,12 @@
<a-space>
<a-link v-permission="['project:detail']" title="详情" @click="viewDetail(record)">详情</a-link>
<a-link v-permission="['project:update']" title="修改" @click="openEditModal(record)">修改</a-link>
<a-link v-permission="['project:delete']" status="danger" title="删除" @click="confirmDelete(record)">
<a-link
v-permission="['project:delete']"
status="danger"
title="删除"
@click="confirmDelete(record)"
>
删除
</a-link>
</a-space>
@ -71,10 +84,22 @@
</GiTable>
<!-- 新增/编辑项目弹窗 -->
<a-modal v-model:visible="addModalVisible" :title="modalTitle" @cancel="resetForm"
:ok-button-props="{ loading: submitLoading }" @ok="handleSubmit" width="800px" modal-class="project-form-modal">
<a-form ref="formRef" :model="form" :rules="formRules" layout="vertical"
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }">
<a-modal
v-model:visible="addModalVisible"
:title="modalTitle"
@cancel="resetForm"
:ok-button-props="{ loading: submitLoading }"
@ok="handleSubmit"
width="800px"
modal-class="project-form-modal"
>
<a-form
ref="formRef"
:model="form"
:rules="formRules"
layout="vertical"
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"
>
<!-- 基本信息 -->
<a-divider orientation="left">基本信息</a-divider>
<a-row :gutter="16">
@ -83,17 +108,6 @@
<a-input v-model="form.projectName" placeholder="请输入项目名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="farmAddress" label="地址">
<a-input v-model="form.farmAddress" placeholder="请输入地址" />
</a-form-item>
</a-col>
<a-col><a-button size="mini" @click="() => { Message.info(`待开发`) }">
<template #icon><icon-location /></template>
地图选点
</a-button></a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="projectManagerId" label="项目经理" required>
<a-select v-model="form.projectManagerId" placeholder="请选择项目经理" :loading="userLoading">
@ -104,54 +118,10 @@
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionUnit" label="业主">
<a-input v-model="form.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (form.farmName = val)" />
<!--风场名称同步业主 -->
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="inspectionContact" label="业主单位联系人">
<a-input v-model="form.inspectionContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionPhone" label="业主单位联系电话">
<a-input v-model="form.inspectionPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="client" label="委托单位">
<a-input v-model="form.client" placeholder="请输入委托单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="clientContact" label="委托单位联系人">
<a-input v-model="form.clientContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="clientPhone" label="委托单位联系电话">
<a-input v-model="form.clientPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-form-item field="projectContent" label="项目内容">
<a-textarea v-model="form.coverUrl" placeholder="请输入项目内容" :rows="4" />
</a-form-item>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="status" label="项目状态">
<a-form-item field="status" label="项目状态" >
<a-select v-model="form.status" placeholder="请选择状态">
<a-option v-for="option in PROJECT_STATUS_OPTIONS" :key="option.value" :value="option.value">
{{ option.label }}
@ -160,57 +130,166 @@
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="scale" label="项目规模">
<a-input-number v-model="form.scale" placeholder="请输入项目规模" :min="0" :max="999" :step="1" />
<a-form-item field="scale" label="项目规模" >
<a-input v-model="form.scale" placeholder="请输入项目规模" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="startDate" label="开始时间">
<a-form-item field="startDate" label="开始时间" >
<a-date-picker v-model="form.startDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="endDate" label="结束时间">
<a-form-item field="endDate" label="结束时间" >
<a-date-picker v-model="form.endDate" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item field="coverUrl" label="项目封面">
<a-input v-model="form.coverUrl" placeholder="请输入项目封面URL" />
</a-form-item>
</a-col>
</a-row>
<!-- 委托方信息 -->
<a-divider orientation="left">委托方信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="turbineModel" label="风机型号">
<a-form-item field="client" label="委托单位" >
<a-input v-model="form.client" placeholder="请输入委托单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="clientContact" label="委托单位联系人" >
<a-input v-model="form.clientContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="clientPhone" label="委托单位联系电话" >
<a-input v-model="form.clientPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<!-- 检查方信息 -->
<a-divider orientation="left">检查方信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionUnit" label="检查单位" >
<a-input v-model="form.inspectionUnit" placeholder="请输入检查单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="inspectionContact" label="检查单位联系人" >
<a-input v-model="form.inspectionContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionPhone" label="检查单位联系电话" >
<a-input v-model="form.inspectionPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<!-- 风场信息 -->
<a-divider orientation="left">风场信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="farmName" label="风场名称" >
<a-input v-model="form.farmName" placeholder="请输入风场名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="farmAddress" label="风场地址" >
<a-input v-model="form.farmAddress" placeholder="请输入风场地址" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="turbineModel" label="风机型号" >
<a-input v-model="form.turbineModel" placeholder="请输入风机型号" />
</a-form-item>
</a-col>
</a-row>
<!-- 风场信息 -->
<a-divider orientation="left">风场信息可视化</a-divider>
<!-- 项目团队 -->
<a-divider orientation="left">项目团队</a-divider>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="机组网格布局">
<a-space direction="vertical" style="width: 100%">
<TurbineGrid v-model:="form.turbineList"></TurbineGrid>
</a-space>
<a-col :span="12">
<a-form-item field="constructionTeamLeaderId" label="施工组长" >
<a-select v-model="form.constructionTeamLeaderId" placeholder="请选择施工组长" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="constructorIds" label="施工人员" >
<a-select v-model="form.constructorIds" multiple placeholder="请选择施工人员" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="qualityOfficerId" label="质量员" >
<a-select v-model="form.qualityOfficerId" placeholder="请选择质量员" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="auditorId" label="安全员" >
<a-select v-model="form.auditorId" placeholder="请选择安全员" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="middle">地图</a-divider>
</a-form>
</a-modal>
<!-- 导入项目弹窗 -->
<a-modal v-model:visible="importModalVisible" title="导入文件" @cancel="handleCancelImport" @before-ok="handleImport">
<a-modal
v-model:visible="importModalVisible"
title="导入文件"
@cancel="handleCancelImport"
@before-ok="handleImport"
>
<div class="flex flex-col items-center justify-center p-8">
<div class="text-primary text-4xl mb-4">
<icon-file-upload />
</div>
<div class="text-lg font-medium mb-2">批量导入文件</div>
<div class="text-gray-500 mb-4">拖动文件到此处或点击下方按钮上传</div>
<a-upload :file-list="fileList" :limit="1" @change="handleFileChange">
<a-upload
:file-list="fileList"
:limit="1"
@change="handleFileChange"
>
<template #upload-button>
<a-button type="primary">选择文件</a-button>
</template>
@ -232,7 +311,7 @@ import type { ColumnItem } from '@/components/GiForm'
import type { TableColumnData } from '@arco-design/web-vue'
import type { ProjectResp, ProjectPageQuery } from '@/apis/project/type'
import * as T from '@/apis/project/type'
import TurbineGrid from './TurbineGrid.vue'
defineOptions({ name: 'ProjectManagement' })
// (API)
@ -245,7 +324,7 @@ const PROJECT_STATUS = {
//
const PROJECT_STATUS_MAP = {
0: '待施工',
1: '施工中',
1: '施工中',
2: '已完成'
} as const
@ -300,11 +379,11 @@ const queryFormColumns: ColumnItem[] = reactive([
},
{
type: 'input',
label: '业主',
field: 'inspectionUnit',
label: '风场名称',
field: 'fieldName', // 使fieldNameAPI使
span: { xs: 24, sm: 8, xxl: 8 },
props: {
placeholder: '请输入业主名称',
placeholder: '请输入风场名称',
},
},
{
@ -326,22 +405,21 @@ const form = reactive({
client: '', //
clientContact: '', //
clientPhone: '', //
inspectionUnit: '', //
inspectionContact: '', //
inspectionPhone: '', //
farmName: '', //
farmAddress: '', //
scale: '', //
inspectionUnit: '', //
inspectionContact: '', //
inspectionPhone: '', //
farmName: '', //
farmAddress: '', //
scale: '', //
turbineModel: '', //
status: '', // 01234
status: 0, // 01234
startDate: '', //
endDate: '', //
coverUrl: '', //
coverUrl: '', //
constructionTeamLeaderId: '', // id
constructorIds: '', // id
qualityOfficerId: '', // id
auditorId: '', // id
turbineList: [] as { id: number; turbineNo: string; lat?: number; lng?: number; status: 0 | 1 | 2 }[], //
auditorId: '' // id
})
const pagination = reactive({
@ -352,9 +430,7 @@ const pagination = reactive({
showJumper: true,
showPageSize: true
})
const openMapModal = (item: any) => {
Message.info(`地图选点功能待开发,当前机组编号:${item.turbineNo}`)
}
const tableColumns = ref<TableColumnData[]>([
{
title: '序号',
@ -363,91 +439,91 @@ const tableColumns = ref<TableColumnData[]>([
render: ({ rowIndex }) => rowIndex + 1 + (pagination.current - 1) * pagination.pageSize,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '项目编号',
dataIndex: 'projectCode',
width: 120,
ellipsis: true,
{
title: '项目编号',
dataIndex: 'projectCode',
width: 120,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '项目名称',
dataIndex: 'projectName',
minWidth: 140,
ellipsis: true,
{
title: '项目名称',
dataIndex: 'projectName',
minWidth: 140,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '地点',
slotName: 'fieldInfo',
minWidth: 180,
ellipsis: true,
tooltip: true
{
title: '风场名称/风场地址',
slotName: 'fieldInfo',
minWidth: 180,
ellipsis: true,
tooltip: true
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100
},
{
title: '委托单位',
dataIndex: 'commissionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
{
title: '委托单位',
dataIndex: 'commissionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
},
{
title: '委托单位联系人/电话',
slotName: 'commissionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
{
title: '委托单位联系人/电话',
slotName: 'commissionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '业主',
dataIndex: 'inspectionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
{
title: '检查单位',
dataIndex: 'inspectionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
},
{
title: '业主联系人/电话',
slotName: 'inspectionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
{
title: '检查单位联系人/电话',
slotName: 'inspectionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '项目规模',
dataIndex: 'projectScale',
width: 100,
ellipsis: true,
tooltip: true
{
title: '项目规模',
dataIndex: 'projectScale',
width: 100,
ellipsis: true,
tooltip: true
},
{
title: '机组型号',
dataIndex: 'orgNumber',
width: 100,
ellipsis: true,
tooltip: true
{
title: '机组型号',
dataIndex: 'orgNumber',
width: 100,
ellipsis: true,
tooltip: true
},
{
title: '项目经理/施工人员',
slotName: 'projectManager',
minWidth: 160,
ellipsis: true,
tooltip: true
{
title: '项目经理/施工人员',
slotName: 'projectManager',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '项目周期',
slotName: 'projectPeriod',
minWidth: 180,
ellipsis: true,
tooltip: true
{
title: '项目周期',
slotName: 'projectPeriod',
minWidth: 180,
ellipsis: true,
tooltip: true
},
{
title: '操作',
@ -457,20 +533,7 @@ const tableColumns = ref<TableColumnData[]>([
fixed: !isMobile() ? 'right' : undefined,
},
])
watch(() => form.scale, (newVal) => {
const count = Number(newVal)
if (count > 0 && count <= 999) {
form.turbineList = Array.from({ length: count }, (_, i) => ({
id: i + 1,
turbineNo: `${String(i + 1).padStart(3, '0')}`,
lat: undefined,
lng: undefined,
status: form.status,
}))
} else {
form.turbineList = []
}
}, { immediate: true })
const modalTitle = computed(() => isEdit.value ? '编辑项目' : '新增项目')
const getStatusColor = (status: number) => {
@ -494,37 +557,37 @@ const fetchData = async () => {
page: pagination.current,
size: pagination.pageSize
}
const res = await listProject(params)
if (res.success && res.data) {
// API
const projects = Array.isArray(res.data) ? res.data : []
//
dataList.value = projects.map((item: any) => {
const mappedItem: T.ProjectResp = {
...item,
//
id: item.projectId,
fieldName: item.farmName,
fieldLocation: item.farmAddress,
commissionUnit: item.client,
commissionContact: item.clientContact,
commissionPhone: item.clientPhone,
orgNumber: item.turbineModel,
projectManager: item.projectManagerName,
...item,
//
id: item.projectId,
fieldName: item.farmName,
fieldLocation: item.farmAddress,
commissionUnit: item.client,
commissionContact: item.clientContact,
commissionPhone: item.clientPhone,
orgNumber: item.turbineModel,
projectManager: item.projectManagerName,
projectScale: item.scale,
//
//
projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : []
};
return mappedItem;
})
// APItotal使
// total
pagination.total = projects.length
//
if (projects.length < pagination.pageSize) {
pagination.total = (pagination.current - 1) * pagination.pageSize + projects.length
@ -556,7 +619,7 @@ const reset = () => {
fieldName: '', // 使fieldNameAPI使
status: undefined,
})
//
pagination.current = 1
search()
@ -598,7 +661,7 @@ const resetForm = () => {
qualityOfficerId: '', // id
auditorId: '' // id
})
isEdit.value = false
currentId.value = null
}
@ -611,13 +674,13 @@ const openAddModal = () => {
const openEditModal = (record: T.ProjectResp) => {
isEdit.value = true
currentId.value = record.id || record.projectId || null
//
Object.keys(form).forEach(key => {
// @ts-ignore
form[key] = ''
})
//
Object.keys(form).forEach(key => {
if (key in record && record[key as keyof T.ProjectResp] !== undefined) {
@ -625,7 +688,7 @@ const openEditModal = (record: T.ProjectResp) => {
form[key] = record[key as keyof T.ProjectResp]
}
})
//
if (record.farmName) form.farmName = record.farmName
if (record.farmAddress) form.farmAddress = record.farmAddress
@ -634,11 +697,11 @@ const openEditModal = (record: T.ProjectResp) => {
if (record.clientPhone) form.clientPhone = record.clientPhone
if (record.turbineModel) form.turbineModel = record.turbineModel
if (record.scale) form.scale = record.scale
//
if (record.startDate) form.startDate = record.startDate
if (record.endDate) form.endDate = record.endDate
addModalVisible.value = true
}
@ -653,12 +716,12 @@ const submitLoading = ref(false)
const handleSubmit = async () => {
console.log('表单提交开始', form)
submitLoading.value = true
try {
//
console.log('开始验证表单', formRef.value)
await formRef.value.validate()
//
const submitData = {
...form,
@ -672,7 +735,7 @@ const handleSubmit = async () => {
}
console.log('提交数据:', submitData)
let res
if (isEdit.value && currentId.value) {
//
@ -683,19 +746,18 @@ const handleSubmit = async () => {
//
console.log('新增模式提交')
res = await addProject(submitData)
Message.success('添加成功')
}
console.log('API响应结果:', res)
//
if (res && res.success === false) {
Message.error(res.msg || '操作失败')
submitLoading.value = false
return
}
addModalVisible.value = false
fetchData()
} catch (error: any) {
@ -725,16 +787,16 @@ const deleteItem = async (record: T.ProjectResp) => {
Message.error('项目ID不存在')
return
}
try {
const res = await deleteProject(projectId)
//
if (res && res.success === false) {
Message.error(res.msg || '删除失败')
return
}
Message.success('删除成功')
fetchData()
} catch (error) {
@ -749,7 +811,7 @@ const viewDetail = (record: T.ProjectResp) => {
Message.error('项目ID不存在')
return
}
router.push({
name: 'ProjectDetail',
params: {
@ -777,24 +839,24 @@ const handleImport = async () => {
Message.warning('请选择文件')
return false
}
try {
const fileItem = fileList.value[0] as any
const file = fileItem?.file || fileItem
if (!file) {
Message.warning('请选择有效的文件')
return false
}
// API
const res = await importProject(file)
if (res && res.success === false) {
Message.error(res.msg || '导入失败')
return false
}
Message.success('导入成功')
handleCancelImport()
fetchData() //
@ -813,7 +875,7 @@ const exportData = async () => {
status: searchForm.status,
fieldName: searchForm.fieldName, // 使fieldNameAPI使
}
await exportProject(params)
Message.success('导出成功')
} catch (error) {
@ -860,11 +922,11 @@ onMounted(() => {
.arco-form-item {
margin-bottom: 16px;
}
.arco-divider {
margin: 16px 0;
font-weight: 500;
color: var(--color-text-2);
}
}
</style>
</style>

View File

@ -1,242 +0,0 @@
<template>
<a-modal
:visible="visible"
@update:visible="val => emit('update:visible', val)"
:title="title"
:width="900"
:footer="true"
@cancel="handleCancel"
>
<a-descriptions :data="descriptionsData" bordered :column="2">
<a-descriptions-item label="员工姓名">{{ data?.employeeName }}</a-descriptions-item>
<a-descriptions-item label="员工类型">{{ employeeTypeText }}</a-descriptions-item>
<a-descriptions-item label="身份证号">{{ data?.idCard }}</a-descriptions-item>
<a-descriptions-item label="联系电话">{{ data?.phone }}</a-descriptions-item>
<a-descriptions-item label="银行卡号">{{ data?.bankCard }}</a-descriptions-item>
<a-descriptions-item label="开户银行">{{ data?.bankName }}</a-descriptions-item>
<a-descriptions-item label="项目名称">{{ data?.projectName }}</a-descriptions-item>
<a-descriptions-item label="项目周期">{{ data?.projectPeriod }}</a-descriptions-item>
<a-descriptions-item label="工作天数">{{ data?.workDays }}</a-descriptions-item>
<a-descriptions-item label="海上作业天数">{{ data?.offshoreDays }}</a-descriptions-item>
</a-descriptions>
<a-divider orientation="left">薪酬明细</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-statistic
title="基本工资"
:value="data?.baseSalary"
:precision="2"
:value-style="{ color: '#3f8600' }"
/>
</a-col>
<a-col :span="12">
<a-statistic
title="绩效奖金"
:value="data?.performanceBonus"
:precision="2"
:value-style="{ color: '#3f8600' }"
/>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px">
<a-col :span="12">
<a-statistic
title="各类补助"
:value="data?.allowances"
:precision="2"
:value-style="{ color: '#3f8600' }"
/>
</a-col>
<a-col :span="12">
<a-statistic
title="应发总额"
:value="data?.totalPayable"
:precision="2"
:value-style="{ color: '#1890ff' }"
/>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px">
<a-col :span="12">
<a-statistic
title="扣款金额"
:value="data?.deductions"
:precision="2"
:value-style="{ color: '#ff4d4f' }"
/>
</a-col>
<a-col :span="12">
<a-statistic
title="实发金额"
:value="data?.netPay"
:precision="2"
:value-style="{ color: '#52c41a', fontSize: '24px', fontWeight: 'bold' }"
/>
</a-col>
</a-row>
<a-divider orientation="left">绩效评价</a-divider>
<a-table :data="data?.performanceItems" :columns="performanceColumns" :pagination="false">
<template #score="{ record }">
<a-tag :color="getScoreColor(record.score, record.maxScore)">
{{ record.score }}/{{ record.maxScore }}
</a-tag>
</template>
<template #amount="{ record }">
¥{{ (record.score / record.maxScore * record.amount).toFixed(2) }}
</template>
</a-table>
<a-divider orientation="left">补助明细</a-divider>
<a-table :data="data?.allowanceItems" :columns="allowanceColumns" :pagination="false">
<template #total="{ record }">
¥{{ record.total.toFixed(2) }}
</template>
</a-table>
<a-divider orientation="left">审批流程</a-divider>
<a-steps :current="approvalCurrent" direction="vertical">
<a-step
v-for="step in data?.approvalFlow"
:key="step.step"
:title="step.step"
:description="`${step.role} - ${step.approver}`"
>
<template #icon>
<icon-check-circle v-if="step.status === 'approved'" style="color: #52c41a" />
<icon-close-circle v-else-if="step.status === 'rejected'" style="color: #ff4d4f" />
<icon-clock-circle v-else style="color: #faad14" />
</template>
</a-step>
</a-steps>
<template #footer>
<a-space>
<a-button @click="handleCancel">关闭</a-button>
<a-button type="primary" @click="handleExport">
<template #icon><icon-download /></template>
导出Excel
</a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { SalaryRecord } from '../types'
interface Props {
visible: boolean
data: SalaryRecord | null
}
interface Emits {
(e: 'update:visible', visible: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const title = computed(() => `${props.data?.employeeName} - 工资详情`)
const employeeTypeText = computed(() => {
const typeMap = {
intern: '实习生',
fulltime: '正式员工',
parttime: '兼职员工',
}
return props.data?.employeeType ? typeMap[props.data.employeeType] : ''
})
const approvalCurrent = computed(() => {
if (!props.data?.approvalFlow) return 0
const approvedSteps = props.data.approvalFlow.filter((step) => step.status === 'approved').length
return approvedSteps
})
const descriptionsData = computed(() => [
{ label: '员工姓名', value: props.data?.employeeName },
{ label: '员工类型', value: employeeTypeText.value },
{ label: '身份证号', value: props.data?.idCard },
{ label: '联系电话', value: props.data?.phone },
{ label: '银行卡号', value: props.data?.bankCard },
{ label: '开户银行', value: props.data?.bankName },
{ label: '项目名称', value: props.data?.projectName },
{ label: '项目周期', value: props.data?.projectPeriod },
{ label: '工作天数', value: `${props.data?.workDays}` },
{ label: '海上作业天数', value: `${props.data?.offshoreDays}` },
])
const performanceColumns = [
{ title: '评价项', dataIndex: 'name', width: 200 },
{ title: '权重', dataIndex: 'weight', width: 80 },
{ title: '得分', dataIndex: 'score', slotName: 'score', width: 100 },
{ title: '满分', dataIndex: 'maxScore', width: 80 },
{ title: '金额', dataIndex: 'amount', slotName: 'amount', width: 100 },
]
const allowanceColumns = [
{ title: '补助类型', dataIndex: 'name', width: 200 },
{ title: '标准', dataIndex: 'amount', width: 100 },
{ title: '数量', dataIndex: 'quantity', width: 100 },
{ title: '小计', dataIndex: 'total', slotName: 'total', width: 100 },
]
const getScoreColor = (score: number, maxScore: number) => {
const ratio = score / maxScore
if (ratio >= 0.9) return 'green'
if (ratio >= 0.7) return 'blue'
if (ratio >= 0.6) return 'orange'
return 'red'
}
const handleCancel = () => {
emit('update:visible', false)
}
const handleExport = () => {
if (!props.data) return
//
const exportData = {
employee: {
name: props.data.employeeName,
type: employeeTypeText.value,
idCard: props.data.idCard,
phone: props.data.phone,
bankCard: props.data.bankCard,
bankName: props.data.bankName,
},
project: {
name: props.data.projectName,
period: props.data.projectPeriod,
workDays: props.data.workDays,
offshoreDays: props.data.offshoreDays,
},
salary: {
baseSalary: props.data.baseSalary,
performanceBonus: props.data.performanceBonus,
allowances: props.data.allowances,
totalPayable: props.data.totalPayable,
deductions: props.data.deductions,
netPay: props.data.netPay,
},
performanceItems: props.data.performanceItems,
allowanceItems: props.data.allowanceItems,
}
//
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${props.data.employeeName}_工资单_${props.data.projectPeriod}.json`
a.click()
URL.revokeObjectURL(url)
}
</script>

View File

@ -1,468 +0,0 @@
<template>
<a-drawer
:visible="visible"
@update:visible="val => emit('update:visible', val)"
:title="title"
:width="800"
:footer="true"
@cancel="handleCancel"
>
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
<!-- 基本信息 -->
<a-card title="基本信息" :bordered="false">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="员工姓名" field="employeeName">
<a-input v-model="formData.employeeName" placeholder="请输入员工姓名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="员工类型" field="employeeType">
<a-select v-model="formData.employeeType" placeholder="请选择员工类型">
<a-option value="intern">实习生</a-option>
<a-option value="fulltime">正式员工</a-option>
<a-option value="parttime">兼职员工</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="身份证号" field="idCard">
<a-input v-model="formData.idCard" placeholder="请输入身份证号" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系电话" field="phone">
<a-input v-model="formData.phone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="银行卡号" field="bankCard">
<a-input v-model="formData.bankCard" placeholder="请输入银行卡号" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="开户银行" field="bankName">
<a-input v-model="formData.bankName" placeholder="请输入开户银行" />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 项目信息 -->
<a-card title="项目信息" :bordered="false" style="margin-top: 16px">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="项目名称" field="projectName">
<a-input v-model="formData.projectName" placeholder="请输入项目名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="项目周期" field="projectPeriod">
<a-input v-model="formData.projectPeriod" placeholder="如2025-07" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="工作天数" field="workDays">
<a-input-number
v-model="formData.workDays"
:min="0"
:precision="0"
placeholder="请输入工作天数"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="海上作业天数" field="offshoreDays">
<a-input-number
v-model="formData.offshoreDays"
:min="0"
:precision="0"
placeholder="请输入海上作业天数"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 薪酬计算 -->
<a-card title="薪酬计算" :bordered="false" style="margin-top: 16px">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="基本工资标准" field="baseSalaryStandard">
<a-input-number
v-model="formData.baseSalaryStandard"
:min="0"
:precision="2"
placeholder="请输入基本工资标准"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="基本工资" field="baseSalary">
<a-input-number
v-model="formData.baseSalary"
:min="0"
:precision="2"
placeholder="自动计算"
style="width: 100%"
readonly
/>
</a-form-item>
</a-col>
</a-row>
<!-- 绩效评价 -->
<a-divider orientation="left">绩效评价</a-divider>
<a-table :data="formData.performanceItems" :columns="performanceColumns" :pagination="false">
<template #score="{ record, rowIndex }">
<a-input-number
v-model="record.score"
:min="0"
:max="record.maxScore"
:precision="0"
@change="calculatePerformance"
/>
</template>
<template #amount="{ record }">
{{ record.amount }}
</template>
</a-table>
<!-- 补助明细 -->
<a-divider orientation="left">补助明细</a-divider>
<a-table :data="formData.allowanceItems" :columns="allowanceColumns" :pagination="false">
<template #quantity="{ record, rowIndex }">
<a-input-number
v-model="record.quantity"
:min="0"
:precision="0"
@change="calculateAllowances"
/>
</template>
<template #total="{ record }">
{{ record.total }}
</template>
</a-table>
<!-- 计算结果 -->
<a-divider orientation="left">计算结果</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="应发总额">
<a-input-number
v-model="formData.totalPayable"
:min="0"
:precision="2"
readonly
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="扣款金额">
<a-input-number
v-model="formData.deductions"
:min="0"
:precision="2"
placeholder="请输入扣款金额"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="实发金额">
<a-input-number
v-model="formData.netPay"
:min="0"
:precision="2"
readonly
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
</a-form>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleSubmit">保存</a-button>
</a-space>
</template>
</a-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { SalaryRecord, AllowanceItem, PerformanceItem } from '../types'
interface Props {
visible: boolean
data: SalaryRecord | null
mode: 'add' | 'edit'
}
interface Emits {
(e: 'update:visible', visible: boolean): void
(e: 'success'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const formRef = ref()
const formData = reactive<SalaryRecord>({
id: '',
employeeId: '',
employeeName: '',
employeeType: 'intern',
idCard: '',
phone: '',
bankCard: '',
bankName: '',
projectName: '',
projectPeriod: '',
workDays: 0,
offshoreDays: 0,
baseSalary: 0,
baseSalaryStandard: 3500,
performanceBonus: 0,
allowances: 0,
performanceItems: [],
allowanceItems: [],
totalPayable: 0,
deductions: 0,
netPay: 0,
status: 'draft',
approvalFlow: [],
salaryMonth: '',
createdAt: '',
updatedAt: '',
})
const performanceColumns = [
{ title: '评价项', dataIndex: 'name', width: 200 },
{ title: '权重', dataIndex: 'weight', width: 80 },
{ title: '得分', dataIndex: 'score', slotName: 'score', width: 100 },
{ title: '满分', dataIndex: 'maxScore', width: 80 },
{ title: '金额', dataIndex: 'amount', slotName: 'amount', width: 100 },
]
const allowanceColumns = [
{ title: '补助类型', dataIndex: 'name', width: 200 },
{ title: '标准', dataIndex: 'amount', width: 100 },
{ title: '单位', dataIndex: 'unit', width: 80 },
{ title: '数量', dataIndex: 'quantity', slotName: 'quantity', width: 100 },
{ title: '小计', dataIndex: 'total', slotName: 'total', width: 100 },
]
const title = computed(() => (props.mode === 'add' ? '新建工资单' : '编辑工资单'))
const rules = {
employeeName: [{ required: true, message: '请输入员工姓名' }],
employeeType: [{ required: true, message: '请选择员工类型' }],
idCard: [{ required: true, message: '请输入身份证号' }],
phone: [{ required: true, message: '请输入联系电话' }],
projectName: [{ required: true, message: '请输入项目名称' }],
workDays: [{ required: true, message: '请输入工作天数' }],
}
//
const initInternConfig = () => {
formData.performanceItems = [
{
id: '1',
name: '平时沟通态度',
weight: 30,
score: 0,
maxScore: 100,
amount: 200,
},
{
id: '2',
name: '积极主动性',
weight: 50,
score: 0,
maxScore: 100,
amount: 200,
},
{
id: '3',
name: '成长性心态',
weight: 20,
score: 0,
maxScore: 100,
amount: 100,
},
]
formData.allowanceItems = [
{
type: 'construction',
name: '施工绩效',
amount: 100,
unit: 'day',
quantity: 0,
total: 0,
},
{
type: 'offshore',
name: '海上作业补助',
amount: 30,
unit: 'day',
quantity: 0,
total: 0,
},
{
type: 'meal',
name: '务餐补助',
amount: 45,
unit: 'day',
quantity: 0,
total: 0,
},
]
}
//
const calculateBaseSalary = () => {
formData.baseSalary = (formData.baseSalaryStandard / 30) * formData.workDays
calculateTotal()
}
//
const calculatePerformance = () => {
formData.performanceBonus = formData.performanceItems.reduce(
(sum, item) => sum + (item.score / item.maxScore) * item.amount,
0,
)
calculateTotal()
}
//
const calculateAllowances = () => {
formData.allowanceItems.forEach((item) => {
item.total = item.amount * item.quantity
})
formData.allowances = formData.allowanceItems.reduce((sum, item) => sum + item.total, 0)
calculateTotal()
}
//
const calculateTotal = () => {
formData.totalPayable = formData.baseSalary + formData.performanceBonus + formData.allowances
formData.netPay = formData.totalPayable - formData.deductions
}
//
watch(
() => formData.workDays,
() => {
calculateBaseSalary()
//
const mealItem = formData.allowanceItems.find((item) => item.type === 'meal')
if (mealItem) {
mealItem.quantity = formData.workDays
calculateAllowances()
}
},
)
//
watch(
() => formData.offshoreDays,
() => {
//
const offshoreItem = formData.allowanceItems.find((item) => item.type === 'offshore')
if (offshoreItem) {
offshoreItem.quantity = formData.offshoreDays
calculateAllowances()
}
},
)
//
watch(() => formData.deductions, calculateTotal)
const handleCancel = () => {
emit('update:visible', false)
formRef.value?.resetFields()
}
const handleSubmit = async () => {
try {
await formRef.value?.validate()
// API
Message.success('保存成功')
emit('success')
} catch (error) {
console.error('表单验证失败:', error)
}
}
//
watch(
() => props.data,
(newData) => {
if (newData) {
Object.assign(formData, newData)
} else {
//
Object.assign(formData, {
id: '',
employeeId: '',
employeeName: '',
employeeType: 'intern',
idCard: '',
phone: '',
bankCard: '',
bankName: '',
projectName: '',
projectPeriod: '',
workDays: 0,
offshoreDays: 0,
baseSalary: 0,
baseSalaryStandard: 3500,
performanceBonus: 0,
allowances: 0,
performanceItems: [],
allowanceItems: [],
totalPayable: 0,
deductions: 0,
netPay: 0,
status: 'draft',
approvalFlow: [],
salaryMonth: '',
createdAt: '',
updatedAt: '',
})
initInternConfig()
}
},
{ immediate: true },
)
//
watch(
() => formData.employeeType,
(newType) => {
if (newType === 'intern') {
initInternConfig()
} else {
//
formData.performanceItems = []
formData.allowanceItems = []
}
},
)
</script>

View File

@ -1,229 +0,0 @@
<template>
<div class="salary-management">
<GiPageLayout>
<template #header>
<a-row justify="space-between" align="center">
<a-col>
<h1>工资管理系统</h1>
</a-col>
<a-col>
<a-space>
<a-button type="primary" @click="handleAdd">
<template #icon><icon-plus /></template>
新建工资单
</a-button>
<a-button @click="handleExport">
<template #icon><icon-download /></template>
导出Excel
</a-button>
</a-space>
</a-col>
</a-row>
</template>
<a-card>
<a-row :gutter="16" class="search-section">
<a-col :span="6">
<a-input
v-model="searchParams.keyword"
placeholder="搜索员工姓名/工号"
allow-clear
/>
</a-col>
<a-col :span="6">
<a-select
v-model="searchParams.employeeType"
placeholder="员工类型"
allow-clear
>
<a-option value="intern">实习生</a-option>
<a-option value="fulltime">正式员工</a-option>
<a-option value="parttime">兼职员工</a-option>
</a-select>
</a-col>
<a-col :span="6">
<a-range-picker
v-model="searchParams.dateRange"
style="width: 100%"
/>
</a-col>
<a-col :span="6">
<a-space>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-col>
</a-row>
<a-table
:data="tableData"
:columns="columns"
:loading="loading"
row-key="id"
:scroll="{ x: 1500 }"
>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="handleEdit(record)">编辑</a-button>
<a-button type="text" @click="handleDetail(record)">详情</a-button>
<a-button type="text" status="danger" @click="handleDelete(record)">删除</a-button>
</a-space>
</template>
</a-table>
</a-card>
</GiPageLayout>
<SalaryFormDrawer
v-model:visible="drawerVisible"
:data="currentData"
:mode="drawerMode"
@success="handleSuccess"
/>
<SalaryDetailModal
v-model:visible="detailVisible"
:data="currentData"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import SalaryFormDrawer from './components/SalaryFormDrawer.vue'
import SalaryDetailModal from './components/SalaryDetailModal.vue'
import type { SalaryRecord } from './types'
const loading = ref(false)
const drawerVisible = ref(false)
const detailVisible = ref(false)
const drawerMode = ref<'add' | 'edit'>('add')
const currentData = ref<SalaryRecord | null>(null)
const searchParams = reactive({
keyword: '',
employeeType: '',
dateRange: [],
})
const tableData = ref<SalaryRecord[]>([])
const columns = [
{ title: '员工姓名', dataIndex: 'employeeName', fixed: 'left', width: 100 },
{ title: '员工类型', dataIndex: 'employeeType', width: 100 },
{ title: '项目名称', dataIndex: 'projectName', width: 150 },
{ title: '工作天数', dataIndex: 'workDays', width: 80 },
{ title: '基本工资', dataIndex: 'baseSalary', width: 100 },
{ title: '绩效奖金', dataIndex: 'performanceBonus', width: 100 },
{ title: '各类补助', dataIndex: 'allowances', width: 100 },
{ title: '应发总额', dataIndex: 'totalPayable', width: 100 },
{ title: '实发金额', dataIndex: 'netPay', width: 100 },
{ title: '状态', dataIndex: 'status', width: 80 },
{ title: '创建时间', dataIndex: 'createdAt', width: 150 },
{ title: '操作', slotName: 'action', fixed: 'right', width: 150 },
]
const loadData = async () => {
loading.value = true
try {
//
tableData.value = [
{
id: '1',
employeeId: 'E001',
employeeName: '张三',
employeeType: 'intern',
idCard: '123456789012345678',
phone: '13800000000',
bankCard: '6222021234567890',
bankName: '中国银行',
projectName: '海上风电项目A',
projectPeriod: '2025-06',
workDays: 22,
offshoreDays: 5,
baseSalary: 3500,
baseSalaryStandard: 3500,
performanceBonus: 2200,
allowances: 660,
performanceItems: [],
allowanceItems: [],
totalPayable: 6360,
deductions: 0,
netPay: 6360,
status: 'approved',
approvalFlow: [],
salaryMonth: '2025-07',
createdAt: '2025-07-01',
updatedAt: '2025-07-01',
},
]
} finally {
loading.value = false
}
}
const handleAdd = () => {
drawerMode.value = 'add'
currentData.value = null
drawerVisible.value = true
}
const handleEdit = (record: SalaryRecord) => {
drawerMode.value = 'edit'
currentData.value = record
drawerVisible.value = true
}
const handleDetail = (record: SalaryRecord) => {
currentData.value = record
detailVisible.value = true
}
const handleDelete = async (_record: SalaryRecord) => {
try {
await Modal.confirm({
title: '确认删除',
content: '确定要删除这条记录吗?',
})
Message.success('删除成功')
loadData()
} catch {
//
}
}
const handleSearch = () => {
loadData()
}
const handleReset = () => {
searchParams.keyword = ''
searchParams.employeeType = ''
searchParams.dateRange = []
loadData()
}
const handleExport = () => {
// Excel
Message.success('导出功能开发中...')
}
const handleSuccess = () => {
drawerVisible.value = false
loadData()
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.salary-management {
padding: 16px;
}
.search-section {
margin-bottom: 16px;
}
</style>

View File

@ -1,127 +0,0 @@
// 员工类型
export type EmployeeType = 'intern' | 'fulltime' | 'parttime'
// 工资单状态
export type SalaryStatus = 'draft' | 'pending' | 'approved' | 'rejected' | 'paid'
// 补助类型
export interface AllowanceItem {
type: string
name: string
amount: number
unit: 'day' | 'project' | 'fixed'
quantity: number
total: number
}
// 绩效评价项
export interface PerformanceItem {
id: string
name: string
weight: number
score: number
maxScore: number
amount: number
description?: string
}
// 工资记录
export interface SalaryRecord {
id: string
employeeId: string
employeeName: string
employeeType: EmployeeType
idCard: string
phone: string
bankCard: string
bankName: string
// 项目信息
projectName: string
projectPeriod: string
workDays: number
offshoreDays: number
// 薪酬结构
baseSalary: number
baseSalaryStandard: number
performanceBonus: number
allowances: number
performanceItems: PerformanceItem[]
allowanceItems: AllowanceItem[]
// 计算结果
totalPayable: number
deductions: number
netPay: number
// 状态
status: SalaryStatus
approvalFlow: ApprovalStep[]
// 时间
salaryMonth: string
createdAt: string
updatedAt: string
}
// 审批步骤
export interface ApprovalStep {
step: string
role: string
approver: string
status: 'pending' | 'approved' | 'rejected'
comment?: string
date?: string
}
// 查询参数
export interface SalaryQuery {
keyword?: string
employeeType?: EmployeeType
status?: SalaryStatus
dateRange?: string[]
projectName?: string
}
// 实习生专用配置
export interface InternConfig {
baseSalaryStandard: {
withCertification: number
withoutCertification: number
}
performanceRates: {
construction: number
offshore: number
}
allowances: {
offshoreCert: number
towerWork: {
land: {
bladeInspection: number
lightning: number
}
sea: {
bladeInspection: number
lightning: number
}
}
droneWork: number
meal: number
}
}
// 工资单创建请求
export interface SalaryCreateRequest {
employeeId: string
projectId: string
workDays: number
offshoreDays: number
performanceItems: Omit<PerformanceItem, 'id'>[]
allowanceItems: Omit<AllowanceItem, 'total'>[]
deductions?: {
type: string
amount: number
reason: string
}[]
}

View File

@ -1,255 +0,0 @@
import * as XLSX from 'xlsx'
import type { SalaryRecord } from '../types'
// 导出工资单到Excel
export const exportSalaryToExcel = (records: SalaryRecord[], filename: string) => {
// 创建工作簿
const workbook = XLSX.utils.book_new()
// 主表数据
const mainData = records.map((record) => ({
员工姓名: record.employeeName,
员工类型: getEmployeeTypeText(record.employeeType),
身份证号: record.idCard,
联系电话: record.phone,
银行卡号: record.bankCard,
开户银行: record.bankName,
项目名称: record.projectName,
项目周期: record.projectPeriod,
工作天数: record.workDays,
海上作业天数: record.offshoreDays,
基本工资: record.baseSalary,
绩效奖金: record.performanceBonus,
各类补助: record.allowances,
应发总额: record.totalPayable,
扣款金额: record.deductions,
实发金额: record.netPay,
状态: getStatusText(record.status),
创建时间: record.createdAt,
}))
// 创建主表
const mainSheet = XLSX.utils.json_to_sheet(mainData)
XLSX.utils.book_append_sheet(workbook, mainSheet, '工资汇总')
// 绩效明细表
const performanceData: any[] = []
records.forEach((record) => {
record.performanceItems.forEach((item) => {
performanceData.push({
员工姓名: record.employeeName,
项目名称: record.projectName,
评价项: item.name,
权重: item.weight,
得分: item.score,
满分: item.maxScore,
: (item.score / item.maxScore * item.amount).toFixed(2),
})
})
})
if (performanceData.length > 0) {
const performanceSheet = XLSX.utils.json_to_sheet(performanceData)
XLSX.utils.book_append_sheet(workbook, performanceSheet, '绩效明细')
}
// 补助明细表
const allowanceData: any[] = []
records.forEach((record) => {
record.allowanceItems.forEach((item) => {
allowanceData.push({
员工姓名: record.employeeName,
项目名称: record.projectName,
补助类型: item.name,
标准: item.amount,
单位: getUnitText(item.unit),
数量: item.quantity,
小计: item.total,
})
})
})
if (allowanceData.length > 0) {
const allowanceSheet = XLSX.utils.json_to_sheet(allowanceData)
XLSX.utils.book_append_sheet(workbook, allowanceSheet, '补助明细')
}
// 导出文件
XLSX.writeFile(workbook, filename)
}
// 导出单个工资单详情
export const exportSingleSalaryToExcel = (record: SalaryRecord) => {
const workbook = XLSX.utils.book_new()
// 基本信息
const basicInfo = [{
: '员工姓名',
: record.employeeName,
}, {
: '员工类型',
: getEmployeeTypeText(record.employeeType),
}, {
: '身份证号',
: record.idCard,
}, {
: '联系电话',
: record.phone,
}, {
: '银行卡号',
: record.bankCard,
}, {
: '开户银行',
: record.bankName,
}, {
: '项目名称',
: record.projectName,
}, {
: '项目周期',
: record.projectPeriod,
}, {
: '工作天数',
: record.workDays,
}, {
: '海上作业天数',
: record.offshoreDays,
}]
const basicSheet = XLSX.utils.json_to_sheet(basicInfo)
XLSX.utils.book_append_sheet(workbook, basicSheet, '基本信息')
// 薪酬汇总
const salarySummary = [{
: '基本工资',
金额: record.baseSalary,
}, {
: '绩效奖金',
金额: record.performanceBonus,
}, {
: '各类补助',
金额: record.allowances,
}, {
: '应发总额',
金额: record.totalPayable,
}, {
: '扣款金额',
金额: record.deductions,
}, {
: '实发金额',
金额: record.netPay,
}]
const salarySheet = XLSX.utils.json_to_sheet(salarySummary)
XLSX.utils.book_append_sheet(workbook, salarySheet, '薪酬汇总')
// 绩效明细
if (record.performanceItems.length > 0) {
const performanceSheet = XLSX.utils.json_to_sheet(record.performanceItems.map((item) => ({
评价项: item.name,
权重: item.weight,
得分: item.score,
满分: item.maxScore,
: (item.score / item.maxScore * item.amount).toFixed(2),
})))
XLSX.utils.book_append_sheet(workbook, performanceSheet, '绩效明细')
}
// 补助明细
if (record.allowanceItems.length > 0) {
const allowanceSheet = XLSX.utils.json_to_sheet(record.allowanceItems.map((item) => ({
补助类型: item.name,
标准: item.amount,
单位: getUnitText(item.unit),
数量: item.quantity,
小计: item.total,
})))
XLSX.utils.book_append_sheet(workbook, allowanceSheet, '补助明细')
}
// 导出文件
const filename = `${record.employeeName}_工资单_${record.projectPeriod}.xlsx`
XLSX.writeFile(workbook, filename)
}
// 导出实习报酬申领表格式
export const exportInternCompensationForm = (record: SalaryRecord) => {
const workbook = XLSX.utils.book_new()
// 实习生基本信息
const basicInfo = [
['实习生基本信息', '', '', '', ''],
['类别', '填写内容', '', '备注', ''],
['姓名', record.employeeName, '', '', ''],
['身份证号', record.idCard, '', '', ''],
['联系电话', record.phone, '', '', ''],
['银行卡号', record.bankCard, '', '', ''],
['开户银行', record.bankName, '', '', ''],
['', '', '', '', ''],
['实习项目及项目实习相关信息', '', '', '', ''],
['序号', '项目名称', '在项目时间', '出外业施工作业时间(天)', '备注(时间段)'],
['1', record.projectName, record.workDays, record.workDays - record.offshoreDays, record.projectPeriod],
['', '', '', '', ''],
['薪酬、绩效、补助(一)', '', '', '', ''],
['项目类别', '计算标准(元/天)或(元/台)', '计酬天数统计(日)', '应发金额', '备注'],
['基本工资', record.baseSalaryStandard, record.workDays, record.baseSalary, ''],
['施工绩效', 100, record.workDays - record.offshoreDays, record.performanceBonus, ''],
['海上作业绩效补助', 30, record.offshoreDays, record.offshoreDays * 30, ''],
['务餐补助', 45, record.workDays, record.allowances, ''],
['合计应发金额', '', '', record.totalPayable, ''],
]
const sheet = XLSX.utils.aoa_to_sheet(basicInfo)
// 设置列宽
const cols = [
{ wch: 20 },
{ wch: 20 },
{ wch: 15 },
{ wch: 25 },
{ wch: 20 },
]
sheet['!cols'] = cols
// 合并单元格
sheet['!merges'] = [
{ s: { r: 0, c: 0 }, e: { r: 0, c: 4 } },
{ s: { r: 8, c: 0 }, e: { r: 8, c: 4 } },
{ s: { r: 12, c: 0 }, e: { r: 12, c: 4 } },
]
XLSX.utils.book_append_sheet(workbook, sheet, '实习报酬申领表')
// 导出文件
const filename = `${record.employeeName}_实习报酬申领表_${record.projectPeriod}.xlsx`
XLSX.writeFile(workbook, filename)
}
// 辅助函数
function getEmployeeTypeText(type: string) {
const typeMap = {
intern: '实习生',
fulltime: '正式员工',
parttime: '兼职员工',
}
return typeMap[type as keyof typeof typeMap] || type
}
function getStatusText(status: string) {
const statusMap = {
draft: '草稿',
pending: '待审批',
approved: '已批准',
rejected: '已拒绝',
paid: '已发放',
}
return statusMap[status as keyof typeof statusMap] || status
}
function getUnitText(unit: string) {
const unitMap = {
day: '天',
project: '项目',
fixed: '固定',
}
return unitMap[unit as keyof typeof unitMap] || unit
}

View File

@ -9,15 +9,13 @@
<GiTable
v-show="viewType === 'table'"
ref="tableRef"
row-key="deptId"
row-key="id"
: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 }">
@ -111,10 +109,9 @@ const {
} = useTable<DeptResp>(() => getDeptTree(queryForm), {
immediate: true,
onSuccess: () => {
// nextTick(() => {
// tableRef.value?.tableRef?.expandAll(true)
// })
//
nextTick(() => {
tableRef.value?.tableRef?.expandAll(true)
})
},
})
//
@ -127,8 +124,6 @@ const menus = [
]
//
const nodeExpandAll = ref<boolean>(true)
//
const expandedKeys = ref<string[]>([])
//
const searchData = (name: string) => {
const loop = (data: DeptResp[]) => {
@ -209,17 +204,6 @@ 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)
}
}
</script>
<style scoped lang="scss">

View File

@ -1,5 +1,4 @@
<template>
<h1>这个暂时写不了角色管理采用菜单权限的设置</h1>
<GiPageLayout :header-style="{ padding: 0, borderBottom: 'none' }">
<template #left>
<RoleTree @node-click="handleSelectRole" />