This commit is contained in:
Mr.j 2025-08-08 16:37:46 +08:00
commit 37d2d21318
11 changed files with 4053 additions and 200 deletions

View File

@ -0,0 +1,98 @@
import type * as T from './type'
import http from '@/utils/http'
export type * from './type'
const BASE_URL = '/project-member'
// ==================== 项目看板相关接口 ====================
/** @desc 获取项目看板统计数据 */
export function getProjectKanbanStats() {
return http.get<T.ProjectKanbanStats>(`${BASE_URL}/kanban/stats`)
}
/** @desc 获取项目看板数据 */
export function getProjectKanbanData() {
return http.get<T.ProjectKanbanData>(`${BASE_URL}/kanban/data`)
}
/** @desc 获取项目详情 */
export function getProjectDetail(id: string | number) {
return http.get<T.ProjectDetailResp>(`${BASE_URL}/project/${id}/detail`)
}
// ==================== 团队成员管理 ====================
/** @desc 获取项目团队成员列表(支持筛选、分页、搜索) */
export function getProjectTeamMembers(params: T.TeamMemberQuery) {
const { projectId, ...queryParams } = params
return http.get<T.BackendTeamMemberResp[]>(`${BASE_URL}/project/${projectId}/team-members`, { params: queryParams })
}
/** @desc 创建团队成员 */
export function createTeamMember(data: T.CreateTeamMemberForm) {
return http.post<T.TeamMemberResp>(`${BASE_URL}/team-member`, data)
}
/** @desc 更新团队成员信息(支持更新状态) */
export function updateTeamMember(id: string | number, data: T.UpdateTeamMemberForm) {
return http.put<T.TeamMemberResp>(`${BASE_URL}/team-member/${id}`, data)
}
/** @desc 删除团队成员(支持单个或批量删除) */
export function deleteTeamMembers(ids: string | number | (string | number)[]) {
const idArray = Array.isArray(ids) ? ids : [ids]
return http.del(`${BASE_URL}/team-member/batch`, { data: { ids: idArray } })
}
/** @desc 导入团队成员数据 */
export function importTeamMembers(projectId: string | number, file: File) {
const formData = new FormData()
formData.append('file', file)
formData.append('projectId', projectId.toString())
return http.post(`${BASE_URL}/team-member/import`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/** @desc 导出团队成员数据 */
export function exportTeamMembers(params: T.TeamMemberExportQuery) {
return http.get(`${BASE_URL}/team-member/export`, {
params,
responseType: 'blob'
})
}
/** @desc 下载导入模板 */
export function downloadImportTemplate() {
return http.get(`${BASE_URL}/team-member/template`, {
responseType: 'blob'
})
}
// ==================== 项目需求管理 ====================
/** @desc 获取项目需求列表 */
export function getProjectRequirements(projectId: string | number) {
return http.get<T.ProjectRequirementResp[]>(`${BASE_URL}/project/${projectId}/requirements`)
}
/** @desc 发布项目需求 */
export function createProjectRequirement(data: T.CreateProjectRequirementForm) {
return http.post(`${BASE_URL}/requirement`, data)
}
/** @desc 更新项目需求状态 */
export function updateRequirementStatus(data: T.UpdateRequirementStatusForm) {
return http.patch(`${BASE_URL}/requirement/status`, data)
}
/** @desc 删除项目需求 */
export function deleteProjectRequirement(requirementId: string | number) {
return http.del(`${BASE_URL}/requirement/${requirementId}`)
}

View File

@ -86,3 +86,249 @@ export interface TaskQuery {
}
export interface TaskPageQuery extends TaskQuery, PageQuery {}
// ==================== 人员调度相关类型 ====================
/** 项目看板统计数据 */
export interface ProjectKanbanStats {
totalProjectsCount: string
pendingProjectCount: string
inProgressProjectCount: string
completedProjectCount: string
auditedProjectCount: string
acceptedProjectCount: string
totalTurbineCount: string
pendingTurbineCount: string
inProgressTurbineCount: string
completedTurbineCount: string
auditedTurbineCount: string
acceptedTurbineCount: string
totalTaskCount: string
pendingTaskCount: string
inProgressTaskCount: string
completedTaskCount: string
totalMemberCount: string
projectManagerCount: string
safetyOfficerCount: string
qualityOfficerCount: string
constructorCount: string
teamLeaderCount: string
}
/** 项目看板数据 */
export interface ProjectKanbanData {
inProgressProjects: never[]
preparingProjects: ProjectCard[]
ongoingProjects: ProjectCard[]
pendingProjects: ProjectCard[]
}
/** 项目卡片信息 */
export interface ProjectCard {
id: string | number
name: string
status: 'preparing' | 'ongoing' | 'pending'
budget: number
manager: string
teamSize: number
preparationProgress?: number
progress?: number
startDate?: string
endDate?: string
plannedStartDate?: string
alerts?: ProjectAlert[]
teamMembers: TeamMemberResp[]
requirements: ProjectRequirementResp[]
}
/** 项目异常信息 */
export interface ProjectAlert {
type: 'cost' | 'personnel' | 'schedule' | 'quality'
message: string
}
/** 项目详情响应 */
export interface ProjectDetailResp extends ProjectCard {
// 继承ProjectCard的所有字段可以添加更多详情字段
}
/** 团队成员响应 */
export interface TeamMemberResp {
id: string | number
name: string
position: string
phone?: string
email?: string
avatar?: string
joinDate?: string
performance?: number
remark?: string
status?: 'available' | 'busy' | 'offline'
}
/** 后端返回的团队成员数据结构 */
export interface BackendTeamMemberResp {
memberId: string
projectId: string
projectName: string
turbineId: string | null
turbineName: string | null
taskGroupId: string | null
taskGroupName: string | null
taskId: string | null
taskName: string | null
userId: string
name: string
phone: string | null
email: string | null
position: string
status: 'ACTIVE' | 'BUSY' | 'OFFLINE'
skills: string
joinDate: string
remark: string
userAccount: string
userAvatar: string | null
roleType: string
roleTypeDesc: string
jobCode: string
jobCodeDesc: string
jobDesc: string
leaveDate: string | null
statusDesc: string
}
/** 团队成员查询参数(支持筛选、分页、搜索) */
export interface TeamMemberQuery extends PageQuery {
projectId: string | number
name?: string // 姓名搜索
position?: string // 岗位筛选
status?: string // 状态筛选
joinDateStart?: string // 入职日期开始
joinDateEnd?: string // 入职日期结束
sortBy?: 'name' | 'position' | 'joinDate' | 'status' // 排序字段
sortOrder?: 'asc' | 'desc' // 排序方向
}
/** 团队成员导出查询参数(不包含分页) */
export interface TeamMemberExportQuery {
projectId: string | number
name?: string // 姓名搜索
position?: string // 岗位筛选
status?: string // 状态筛选
joinDateStart?: string // 入职日期开始
joinDateEnd?: string // 入职日期结束
sortBy?: 'name' | 'position' | 'joinDate' | 'status' // 排序字段
sortOrder?: 'asc' | 'desc' // 排序方向
}
/** 创建团队成员表单 */
export interface CreateTeamMemberForm {
projectId: string | number
name: string
phone: string
email?: string
position: string
status?: 'available' | 'busy' | 'offline'
joinDate?: string
remark?: string
}
/** 更新团队成员表单 */
export interface UpdateTeamMemberForm {
name?: string
phone?: string
email?: string
position?: string
status?: 'available' | 'busy' | 'offline'
joinDate?: string
remark?: string
}
/** 批量操作表单 */
export interface BatchOperationForm {
ids: (string | number)[]
operation: 'delete' | 'updateStatus'
status?: 'available' | 'busy' | 'offline'
}
/** 导入结果响应 */
export interface ImportResultResp {
success: boolean
totalCount: number
successCount: number
failCount: number
errors?: Array<{
row: number
message: string
}>
}
/** 人员需求响应 */
export interface PersonnelRequirementResp {
id: string | number
title: string
type: 'personnel' | 'equipment'
position?: string
equipmentType?: string
count: number
skills: string[]
description?: string
priority: 'low' | 'medium' | 'high'
status: 'pending' | 'recruiting' | 'completed'
createTime: string
updateTime: string
}
/** 创建需求表单 */
export interface CreateRequirementForm {
projectId: string | number
type: 'personnel' | 'equipment'
position?: string
equipmentType?: string
count: number
skills: string[]
description?: string
priority: 'low' | 'medium' | 'high'
}
/** 简化项目需求响应类型 */
export interface ProjectRequirementResp {
id: string | number
title: string
description: string
priority: 'low' | 'medium' | 'high'
status: 'pending' | 'recruiting' | 'completed'
createTime: string
updateTime: string
}
/** 简化创建项目需求表单类型 */
export interface CreateProjectRequirementForm {
projectId: string | number
description: string
priority: 'low' | 'medium' | 'high'
}
/** 更新需求状态表单 */
export interface UpdateRequirementStatusForm {
requirementId: string | number
status: 'pending' | 'recruiting' | 'completed'
}
/** 分页查询基础参数 */
export interface PageQuery {
page: number
pageSize: number
}
/** 分页响应 */
export interface PageRes<T> {
list: T[]
total: number
page: number
pageSize: number
}

View File

@ -830,6 +830,29 @@ export const systemRoutes: RouteRecordRaw[] = [
},
],
},
//项目中控台
{
path: '/project-management/projects/personnel-dispatch',
name: 'PersonnelDispatch',
component: () => import('@/views/project-management/personnel-dispatch/index.vue'),
meta: {
title: '项目中控台',
icon: 'user-group',
hidden: false,
},
},
{
path: '/project-management/personnel-dispatch/construction-personnel',
name: 'ConstructionPersonnel',
component: () => import('@/views/project-management/personnel-dispatch/construction-personnel.vue'),
meta: {
title: '团队成员管理',
icon: 'user',
hidden: true,
},
},
],
},

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -552,7 +552,7 @@
<script setup>
//
import { ref, reactive, onMounted, computed, watch, nextTick } from 'vue';
import { ref, reactive, onMounted, computed, watch, nextTick, h } from 'vue';
import {
IconFolder,
IconFile,
@ -1358,9 +1358,15 @@ const handlePreview = async (file) => {
// PDF
window.open(url, '_blank');
Message.success('PDF文件已在新窗口打开');
} else if (['txt', 'md', 'json', 'xml', 'csv'].includes(ext)) {
} else if (['txt', 'md', 'json', 'xml', 'csv', 'log'].includes(ext)) {
//
showTextPreview(blob, fileName);
} else if (['mp4', 'avi', 'mov', 'wmv', 'flv'].includes(ext)) {
//
showVideoPreview(url, fileName);
} else if (['mp3', 'wav', 'flac', 'aac'].includes(ext)) {
//
showAudioPreview(url, fileName);
} else if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) {
// Office
Modal.confirm({
@ -1409,47 +1415,267 @@ const handlePreview = async (file) => {
//
const showImagePreview = (url, fileName) => {
const container = document.createElement('div');
container.style.cssText = `
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
`;
const imageScale = ref(1);
const imageRotation = ref(0);
const img = document.createElement('img');
img.src = url;
img.style.cssText = `
max-width: 100%;
max-height: 70vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
`;
const container = h('div', {
style: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '24px',
boxSizing: 'border-box',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '16px',
position: 'relative',
overflow: 'hidden'
}
}, [
//
h('div', {
style: {
position: 'absolute',
top: '-50%',
left: '-50%',
width: '200%',
height: '200%',
background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)',
animation: 'float 6s ease-in-out infinite'
}
}),
const title = document.createElement('div');
title.textContent = fileName;
title.style.cssText = `
margin-top: 16px;
font-size: 14px;
color: #666;
text-align: center;
word-break: break-all;
`;
//
h('div', {
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '16px',
marginBottom: '24px',
padding: '16px 24px',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderRadius: '16px',
boxShadow: '0 8px 32px rgba(0,0,0,0.15)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.2)',
position: 'relative',
zIndex: 10
}
}, [
h('button', {
onClick: () => imageScale.value = Math.max(0.3, imageScale.value - 0.2),
style: {
padding: '10px 16px',
border: 'none',
borderRadius: '12px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'all 0.3s ease',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)'
},
onMouseenter: (e) => {
e.target.style.transform = 'translateY(-2px)';
e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)';
},
onMouseleave: (e) => {
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)';
}
}, [
h('span', { style: { fontSize: '18px' } }, '🔍'),
'缩小'
]),
h('button', {
onClick: () => imageScale.value = Math.min(4, imageScale.value + 0.2),
style: {
padding: '10px 16px',
border: 'none',
borderRadius: '12px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'all 0.3s ease',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)'
},
onMouseenter: (e) => {
e.target.style.transform = 'translateY(-2px)';
e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)';
},
onMouseleave: (e) => {
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)';
}
}, [
h('span', { style: { fontSize: '18px' } }, '🔍'),
'放大'
]),
h('button', {
onClick: () => imageRotation.value += 90,
style: {
padding: '10px 16px',
border: 'none',
borderRadius: '12px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'all 0.3s ease',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)'
},
onMouseenter: (e) => {
e.target.style.transform = 'translateY(-2px)';
e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)';
},
onMouseleave: (e) => {
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)';
}
}, [
h('span', { style: { fontSize: '18px' } }, '🔄'),
'旋转'
]),
h('button', {
onClick: () => {
imageScale.value = 1;
imageRotation.value = 0;
},
style: {
padding: '10px 16px',
border: 'none',
borderRadius: '12px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'all 0.3s ease',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)'
},
onMouseenter: (e) => {
e.target.style.transform = 'translateY(-2px)';
e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)';
},
onMouseleave: (e) => {
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)';
}
}, [
h('span', { style: { fontSize: '18px' } }, '🏠'),
'重置'
])
]),
container.appendChild(img);
container.appendChild(title);
//
h('div', {
style: {
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'auto',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderRadius: '20px',
padding: '32px',
boxShadow: '0 16px 48px rgba(0,0,0,0.2)',
minHeight: '500px',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.3)',
position: 'relative',
zIndex: 5
}
}, [
h('img', {
src: url,
style: {
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
borderRadius: '16px',
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
transform: `scale(${imageScale.value}) rotate(${imageRotation.value}deg)`,
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
border: '2px solid rgba(255,255,255,0.8)'
}
})
]),
//
h('div', {
style: {
marginTop: '20px',
fontSize: '16px',
color: 'white',
textAlign: 'center',
wordBreak: 'break-all',
padding: '12px 24px',
backgroundColor: 'rgba(255, 255, 255, 0.15)',
borderRadius: '12px',
boxShadow: '0 4px 16px rgba(0,0,0,0.1)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.2)',
fontWeight: '500',
position: 'relative',
zIndex: 5
}
}, fileName)
]);
Modal.info({
title: '图片预览',
title: h('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '12px',
fontSize: '20px',
fontWeight: '600',
color: '#2c3e50',
textAlign: 'center',
width: '100%',
justifyContent: 'center'
}
}, [
h('span', { style: { fontSize: '24px' } }, '🖼️'),
'图片预览'
]),
content: container,
width: '80%',
width: '95%',
style: {
maxWidth: '1600px',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
borderRadius: '20px',
overflow: 'hidden',
boxShadow: '0 32px 96px rgba(0,0,0,0.3)',
border: '3px solid rgba(255,255,255,0.2)'
},
mask: true,
maskClosable: true,
footer: null
footer: null,
closable: true,
okText: null,
cancelText: null
});
};
@ -1457,29 +1683,208 @@ const showImagePreview = (url, fileName) => {
const showTextPreview = async (blob, fileName) => {
try {
const text = await blob.text();
const container = document.createElement('div');
container.style.cssText = `
width: 100%;
height: 400px;
border: 1px solid #e5e5e5;
border-radius: 6px;
padding: 16px;
background-color: #fafafa;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
`;
container.textContent = text;
const fontSize = ref(16);
const container = h('div', {
style: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
padding: '24px',
boxSizing: 'border-box',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '16px',
position: 'relative',
overflow: 'hidden'
}
}, [
//
h('div', {
style: {
position: 'absolute',
top: '-50%',
left: '-50%',
width: '200%',
height: '200%',
background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)',
animation: 'float 6s ease-in-out infinite'
}
}),
//
h('div', {
style: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '16px',
marginBottom: '24px',
padding: '16px 24px',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderRadius: '16px',
boxShadow: '0 8px 32px rgba(0,0,0,0.15)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.2)',
position: 'relative',
zIndex: 10
}
}, [
h('button', {
onClick: () => fontSize.value = Math.max(12, fontSize.value - 2),
style: {
padding: '10px 16px',
border: 'none',
borderRadius: '12px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'all 0.3s ease',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)'
},
onMouseenter: (e) => {
e.target.style.transform = 'translateY(-2px)';
e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)';
},
onMouseleave: (e) => {
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)';
}
}, [
h('span', { style: { fontSize: '18px' } }, '🔍'),
'缩小字体'
]),
h('button', {
onClick: () => fontSize.value = Math.min(28, fontSize.value + 2),
style: {
padding: '10px 16px',
border: 'none',
borderRadius: '12px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'all 0.3s ease',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)'
},
onMouseenter: (e) => {
e.target.style.transform = 'translateY(-2px)';
e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)';
},
onMouseleave: (e) => {
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)';
}
}, [
h('span', { style: { fontSize: '18px' } }, '🔍'),
'放大字体'
]),
h('button', {
onClick: () => {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
Message.success('文本已复制到剪贴板');
},
style: {
padding: '10px 16px',
border: 'none',
borderRadius: '12px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'all 0.3s ease',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)'
},
onMouseenter: (e) => {
e.target.style.transform = 'translateY(-2px)';
e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)';
},
onMouseleave: (e) => {
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)';
}
}, [
h('span', { style: { fontSize: '18px' } }, '📋'),
'复制文本'
])
]),
//
h('div', {
style: {
flex: 1,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderRadius: '20px',
padding: '32px',
boxShadow: '0 16px 48px rgba(0,0,0,0.2)',
overflow: 'auto',
fontFamily: "'JetBrains Mono', 'Fira Code', 'Courier New', 'Monaco', 'Menlo', monospace",
fontSize: `${fontSize.value}px`,
lineHeight: '1.8',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: '#2c3e50',
border: '1px solid rgba(255,255,255,0.3)',
backdropFilter: 'blur(10px)',
position: 'relative',
zIndex: 5
}
}, text)
]);
Modal.info({
title: `文本预览 - ${fileName}`,
title: h('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '12px',
fontSize: '20px',
fontWeight: '600',
color: '#2c3e50',
textAlign: 'center',
width: '100%',
justifyContent: 'center'
}
}, [
h('span', { style: { fontSize: '24px' } }, '📄'),
`文本预览 - ${fileName}`
]),
content: container,
width: '70%',
width: '90%',
style: {
maxWidth: '1400px',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
borderRadius: '20px',
overflow: 'hidden',
boxShadow: '0 32px 96px rgba(0,0,0,0.3)',
border: '3px solid rgba(255,255,255,0.2)'
},
mask: true,
maskClosable: true,
footer: null
footer: null,
closable: true,
okText: null,
cancelText: null
});
} catch (error) {
console.error('文本预览失败:', error);
@ -1487,6 +1892,249 @@ const showTextPreview = async (blob, fileName) => {
}
};
//
const showVideoPreview = (url, fileName) => {
const container = h('div', {
style: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '24px',
boxSizing: 'border-box',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '16px',
position: 'relative',
overflow: 'hidden'
}
}, [
//
h('div', {
style: {
position: 'absolute',
top: '-50%',
left: '-50%',
width: '200%',
height: '200%',
background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)',
animation: 'float 6s ease-in-out infinite'
}
}),
//
h('div', {
style: {
width: '100%',
maxWidth: '1000px',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderRadius: '20px',
padding: '32px',
boxShadow: '0 16px 48px rgba(0,0,0,0.2)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.3)',
position: 'relative',
zIndex: 5
}
}, [
h('video', {
src: url,
controls: true,
style: {
width: '100%',
maxHeight: '70vh',
borderRadius: '16px',
background: '#000',
boxShadow: '0 12px 32px rgba(0,0,0,0.3)',
border: '3px solid rgba(255,255,255,0.8)',
transition: 'all 0.3s ease'
}
})
]),
//
h('div', {
style: {
marginTop: '20px',
fontSize: '16px',
color: 'white',
wordBreak: 'break-all',
textAlign: 'center',
padding: '12px 24px',
backgroundColor: 'rgba(255, 255, 255, 0.15)',
borderRadius: '12px',
boxShadow: '0 4px 16px rgba(0,0,0,0.1)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.2)',
fontWeight: '500',
position: 'relative',
zIndex: 5
}
}, fileName)
]);
Modal.info({
title: h('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '12px',
fontSize: '20px',
fontWeight: '600',
color: '#2c3e50',
textAlign: 'center',
width: '100%',
justifyContent: 'center'
}
}, [
h('span', { style: { fontSize: '24px' } }, '🎬'),
'视频预览'
]),
content: container,
width: '90%',
style: {
maxWidth: '1400px',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
borderRadius: '20px',
overflow: 'hidden',
boxShadow: '0 32px 96px rgba(0,0,0,0.3)',
border: '3px solid rgba(255,255,255,0.2)'
},
mask: true,
maskClosable: true,
footer: null,
closable: true,
okText: null,
cancelText: null
});
};
//
const showAudioPreview = (url, fileName) => {
const container = h('div', {
style: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '24px',
boxSizing: 'border-box',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: '16px',
position: 'relative',
overflow: 'hidden'
}
}, [
//
h('div', {
style: {
position: 'absolute',
top: '-50%',
left: '-50%',
width: '200%',
height: '200%',
background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)',
animation: 'float 6s ease-in-out infinite'
}
}),
//
h('div', {
style: {
width: '100%',
maxWidth: '500px',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderRadius: '20px',
padding: '40px',
boxShadow: '0 16px 48px rgba(0,0,0,0.2)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.3)',
position: 'relative',
zIndex: 5,
textAlign: 'center'
}
}, [
//
h('div', {
style: {
fontSize: '64px',
marginBottom: '24px',
animation: 'pulse 2s ease-in-out infinite'
}
}, '🎵'),
//
h('audio', {
src: url,
controls: true,
style: {
width: '100%',
height: '50px',
borderRadius: '12px',
marginBottom: '20px'
}
}),
//
h('div', {
style: {
fontSize: '16px',
color: '#2c3e50',
wordBreak: 'break-all',
textAlign: 'center',
padding: '12px 16px',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
borderRadius: '12px',
fontWeight: '500',
border: '1px solid rgba(102, 126, 234, 0.2)'
}
}, fileName)
])
]);
Modal.info({
title: h('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '12px',
fontSize: '20px',
fontWeight: '600',
color: '#2c3e50',
textAlign: 'center',
width: '100%',
justifyContent: 'center'
}
}, [
h('span', { style: { fontSize: '24px' } }, '🎵'),
'音频预览'
]),
content: container,
width: '70%',
style: {
maxWidth: '800px',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
borderRadius: '20px',
overflow: 'hidden',
boxShadow: '0 32px 96px rgba(0,0,0,0.3)',
border: '3px solid rgba(255,255,255,0.2)'
},
mask: true,
maskClosable: true,
footer: null,
closable: true,
okText: null,
cancelText: null
});
};
//
const handleDownload = async (file) => {
try {
@ -2393,6 +3041,95 @@ onMounted(() => {
to { transform: rotate(360deg); }
}
/* 预览弹窗动画效果 */
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
25% { transform: translateY(-10px) rotate(1deg); }
50% { transform: translateY(-5px) rotate(-1deg); }
75% { transform: translateY(-15px) rotate(0.5deg); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.8; }
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-30px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 预览弹窗样式增强 */
:deep(.arco-modal) {
animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
:deep(.arco-modal-mask) {
animation: fadeIn 0.3s ease;
backdrop-filter: blur(8px);
}
:deep(.arco-modal-content) {
border-radius: 20px !important;
overflow: hidden;
box-shadow: 0 32px 96px rgba(0,0,0,0.3) !important;
border: 3px solid rgba(255,255,255,0.2) !important;
}
:deep(.arco-modal-header) {
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
border-bottom: 1px solid rgba(255,255,255,0.2);
padding: 20px 24px;
}
:deep(.arco-modal-title) {
font-weight: 600;
color: #2c3e50;
}
:deep(.arco-modal-close) {
background: rgba(255,255,255,0.9);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
&:hover {
background: rgba(255,255,255,1);
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
}
/* 按钮悬停效果增强 */
:deep(.arco-btn) {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
}
&:active {
transform: translateY(0);
}
}
/* 骨架屏样式 */
.skeleton-item {
height: 36px;

View File

@ -0,0 +1,155 @@
<template>
<a-spin :loading="loading">
<div v-if="contractDetail">
<a-descriptions
:column="1"
size="medium"
:label-style="{ width: '120px' }"
>
<a-descriptions-item label="合同编号">
{{ contractDetail.code }}
</a-descriptions-item>
<a-descriptions-item label="项目名称">
{{ contractDetail.projectName }}
</a-descriptions-item>
<a-descriptions-item label="客户名称">
{{ contractDetail.customer }}
</a-descriptions-item>
<a-descriptions-item label="合同金额">
<span class="font-medium text-green-600">{{ (contractDetail.amount || 0).toLocaleString() }}</span>
</a-descriptions-item>
<a-descriptions-item label="已收款金额">
<span class="font-medium text-blue-600">{{ (contractDetail.receivedAmount || 0).toLocaleString() }}</span>
</a-descriptions-item>
<a-descriptions-item label="未收款金额">
<span class="font-medium text-orange-600">{{ (contractDetail.pendingAmount || 0).toLocaleString() }}</span>
</a-descriptions-item>
<a-descriptions-item label="签署日期">
{{ contractDetail.signDate }}
</a-descriptions-item>
<a-descriptions-item label="履约期限">
{{ contractDetail.performanceDeadline }}
</a-descriptions-item>
<a-descriptions-item label="付款日期">
{{ contractDetail.paymentDate }}
</a-descriptions-item>
<a-descriptions-item label="合同状态">
<a-tag :color="getStatusColor(contractDetail.contractStatus)">
{{ getStatusText(contractDetail.contractStatusLabel || contractDetail.contractStatus) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="销售人员">
{{ contractDetail.salespersonName }}
</a-descriptions-item>
<a-descriptions-item label="销售部门">
{{ contractDetail.salespersonDeptName }}
</a-descriptions-item>
<a-descriptions-item label="产品服务">
{{ contractDetail.productService }}
</a-descriptions-item>
<a-descriptions-item label="备注">
{{ contractDetail.notes }}
</a-descriptions-item>
</a-descriptions>
</div>
<div v-else-if="!loading" class="empty-container">
<a-empty description="暂无信息" />
</div>
</a-spin>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import http from '@/utils/http'
import { Message } from '@arco-design/web-vue'
interface ContractDetail {
contractId: string
customer: string
code: string
projectId: string
type: string
productService: string
paymentDate: string | null
performanceDeadline: string | null
paymentAddress: string
amount: number
accountNumber: string
notes: string
contractStatus: string
contractText: string | null
projectName: string
salespersonName: string | null
salespersonDeptName: string
settlementAmount: number | null
receivedAmount: number | null
contractStatusLabel: string | null
createBy: string | null
updateBy: string | null
createTime: string
updateTime: string
page: number
pageSize: number
signDate: string
duration: string
pendingAmount?: number
}
const props = defineProps({
contractId: {
type: String,
required: true
}
})
const contractDetail = ref<ContractDetail | null>(null)
const loading = ref(false)
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
未确认: 'gray',
待审批: 'orange',
已签署: 'blue',
执行中: 'cyan',
已完成: 'green',
已终止: 'red'
}
return colorMap[status] || 'gray'
}
const getStatusText = (status: string) => {
return status || '未知状态'
}
const fetchContractDetail = async () => {
try {
loading.value = true
const response = await http.get(`/contract/${props.contractId}`)
if (response.code === 200) {
contractDetail.value = response.data
//
if (contractDetail.value) {
contractDetail.value.pendingAmount = (contractDetail.value.amount || 0) - (contractDetail.value.receivedAmount || 0)
}
} else {
Message.error(response.msg || '获取合同详情失败')
}
} catch (error) {
console.error('获取合同详情失败:', error)
Message.error('获取合同详情失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchContractDetail()
})
</script>
<style scoped>
.empty-container {
text-align: center;
padding: 40px 0;
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<a-form :model="contractData" layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="code" label="合同编号">
<a-input v-model="contractData.code" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="projectName" label="项目名称">
<a-input v-model="contractData.projectName" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="customer" label="客户名称">
<a-input v-model="contractData.customer" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="amount" label="合同金额">
<a-input-number v-model="contractData.amount" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="accountNumber" label="收款账号">
<a-input v-model="contractData.accountNumber" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="contractStatus" label="合同状态">
<a-select v-model="contractData.contractStatus">
<a-option value="未确认">未确认</a-option>
<a-option value="待审批">待审批</a-option>
<a-option value="已签署">已签署</a-option>
<a-option value="执行中">执行中</a-option>
<a-option value="已完成">已完成</a-option>
<a-option value="已终止">已终止</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="signDate" label="签订日期">
<a-date-picker v-model="contractData.signDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="performanceDeadline" label="履约期限">
<a-date-picker v-model="contractData.performanceDeadline" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="paymentDate" label="付款日期">
<a-date-picker v-model="contractData.paymentDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="productService" label="产品或服务">
<a-input v-model="contractData.productService" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="paymentAddress" label="付款地址">
<a-input v-model="contractData.paymentAddress" />
</a-form-item>
<a-form-item field="notes" label="备注">
<a-textarea v-model="contractData.notes" />
</a-form-item>
<a-form-item field="contractText" label="合同内容">
<a-textarea v-model="contractData.contractText" />
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { ContractItem } from './index.vue'
const props = defineProps<{
contractData: ContractItem
}>()
const emit = defineEmits<{
(e: 'update:contractData', data: ContractItem): void
}>()
const contractData = ref({ ...props.contractData })
// props
watch(
() => props.contractData,
(newVal) => {
if (newVal) {
contractData.value = { ...newVal }
}
},
{ immediate: true },
)
//
watch(
contractData,
(newVal) => {
emit('update:contractData', newVal)
},
{ deep: true },
)
</script>

View File

@ -38,48 +38,110 @@
<!-- 合同状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
<a-tag :color="getStatusColor(record.contractStatus)">
{{ getStatusText(record.contractStatusLabel || record.contractStatus) }}
</a-tag>
</template>
<!-- 合同金额 -->
<template #contractAmount="{ record }">
<span class="font-medium text-green-600">{{ record.contractAmount.toLocaleString() }}</span>
<span class="font-medium text-green-600">{{ (record.amount || 0).toLocaleString() }}</span>
</template>
<!-- 已收款金额 -->
<template #receivedAmount="{ record }">
<span class="font-medium text-blue-600">{{ record.receivedAmount.toLocaleString() }}</span>
<span class="font-medium text-blue-600">{{ (record.receivedAmount || 0).toLocaleString() }}</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)" v-if="record.status === 'draft'">编辑</a-link>
<a-link @click="approveContract(record)" v-if="record.status === 'pending'">审批</a-link>
<a-link v-if="record.contractStatus === '未确认'" @click="editRecord(record)">编辑</a-link>
<a-link v-if="record.contractStatus === '待审批'" @click="approveContract(record)">审批</a-link>
<a-link @click="viewPayment(record)">收款记录</a-link>
<a-link v-if="record.contractStatus !== '已签署' && record.contractStatus !== '已完成'" @click="deleteContract(record)">删除</a-link>
</a-space>
</template>
</GiTable>
<!-- 合同详情弹窗 -->
<a-modal
v-model:visible="showDetailModal"
title="合同详情"
:width="800"
:footer="false"
@cancel="closeDetailModal"
>
<ContractDetail v-if="showDetailModal" :contract-id="selectedContractId" />
</a-modal>
<!-- 合同编辑弹窗 -->
<a-modal
v-model:visible="showEditModal"
title="编辑合同"
:width="800"
@cancel="closeEditModal"
@before-ok="handleEditSubmit"
>
<ContractEdit
v-if="showEditModal"
:contract-data="selectedContractData"
@update:contract-data="handleContractDataUpdate"
/>
</a-modal>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { onMounted, reactive, ref } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import ContractEdit from './ContractEdit.vue'
import ContractDetail from './ContractDetail.vue'
import http from '@/utils/http'
//
interface ContractItem {
contractId: string
customer: string
code: string
projectId: string
type: string
productService: string
paymentDate: string | null
performanceDeadline: string | null
paymentAddress: string
amount: number
accountNumber: string
notes: string
contractStatus: string
contractText: string | null
projectName: string
salespersonName: string | null
salespersonDeptName: string
settlementAmount: number | null
receivedAmount: number | null
contractStatusLabel: string | null
createBy: string | null
updateBy: string | null
createTime: string
updateTime: string
page: number
pageSize: number
signDate: string
duration: string
}
//
let searchForm = reactive({
const searchForm = reactive({
contractName: '',
contractCode: '',
client: '',
status: '',
signDate: '',
page: 1,
size: 10
size: 10,
})
//
@ -89,16 +151,16 @@ const queryFormColumns = [
label: '合同名称',
type: 'input' as const,
props: {
placeholder: '请输入合同名称'
}
placeholder: '请输入合同名称',
},
},
{
field: 'client',
label: '客户',
type: 'input' as const,
props: {
placeholder: '请输入客户名称'
}
placeholder: '请输入客户名称',
},
},
{
field: 'status',
@ -107,132 +169,110 @@ const queryFormColumns = [
props: {
placeholder: '请选择合同状态',
options: [
{ label: '草稿', value: 'draft' },
{ label: '待审批', value: 'pending' },
{ label: '已签署', value: 'signed' },
{ label: '执行中', value: 'executing' },
{ label: '已完成', value: 'completed' },
{ label: '已终止', value: 'terminated' }
]
}
}
{ label: '未确认', value: '未确认' },
{ label: '待审批', value: '待审批' },
{ label: '已签署', value: '已签署' },
{ label: '执行中', value: '执行中' },
{ label: '已完成', value: '已完成' },
{ label: '已终止', value: '已终止' },
],
},
},
]
//
const tableColumns: TableColumnData[] = [
{ title: '合同编号', dataIndex: 'contractCode', width: 150 },
{ title: '合同名称', dataIndex: 'contractName', width: 250, ellipsis: true, tooltip: true },
{ title: '客户名称', dataIndex: 'client', width: 200, ellipsis: true, tooltip: true },
{ title: '合同金额', dataIndex: 'contractAmount', slotName: 'contractAmount', width: 120 },
{ title: '合同编号', dataIndex: 'code', width: 150 },
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
{ title: '客户名称', dataIndex: 'customer', width: 200, ellipsis: true, tooltip: true },
{ title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 },
{ title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 },
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 },
{ title: '签署日期', dataIndex: 'signDate', width: 120 },
{ title: '开始日期', dataIndex: 'startDate', width: 120 },
{ title: '结束日期', dataIndex: 'endDate', width: 120 },
{ title: '合同状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '项目经理', dataIndex: 'projectManager', width: 100 },
{ title: '销售经理', dataIndex: 'salesManager', width: 100 },
{ title: '完成进度', dataIndex: 'progress', width: 100 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
{ title: '履约期限', dataIndex: 'performanceDeadline', width: 120 },
{ title: '付款日期', dataIndex: 'paymentDate', width: 120 },
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 },
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
{ title: '产品服务', dataIndex: 'productService', width: 120, ellipsis: true, tooltip: true },
{ title: '备注', dataIndex: 'notes', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' },
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
contractCode: 'RC2024001',
contractName: '华能新能源风电场叶片检测服务合同',
client: '华能新能源股份有限公司',
contractAmount: 320,
receivedAmount: 192,
pendingAmount: 128,
signDate: '2024-02-20',
startDate: '2024-03-01',
endDate: '2024-04-30',
status: 'executing',
projectManager: '张项目经理',
salesManager: '李销售经理',
progress: '60%',
remark: '项目进展顺利,客户满意度高'
},
{
id: 2,
contractCode: 'RC2024002',
contractName: '大唐风电场防雷检测项目合同',
client: '大唐新能源股份有限公司',
contractAmount: 268,
receivedAmount: 134,
pendingAmount: 134,
signDate: '2024-02-25',
startDate: '2024-03-05',
endDate: '2024-04-20',
status: 'executing',
projectManager: '王项目经理',
salesManager: '赵销售经理',
progress: '45%',
remark: '按计划执行中'
},
{
id: 3,
contractCode: 'RC2024003',
contractName: '中广核风电场设备维护服务合同',
client: '中广核新能源投资有限公司',
contractAmount: 450,
receivedAmount: 450,
pendingAmount: 0,
signDate: '2024-01-15',
startDate: '2024-01-20',
endDate: '2024-01-31',
status: 'completed',
projectManager: '刘项目经理',
salesManager: '孙销售经理',
progress: '100%',
remark: '项目已完成,客户验收通过'
const dataList = ref<ContractItem[]>([])
// API
const fetchContractList = async () => {
try {
loading.value = true
const params = {
page: searchForm.page,
pageSize: searchForm.size,
contractName: searchForm.contractName,
code: searchForm.contractCode,
customer: searchForm.client,
contractStatus: searchForm.status,
signDate: searchForm.signDate,
}
])
const response = await http.get('/contract/list', params)
if (response.code === 200) {
// ""
const allContracts = response.rows || []
const revenueContracts = allContracts.filter((item: ContractItem) => item.type === '收入合同')
//
dataList.value = revenueContracts.map((item: ContractItem) => ({
...item,
pendingAmount: (item.amount || 0) - (item.receivedAmount || 0),
}))
pagination.total = Number.parseInt(response.total) || 0
} else {
Message.error(response.msg || '获取合同列表失败')
dataList.value = []
}
} catch (error) {
console.error('获取合同列表失败:', error)
Message.error('获取合同列表失败')
dataList.value = []
} finally {
loading.value = false
}
}
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
total: 0,
showTotal: true,
showPageSize: true
showPageSize: true,
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'draft': 'gray',
'pending': 'orange',
'signed': 'blue',
'executing': 'cyan',
'completed': 'green',
'terminated': 'red'
未确认: 'gray',
待审批: 'orange',
已签署: 'blue',
执行中: 'cyan',
已完成: 'green',
已终止: 'red',
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'draft': '草稿',
'pending': '待审批',
'signed': '已签署',
'executing': '执行中',
'completed': '已完成',
'terminated': '已终止'
}
return textMap[status] || status
// contractStatusLabel使使contractStatus
return status || '未知状态'
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
await fetchContractList()
}
const reset = () => {
@ -243,7 +283,7 @@ const reset = () => {
status: '',
signDate: '',
page: 1,
size: 10
size: 10,
})
pagination.current = 1
search()
@ -273,23 +313,131 @@ const exportContract = () => {
Message.info('导出合同功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看合同详情: ${record.contractName}`)
//
const showEditModal = ref(false)
const selectedContractData = ref<ContractItem | null>(null)
const editedContractData = ref<ContractItem | null>(null)
const editRecord = (record: ContractItem) => {
//
const completeRecord = {
...record,
amount: record.amount || 0,
projectId: record.projectId || '',
type: record.type || '收入合同',
contractStatus: record.contractStatus || '未确认',
}
selectedContractData.value = completeRecord
showEditModal.value = true
}
const editRecord = (record: any) => {
Message.info(`编辑合同: ${record.contractName}`)
const closeEditModal = () => {
showEditModal.value = false
selectedContractData.value = null
editedContractData.value = null
}
const approveContract = (record: any) => {
Message.info(`审批合同: ${record.contractName}`)
const handleContractDataUpdate = (data: ContractItem) => {
editedContractData.value = data
}
const viewPayment = (record: any) => {
Message.info(`查看收款记录: ${record.contractName}`)
const handleEditSubmit = async () => {
if (!editedContractData.value) return false;
try {
const requestData = {
...editedContractData.value,
accountNumber: editedContractData.value.accountNumber || '',
amount: editedContractData.value.amount || 0,
code: editedContractData.value.code || '',
contractId: editedContractData.value.contractId,
contractStatus: editedContractData.value.contractStatus || '',
contractText: editedContractData.value.contractText || '',
customer: editedContractData.value.customer || '',
departmentId: editedContractData.value.departmentId || '',
duration: editedContractData.value.duration || '',
notes: editedContractData.value.notes || '',
paymentAddress: editedContractData.value.paymentAddress || '',
paymentDate: editedContractData.value.paymentDate || null,
performanceDeadline: editedContractData.value.performanceDeadline || null,
productService: editedContractData.value.productService || '',
projectId: editedContractData.value.projectId || '',
salespersonId: editedContractData.value.salespersonId || '',
signDate: editedContractData.value.signDate || null,
type: editedContractData.value.type || '',
};
console.log('Edited Contract Data:', requestData); // 便
// /contract PUT
const response = await http.put('/contract', requestData);
//
if (response.status === 200 && response.code === 200) {
Message.success('合同编辑成功');
closeEditModal();
search(); //
return true;
} else {
Message.error(response.msg || '合同编辑失败');
return false;
}
} catch (error) {
console.error('合同编辑失败:', error);
Message.error('合同编辑失败: ' + (error.message || '请稍后再试'));
return false;
}
}
//
const deleteContract = async (record: ContractItem) => {
try {
await Modal.confirm({
title: '确认删除',
content: `确定要删除合同 "${record.projectName}" 吗?`,
})
const response = await http.delete(`/contract/${record.contractId}`)
if (response.code === 200) {
Message.success('合同删除成功')
search() //
} else {
Message.error(response.msg || '合同删除失败')
}
} catch (error) {
//
if (error !== 'cancel') {
console.error('合同删除失败:', error)
Message.error('合同删除失败')
}
}
}
//
const showDetailModal = ref(false)
const selectedContractId = ref<string | null>(null)
const viewDetail = (record: ContractItem) => {
selectedContractId.value = record.contractId
showDetailModal.value = true
}
const closeDetailModal = () => {
showDetailModal.value = false
selectedContractId.value = null
}
const approveContract = (record: ContractItem) => {
Message.info(`审批合同: ${record.projectName}`)
}
const viewPayment = (record: ContractItem) => {
Message.info(`查看收款记录: ${record.projectName}`)
}
onMounted(() => {
search()
fetchContractList()
})
</script>

View File

@ -0,0 +1,952 @@
<!--
团队成员管理页面
功能特性:
1. 团队成员列表展示
2. 新增团队成员
3. 编辑团队成员信息
4. 删除团队成员
5. 导入导出团队成员数据
-->
<template>
<GiPageLayout>
<!-- 页面头部 -->
<div class="page-header">
<div class="header-left">
<a-button @click="$router.back()" class="back-btn">
<template #icon><icon-left /></template>
返回
</a-button>
<div class="header-title">
<icon-user-group class="title-icon" />
<h1>团队成员管理</h1>
<span v-if="projectId" class="project-info">项目ID: {{ projectId }}</span>
</div>
</div>
<div class="header-actions">
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
新增成员
</a-button>
<a-button @click="openImportModal">
<template #icon><icon-upload /></template>
导入
</a-button>
<a-button @click="exportData">
<template #icon><icon-download /></template>
导出
</a-button>
</div>
</div>
<!-- 简洁搜索区域 -->
<div class="search-section">
<a-card :bordered="false" class="search-card">
<a-form layout="inline" :model="searchForm">
<a-form-item label="姓名">
<a-input
v-model="searchForm.name"
placeholder="请输入姓名"
style="width: 180px"
allow-clear
@press-enter="handleSearch"
/>
</a-form-item>
<a-form-item label="岗位">
<a-select
v-model="searchForm.position"
placeholder="请选择岗位"
style="width: 150px"
allow-clear
>
<a-option
v-for="option in POSITION_OPTIONS"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model="searchForm.status"
placeholder="请选择状态"
style="width: 120px"
allow-clear
>
<a-option
v-for="option in STATUS_OPTIONS"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">
<template #icon><icon-search /></template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon><icon-refresh /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
</div>
<!-- 数据表格 -->
<GiTable
row-key="id"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', minWidth: 1200 }"
:pagination="pagination"
:disabled-tools="['size']"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="loadData"
>
<!-- 状态列 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status || 'offline')">
{{ getStatusText(record.status || 'offline') }}
</a-tag>
</template>
<!-- 备注列 -->
<template #remark="{ record }">
<div class="remark-content" v-if="record.remark">
{{ record.remark }}
</div>
<span v-else class="no-remark">暂无备注</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link title="编辑" @click="openEditModal(record)">编辑</a-link>
<a-link title="状态调整" @click="openStatusModal(record)">状态</a-link>
<a-link status="danger" title="删除" @click="confirmDelete(record)">删除</a-link>
</a-space>
</template>
</GiTable>
<!-- 新增/编辑弹窗 -->
<a-modal
v-model:visible="memberModalVisible"
:title="isEdit ? '编辑团队成员' : '新增团队成员'"
width="600px"
@ok="saveMember"
@cancel="cancelMember"
>
<div class="member-form">
<div class="form-row">
<div class="form-item">
<label>姓名 <span class="required">*</span></label>
<a-input v-model="memberForm.name" placeholder="请输入姓名" />
</div>
<div class="form-item">
<label>联系电话 <span class="required">*</span></label>
<a-input v-model="memberForm.phone" placeholder="请输入联系电话" />
</div>
</div>
<div class="form-row">
<div class="form-item">
<label>岗位 <span class="required">*</span></label>
<a-select v-model="memberForm.position" placeholder="请选择岗位">
<a-option
v-for="option in POSITION_OPTIONS"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
</div>
<div class="form-item">
<label>状态</label>
<a-select v-model="memberForm.status" placeholder="请选择状态">
<a-option
v-for="option in STATUS_OPTIONS"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
</div>
</div>
<div class="form-row">
<div class="form-item">
<label>邮箱</label>
<a-input v-model="memberForm.email" placeholder="请输入邮箱" />
</div>
<div class="form-item">
<label>入职日期</label>
<a-date-picker v-model="memberForm.joinDate" placeholder="请选择入职日期" />
</div>
</div>
<div class="form-item">
<label>备注信息</label>
<a-textarea
v-model="memberForm.remark"
placeholder="请输入备注信息,格式:主要负责:项目经理,次要负责:安全员等"
:rows="3"
/>
</div>
</div>
</a-modal>
<!-- 状态调整弹窗 -->
<a-modal
v-model:visible="statusModalVisible"
title="调整状态"
width="400px"
@ok="saveStatus"
@cancel="cancelStatus"
>
<div class="status-form">
<div class="form-item">
<label>成员姓名:</label>
<span>{{ statusForm.name }}</span>
</div>
<div class="form-item">
<label>当前状态:</label>
<span>{{ getStatusText(statusForm.currentStatus) }}</span>
</div>
<div class="form-item">
<label>新状态:</label>
<a-select v-model="statusForm.newStatus" placeholder="请选择新状态">
<a-option
v-for="option in STATUS_OPTIONS"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
</div>
</div>
</a-modal>
<!-- 导入弹窗 -->
<a-modal
v-model:visible="importModalVisible"
title="导入团队成员"
width="500px"
@ok="confirmImport"
@cancel="cancelImport"
>
<div class="import-form">
<div class="form-item">
<label>选择文件:</label>
<a-upload
v-model:file-list="fileList"
:custom-request="customUpload"
:show-file-list="true"
accept=".xlsx,.xls,.csv"
:limit="1"
>
<a-button>
<template #icon><icon-upload /></template>
选择文件
</a-button>
</a-upload>
</div>
<div class="form-item">
<label>下载模板:</label>
<a-button type="text" @click="downloadTemplate">
<template #icon><icon-download /></template>
下载导入模板
</a-button>
</div>
</div>
</a-modal>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { useRoute } from 'vue-router'
import type { TeamMemberResp, TeamMemberQuery, TeamMemberExportQuery, CreateTeamMemberForm, UpdateTeamMemberForm, BackendTeamMemberResp } from '@/apis/project/type'
import {
getProjectTeamMembers,
createTeamMember,
updateTeamMember,
deleteTeamMembers,
importTeamMembers,
exportTeamMembers,
downloadImportTemplate
} from '@/apis/project/personnel-dispatch'
//
const route = useRoute()
const projectId = route.query.projectId as string
//
const POSITION_OPTIONS = [
{ label: '项目经理', value: '项目经理' },
{ label: '技术负责人', value: '技术负责人' },
{ label: '安全员', value: '安全员' },
{ label: '质量员', value: '质量员' },
{ label: '施工员', value: '施工员' },
{ label: '材料员', value: '材料员' },
{ label: '资料员', value: '资料员' },
{ label: '实习生', value: '实习生' },
{ label: '技术工人', value: '技术工人' },
{ label: '普通工人', value: '普通工人' }
]
const STATUS_OPTIONS = [
{ label: '可用', value: 'available' },
{ label: '忙碌', value: 'busy' },
{ label: '离线', value: 'offline' }
]
//
const loading = ref(false)
const dataList = ref<TeamMemberResp[]>([])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true
})
//
const searchForm = reactive<{
name: string
position: string
status: string
}>({
name: '',
position: '',
status: ''
})
//
const tableColumns = [
{ title: '姓名', dataIndex: 'name', width: 100, fixed: 'left' },
{ title: '联系电话', dataIndex: 'phone', width: 120 },
{ title: '邮箱', dataIndex: 'email', width: 150 },
{ title: '项目岗位', dataIndex: 'position', width: 120 },
{ title: '状态', dataIndex: 'status', width: 100, slotName: 'status' },
{ title: '入职日期', dataIndex: 'joinDate', width: 120 },
{ title: '备注', dataIndex: 'remark', width: 200, slotName: 'remark' },
{ title: '操作', dataIndex: 'action', width: 180, fixed: 'right', slotName: 'action' }
]
//
const memberModalVisible = ref(false)
const statusModalVisible = ref(false)
const importModalVisible = ref(false)
const isEdit = ref(false)
//
const memberForm = reactive<CreateTeamMemberForm & { id?: string | number }>({
projectId: projectId,
name: '',
phone: '',
email: '',
position: '',
status: 'available',
joinDate: '',
remark: ''
})
const statusForm = reactive<{
id: string | number
name: string
currentStatus: string
newStatus: 'available' | 'busy' | 'offline'
}>({
id: '',
name: '',
currentStatus: '',
newStatus: 'available'
})
const fileList = ref<any[]>([])
//
const loadData = async () => {
if (!projectId) {
console.warn('未获取到项目ID无法加载团队成员数据')
Message.warning('未获取到项目信息,请从项目详情页面进入')
return
}
loading.value = true
try {
console.log('正在加载项目团队成员数据项目ID:', projectId)
//
const queryParams: TeamMemberQuery = {
projectId: projectId,
page: pagination.current,
pageSize: pagination.pageSize,
name: searchForm.name || undefined,
position: searchForm.position || undefined,
status: searchForm.status || undefined
}
const response = await getProjectTeamMembers(queryParams)
console.log('API响应数据:', response.data)
// response.data
const rawData = Array.isArray(response.data) ? response.data : [response.data]
console.log('处理后的原始数据:', rawData)
//
const mappedData = rawData.map((item: BackendTeamMemberResp) => {
const mappedItem: TeamMemberResp = {
id: item.memberId,
name: item.name || '',
phone: item.phone || '',
email: item.email || '',
position: item.position || '',
status: (item.status === 'ACTIVE' ? 'available' : item.status === 'BUSY' ? 'busy' : 'offline') as 'available' | 'busy' | 'offline',
joinDate: item.joinDate || '',
remark: item.remark || '',
avatar: item.userAvatar || ''
}
console.log('映射后的数据项:', mappedItem)
return mappedItem
})
dataList.value = mappedData
pagination.total = mappedData.length
console.log('团队成员数据加载完成,显示数据:', dataList.value.length, '条,总计:', pagination.total, '条')
} catch (error) {
console.error('团队成员数据加载失败:', error)
Message.error('团队成员数据加载失败')
} finally {
loading.value = false
}
}
const handleSearch = async () => {
pagination.current = 1
await loadData()
}
const handleReset = () => {
console.log('重置搜索表单')
Object.assign(searchForm, {
name: '',
position: '',
status: ''
})
pagination.current = 1
loadData()
}
const onPageChange = (page: number) => {
pagination.current = page
loadData()
}
const onPageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
loadData()
}
const openAddModal = () => {
isEdit.value = false
resetMemberForm()
memberModalVisible.value = true
}
const openEditModal = (record: TeamMemberResp) => {
isEdit.value = true
Object.assign(memberForm, {
id: record.id,
projectId: projectId,
name: record.name,
phone: record.phone || '',
email: record.email || '',
position: record.position,
status: record.status || 'available',
joinDate: record.joinDate || '',
remark: record.remark || ''
})
memberModalVisible.value = true
}
const saveMember = async () => {
if (!memberForm.name || !memberForm.phone || !memberForm.position) {
Message.error('请填写必填项')
return
}
try {
if (isEdit.value && memberForm.id) {
//
const updateData: UpdateTeamMemberForm = {
name: memberForm.name,
phone: memberForm.phone,
email: memberForm.email,
position: memberForm.position,
status: memberForm.status,
joinDate: memberForm.joinDate,
remark: memberForm.remark
}
await updateTeamMember(memberForm.id, updateData)
Message.success('更新成功')
} else {
//
const createData: CreateTeamMemberForm = {
projectId: projectId,
name: memberForm.name,
phone: memberForm.phone,
email: memberForm.email,
position: memberForm.position,
status: memberForm.status,
joinDate: memberForm.joinDate,
remark: memberForm.remark
}
await createTeamMember(createData)
Message.success('添加成功')
}
memberModalVisible.value = false
loadData()
} catch (error) {
console.error('保存团队成员失败:', error)
Message.error(isEdit.value ? '更新失败' : '添加失败')
}
}
const cancelMember = () => {
memberModalVisible.value = false
resetMemberForm()
}
const resetMemberForm = () => {
Object.assign(memberForm, {
id: undefined,
projectId: projectId,
name: '',
phone: '',
email: '',
position: '',
status: 'available',
joinDate: '',
remark: ''
})
}
const openStatusModal = (record: TeamMemberResp) => {
Object.assign(statusForm, {
id: record.id,
name: record.name,
currentStatus: record.status || 'available',
newStatus: record.status || 'available'
})
statusModalVisible.value = true
}
const saveStatus = async () => {
try {
await updateTeamMember(statusForm.id, {
status: statusForm.newStatus as 'available' | 'busy' | 'offline'
})
Message.success('状态更新成功')
statusModalVisible.value = false
loadData()
} catch (error) {
console.error('状态更新失败:', error)
Message.error('状态更新失败')
}
}
const cancelStatus = () => {
statusModalVisible.value = false
}
const confirmDelete = (record: TeamMemberResp) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除团队成员"${record.name}"吗?`,
onOk: async () => {
try {
await deleteTeamMembers(record.id)
Message.success('删除成功')
loadData()
} catch (error) {
console.error('删除失败:', error)
Message.error('删除失败')
}
}
})
}
const openImportModal = () => {
importModalVisible.value = true
}
const customUpload = (options: any) => {
const { file } = options
fileList.value = [file]
}
const confirmImport = async () => {
if (fileList.value.length === 0) {
Message.error('请选择要导入的文件')
return
}
try {
const file = fileList.value[0].originFileObj
await importTeamMembers(projectId, file)
Message.success('导入成功')
importModalVisible.value = false
fileList.value = []
loadData()
} catch (error) {
console.error('导入失败:', error)
Message.error('导入失败')
}
}
const cancelImport = () => {
importModalVisible.value = false
fileList.value = []
}
const downloadTemplate = async () => {
try {
const response = await downloadImportTemplate()
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '团队成员导入模板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
Message.success('模板下载成功')
} catch (error) {
console.error('模板下载失败:', error)
Message.error('模板下载失败')
}
}
const exportData = async () => {
try {
const queryParams: TeamMemberExportQuery = {
projectId: projectId,
name: searchForm.name || undefined,
position: searchForm.position || undefined,
status: searchForm.status || undefined
}
const response = await exportTeamMembers(queryParams)
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `团队成员数据_${new Date().toISOString().split('T')[0]}.xlsx`
link.click()
window.URL.revokeObjectURL(url)
Message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
Message.error('导出失败')
}
}
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
available: 'green',
busy: 'orange',
offline: 'gray'
}
return colorMap[status] || 'gray'
}
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
available: '可用',
busy: '忙碌',
offline: '离线'
}
return textMap[status] || '未知'
}
//
onMounted(() => {
console.log('团队成员管理页面加载项目ID:', projectId)
if (projectId) {
loadData()
} else {
console.warn('未获取到项目ID无法加载团队成员数据')
Message.warning('未获取到项目信息,请从项目详情页面进入')
}
})
</script>
<style lang="scss" scoped>
//
:deep(.gi-page-layout) {
height: auto !important;
min-height: 100vh;
overflow-y: visible;
}
:deep(.gi-page-layout__body) {
height: auto !important;
overflow-y: visible;
}
//
:deep(body), :deep(html) {
overflow-y: auto !important;
height: auto !important;
}
//
:deep(.app-main), :deep(.main-content), :deep(.layout-content) {
overflow-y: auto !important;
height: auto !important;
}
//
:deep(.arco-table-container) {
overflow-x: auto;
overflow-y: visible;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.header-left {
display: flex;
align-items: center;
gap: 16px;
.back-btn {
border: none;
background: #f7f8fa;
color: #4e5969;
&:hover {
background: #e5e6eb;
color: #1d2129;
}
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
.title-icon {
font-size: 24px;
color: #667eea;
}
h1 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #1d2129;
}
.project-info {
font-size: 14px;
color: #969fa8;
}
}
}
.header-actions {
display: flex;
gap: 12px;
}
}
.search-section {
margin-bottom: 16px;
.search-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.arco-card-body) {
padding: 20px;
}
:deep(.arco-form-item) {
margin-bottom: 0;
margin-right: 16px;
}
:deep(.arco-form-item-label) {
font-weight: 500;
color: #4e5969;
}
:deep(.arco-input),
:deep(.arco-select) {
width: 100%;
}
}
}
//
:deep(.gi-table) {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 24px;
.arco-table-container {
overflow-x: auto;
overflow-y: visible;
}
.arco-table {
overflow: visible;
}
}
.remark-content {
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #4e5969;
}
.no-remark {
color: #c9cdd4;
font-style: italic;
}
.member-form {
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.form-item {
margin-bottom: 16px;
label {
display: block;
font-weight: 500;
color: #4e5969;
margin-bottom: 6px;
.required {
color: #f53f3f;
}
}
.arco-input,
.arco-select,
.arco-textarea,
.arco-date-picker {
width: 100%;
}
}
}
.status-form {
.form-item {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
label {
font-weight: 500;
color: #4e5969;
min-width: 80px;
}
.arco-select {
flex: 1;
}
}
}
.import-form {
.form-item {
margin-bottom: 16px;
label {
display: block;
font-weight: 500;
color: #4e5969;
margin-bottom: 6px;
}
}
}
//
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
.header-actions {
justify-content: center;
}
}
.search-section {
.search-card {
:deep(.arco-form) {
flex-direction: column;
align-items: stretch;
}
:deep(.arco-form-item) {
margin-right: 0;
margin-bottom: 16px;
}
}
}
.member-form {
.form-row {
grid-template-columns: 1fr;
}
}
}
</style>

File diff suppressed because it is too large Load Diff