Merge branch 'devlopment' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web into devlopment
This commit is contained in:
commit
9bfc033b41
|
@ -0,0 +1,486 @@
|
||||||
|
// ==================== 人员组织管理专用类型定义 ====================
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ProjectResp,
|
||||||
|
ConstructorResp,
|
||||||
|
CertificationResp,
|
||||||
|
WorkGroup,
|
||||||
|
WorkTypeGroup,
|
||||||
|
WorkGroupMember
|
||||||
|
} from './type'
|
||||||
|
|
||||||
|
// ==================== 项目相关类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展项目类型,包含工作组信息
|
||||||
|
*/
|
||||||
|
export interface ProjectWithWorkGroups extends ProjectResp {
|
||||||
|
workGroups?: WorkGroup[]
|
||||||
|
// 添加一些前端显示需要的字段
|
||||||
|
statusLabel?: string // 状态标签文本
|
||||||
|
workGroupCount?: number // 工作组数量
|
||||||
|
totalRequiredWorkers?: number // 总需求工人数
|
||||||
|
totalAssignedWorkers?: number // 总已分配工人数
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目选择器查询参数
|
||||||
|
*/
|
||||||
|
export interface ProjectSelectorQuery {
|
||||||
|
status?: number
|
||||||
|
projectName?: string
|
||||||
|
projectCode?: string
|
||||||
|
farmName?: string
|
||||||
|
hasWorkGroups?: boolean // 是否包含工作组
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工作组表单类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作组创建表单
|
||||||
|
*/
|
||||||
|
export interface WorkGroupFormData {
|
||||||
|
id?: string // 工作组ID(编辑时使用)
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
projectId: string
|
||||||
|
workTypeGroups: WorkTypeGroupForm[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工种分组表单
|
||||||
|
*/
|
||||||
|
export interface WorkTypeGroupForm {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
requiredCount: number
|
||||||
|
priority: 'HIGH' | 'MEDIUM' | 'LOW'
|
||||||
|
assignedCount?: number
|
||||||
|
workers?: ConstructorResp[] // 已分配的工人
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作组创建请求
|
||||||
|
*/
|
||||||
|
export interface CreateWorkGroupRequest {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
projectId: string
|
||||||
|
workTypeGroups: Array<{
|
||||||
|
name: string
|
||||||
|
requiredCount: number
|
||||||
|
priority: 'HIGH' | 'MEDIUM' | 'LOW'
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工人筛选和分配类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工人筛选表单
|
||||||
|
*/
|
||||||
|
export interface WorkerFilterForm {
|
||||||
|
workTypes: string[]
|
||||||
|
certificateKeyword: string
|
||||||
|
experienceMin?: number
|
||||||
|
experienceMax?: number
|
||||||
|
status?: string
|
||||||
|
ratingMin?: number
|
||||||
|
ratingMax?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工人分配数据
|
||||||
|
*/
|
||||||
|
export interface WorkerAssignment {
|
||||||
|
workTypeName: string
|
||||||
|
workers: ConstructorResp[]
|
||||||
|
requiredCount: number
|
||||||
|
assignedCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工人分配映射
|
||||||
|
*/
|
||||||
|
export interface WorkerAssignmentMap {
|
||||||
|
[workTypeName: string]: ConstructorResp[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖拽分配事件数据
|
||||||
|
*/
|
||||||
|
export interface DragAssignmentEvent {
|
||||||
|
worker: ConstructorResp
|
||||||
|
fromWorkType?: string
|
||||||
|
toWorkType: string
|
||||||
|
workGroupId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 证书查看类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工人证书查看数据
|
||||||
|
*/
|
||||||
|
export interface WorkerCertificateView {
|
||||||
|
worker: ConstructorResp
|
||||||
|
certificates: CertificationResp[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 证书状态显示
|
||||||
|
*/
|
||||||
|
export interface CertificateStatusDisplay {
|
||||||
|
status: string
|
||||||
|
color: string
|
||||||
|
text: string
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工作组详情类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作组详情显示
|
||||||
|
*/
|
||||||
|
export interface WorkGroupDetailView {
|
||||||
|
workGroup: WorkGroup
|
||||||
|
project: ProjectWithWorkGroups
|
||||||
|
memberDetails: WorkGroupMemberDetail[]
|
||||||
|
statistics: WorkGroupStatistics
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作组成员详情
|
||||||
|
*/
|
||||||
|
export interface WorkGroupMemberDetail extends WorkGroupMember {
|
||||||
|
workTypes: string[]
|
||||||
|
certifications: CertificationResp[]
|
||||||
|
experience: number
|
||||||
|
// rating: number // 已注销
|
||||||
|
joinDate: string
|
||||||
|
lastActiveDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作组统计信息
|
||||||
|
*/
|
||||||
|
export interface WorkGroupStatistics {
|
||||||
|
totalMembers: number
|
||||||
|
totalRequired: number
|
||||||
|
completionRate: number
|
||||||
|
workTypeDistribution: Array<{
|
||||||
|
workTypeName: string
|
||||||
|
requiredCount: number
|
||||||
|
assignedCount: number
|
||||||
|
completionRate: number
|
||||||
|
}>
|
||||||
|
skillCoverage: Array<{
|
||||||
|
skillName: string
|
||||||
|
memberCount: number
|
||||||
|
coverageRate: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工作流步骤类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人员组织工作流步骤
|
||||||
|
*/
|
||||||
|
export interface PersonnelWorkflowStep {
|
||||||
|
step: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
|
status: 'pending' | 'in-progress' | 'completed' | 'error'
|
||||||
|
message?: string
|
||||||
|
canProceed: boolean
|
||||||
|
canGoBack: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流状态
|
||||||
|
*/
|
||||||
|
export interface PersonnelWorkflowStatus {
|
||||||
|
currentStep: number
|
||||||
|
steps: PersonnelWorkflowStep[]
|
||||||
|
projectId: string
|
||||||
|
lastUpdated: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 统计和报表类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人员组织统计概览
|
||||||
|
*/
|
||||||
|
export interface PersonnelOrganizationOverview {
|
||||||
|
totalProjects: number
|
||||||
|
activeProjects: number
|
||||||
|
totalWorkGroups: number
|
||||||
|
totalWorkers: number
|
||||||
|
assignedWorkers: number
|
||||||
|
availableWorkers: number
|
||||||
|
overallCompletionRate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目人员统计
|
||||||
|
*/
|
||||||
|
export interface ProjectPersonnelStats {
|
||||||
|
projectId: string
|
||||||
|
projectName: string
|
||||||
|
workGroupCount: number
|
||||||
|
totalRequiredWorkers: number
|
||||||
|
totalAssignedWorkers: number
|
||||||
|
completionRate: number
|
||||||
|
workTypeStats: Array<{
|
||||||
|
workTypeName: string
|
||||||
|
requiredCount: number
|
||||||
|
assignedCount: number
|
||||||
|
shortageCount: number
|
||||||
|
priority: 'HIGH' | 'MEDIUM' | 'LOW'
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工人技能统计
|
||||||
|
*/
|
||||||
|
export interface WorkerSkillStats {
|
||||||
|
skillName: string
|
||||||
|
totalWorkers: number
|
||||||
|
availableWorkers: number
|
||||||
|
assignedWorkers: number
|
||||||
|
averageExperience: number
|
||||||
|
// averageRating: number // 已注销
|
||||||
|
certificationCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 导入导出类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工人数据导入模板
|
||||||
|
*/
|
||||||
|
export interface WorkerImportTemplate {
|
||||||
|
headers: string[]
|
||||||
|
requiredFields: string[]
|
||||||
|
optionalFields: string[]
|
||||||
|
sampleData: string[][]
|
||||||
|
validationRules: Array<{
|
||||||
|
field: string
|
||||||
|
rule: string
|
||||||
|
message: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入结果详情
|
||||||
|
*/
|
||||||
|
export interface ImportResultDetail {
|
||||||
|
row: number
|
||||||
|
data: Record<string, any>
|
||||||
|
status: 'success' | 'error' | 'warning'
|
||||||
|
message: string
|
||||||
|
field?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入统计
|
||||||
|
*/
|
||||||
|
export interface ImportStatistics {
|
||||||
|
totalRows: number
|
||||||
|
successRows: number
|
||||||
|
errorRows: number
|
||||||
|
warningRows: number
|
||||||
|
skippedRows: number
|
||||||
|
details: ImportResultDetail[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 通知和提醒类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人员分配通知
|
||||||
|
*/
|
||||||
|
export interface PersonnelAssignmentNotification {
|
||||||
|
id: string
|
||||||
|
type: 'email' | 'sms' | 'system' | 'push'
|
||||||
|
title: string
|
||||||
|
message: string
|
||||||
|
recipients: string[]
|
||||||
|
workGroupId: string
|
||||||
|
workGroupName: string
|
||||||
|
projectId: string
|
||||||
|
projectName: string
|
||||||
|
status: 'pending' | 'sent' | 'delivered' | 'failed'
|
||||||
|
sendTime?: string
|
||||||
|
deliveryTime?: string
|
||||||
|
readTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知模板
|
||||||
|
*/
|
||||||
|
export interface NotificationTemplate {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: 'email' | 'sms' | 'system'
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
variables: string[]
|
||||||
|
isDefault: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 权限和角色类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 人员组织操作权限
|
||||||
|
*/
|
||||||
|
export interface PersonnelOrganizationPermissions {
|
||||||
|
canViewProjects: boolean
|
||||||
|
canCreateWorkGroups: boolean
|
||||||
|
canEditWorkGroups: boolean
|
||||||
|
canDeleteWorkGroups: boolean
|
||||||
|
canAssignWorkers: boolean
|
||||||
|
canRemoveWorkers: boolean
|
||||||
|
canViewWorkerDetails: boolean
|
||||||
|
canExportData: boolean
|
||||||
|
canImportData: boolean
|
||||||
|
canSendNotifications: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户角色权限
|
||||||
|
*/
|
||||||
|
export interface UserRolePermissions {
|
||||||
|
roleId: string
|
||||||
|
roleName: string
|
||||||
|
permissions: PersonnelOrganizationPermissions
|
||||||
|
projectScope: 'all' | 'assigned' | 'managed'
|
||||||
|
dataScope: 'all' | 'department' | 'project'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 搜索和查询类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高级搜索条件
|
||||||
|
*/
|
||||||
|
export interface AdvancedSearchCriteria {
|
||||||
|
projectCriteria: {
|
||||||
|
status?: number[]
|
||||||
|
farmName?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
projectManager?: string
|
||||||
|
}
|
||||||
|
workerCriteria: {
|
||||||
|
workTypes?: string[]
|
||||||
|
experienceRange?: [number, number]
|
||||||
|
// ratingRange?: [number, number] // 已注销
|
||||||
|
certificationTypes?: string[]
|
||||||
|
status?: string[]
|
||||||
|
}
|
||||||
|
workGroupCriteria: {
|
||||||
|
status?: string[]
|
||||||
|
memberCountRange?: [number, number]
|
||||||
|
createdDateRange?: [string, string]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索结果
|
||||||
|
*/
|
||||||
|
export interface PersonnelSearchResult {
|
||||||
|
projects: ProjectWithWorkGroups[]
|
||||||
|
workers: ConstructorResp[]
|
||||||
|
workGroups: WorkGroup[]
|
||||||
|
totalCount: number
|
||||||
|
searchTime: string
|
||||||
|
searchCriteria: AdvancedSearchCriteria
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 响应和错误类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API响应包装
|
||||||
|
*/
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
success: boolean
|
||||||
|
data?: T
|
||||||
|
message?: string
|
||||||
|
code?: string | number
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页响应包装
|
||||||
|
*/
|
||||||
|
export interface PaginatedResponse<T = any> {
|
||||||
|
list: T[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalPages: number
|
||||||
|
hasNext: boolean
|
||||||
|
hasPrev: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误响应
|
||||||
|
*/
|
||||||
|
export interface ErrorResponse {
|
||||||
|
success: false
|
||||||
|
error: {
|
||||||
|
code: string | number
|
||||||
|
message: string
|
||||||
|
details?: any
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具类型 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态映射配置
|
||||||
|
*/
|
||||||
|
export interface StatusMapping<T = string> {
|
||||||
|
[key: string]: T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 颜色映射配置
|
||||||
|
*/
|
||||||
|
export interface ColorMapping {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图标映射配置
|
||||||
|
*/
|
||||||
|
export interface IconMapping {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单验证规则
|
||||||
|
*/
|
||||||
|
export interface ValidationRule {
|
||||||
|
required?: boolean
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
pattern?: RegExp
|
||||||
|
message?: string
|
||||||
|
validator?: (value: any) => boolean | string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单字段配置
|
||||||
|
*/
|
||||||
|
export interface FormFieldConfig {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
type: 'input' | 'select' | 'textarea' | 'number' | 'date' | 'checkbox' | 'radio'
|
||||||
|
required?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
options?: Array<{ label: string; value: any }>
|
||||||
|
validation?: ValidationRule[]
|
||||||
|
disabled?: boolean
|
||||||
|
hidden?: boolean
|
||||||
|
}
|
|
@ -1,54 +1,300 @@
|
||||||
import type * as T from './type'
|
import request from '@/utils/http'
|
||||||
import http from '@/utils/http'
|
import type {
|
||||||
|
ProjectWithWorkGroups,
|
||||||
|
ConstructorResp,
|
||||||
|
CertificationResp,
|
||||||
|
WorkGroup,
|
||||||
|
WorkTypeGroup,
|
||||||
|
CreateWorkGroupReq,
|
||||||
|
AssignWorkerReq,
|
||||||
|
RemoveWorkerReq,
|
||||||
|
PageRes,
|
||||||
|
PageQuery
|
||||||
|
} from './type'
|
||||||
|
|
||||||
const BASE_URL = '/project/personnel-organization'
|
// ==================== 项目相关接口 ====================
|
||||||
|
|
||||||
/** @desc 获取项目列表 */
|
/**
|
||||||
export function getProjectList(query?: T.ProjectQuery) {
|
* 获取项目列表(包含工作组信息)
|
||||||
return http.get<PageRes<T.ProjectResp[]>>('/project/list', query)
|
*/
|
||||||
|
export function getProjectsWithWorkGroups(params?: {
|
||||||
|
status?: number
|
||||||
|
projectName?: string
|
||||||
|
projectCode?: string
|
||||||
|
farmName?: string
|
||||||
|
}) {
|
||||||
|
return request.get<ProjectWithWorkGroups[]>('/api/projects/with-work-groups', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 获取项目人员组织信息 */
|
/**
|
||||||
export function getPersonnelOrganization(projectId: string | number) {
|
* 获取项目详情(包含工作组信息)
|
||||||
return http.get<T.PersonnelOrganizationResp>(`${BASE_URL}/${projectId}`)
|
*/
|
||||||
|
export function getProjectWithWorkGroups(projectId: string) {
|
||||||
|
return request.get<ProjectWithWorkGroups>(`/api/projects/${projectId}/with-work-groups`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 获取工种列表 */
|
// ==================== 工作组管理接口 ====================
|
||||||
export function getWorkTypeList() {
|
|
||||||
return http.get<T.WorkTypeResp[]>('/work-type/list')
|
/**
|
||||||
|
* 创建工作组
|
||||||
|
*/
|
||||||
|
export function createWorkGroup(data: CreateWorkGroupReq) {
|
||||||
|
return request.post<WorkGroup>('/api/work-groups', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 获取施工人员列表 */
|
/**
|
||||||
export function getConstructorList(query: T.ConstructorQuery) {
|
* 获取项目的工作组列表
|
||||||
return http.get<PageRes<T.ConstructorResp[]>>('/constructor/list', query)
|
*/
|
||||||
|
export function getWorkGroupsByProject(projectId: string) {
|
||||||
|
return request.get<WorkGroup[]>(`/api/projects/${projectId}/work-groups`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 获取施工人员技能证书 */
|
/**
|
||||||
export function getConstructorCertifications(constructorId: string | number) {
|
* 更新工作组
|
||||||
return http.get<T.CertificationResp[]>(`/constructor/${constructorId}/certifications`)
|
*/
|
||||||
|
export function updateWorkGroup(id: string, data: Partial<CreateWorkGroupReq>) {
|
||||||
|
return request.put<WorkGroup>(`/api/work-groups/${id}`, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 分配施工人员到项目 */
|
/**
|
||||||
export function assignConstructorToProject(data: T.AssignConstructorReq) {
|
* 删除工作组
|
||||||
return http.post(`${BASE_URL}/assign`, data)
|
*/
|
||||||
|
export function deleteWorkGroup(id: string) {
|
||||||
|
return request.del(`/api/work-groups/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 移除项目施工人员 */
|
/**
|
||||||
export function removeConstructorFromProject(data: T.RemoveConstructorReq) {
|
* 获取工作组详情
|
||||||
return http.delete(`${BASE_URL}/remove`, data)
|
*/
|
||||||
|
export function getWorkGroupDetail(id: string) {
|
||||||
|
return request.get<WorkGroup>(`/api/work-groups/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 更新施工人员分配信息 */
|
// ==================== 工人管理接口 ====================
|
||||||
export function updateConstructorAssignment(data: T.UpdateAssignmentReq) {
|
|
||||||
return http.put(`${BASE_URL}/update`, data)
|
/**
|
||||||
|
* 获取工人列表(支持筛选)
|
||||||
|
*/
|
||||||
|
export function getWorkers(params?: {
|
||||||
|
workTypes?: string[]
|
||||||
|
certificateKeyword?: string
|
||||||
|
status?: string
|
||||||
|
experienceMin?: number
|
||||||
|
experienceMax?: number
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
}) {
|
||||||
|
return request.get<PageRes<ConstructorResp>>('/api/workers', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 获取项目人员统计 */
|
/**
|
||||||
export function getPersonnelStats(projectId: string | number) {
|
* 获取所有工人(不分页,用于分配)
|
||||||
return http.get<T.PersonnelStatsResp>(`${BASE_URL}/${projectId}/stats`)
|
*/
|
||||||
|
export function getAllWorkers(params?: {
|
||||||
|
workTypes?: string[]
|
||||||
|
certificateKeyword?: string
|
||||||
|
status?: string
|
||||||
|
experienceMin?: number
|
||||||
|
experienceMax?: number
|
||||||
|
}) {
|
||||||
|
return request.get<ConstructorResp[]>('/api/workers/all', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @desc 导出项目人员组织 */
|
/**
|
||||||
export function exportPersonnelOrganization(projectId: string | number) {
|
* 获取工人详情
|
||||||
return http.download(`${BASE_URL}/${projectId}/export`)
|
*/
|
||||||
|
export function getWorkerDetail(id: string) {
|
||||||
|
return request.get<ConstructorResp>(`/api/workers/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工人证书列表
|
||||||
|
*/
|
||||||
|
export function getWorkerCertifications(workerId: string) {
|
||||||
|
return request.get<CertificationResp[]>(`/api/workers/${workerId}/certifications`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工人分配接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分配工人到工作组
|
||||||
|
*/
|
||||||
|
export function assignWorkerToGroup(data: AssignWorkerReq) {
|
||||||
|
return request.post('/api/work-groups/assign-worker', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从工作组移除工人
|
||||||
|
*/
|
||||||
|
export function removeWorkerFromGroup(data: RemoveWorkerReq) {
|
||||||
|
return request.post('/api/work-groups/remove-worker', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量分配工人到工作组
|
||||||
|
*/
|
||||||
|
export function batchAssignWorkers(data: {
|
||||||
|
workGroupId: string
|
||||||
|
assignments: Array<{
|
||||||
|
workTypeName: string
|
||||||
|
workerIds: string[]
|
||||||
|
}>
|
||||||
|
}) {
|
||||||
|
return request.post('/api/work-groups/batch-assign', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工作组的工人分配情况
|
||||||
|
*/
|
||||||
|
export function getWorkGroupAssignments(workGroupId: string) {
|
||||||
|
return request.get<{
|
||||||
|
workTypeName: string
|
||||||
|
workers: ConstructorResp[]
|
||||||
|
requiredCount: number
|
||||||
|
assignedCount: number
|
||||||
|
}[]>(`/api/work-groups/${workGroupId}/assignments`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 统计和查询接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目人员组织统计
|
||||||
|
*/
|
||||||
|
export function getProjectPersonnelStats(projectId: string) {
|
||||||
|
return request.get<{
|
||||||
|
totalWorkers: number
|
||||||
|
assignedWorkers: number
|
||||||
|
availableWorkers: number
|
||||||
|
workTypeStats: Array<{
|
||||||
|
workTypeName: string
|
||||||
|
requiredCount: number
|
||||||
|
assignedCount: number
|
||||||
|
shortageCount: number
|
||||||
|
}>
|
||||||
|
}>(`/api/projects/${projectId}/personnel-stats`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工种统计信息
|
||||||
|
*/
|
||||||
|
export function getWorkTypeStats() {
|
||||||
|
return request.get<Array<{
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
availableWorkers: number
|
||||||
|
totalWorkers: number
|
||||||
|
}>>('/api/work-types/stats')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索工人(支持姓名、工种、证书等关键词搜索)
|
||||||
|
*/
|
||||||
|
export function searchWorkers(keyword: string, params?: {
|
||||||
|
workTypes?: string[]
|
||||||
|
status?: string
|
||||||
|
limit?: number
|
||||||
|
}) {
|
||||||
|
return request.get<ConstructorResp[]>('/api/workers/search', {
|
||||||
|
keyword,
|
||||||
|
...params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 导入导出接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出项目人员组织数据
|
||||||
|
*/
|
||||||
|
export function exportProjectPersonnel(projectId: string, format: 'excel' | 'pdf' = 'excel') {
|
||||||
|
return request.get(`/api/projects/${projectId}/personnel-export`, {
|
||||||
|
format
|
||||||
|
}, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入工人数据
|
||||||
|
*/
|
||||||
|
export function importWorkers(file: File, projectId?: string) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
if (projectId) {
|
||||||
|
formData.append('projectId', projectId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.post<{
|
||||||
|
success: boolean
|
||||||
|
totalCount: number
|
||||||
|
successCount: number
|
||||||
|
failCount: number
|
||||||
|
errors?: Array<{
|
||||||
|
row: number
|
||||||
|
message: string
|
||||||
|
}>
|
||||||
|
}>('/api/workers/import', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工作流相关接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人员组织工作流状态
|
||||||
|
*/
|
||||||
|
export function getPersonnelWorkflowStatus(projectId: string) {
|
||||||
|
return request.get<{
|
||||||
|
currentStep: number
|
||||||
|
steps: Array<{
|
||||||
|
step: number
|
||||||
|
name: string
|
||||||
|
status: 'pending' | 'in-progress' | 'completed' | 'error'
|
||||||
|
message?: string
|
||||||
|
}>
|
||||||
|
}>(`/api/projects/${projectId}/personnel-workflow`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新工作流步骤状态
|
||||||
|
*/
|
||||||
|
export function updateWorkflowStep(projectId: string, step: number, status: string, message?: string) {
|
||||||
|
return request.patch(`/api/projects/${projectId}/personnel-workflow/step`, {
|
||||||
|
step,
|
||||||
|
status,
|
||||||
|
message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 通知和提醒接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人员分配通知
|
||||||
|
*/
|
||||||
|
export function sendAssignmentNotification(data: {
|
||||||
|
workGroupId: string
|
||||||
|
workerIds: string[]
|
||||||
|
message: string
|
||||||
|
type: 'email' | 'sms' | 'system'
|
||||||
|
}) {
|
||||||
|
return request.post('/api/notifications/assignment', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取人员分配通知历史
|
||||||
|
*/
|
||||||
|
export function getAssignmentNotifications(workGroupId?: string, params?: PageQuery) {
|
||||||
|
return request.get<PageRes<{
|
||||||
|
id: string
|
||||||
|
workGroupId: string
|
||||||
|
workGroupName: string
|
||||||
|
message: string
|
||||||
|
type: string
|
||||||
|
status: 'sent' | 'delivered' | 'failed'
|
||||||
|
sendTime: string
|
||||||
|
}>>('/api/notifications/assignment', {
|
||||||
|
workGroupId,
|
||||||
|
...params
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="notification-center">
|
<div class="notification-center">
|
||||||
<!-- 聊天信息图标 -->
|
<!-- 1. 触发按钮 -->
|
||||||
<div class="notification-trigger">
|
<div class="notification-trigger">
|
||||||
<a-button type="text" class="notification-btn" title="聊天信息">
|
<a-button type="text" class="notification-btn" title="聊天信息" @click="openChat">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconNotification />
|
<IconNotification />
|
||||||
</template>
|
</template>
|
||||||
|
@ -10,664 +10,48 @@
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 消息中心弹窗 -->
|
<!-- 2. 聊天平台弹窗 -->
|
||||||
<a-modal
|
<a-modal v-model:visible="chatVisible" title="聊天平台(注册验证码666666)" width=80% :footer="false" :mask-closable="true"
|
||||||
v-model:visible="modalVisible"
|
:destroy-on-close="false" @close="chatVisible = false">
|
||||||
title="消息中心"
|
<!-- 3. 嵌入 React 聊天平台 -->
|
||||||
width="800px"
|
<iframe ref="chatFrame" src="http://pms.dtyx.net:11001/" frameborder="0"
|
||||||
:footer="false"
|
style="width: 100%; height: 600px; border: none;" allow="camera; microphone"></iframe>
|
||||||
:mask-closable="true"
|
|
||||||
:closable="true"
|
|
||||||
:destroy-on-close="false"
|
|
||||||
:z-index="1000"
|
|
||||||
class="notification-modal"
|
|
||||||
>
|
|
||||||
<!-- 消息中心头部 -->
|
|
||||||
<div class="notification-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<h3>消息中心</h3>
|
|
||||||
<span class="notification-count">{{ unreadCount }} 条未读</span>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<a-button type="text" size="small" @click="markAllAsRead">
|
|
||||||
全部已读
|
|
||||||
</a-button>
|
|
||||||
<a-button type="text" size="small" @click="clearRead">
|
|
||||||
清空已读
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 消息类型标签 -->
|
|
||||||
<div class="notification-tabs">
|
|
||||||
<a-tabs v-model:active-key="activeTab" size="small">
|
|
||||||
<a-tab-pane key="all" title="全部">
|
|
||||||
<template #title>
|
|
||||||
<span>全部 ({{ totalCount }})</span>
|
|
||||||
</template>
|
|
||||||
</a-tab-pane>
|
|
||||||
<a-tab-pane key="pending" title="待审批">
|
|
||||||
<template #title>
|
|
||||||
<span>待审批 ({{ pendingCount }})</span>
|
|
||||||
</template>
|
|
||||||
</a-tab-pane>
|
|
||||||
<a-tab-pane key="equipment" title="设备">
|
|
||||||
<template #title>
|
|
||||||
<span>设备 ({{ equipmentCount }})</span>
|
|
||||||
</template>
|
|
||||||
</a-tab-pane>
|
|
||||||
<a-tab-pane key="urgent" title="紧急">
|
|
||||||
<template #title>
|
|
||||||
<span>紧急 ({{ urgentCount }})</span>
|
|
||||||
</template>
|
|
||||||
</a-tab-pane>
|
|
||||||
</a-tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 消息列表 -->
|
|
||||||
<div class="notification-list">
|
|
||||||
<div v-if="filteredNotifications.length === 0" class="empty-state">
|
|
||||||
<IconInfo style="font-size: 48px; color: #d9d9d9; margin-bottom: 16px;" />
|
|
||||||
<p>暂无消息</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="notification in filteredNotifications"
|
|
||||||
:key="notification.id"
|
|
||||||
class="notification-item"
|
|
||||||
:class="{
|
|
||||||
'unread': !notification.read,
|
|
||||||
'urgent': notification.priority === 'URGENT',
|
|
||||||
'high': notification.priority === 'HIGH'
|
|
||||||
}"
|
|
||||||
@click="handleNotificationClick(notification)"
|
|
||||||
>
|
|
||||||
<!-- 消息图标 -->
|
|
||||||
<div class="notification-icon">
|
|
||||||
<component :is="getNotificationIcon(notification.type)" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 消息内容 -->
|
|
||||||
<div class="notification-content">
|
|
||||||
<div class="notification-title">
|
|
||||||
{{ notification.title }}
|
|
||||||
<a-tag
|
|
||||||
v-if="notification.actionRequired"
|
|
||||||
size="small"
|
|
||||||
color="red"
|
|
||||||
>
|
|
||||||
需操作
|
|
||||||
</a-tag>
|
|
||||||
<a-tag
|
|
||||||
v-if="notification.reminderType"
|
|
||||||
size="small"
|
|
||||||
color="blue"
|
|
||||||
>
|
|
||||||
{{ getReminderTypeText(notification.reminderType) }}
|
|
||||||
</a-tag>
|
|
||||||
</div>
|
|
||||||
<div class="notification-message">{{ notification.content }}</div>
|
|
||||||
<div class="notification-meta">
|
|
||||||
<span class="notification-time">{{ formatTime(notification.createTime) }}</span>
|
|
||||||
<span class="notification-category">{{ notification.category }}</span>
|
|
||||||
<span v-if="notification.source" class="notification-source">{{ notification.source }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 消息操作 -->
|
|
||||||
<div class="notification-actions">
|
|
||||||
<a-button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
@click.stop="toggleReminder(notification)"
|
|
||||||
:title="notification.reminderTime ? '取消提醒' : '设置提醒'"
|
|
||||||
>
|
|
||||||
<IconClockCircle v-if="!notification.reminderTime" />
|
|
||||||
<IconClose v-else />
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
@click.stop="removeNotification(notification.id)"
|
|
||||||
title="删除消息"
|
|
||||||
>
|
|
||||||
<IconDelete />
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 消息底部 -->
|
|
||||||
<div class="notification-footer">
|
|
||||||
<a-button type="text" size="small" @click="viewAllNotifications">
|
|
||||||
查看全部消息
|
|
||||||
</a-button>
|
|
||||||
<a-button type="text" size="small" @click="exportNotifications">
|
|
||||||
导出消息
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</a-modal>
|
|
||||||
|
|
||||||
<!-- 提醒设置弹窗 -->
|
|
||||||
<a-modal
|
|
||||||
v-model:visible="reminderModalVisible"
|
|
||||||
title="设置消息提醒"
|
|
||||||
width="400px"
|
|
||||||
@ok="saveReminder"
|
|
||||||
@cancel="cancelReminder"
|
|
||||||
>
|
|
||||||
<a-form :model="reminderForm" layout="vertical">
|
|
||||||
<a-form-item label="提醒类型">
|
|
||||||
<a-radio-group v-model="reminderForm.type">
|
|
||||||
<a-radio value="IMMEDIATE">立即提醒</a-radio>
|
|
||||||
<a-radio value="DELAYED">延迟提醒</a-radio>
|
|
||||||
<a-radio value="RECURRING">重复提醒</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<a-form-item v-if="reminderForm.type === 'DELAYED'" label="提醒时间">
|
|
||||||
<a-date-picker
|
|
||||||
v-model="reminderForm.time"
|
|
||||||
show-time
|
|
||||||
placeholder="选择提醒时间"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<a-form-item v-if="reminderForm.type === 'RECURRING'" label="重复间隔">
|
|
||||||
<a-input-number
|
|
||||||
v-model="reminderForm.interval"
|
|
||||||
:min="1"
|
|
||||||
:max="1440"
|
|
||||||
placeholder="间隔分钟"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { ref, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { IconNotification } from '@arco-design/web-vue/es/icon'
|
||||||
import {
|
|
||||||
IconNotification,
|
|
||||||
IconInfo,
|
|
||||||
IconClockCircle,
|
|
||||||
IconClose,
|
|
||||||
IconDelete,
|
|
||||||
IconCheckCircle,
|
|
||||||
IconClockCircle as IconPending,
|
|
||||||
IconApps,
|
|
||||||
IconExclamationCircle,
|
|
||||||
IconExclamationCircle as IconWarning,
|
|
||||||
IconSettings
|
|
||||||
} from '@arco-design/web-vue/es/icon'
|
|
||||||
import message from '@arco-design/web-vue/es/message'
|
|
||||||
import notificationService from '@/services/notificationService'
|
|
||||||
import websocketService from '@/services/websocketService'
|
|
||||||
|
|
||||||
defineOptions({ name: 'NotificationCenter' })
|
const chatVisible = ref(false)
|
||||||
|
const chatFrame = ref<HTMLIFrameElement>()
|
||||||
|
const url = ref(import.meta.env.VITE_API_BASE_URL + ":11001")
|
||||||
|
/* 打开聊天窗口 */
|
||||||
|
function openChat() {
|
||||||
|
chatVisible.value = true
|
||||||
|
|
||||||
const router = useRouter()
|
// 如果 React 平台需要登录信息,可在 iframe 加载完成后 postMessage
|
||||||
|
// nextTick(() => {
|
||||||
// 响应式数据
|
// // 确保 iframe 已挂载
|
||||||
const modalVisible = ref(false)
|
// if (chatFrame.value) {
|
||||||
const activeTab = ref('all')
|
// chatFrame.value.onload = () => {
|
||||||
const reminderModalVisible = ref(false)
|
// // 把当前登录用户信息发给 React
|
||||||
const currentNotification = ref<any>(null)
|
// chatFrame.value?.contentWindow?.postMessage(
|
||||||
|
// {
|
||||||
// 提醒表单
|
// type: 'INIT_IM',
|
||||||
const reminderForm = ref({
|
// payload: {
|
||||||
type: 'IMMEDIATE' as 'IMMEDIATE' | 'DELAYED' | 'RECURRING',
|
// userID: 'your-user-id', // 换成真实 ID
|
||||||
time: null as Date | null,
|
// token: 'your-token', // 换成真实 token
|
||||||
interval: 30
|
// },
|
||||||
})
|
// },
|
||||||
|
// '*' // 生产环境可改成 React 平台的 origin
|
||||||
// 计算属性
|
// )
|
||||||
const notifications = computed(() => notificationService.getAllNotifications())
|
// }
|
||||||
const unreadCount = computed(() => {
|
// }
|
||||||
const count = notificationService.unreadCount.value
|
// })
|
||||||
// 确保返回有效的数字,避免NaN
|
|
||||||
if (typeof count === 'number' && !isNaN(count) && isFinite(count)) {
|
|
||||||
return count
|
|
||||||
}
|
}
|
||||||
return 0
|
|
||||||
})
|
|
||||||
const totalCount = computed(() => notifications.value.length)
|
|
||||||
const pendingCount = computed(() => {
|
|
||||||
const count = notificationService.pendingCount.value
|
|
||||||
if (typeof count === 'number' && !isNaN(count) && isFinite(count)) {
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
const equipmentCount = computed(() => {
|
|
||||||
const borrowCount = notificationService.equipmentBorrowCount.value || 0
|
|
||||||
const returnCount = notificationService.equipmentReturnCount.value || 0
|
|
||||||
const maintenanceCount = notificationService.equipmentMaintenanceCount.value || 0
|
|
||||||
const alertCount = notificationService.equipmentAlertCount.value || 0
|
|
||||||
|
|
||||||
return borrowCount + returnCount + maintenanceCount + alertCount
|
|
||||||
})
|
|
||||||
const urgentCount = computed(() => {
|
|
||||||
const count = notificationService.urgentCount.value
|
|
||||||
if (typeof count === 'number' && !isNaN(count) && isFinite(count)) {
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
const hasUrgentNotifications = computed(() => urgentCount.value > 0)
|
|
||||||
|
|
||||||
// 过滤后的消息列表
|
|
||||||
const filteredNotifications = computed(() => {
|
|
||||||
let filtered = notifications.value
|
|
||||||
|
|
||||||
switch (activeTab.value) {
|
|
||||||
case 'pending':
|
|
||||||
filtered = filtered.filter(n => n.type === 'PENDING')
|
|
||||||
break
|
|
||||||
case 'equipment':
|
|
||||||
filtered = filtered.filter(n =>
|
|
||||||
['EQUIPMENT_BORROW', 'EQUIPMENT_RETURN', 'EQUIPMENT_MAINTENANCE', 'EQUIPMENT_ALERT'].includes(n.type)
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case 'urgent':
|
|
||||||
filtered = filtered.filter(n => n.priority === 'URGENT' || n.priority === 'HIGH')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按优先级和时间排序
|
|
||||||
return filtered.sort((a, b) => {
|
|
||||||
const priorityOrder = { 'URGENT': 4, 'HIGH': 3, 'NORMAL': 2, 'LOW': 1 }
|
|
||||||
const aPriority = priorityOrder[a.priority || 'NORMAL'] || 2
|
|
||||||
const bPriority = priorityOrder[b.priority || 'NORMAL'] || 2
|
|
||||||
|
|
||||||
if (aPriority !== bPriority) {
|
|
||||||
return bPriority - aPriority
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
|
||||||
}).slice(0, 20) // 只显示前20条
|
|
||||||
})
|
|
||||||
|
|
||||||
// 方法
|
|
||||||
const toggleDropdown = () => {
|
|
||||||
console.log('打开消息中心弹窗')
|
|
||||||
modalVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const markAllAsRead = () => {
|
|
||||||
notificationService.markAllAsRead()
|
|
||||||
message.success('已标记所有消息为已读')
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearRead = () => {
|
|
||||||
notificationService.clearRead()
|
|
||||||
message.success('已清空已读消息')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNotificationClick = (notification: any) => {
|
|
||||||
console.log('点击消息:', notification)
|
|
||||||
|
|
||||||
// 标记为已读
|
|
||||||
notificationService.markAsRead(notification.id)
|
|
||||||
|
|
||||||
// 构建跳转路径
|
|
||||||
let targetUrl = notification.targetUrl
|
|
||||||
|
|
||||||
// 如果没有targetUrl,根据消息类型和业务信息构建
|
|
||||||
if (!targetUrl) {
|
|
||||||
targetUrl = buildTargetUrl(notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('构建的目标URL:', targetUrl)
|
|
||||||
|
|
||||||
// 如果有目标URL,跳转过去
|
|
||||||
if (targetUrl) {
|
|
||||||
try {
|
|
||||||
router.push(targetUrl)
|
|
||||||
modalVisible.value = false
|
|
||||||
message.success('正在跳转到相关页面...')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('路由跳转失败:', error)
|
|
||||||
message.error('页面跳转失败,请手动导航')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('无法构建跳转路径,消息数据:', notification)
|
|
||||||
message.warning('该消息暂无相关操作页面')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据消息类型构建跳转路径
|
|
||||||
const buildTargetUrl = (notification: any): string | null => {
|
|
||||||
const { type, relatedId, metadata, category } = notification
|
|
||||||
|
|
||||||
console.log('构建跳转路径,消息类型:', type, '相关ID:', relatedId, '元数据:', metadata)
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'PROCUREMENT':
|
|
||||||
case 'PENDING':
|
|
||||||
// 设备采购申请 - 跳转到审批台
|
|
||||||
return '/asset-management/device-management/approval'
|
|
||||||
|
|
||||||
case 'APPROVAL':
|
|
||||||
// 审批相关 - 跳转到审批台
|
|
||||||
return '/asset-management/device-management/approval'
|
|
||||||
|
|
||||||
case 'EQUIPMENT_BORROW':
|
|
||||||
// 设备借用 - 跳转到设备中心
|
|
||||||
return '/asset-management/device-management/device-center'
|
|
||||||
|
|
||||||
case 'EQUIPMENT_RETURN':
|
|
||||||
// 设备归还 - 跳转到设备中心
|
|
||||||
return '/asset-management/device-management/device-center'
|
|
||||||
|
|
||||||
case 'EQUIPMENT_MAINTENANCE':
|
|
||||||
// 设备维护 - 跳转到设备中心
|
|
||||||
return '/asset-management/device-management/device-center'
|
|
||||||
|
|
||||||
case 'EQUIPMENT_ALERT':
|
|
||||||
// 设备告警 - 跳转到设备中心
|
|
||||||
return '/asset-management/device-management/device-center'
|
|
||||||
|
|
||||||
case 'WORKFLOW':
|
|
||||||
// 工作流 - 根据具体类型跳转
|
|
||||||
if (metadata?.workflowType === 'PROJECT') {
|
|
||||||
return '/project-management/project-template/project-aproval'
|
|
||||||
}
|
|
||||||
return '/asset-management/device-management/approval'
|
|
||||||
|
|
||||||
case 'SYSTEM':
|
|
||||||
// 系统消息 - 通常不需要跳转
|
|
||||||
return null
|
|
||||||
|
|
||||||
default:
|
|
||||||
// 默认跳转到审批台
|
|
||||||
return '/asset-management/device-management/approval'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNotificationIcon = (type: string) => {
|
|
||||||
const iconMap: Record<string, any> = {
|
|
||||||
'APPROVAL': IconCheckCircle,
|
|
||||||
'PENDING': IconPending,
|
|
||||||
'PROCUREMENT': IconApps,
|
|
||||||
'EQUIPMENT_BORROW': IconApps,
|
|
||||||
'EQUIPMENT_RETURN': IconApps,
|
|
||||||
'EQUIPMENT_MAINTENANCE': IconSettings,
|
|
||||||
'EQUIPMENT_ALERT': IconWarning,
|
|
||||||
'WORKFLOW': IconSettings,
|
|
||||||
'SYSTEM': IconExclamationCircle
|
|
||||||
}
|
|
||||||
return iconMap[type] || IconNotification
|
|
||||||
}
|
|
||||||
|
|
||||||
const getReminderTypeText = (type: string) => {
|
|
||||||
const typeMap: Record<string, string> = {
|
|
||||||
'IMMEDIATE': '立即',
|
|
||||||
'DELAYED': '延迟',
|
|
||||||
'RECURRING': '重复'
|
|
||||||
}
|
|
||||||
return typeMap[type] || type
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTime = (time: string) => {
|
|
||||||
const date = new Date(time)
|
|
||||||
const now = new Date()
|
|
||||||
const diff = now.getTime() - date.getTime()
|
|
||||||
|
|
||||||
if (diff < 60000) return '刚刚'
|
|
||||||
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
|
|
||||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
|
|
||||||
if (diff < 2592000000) return `${Math.floor(diff / 86400000)}天前`
|
|
||||||
|
|
||||||
return date.toLocaleDateString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleReminder = (notification: any) => {
|
|
||||||
if (notification.reminderTime) {
|
|
||||||
// 取消提醒
|
|
||||||
notificationService.cancelNotificationReminder(notification.id)
|
|
||||||
message.success('已取消提醒')
|
|
||||||
} else {
|
|
||||||
// 设置提醒
|
|
||||||
currentNotification.value = notification
|
|
||||||
reminderModalVisible.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveReminder = () => {
|
|
||||||
if (currentNotification.value) {
|
|
||||||
const reminderTime = reminderForm.value.type === 'DELAYED' && reminderForm.value.time
|
|
||||||
? reminderForm.value.time.toISOString()
|
|
||||||
: new Date().toISOString()
|
|
||||||
|
|
||||||
notificationService.setNotificationReminder(
|
|
||||||
currentNotification.value.id,
|
|
||||||
reminderTime,
|
|
||||||
reminderForm.value.type as any,
|
|
||||||
reminderForm.value.type === 'RECURRING' ? reminderForm.value.interval : undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
message.success('提醒设置成功')
|
|
||||||
reminderModalVisible.value = false
|
|
||||||
resetReminderForm()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelReminder = () => {
|
|
||||||
reminderModalVisible.value = false
|
|
||||||
resetReminderForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetReminderForm = () => {
|
|
||||||
reminderForm.value = {
|
|
||||||
type: 'IMMEDIATE',
|
|
||||||
time: null,
|
|
||||||
interval: 30
|
|
||||||
}
|
|
||||||
currentNotification.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeNotification = (id: string) => {
|
|
||||||
notificationService.removeNotification(id)
|
|
||||||
message.success('消息已删除')
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewAllNotifications = () => {
|
|
||||||
router.push('/notifications')
|
|
||||||
modalVisible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportNotifications = () => {
|
|
||||||
notificationService.exportNotifications()
|
|
||||||
message.success('消息导出成功')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听WebSocket事件
|
|
||||||
const setupWebSocketListeners = () => {
|
|
||||||
console.log('设置WebSocket监听器')
|
|
||||||
|
|
||||||
// 监听新消息
|
|
||||||
websocketService.on('message', (message) => {
|
|
||||||
console.log('收到WebSocket消息:', message)
|
|
||||||
|
|
||||||
// 如果消息包含通知信息,添加到通知服务
|
|
||||||
if (message.data && message.data.notification) {
|
|
||||||
console.log('处理通知消息:', message.data.notification)
|
|
||||||
notificationService.addNotification({
|
|
||||||
type: message.data.notification.type || 'SYSTEM',
|
|
||||||
title: message.data.notification.title || '新通知',
|
|
||||||
content: message.data.notification.content || '',
|
|
||||||
priority: message.data.notification.priority || 'NORMAL',
|
|
||||||
category: message.data.notification.category || '系统',
|
|
||||||
targetUrl: message.data.notification.targetUrl,
|
|
||||||
metadata: message.data.notification.metadata,
|
|
||||||
source: message.data.notification.source || 'WEBSOCKET'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听审批状态变更
|
|
||||||
websocketService.on('approvalStatusChanged', (data) => {
|
|
||||||
console.log('审批状态变更:', data)
|
|
||||||
|
|
||||||
// 添加审批状态变更通知
|
|
||||||
if (data.type === 'SUBMITTED') {
|
|
||||||
notificationService.addNotification({
|
|
||||||
type: 'PENDING',
|
|
||||||
title: '新的审批申请',
|
|
||||||
content: `收到来自 ${data.applicantName || '申请人'} 的${data.businessType || '设备'}申请:${data.equipmentName || '未知设备'}`,
|
|
||||||
targetUrl: '/asset-management/device-management/approval',
|
|
||||||
priority: 'HIGH',
|
|
||||||
category: '审批申请',
|
|
||||||
actionRequired: true,
|
|
||||||
source: 'APPROVAL_SYSTEM',
|
|
||||||
metadata: {
|
|
||||||
approvalId: data.approvalId,
|
|
||||||
equipmentName: data.equipmentName,
|
|
||||||
applicantName: data.applicantName,
|
|
||||||
businessType: data.businessType,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听设备状态变更
|
|
||||||
websocketService.on('equipmentStatusChanged', (data) => {
|
|
||||||
console.log('设备状态变更:', data)
|
|
||||||
|
|
||||||
// 添加设备状态变更通知
|
|
||||||
notificationService.addNotification({
|
|
||||||
type: 'EQUIPMENT_ALERT',
|
|
||||||
title: '设备状态更新',
|
|
||||||
content: `设备"${data.equipmentName || '未知设备'}"状态已更新为:${data.newStatus}`,
|
|
||||||
targetUrl: '/asset-management/device-management/device-center',
|
|
||||||
priority: 'NORMAL',
|
|
||||||
category: '设备状态',
|
|
||||||
source: 'EQUIPMENT_SYSTEM',
|
|
||||||
metadata: {
|
|
||||||
equipmentId: data.equipmentId,
|
|
||||||
equipmentName: data.equipmentName,
|
|
||||||
oldStatus: data.oldStatus,
|
|
||||||
newStatus: data.newStatus,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听采购状态变更
|
|
||||||
websocketService.on('procurementStatusChanged', (data) => {
|
|
||||||
console.log('采购状态变更:', data)
|
|
||||||
|
|
||||||
if (data.type === 'SUBMITTED') {
|
|
||||||
notificationService.addNotification({
|
|
||||||
type: 'PROCUREMENT',
|
|
||||||
title: '新的采购申请',
|
|
||||||
content: `收到来自 ${data.applicantName || '申请人'} 的设备采购申请:${data.equipmentName || '未知设备'}`,
|
|
||||||
targetUrl: '/asset-management/device-management/approval',
|
|
||||||
priority: 'HIGH',
|
|
||||||
category: '设备采购',
|
|
||||||
actionRequired: true,
|
|
||||||
source: 'PROCUREMENT_SYSTEM',
|
|
||||||
metadata: {
|
|
||||||
procurementId: data.procurementId,
|
|
||||||
equipmentName: data.equipmentName,
|
|
||||||
applicantName: data.applicantName,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听新审批申请
|
|
||||||
websocketService.on('newApprovalRequest', (data) => {
|
|
||||||
console.log('新审批申请:', data)
|
|
||||||
|
|
||||||
notificationService.addNotification({
|
|
||||||
type: 'PENDING',
|
|
||||||
title: '新的审批申请',
|
|
||||||
content: `收到来自 ${data.applicantName || '申请人'} 的${data.businessType || '设备'}申请:${data.equipmentName || '未知设备'}`,
|
|
||||||
targetUrl: '/asset-management/device-management/approval',
|
|
||||||
priority: 'HIGH',
|
|
||||||
category: '审批申请',
|
|
||||||
actionRequired: true,
|
|
||||||
source: 'APPROVAL_SYSTEM',
|
|
||||||
metadata: {
|
|
||||||
approvalId: data.approvalId,
|
|
||||||
equipmentName: data.equipmentName,
|
|
||||||
applicantName: data.applicantName,
|
|
||||||
businessType: data.businessType,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听WebSocket连接状态
|
|
||||||
websocketService.on('connected', () => {
|
|
||||||
console.log('WebSocket已连接')
|
|
||||||
})
|
|
||||||
|
|
||||||
websocketService.on('disconnected', (data) => {
|
|
||||||
console.log('WebSocket已断开:', data)
|
|
||||||
})
|
|
||||||
|
|
||||||
websocketService.on('error', (error) => {
|
|
||||||
console.error('WebSocket错误:', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理WebSocket监听器
|
|
||||||
const cleanupWebSocketListeners = () => {
|
|
||||||
console.log('清理WebSocket监听器')
|
|
||||||
|
|
||||||
// 移除所有事件监听器
|
|
||||||
websocketService.off('message', () => {})
|
|
||||||
websocketService.off('approvalStatusChanged', () => {})
|
|
||||||
websocketService.off('equipmentStatusChanged', () => {})
|
|
||||||
websocketService.off('procurementStatusChanged', () => {})
|
|
||||||
websocketService.off('newApprovalRequest', () => {})
|
|
||||||
websocketService.off('connected', () => {})
|
|
||||||
websocketService.off('disconnected', () => {})
|
|
||||||
websocketService.off('error', () => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查提醒
|
|
||||||
let reminderCheckInterval: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
const startReminderCheck = () => {
|
|
||||||
reminderCheckInterval = setInterval(() => {
|
|
||||||
const reminderNotifications = notificationService.getReminderNotifications()
|
|
||||||
if (reminderNotifications.length > 0) {
|
|
||||||
// 显示提醒通知
|
|
||||||
reminderNotifications.forEach(notification => {
|
|
||||||
message.info(`${notification.title}: ${notification.content}`)
|
|
||||||
notificationService.markReminderProcessed(notification.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, 60000) // 每分钟检查一次
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
onMounted(() => {
|
|
||||||
setupWebSocketListeners()
|
|
||||||
startReminderCheck()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
cleanupWebSocketListeners()
|
|
||||||
if (reminderCheckInterval) {
|
|
||||||
clearInterval(reminderCheckInterval)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
|
||||||
defineExpose({
|
|
||||||
toggleDropdown
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
<a-descriptions-item label="合同金额">
|
<a-descriptions-item label="合同金额">
|
||||||
<span class="font-medium text-green-600">¥{{ (contractDetail.amount || 0).toLocaleString() }}</span>
|
<span class="font-medium text-green-600">¥{{ (contractDetail.amount || 0).toLocaleString() }}</span>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="合同类别">
|
||||||
|
{{ contractDetail.category || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
|
||||||
<a-descriptions-item label="已收款金额">
|
<a-descriptions-item label="已收款金额">
|
||||||
<span class="font-medium text-blue-600">¥{{ (contractDetail.receivedAmount || 0).toLocaleString() }}</span>
|
<span class="font-medium text-blue-600">¥{{ (contractDetail.receivedAmount || 0).toLocaleString() }}</span>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
|
@ -74,6 +78,7 @@ interface ContractDetail {
|
||||||
code: string
|
code: string
|
||||||
projectId: string
|
projectId: string
|
||||||
type: string
|
type: string
|
||||||
|
category?: string
|
||||||
productService: string
|
productService: string
|
||||||
paymentDate: string | null
|
paymentDate: string | null
|
||||||
performanceDeadline: string | null
|
performanceDeadline: string | null
|
||||||
|
|
|
@ -46,6 +46,21 @@
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="category" label="合同类别">
|
||||||
|
<a-select v-model="contractData.category" placeholder="请选择合同类别" allow-clear>
|
||||||
|
<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-row :gutter="16">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item field="signDate" label="签订日期">
|
<a-form-item field="signDate" label="签订日期">
|
||||||
|
|
|
@ -135,6 +135,7 @@ interface ContractItem {
|
||||||
code: string
|
code: string
|
||||||
projectId: string
|
projectId: string
|
||||||
type: string
|
type: string
|
||||||
|
category?: string
|
||||||
productService: string
|
productService: string
|
||||||
paymentDate: string | null
|
paymentDate: string | null
|
||||||
performanceDeadline: string | null
|
performanceDeadline: string | null
|
||||||
|
@ -165,6 +166,7 @@ const searchForm = reactive({
|
||||||
contractCode: '',
|
contractCode: '',
|
||||||
client: '',
|
client: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
category: '',
|
||||||
signDateRange: [] as [string, string] | [],
|
signDateRange: [] as [string, string] | [],
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
|
@ -196,6 +198,21 @@ const queryFormColumns = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'category',
|
||||||
|
label: '合同类别',
|
||||||
|
type: 'select' as const,
|
||||||
|
props: {
|
||||||
|
placeholder: '请选择合同类别',
|
||||||
|
options: [
|
||||||
|
{ label: '框架协议', value: '框架协议' },
|
||||||
|
{ label: '单次合同', value: '单次合同' },
|
||||||
|
{ label: '三年长协', value: '三年长协' },
|
||||||
|
{ label: '两年长协', value: '两年长协' },
|
||||||
|
{ label: '派工单', value: '派工单' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'signDateRange',
|
field: 'signDateRange',
|
||||||
label: '签署时间',
|
label: '签署时间',
|
||||||
|
@ -218,6 +235,7 @@ const tableColumns: TableColumnData[] = [
|
||||||
{ title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
|
{ title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
|
||||||
{ title: '履约期限', dataIndex: 'performanceDeadline', slotName: 'performanceDeadline', width: 120 },
|
{ title: '履约期限', dataIndex: 'performanceDeadline', slotName: 'performanceDeadline', width: 120 },
|
||||||
{ title: '付款日期', dataIndex: 'paymentDate', slotName: 'paymentDate', width: 120 },
|
{ title: '付款日期', dataIndex: 'paymentDate', slotName: 'paymentDate', width: 120 },
|
||||||
|
{ title: '合同类别', dataIndex: 'category', width: 120 },
|
||||||
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
|
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
|
||||||
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 },
|
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 },
|
||||||
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
|
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
|
||||||
|
@ -239,6 +257,7 @@ const fetchContractList = async () => {
|
||||||
code: searchForm.contractCode,
|
code: searchForm.contractCode,
|
||||||
customer: searchForm.client,
|
customer: searchForm.client,
|
||||||
contractStatus: searchForm.status,
|
contractStatus: searchForm.status,
|
||||||
|
category: (searchForm as any).category || undefined,
|
||||||
signDateStart: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[0] : undefined,
|
signDateStart: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[0] : undefined,
|
||||||
signDateEnd: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[1] : undefined,
|
signDateEnd: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[1] : undefined,
|
||||||
}
|
}
|
||||||
|
@ -325,6 +344,8 @@ const reset = () => {
|
||||||
status: '',
|
status: '',
|
||||||
signDateRange: [],
|
signDateRange: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
|
category: '',
|
||||||
|
|
||||||
size: 10,
|
size: 10,
|
||||||
})
|
})
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
|
@ -354,6 +375,7 @@ const newContractData = ref<ContractItem>({
|
||||||
code: '',
|
code: '',
|
||||||
projectId: '',
|
projectId: '',
|
||||||
type: '支出合同',
|
type: '支出合同',
|
||||||
|
category: '',
|
||||||
productService: '',
|
productService: '',
|
||||||
paymentDate: null,
|
paymentDate: null,
|
||||||
performanceDeadline: null,
|
performanceDeadline: null,
|
||||||
|
@ -387,6 +409,7 @@ const openAddModal = () => {
|
||||||
code: '',
|
code: '',
|
||||||
projectId: '',
|
projectId: '',
|
||||||
type: '支出合同',
|
type: '支出合同',
|
||||||
|
category: '',
|
||||||
productService: '',
|
productService: '',
|
||||||
paymentDate: null,
|
paymentDate: null,
|
||||||
performanceDeadline: null,
|
performanceDeadline: null,
|
||||||
|
@ -444,6 +467,7 @@ const handleAddSubmit = async () => {
|
||||||
salespersonId: (newContractData.value as any).salespersonId || '',
|
salespersonId: (newContractData.value as any).salespersonId || '',
|
||||||
signDate: newContractData.value.signDate || null,
|
signDate: newContractData.value.signDate || null,
|
||||||
type: newContractData.value.type || '支出合同',
|
type: newContractData.value.type || '支出合同',
|
||||||
|
category: newContractData.value.category || '',
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有合同ID则不传
|
// 如果没有合同ID则不传
|
||||||
|
@ -535,6 +559,7 @@ const handleEditSubmit = async () => {
|
||||||
productService: editedContractData.value.productService || '',
|
productService: editedContractData.value.productService || '',
|
||||||
projectId: editedContractData.value.projectId || '',
|
projectId: editedContractData.value.projectId || '',
|
||||||
salespersonId: editedContractData.value.salespersonId || '',
|
salespersonId: editedContractData.value.salespersonId || '',
|
||||||
|
category: editedContractData.value.category || '',
|
||||||
signDate: editedContractData.value.signDate || null,
|
signDate: editedContractData.value.signDate || null,
|
||||||
type: editedContractData.value.type || '',
|
type: editedContractData.value.type || '',
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
<a-descriptions-item label="合同金额">
|
<a-descriptions-item label="合同金额">
|
||||||
<span class="font-medium text-green-600">¥{{ (contractDetail.amount || 0).toLocaleString() }}</span>
|
<span class="font-medium text-green-600">¥{{ (contractDetail.amount || 0).toLocaleString() }}</span>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="合同类别">
|
||||||
|
{{ contractDetail.category || '-' }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
|
||||||
<a-descriptions-item label="已收款金额">
|
<a-descriptions-item label="已收款金额">
|
||||||
<span class="font-medium text-blue-600">¥{{ (contractDetail.receivedAmount || 0).toLocaleString() }}</span>
|
<span class="font-medium text-blue-600">¥{{ (contractDetail.receivedAmount || 0).toLocaleString() }}</span>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
|
@ -74,6 +78,7 @@ interface ContractDetail {
|
||||||
code: string
|
code: string
|
||||||
projectId: string
|
projectId: string
|
||||||
type: string
|
type: string
|
||||||
|
category?: string
|
||||||
productService: string
|
productService: string
|
||||||
paymentDate: string | null
|
paymentDate: string | null
|
||||||
performanceDeadline: string | null
|
performanceDeadline: string | null
|
||||||
|
|
|
@ -46,6 +46,20 @@
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="category" label="合同类别">
|
||||||
|
<a-select v-model="contractData.category" placeholder="请选择合同类别" allow-clear>
|
||||||
|
<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-row :gutter="16">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item field="signDate" label="签订日期">
|
<a-form-item field="signDate" label="签订日期">
|
||||||
|
|
|
@ -136,6 +136,7 @@ interface ContractItem {
|
||||||
code: string
|
code: string
|
||||||
projectId: string
|
projectId: string
|
||||||
type: string
|
type: string
|
||||||
|
category?: string
|
||||||
productService: string
|
productService: string
|
||||||
paymentDate: string | null
|
paymentDate: string | null
|
||||||
performanceDeadline: string | null
|
performanceDeadline: string | null
|
||||||
|
@ -166,6 +167,7 @@ const searchForm = reactive({
|
||||||
contractCode: '',
|
contractCode: '',
|
||||||
client: '',
|
client: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
category: '',
|
||||||
signDateRange: [] as [string, string] | [],
|
signDateRange: [] as [string, string] | [],
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
|
@ -186,6 +188,17 @@ const queryFormColumns = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{ field: 'category', label: '合同类别', type: 'select' as const, props: {
|
||||||
|
placeholder: '请选择合同类别',
|
||||||
|
options: [
|
||||||
|
{ label: '框架协议', value: '框架协议' },
|
||||||
|
{ label: '单次合同', value: '单次合同' },
|
||||||
|
{ label: '三年长协', value: '三年长协' },
|
||||||
|
{ label: '两年长协', value: '两年长协' },
|
||||||
|
{ label: '派工单', value: '派工单' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
{ field: 'signDateRange', label: '签署时间', type: 'range-picker' as const, props: {
|
{ field: 'signDateRange', label: '签署时间', type: 'range-picker' as const, props: {
|
||||||
placeholder: ['开始日期', '结束日期'], format: 'YYYY-MM-DD',
|
placeholder: ['开始日期', '结束日期'], format: 'YYYY-MM-DD',
|
||||||
}
|
}
|
||||||
|
@ -198,6 +211,7 @@ const tableColumns: TableColumnData[] = [
|
||||||
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
|
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
|
||||||
{ title: '客户名称', dataIndex: 'customer', width: 200, ellipsis: true, tooltip: true },
|
{ title: '客户名称', dataIndex: 'customer', width: 200, ellipsis: true, tooltip: true },
|
||||||
{ title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 },
|
{ title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 },
|
||||||
|
{ title: '合同类别', dataIndex: 'category', width: 120 },
|
||||||
{ title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 },
|
{ title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 },
|
||||||
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 },
|
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 },
|
||||||
{ title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
|
{ title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
|
||||||
|
@ -312,6 +326,9 @@ const fetchContractList = async () => {
|
||||||
pageSize: searchForm.size,
|
pageSize: searchForm.size,
|
||||||
code: searchForm.contractCode,
|
code: searchForm.contractCode,
|
||||||
customer: searchForm.client,
|
customer: searchForm.client,
|
||||||
|
|
||||||
|
category: (searchForm as any).category || undefined,
|
||||||
|
|
||||||
contractStatus: searchForm.status,
|
contractStatus: searchForm.status,
|
||||||
signDateStart: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[0] : undefined,
|
signDateStart: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[0] : undefined,
|
||||||
signDateEnd: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[1] : undefined,
|
signDateEnd: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[1] : undefined,
|
||||||
|
@ -393,6 +410,7 @@ const reset = () => {
|
||||||
contractCode: '',
|
contractCode: '',
|
||||||
client: '',
|
client: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
category: '',
|
||||||
signDateRange: [],
|
signDateRange: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
|
@ -421,6 +439,7 @@ const showAddModal = ref(false)
|
||||||
const newContractData = ref<ContractItem>({
|
const newContractData = ref<ContractItem>({
|
||||||
contractId: '', customer: '', code: '', projectId: '', type: '收入合同',
|
contractId: '', customer: '', code: '', projectId: '', type: '收入合同',
|
||||||
productService: '', paymentDate: null, performanceDeadline: null,
|
productService: '', paymentDate: null, performanceDeadline: null,
|
||||||
|
category: '',
|
||||||
paymentAddress: '', amount: 0, accountNumber: '', notes: '',
|
paymentAddress: '', amount: 0, accountNumber: '', notes: '',
|
||||||
contractStatus: '未执行', contractText: '', projectName: '',
|
contractStatus: '未执行', contractText: '', projectName: '',
|
||||||
salespersonName: null, salespersonDeptName: '', settlementAmount: null,
|
salespersonName: null, salespersonDeptName: '', settlementAmount: null,
|
||||||
|
@ -432,6 +451,7 @@ const openAddModal = () => {
|
||||||
Object.assign(newContractData.value, {
|
Object.assign(newContractData.value, {
|
||||||
contractId: '', customer: '', code: '', projectId: '', type: '收入合同',
|
contractId: '', customer: '', code: '', projectId: '', type: '收入合同',
|
||||||
productService: '', paymentDate: null, performanceDeadline: null,
|
productService: '', paymentDate: null, performanceDeadline: null,
|
||||||
|
category: '',
|
||||||
paymentAddress: '', amount: 0, accountNumber: '', notes: '',
|
paymentAddress: '', amount: 0, accountNumber: '', notes: '',
|
||||||
contractStatus: '未执行', contractText: '', projectName: '',
|
contractStatus: '未执行', contractText: '', projectName: '',
|
||||||
salespersonName: null, salespersonDeptName: '', settlementAmount: null,
|
salespersonName: null, salespersonDeptName: '', settlementAmount: null,
|
||||||
|
@ -464,6 +484,7 @@ const handleAddSubmit = async () => {
|
||||||
// 新建时不传 projectId,而是传项目名称
|
// 新建时不传 projectId,而是传项目名称
|
||||||
projectName: newContractData.value.projectName || '',
|
projectName: newContractData.value.projectName || '',
|
||||||
salespersonId: (newContractData.value as any).salespersonId || '',
|
salespersonId: (newContractData.value as any).salespersonId || '',
|
||||||
|
category: newContractData.value.category || '',
|
||||||
signDate: newContractData.value.signDate || null,
|
signDate: newContractData.value.signDate || null,
|
||||||
type: newContractData.value.type || '收入合同',
|
type: newContractData.value.type || '收入合同',
|
||||||
}
|
}
|
||||||
|
@ -534,6 +555,7 @@ const handleEditSubmit = async () => {
|
||||||
productService: editedContractData.value.productService || '',
|
productService: editedContractData.value.productService || '',
|
||||||
projectId: editedContractData.value.projectId || '',
|
projectId: editedContractData.value.projectId || '',
|
||||||
salespersonId: editedContractData.value.salespersonId || '',
|
salespersonId: editedContractData.value.salespersonId || '',
|
||||||
|
category: editedContractData.value.category || '',
|
||||||
signDate: editedContractData.value.signDate || null,
|
signDate: editedContractData.value.signDate || null,
|
||||||
type: editedContractData.value.type || '',
|
type: editedContractData.value.type || '',
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,280 @@
|
||||||
|
<template>
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<a-divider orientation="left">基本信息</a-divider>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="projectName" label="项目名称" required>
|
||||||
|
<a-input v-model="model.projectName" placeholder="请输入项目名称" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="farmAddress" label="地址">
|
||||||
|
<a-input v-model="model.farmAddress" placeholder="请输入地址" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col>
|
||||||
|
<a-button size="mini" @click="onMapSelect">
|
||||||
|
<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="model.projectManagerId" 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="inspectionUnit" label="业主">
|
||||||
|
<a-input v-model="model.inspectionUnit" placeholder="请输入业主单位" @input="(val:any) => (model.farmName = val)" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="inspectionContact" label="业主单位联系人">
|
||||||
|
<a-input v-model="model.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="model.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="model.client" placeholder="请输入委托单位" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="clientContact" label="委托单位联系人">
|
||||||
|
<a-input v-model="model.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="model.clientPhone" placeholder="请输入联系电话" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="projectOrigin" label="项目来源" :rules="[{ required: true, message: '请输入项目来源' }]">
|
||||||
|
<a-input v-model="model.projectOrigin" placeholder="请输入项目来源" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-divider orientation="left">任务设置</a-divider>
|
||||||
|
<div class="mb-2">
|
||||||
|
<a-button type="dashed" size="small" @click="addTask">
|
||||||
|
<template #icon><icon-plus /></template>
|
||||||
|
新增任务
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="!model.tasks || model.tasks.length === 0" class="text-gray-500 mb-2">暂无任务。</div>
|
||||||
|
<a-space direction="vertical" fill>
|
||||||
|
<a-card v-for="(task, tIndex) in model.tasks" :key="tIndex" size="small">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>任务 {{ tIndex + 1 }}</span>
|
||||||
|
<a-space>
|
||||||
|
<a-button size="mini" @click="addSubtask(tIndex)">新增子任务</a-button>
|
||||||
|
<a-button size="mini" status="danger" @click="removeTask(tIndex)">删除</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-row :gutter="12">
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.taskName`" label="任务名称" required>
|
||||||
|
<a-input v-model="task.taskName" placeholder="请输入任务名称" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.taskCode`" label="任务编号">
|
||||||
|
<a-input v-model="task.taskCode" placeholder="编号" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.mainUserId`" label="负责人">
|
||||||
|
<a-select v-model="task.mainUserId" placeholder="选择负责人" :loading="userLoading">
|
||||||
|
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="4">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.scales`" label="工量">
|
||||||
|
<a-input-number v-model="task.scales" :min="0" :max="9999" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="12">
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.planStartDate`" label="计划开始">
|
||||||
|
<a-date-picker v-model="task.planStartDate" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.planEndDate`" label="计划结束">
|
||||||
|
<a-date-picker v-model="task.planEndDate" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.taskGroupId`" label="任务组">
|
||||||
|
<a-input-number v-model="task.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<div v-if="task.children && task.children.length">
|
||||||
|
<a-divider orientation="left">子任务</a-divider>
|
||||||
|
<a-card v-for="(sub, sIndex) in task.children" :key="sIndex" size="small" class="mb-2">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>子任务 {{ tIndex + 1 }}-{{ sIndex + 1 }}</span>
|
||||||
|
<a-button size="mini" status="danger" @click="removeSubtask(tIndex, sIndex)">删除</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-row :gutter="12">
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskName`" label="任务名称" required>
|
||||||
|
<a-input v-model="sub.taskName" placeholder="请输入任务名称" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskCode`" label="任务编号">
|
||||||
|
<a-input v-model="sub.taskCode" placeholder="编号" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.mainUserId`" label="负责人">
|
||||||
|
<a-select v-model="sub.mainUserId" placeholder="选择负责人" :loading="userLoading">
|
||||||
|
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="4">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.scales`" label="工量">
|
||||||
|
<a-input-number v-model="sub.scales" :min="0" :max="9999" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="12">
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planStartDate`" label="计划开始">
|
||||||
|
<a-date-picker v-model="sub.planStartDate" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planEndDate`" label="计划结束">
|
||||||
|
<a-date-picker v-model="sub.planEndDate" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8">
|
||||||
|
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskGroupId`" label="任务组">
|
||||||
|
<a-input-number v-model="sub.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</a-space>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="status" label="项目状态">
|
||||||
|
<a-select v-model="model.status" placeholder="请选择状态">
|
||||||
|
<a-option v-for="option in statusOptions" :key="option.value" :value="option.value">{{ option.label }}</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="scale" label="项目规模">
|
||||||
|
<a-input-number v-model="model.scale" placeholder="请输入项目规模" :min="0" :max="999" :step="1" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="startDate" label="开始时间">
|
||||||
|
<a-date-picker v-model="model.startDate" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="endDate" label="结束时间">
|
||||||
|
<a-date-picker v-model="model.endDate" style="width: 100%" />
|
||||||
|
</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="model.turbineModel" placeholder="请输入风机型号" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<template v-if="showTurbineGrid">
|
||||||
|
<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="model.turbineList" />
|
||||||
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRefs } from 'vue'
|
||||||
|
import TurbineGrid from '../TurbineGrid.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
model: any
|
||||||
|
userOptions: { label: string; value: string }[]
|
||||||
|
userLoading: boolean
|
||||||
|
statusOptions: { label: string; value: number | string }[]
|
||||||
|
addTask: () => void
|
||||||
|
removeTask: (index: number) => void
|
||||||
|
addSubtask: (parentIndex: number) => void
|
||||||
|
removeSubtask: (parentIndex: number, index: number) => void
|
||||||
|
onMapSelect: () => void
|
||||||
|
showTurbineGrid?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 保持对父级 props 的响应性,避免解构/复制导致的“卡加载/选项不更新”
|
||||||
|
const {
|
||||||
|
model,
|
||||||
|
userOptions,
|
||||||
|
userLoading,
|
||||||
|
statusOptions,
|
||||||
|
addTask,
|
||||||
|
removeTask,
|
||||||
|
addSubtask,
|
||||||
|
removeSubtask,
|
||||||
|
onMapSelect,
|
||||||
|
showTurbineGrid,
|
||||||
|
} = toRefs(props)
|
||||||
|
</script>
|
||||||
|
|
|
@ -72,263 +72,27 @@
|
||||||
</template>
|
</template>
|
||||||
</GiTable>
|
</GiTable>
|
||||||
|
|
||||||
<!-- 新增/编辑项目弹窗 -->
|
<!-- 新增项目弹窗(与编辑分离) -->
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:visible="addModalVisible" :title="modalTitle" :ok-button-props="{ loading: submitLoading }"
|
v-model:visible="addModalVisible" :title="modalTitle" :ok-button-props="{ loading: submitLoading }"
|
||||||
width="800px" modal-class="project-form-modal" @cancel="resetForm" @ok="handleSubmit"
|
width="800px" modal-class="project-form-modal" @cancel="handleCancelAdd" @ok="handleSubmit"
|
||||||
>
|
>
|
||||||
<a-form
|
<a-form
|
||||||
ref="formRef" :model="form" :rules="formRules" layout="vertical"
|
ref="formRef" :model="form" :rules="formRules" layout="vertical"
|
||||||
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"
|
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"
|
||||||
>
|
>
|
||||||
<!-- 基本信息 -->
|
<ProjectFormFields
|
||||||
<a-divider orientation="left">基本信息</a-divider>
|
:model="form"
|
||||||
<a-row :gutter="16">
|
:user-options="userOptions"
|
||||||
<a-col :span="12">
|
:user-loading="userLoading"
|
||||||
<a-form-item field="projectName" label="项目名称" required>
|
:status-options="PROJECT_STATUS_OPTIONS"
|
||||||
<a-input v-model="form.projectName" placeholder="请输入项目名称" />
|
:add-task="addTask"
|
||||||
</a-form-item>
|
:remove-task="removeTask"
|
||||||
</a-col>
|
:add-subtask="addSubtask"
|
||||||
<a-col :span="12">
|
:remove-subtask="removeSubtask"
|
||||||
<a-form-item field="farmAddress" label="地址">
|
:on-map-select="() => { Message.info('待开发') }"
|
||||||
<a-input v-model="form.farmAddress" placeholder="请输入地址" />
|
:show-turbine-grid="true"
|
||||||
</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">
|
|
||||||
<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="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-col :span="12">
|
|
||||||
<a-form-item field="projectOrigin" label="项目来源" :rules="[{ required: true, message: '请输入项目来源' }]">
|
|
||||||
<a-input v-model="form.projectOrigin" placeholder="请输入项目来源" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
<a-divider orientation="left">任务设置</a-divider>
|
|
||||||
<div class="mb-2">
|
|
||||||
<a-button type="dashed" size="small" @click="addTask">
|
|
||||||
<template #icon><icon-plus /></template>
|
|
||||||
新增任务
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="form.tasks.length === 0" class="text-gray-500 mb-2">暂无任务,请点击“新增任务”。</div>
|
|
||||||
<a-space direction="vertical" fill>
|
|
||||||
<a-card v-for="(task, tIndex) in form.tasks" :key="tIndex" size="small">
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span>任务 {{ tIndex + 1 }}</span>
|
|
||||||
<a-space>
|
|
||||||
<a-button size="mini" @click="addSubtask(tIndex)">新增子任务</a-button>
|
|
||||||
<a-button size="mini" status="danger" @click="removeTask(tIndex)">删除</a-button>
|
|
||||||
</a-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<a-row :gutter="12">
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.taskName`" label="任务名称" required>
|
|
||||||
<a-input v-model="task.taskName" placeholder="请输入任务名称" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="6">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.taskCode`" label="任务编号">
|
|
||||||
<a-input v-model="task.taskCode" placeholder="编号" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="6">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.mainUserId`" label="负责人">
|
|
||||||
<a-select v-model="task.mainUserId" placeholder="选择负责人" :loading="userLoading">
|
|
||||||
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="4">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.scales`" label="工量">
|
|
||||||
<a-input-number v-model="task.scales" :min="0" :max="9999" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
<a-row :gutter="12">
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.planStartDate`" label="计划开始">
|
|
||||||
<a-date-picker v-model="task.planStartDate" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.planEndDate`" label="计划结束">
|
|
||||||
<a-date-picker v-model="task.planEndDate" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.taskGroupId`" label="任务组">
|
|
||||||
<a-input-number v-model="task.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<!-- 子任务 -->
|
|
||||||
<div v-if="task.children && task.children.length">
|
|
||||||
<a-divider orientation="left">子任务</a-divider>
|
|
||||||
<a-card
|
|
||||||
v-for="(sub, sIndex) in task.children"
|
|
||||||
:key="sIndex"
|
|
||||||
size="small"
|
|
||||||
class="mb-2"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span>子任务 {{ tIndex + 1 }}-{{ sIndex + 1 }}</span>
|
|
||||||
<a-button size="mini" status="danger" @click="removeSubtask(tIndex, sIndex)">删除</a-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<a-row :gutter="12">
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskName`" label="任务名称" required>
|
|
||||||
<a-input v-model="sub.taskName" placeholder="请输入任务名称" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="6">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskCode`" label="任务编号">
|
|
||||||
<a-input v-model="sub.taskCode" placeholder="编号" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="6">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.mainUserId`" label="负责人">
|
|
||||||
<a-select v-model="sub.mainUserId" placeholder="选择负责人" :loading="userLoading">
|
|
||||||
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="4">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.scales`" label="工量">
|
|
||||||
<a-input-number v-model="sub.scales" :min="0" :max="9999" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
<a-row :gutter="12">
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planStartDate`" label="计划开始">
|
|
||||||
<a-date-picker v-model="sub.planStartDate" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planEndDate`" label="计划结束">
|
|
||||||
<a-date-picker v-model="sub.planEndDate" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskGroupId`" label="任务组">
|
|
||||||
<a-input-number v-model="sub.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-card>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</a-space>
|
|
||||||
<a-row :gutter="16">
|
|
||||||
<a-col :span="12">
|
|
||||||
<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 }}
|
|
||||||
</a-option>
|
|
||||||
</a-select>
|
|
||||||
</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>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<a-row :gutter="16">
|
|
||||||
<a-col :span="12">
|
|
||||||
<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-date-picker v-model="form.endDate" style="width: 100%" />
|
|
||||||
</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-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-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
<a-divider orientation="middle">地图</a-divider>
|
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
@ -339,9 +103,10 @@
|
||||||
:ok-button-props="{ loading: submitLoading }"
|
:ok-button-props="{ loading: submitLoading }"
|
||||||
width="800px"
|
width="800px"
|
||||||
modal-class="project-form-modal"
|
modal-class="project-form-modal"
|
||||||
@cancel="() => { editModalVisible.value = false }"
|
@cancel="handleCancelEdit"
|
||||||
@ok="handleEditSubmit"
|
@ok="handleEditSubmit"
|
||||||
>
|
>
|
||||||
|
<a-spin :loading="editDetailLoading">
|
||||||
<a-form
|
<a-form
|
||||||
ref="editFormRef"
|
ref="editFormRef"
|
||||||
:model="editForm"
|
:model="editForm"
|
||||||
|
@ -349,229 +114,21 @@
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"
|
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"
|
||||||
>
|
>
|
||||||
<a-divider orientation="left">基本信息</a-divider>
|
<ProjectFormFields
|
||||||
<a-row :gutter="16">
|
:model="editForm"
|
||||||
<a-col :span="12">
|
:user-options="userOptions"
|
||||||
<a-form-item field="projectName" label="项目名称" required>
|
:user-loading="userLoading"
|
||||||
<a-input v-model="editForm.projectName" placeholder="请输入项目名称" />
|
:status-options="PROJECT_STATUS_OPTIONS"
|
||||||
</a-form-item>
|
:add-task="addEditTask"
|
||||||
</a-col>
|
:remove-task="removeEditTask"
|
||||||
<a-col :span="12">
|
:add-subtask="addEditSubtask"
|
||||||
<a-form-item field="farmAddress" label="地址">
|
:remove-subtask="removeEditSubtask"
|
||||||
<a-input v-model="editForm.farmAddress" placeholder="请输入地址" />
|
:on-map-select="() => {}"
|
||||||
</a-form-item>
|
:show-turbine-grid="false"
|
||||||
</a-col>
|
/>
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<a-row :gutter="16">
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form-item field="projectManagerId" label="项目经理" required>
|
|
||||||
<a-select v-model="editForm.projectManagerId" 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="inspectionUnit" label="业主">
|
|
||||||
<a-input v-model="editForm.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (editForm.farmName = val)" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form-item field="inspectionContact" label="业主单位联系人">
|
|
||||||
<a-input v-model="editForm.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="editForm.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="editForm.client" placeholder="请输入委托单位" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form-item field="clientContact" label="委托单位联系人">
|
|
||||||
<a-input v-model="editForm.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="editForm.clientPhone" placeholder="请输入联系电话" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<a-row :gutter="16">
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form-item field="projectOrigin" label="项目来源" :rules="[{ required: true, message: '请输入项目来源' }]">
|
|
||||||
<a-input v-model="editForm.projectOrigin" placeholder="请输入项目来源" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<a-divider orientation="left">任务设置</a-divider>
|
|
||||||
<div class="mb-2">
|
|
||||||
<a-button type="dashed" size="small" @click="addEditTask">
|
|
||||||
<template #icon><icon-plus /></template>
|
|
||||||
新增任务
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="editForm.tasks.length === 0" class="text-gray-500 mb-2">暂无任务。</div>
|
|
||||||
<a-space direction="vertical" fill>
|
|
||||||
<a-card v-for="(task, tIndex) in editForm.tasks" :key="tIndex" size="small">
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span>任务 {{ tIndex + 1 }}</span>
|
|
||||||
<a-space>
|
|
||||||
<a-button size="mini" @click="addEditSubtask(tIndex)">新增子任务</a-button>
|
|
||||||
<a-button size="mini" status="danger" @click="removeEditTask(tIndex)">删除</a-button>
|
|
||||||
</a-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<a-row :gutter="12">
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.taskName`" label="任务名称" required>
|
|
||||||
<a-input v-model="task.taskName" placeholder="请输入任务名称" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="6">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.taskCode`" label="任务编号">
|
|
||||||
<a-input v-model="task.taskCode" placeholder="编号" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="6">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.mainUserId`" label="负责人">
|
|
||||||
<a-select v-model="task.mainUserId" placeholder="选择负责人" :loading="userLoading">
|
|
||||||
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="4">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.scales`" label="工量">
|
|
||||||
<a-input-number v-model="task.scales" :min="0" :max="9999" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
<a-row :gutter="12">
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.planStartDate`" label="计划开始">
|
|
||||||
<a-date-picker v-model="task.planStartDate" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.planEndDate`" label="计划结束">
|
|
||||||
<a-date-picker v-model="task.planEndDate" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.taskGroupId`" label="任务组">
|
|
||||||
<a-input-number v-model="task.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<!-- 子任务 -->
|
|
||||||
<div v-if="task.children && task.children.length">
|
|
||||||
<a-divider orientation="left">子任务</a-divider>
|
|
||||||
<a-card v-for="(sub, sIndex) in task.children" :key="sIndex" size="small" class="mb-2">
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span>子任务 {{ tIndex + 1 }}-{{ sIndex + 1 }}</span>
|
|
||||||
<a-button size="mini" status="danger" @click="removeEditSubtask(tIndex, sIndex)">删除</a-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<a-row :gutter="12">
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskName`" label="任务名称" required>
|
|
||||||
<a-input v-model="sub.taskName" placeholder="请输入任务名称" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="6">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskCode`" label="任务编号">
|
|
||||||
<a-input v-model="sub.taskCode" placeholder="编号" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="6">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.mainUserId`" label="负责人">
|
|
||||||
<a-select v-model="sub.mainUserId" placeholder="选择负责人" :loading="userLoading">
|
|
||||||
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="4">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.scales`" label="工量">
|
|
||||||
<a-input-number v-model="sub.scales" :min="0" :max="9999" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
<a-row :gutter="12">
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planStartDate`" label="计划开始">
|
|
||||||
<a-date-picker v-model="sub.planStartDate" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planEndDate`" label="计划结束">
|
|
||||||
<a-date-picker v-model="sub.planEndDate" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="8">
|
|
||||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskGroupId`" label="任务组">
|
|
||||||
<a-input-number v-model="sub.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-card>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</a-space>
|
|
||||||
|
|
||||||
<a-row :gutter="16">
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form-item field="status" label="项目状态">
|
|
||||||
<a-select v-model="editForm.status" placeholder="请选择状态">
|
|
||||||
<a-option v-for="option in PROJECT_STATUS_OPTIONS" :key="option.value" :value="option.value">
|
|
||||||
{{ option.label }}
|
|
||||||
</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form-item field="scale" label="项目规模">
|
|
||||||
<a-input-number v-model="editForm.scale" placeholder="请输入项目规模" :min="0" :max="999" :step="1" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<a-row :gutter="16">
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form-item field="startDate" label="开始时间">
|
|
||||||
<a-date-picker v-model="editForm.startDate" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form-item field="endDate" label="结束时间">
|
|
||||||
<a-date-picker v-model="editForm.endDate" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-form>
|
</a-form>
|
||||||
|
</a-spin>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
|
||||||
|
@ -651,8 +208,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ProjectFormFields from './components/ProjectFormFields.vue'
|
||||||
|
|
||||||
const editModalVisible = ref(false)
|
const editModalVisible = ref(false)
|
||||||
const editFormRef = ref()
|
const editFormRef = ref()
|
||||||
|
const editDetailLoading = ref(false)
|
||||||
|
|
||||||
// 编辑表单
|
// 编辑表单
|
||||||
const editForm = reactive({
|
const editForm = reactive({
|
||||||
|
@ -702,7 +262,6 @@ import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Message, Modal } from '@arco-design/web-vue'
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
import type { TableColumnData } from '@arco-design/web-vue'
|
import type { TableColumnData } from '@arco-design/web-vue'
|
||||||
import TurbineGrid from './TurbineGrid.vue'
|
|
||||||
import { addProject, deleteProject, exportProject, importProject, listProject, updateProject, getProjectDetail } from '@/apis/project'
|
import { addProject, deleteProject, exportProject, importProject, listProject, updateProject, getProjectDetail } from '@/apis/project'
|
||||||
import { isMobile } from '@/utils'
|
import { isMobile } from '@/utils'
|
||||||
import http from '@/utils/http'
|
import http from '@/utils/http'
|
||||||
|
@ -1046,6 +605,16 @@ const fetchData = async () => {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Message.error(res.msg || '获取数据失败')
|
Message.error(res.msg || '获取数据失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取项目列表失败:', error)
|
||||||
|
Message.error('获取数据失败')
|
||||||
|
dataList.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleEditSubmit = async () => {
|
const handleEditSubmit = async () => {
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
|
@ -1134,18 +703,6 @@ const handleEditSubmit = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dataList.value = []
|
|
||||||
pagination.total = 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取项目列表失败:', error)
|
|
||||||
Message.error('获取数据失败')
|
|
||||||
dataList.value = []
|
|
||||||
pagination.total = 0
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = () => {
|
const search = () => {
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
|
@ -1210,7 +767,29 @@ const resetForm = () => {
|
||||||
|
|
||||||
const openAddModal = () => {
|
const openAddModal = () => {
|
||||||
resetForm()
|
resetForm()
|
||||||
editModalVisible.value = true
|
addModalVisible.value = true
|
||||||
|
}
|
||||||
|
// 编辑任务增删改
|
||||||
|
const addEditTask = () => {
|
||||||
|
;(editForm.tasks as any[]).push({ taskName: '', taskCode: '', mainUserId: undefined, planStartDate: '', planEndDate: '', scales: undefined, taskGroupId: undefined, children: [] })
|
||||||
|
}
|
||||||
|
const removeEditTask = (index: number) => {
|
||||||
|
;(editForm.tasks as any[]).splice(index, 1)
|
||||||
|
}
|
||||||
|
const addEditSubtask = (parentIndex: number) => {
|
||||||
|
const list = (editForm.tasks as any[])
|
||||||
|
if (!list[parentIndex].children) list[parentIndex].children = []
|
||||||
|
list[parentIndex].children!.push({ taskName: '', taskCode: '', mainUserId: undefined, planStartDate: '', planEndDate: '', scales: undefined, taskGroupId: undefined })
|
||||||
|
}
|
||||||
|
const removeEditSubtask = (parentIndex: number, index: number) => {
|
||||||
|
const list = (editForm.tasks as any[])
|
||||||
|
list[parentIndex].children!.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleCancelAdd = () => {
|
||||||
|
addModalVisible.value = false
|
||||||
|
resetForm()
|
||||||
}
|
}
|
||||||
// 任务增删改(仅在新增/编辑弹窗内部使用)
|
// 任务增删改(仅在新增/编辑弹窗内部使用)
|
||||||
const addTask = () => {
|
const addTask = () => {
|
||||||
|
@ -1234,6 +813,10 @@ const openEditModal = async (record: T.ProjectResp) => {
|
||||||
isEdit.value = true
|
isEdit.value = true
|
||||||
currentId.value = record.id || record.projectId || null
|
currentId.value = record.id || record.projectId || null
|
||||||
|
|
||||||
|
// 先打开弹窗并显示加载态
|
||||||
|
editModalVisible.value = true
|
||||||
|
editDetailLoading.value = true
|
||||||
|
|
||||||
// 重置编辑表单
|
// 重置编辑表单
|
||||||
Object.assign(editForm, {
|
Object.assign(editForm, {
|
||||||
projectId: '', projectName: '', projectManagerId: '', client: '', clientContact: '', clientPhone: '',
|
projectId: '', projectName: '', projectManagerId: '', client: '', clientContact: '', clientPhone: '',
|
||||||
|
@ -1242,6 +825,18 @@ const openEditModal = async (record: T.ProjectResp) => {
|
||||||
qualityOfficerId: '', auditorId: '', tasks: [], turbineList: [],
|
qualityOfficerId: '', auditorId: '', tasks: [], turbineList: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 先用列表记录进行快速预填,保证弹窗“秒开”可编辑
|
||||||
|
if (record && typeof record === 'object') {
|
||||||
|
Object.keys(editForm).forEach((key) => {
|
||||||
|
if ((record as any)[key] !== undefined) {
|
||||||
|
// @ts-expect-error 动态赋值
|
||||||
|
editForm[key] = (record as any)[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 预填完成后立即关闭整体加载,避免空任务时长时间转圈
|
||||||
|
editDetailLoading.value = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (currentId.value) {
|
if (currentId.value) {
|
||||||
const res = await getProjectDetail(currentId.value as any)
|
const res = await getProjectDetail(currentId.value as any)
|
||||||
|
@ -1281,9 +876,9 @@ const openEditModal = async (record: T.ProjectResp) => {
|
||||||
const tasksSource: any[] = Array.isArray(detail.tasks)
|
const tasksSource: any[] = Array.isArray(detail.tasks)
|
||||||
? detail.tasks
|
? detail.tasks
|
||||||
: (Array.isArray((detail as any).taskList) ? (detail as any).taskList : [])
|
: (Array.isArray((detail as any).taskList) ? (detail as any).taskList : [])
|
||||||
if (Array.isArray(tasksSource) && tasksSource.length) {
|
|
||||||
;(editForm.tasks as any[]) = tasksSource.map(mapTask)
|
// 任务为空时也要保证是数组,避免渲染卡住
|
||||||
}
|
;(editForm.tasks as any[]) = Array.isArray(tasksSource) ? tasksSource.map(mapTask) : []
|
||||||
|
|
||||||
// projectId 保证存在
|
// projectId 保证存在
|
||||||
editForm.projectId = (detail.projectId ?? currentId.value ?? record.projectId ?? '').toString()
|
editForm.projectId = (detail.projectId ?? currentId.value ?? record.projectId ?? '').toString()
|
||||||
|
@ -1297,10 +892,16 @@ const openEditModal = async (record: T.ProjectResp) => {
|
||||||
editForm[key] = (record as any)[key]
|
editForm[key] = (record as any)[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
editDetailLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开编辑弹窗(与新增分离)
|
// 取消编辑
|
||||||
editModalVisible.value = true
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelEdit = () => {
|
||||||
|
editModalVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加表单验证规则
|
// 添加表单验证规则
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="状态" field="status">
|
<a-form-item label="状态" field="status">
|
||||||
<a-select v-model="form.status" placeholder="请选择状态">
|
<a-select v-model="form.status" placeholder="请选择状态">
|
||||||
<a-option :value="0" label="正常" />
|
<a-option :value="1" label="正常" />
|
||||||
<a-option :value="1" label="停用" />
|
<a-option :value="0" label="停用" />
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="描述" field="remark">
|
<a-form-item label="描述" field="remark">
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Message, type TreeNodeData } from '@arco-design/web-vue'
|
import { Message, type TreeNodeData } from '@arco-design/web-vue'
|
||||||
import { useWindowSize } from '@vueuse/core'
|
import { useWindowSize } from '@vueuse/core'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
import { addUserNew, getUserDetailNew, updateUserNew } from '@/apis/system/user-new'
|
import { addUserNew, getUserDetailNew, updateUserNew } from '@/apis/system/user-new'
|
||||||
import { type ColumnItem, GiForm } from '@/components/GiForm'
|
import { type ColumnItem, GiForm } from '@/components/GiForm'
|
||||||
import type { Status } from '@/types/global'
|
import type { Status } from '@/types/global'
|
||||||
|
@ -186,7 +187,7 @@ const columns: ColumnItem[] = reactive([
|
||||||
field: 'roleIds',
|
field: 'roleIds',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
span: 12,
|
span: 12,
|
||||||
required: true,
|
//required: true,
|
||||||
props: {
|
props: {
|
||||||
options: roleList,
|
options: roleList,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
|
@ -347,15 +348,12 @@ const save = async () => {
|
||||||
// 新增
|
// 新增
|
||||||
const onAdd = async () => {
|
const onAdd = async () => {
|
||||||
reset()
|
reset()
|
||||||
if (!deptList.value.length) {
|
// 优先加载选项,避免 v-model 先赋值后补选项导致未选中
|
||||||
await getDeptList()
|
await Promise.all([
|
||||||
}
|
deptList.value.length ? Promise.resolve() : getDeptList(),
|
||||||
if (!roleList.value.length) {
|
roleList.value.length ? Promise.resolve() : getRoleList(),
|
||||||
await getRoleList()
|
postList.value.length ? Promise.resolve() : getPostList(),
|
||||||
}
|
])
|
||||||
if (!postList.value.length) {
|
|
||||||
await getPostList()
|
|
||||||
}
|
|
||||||
dataId.value = ''
|
dataId.value = ''
|
||||||
visible.value = true
|
visible.value = true
|
||||||
}
|
}
|
||||||
|
@ -365,15 +363,12 @@ const onUpdate = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
reset()
|
reset()
|
||||||
dataId.value = id
|
dataId.value = id
|
||||||
if (!deptList.value.length) {
|
// 先加载选项,确保角色/部门/岗位下拉已有选项
|
||||||
await getDeptList()
|
await Promise.all([
|
||||||
}
|
deptList.value.length ? Promise.resolve() : getDeptList(),
|
||||||
if (!roleList.value.length) {
|
roleList.value.length ? Promise.resolve() : getRoleList(),
|
||||||
await getRoleList()
|
postList.value.length ? Promise.resolve() : getPostList(),
|
||||||
}
|
])
|
||||||
if (!postList.value.length) {
|
|
||||||
await getPostList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用新的API获取用户详情
|
// 使用新的API获取用户详情
|
||||||
const { data } = await getUserDetailNew(id)
|
const { data } = await getUserDetailNew(id)
|
||||||
|
@ -381,13 +376,78 @@ const onUpdate = async (id: string) => {
|
||||||
return Message.error('获取用户详情失败')
|
return Message.error('获取用户详情失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将API返回的数据映射到表单
|
// 将API返回的数据映射到表单(基础字段)
|
||||||
Object.keys(form).forEach(key => {
|
Object.keys(form).forEach(key => {
|
||||||
if (data[key] !== undefined) {
|
if ((data as any)[key] !== undefined) {
|
||||||
form[key] = data[key]
|
;(form as any)[key] = (data as any)[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 规范化/兜底:部门、岗位、角色等 ID 字段类型,统一为字符串列表
|
||||||
|
const normalizeIdArray = (val: any): string[] => {
|
||||||
|
if (Array.isArray(val)) return val.map(v => String(v))
|
||||||
|
if (typeof val === 'string') return val.split(',').map(s => s.trim()).filter(Boolean)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 部门
|
||||||
|
form.deptId = data.deptId ? String(data.deptId) : ''
|
||||||
|
|
||||||
|
// 岗位
|
||||||
|
if ((data as any).postIds !== undefined) {
|
||||||
|
form.postIds = normalizeIdArray((data as any).postIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 角色:兼容 roleIds / roles / roleIdList 等不同返回结构
|
||||||
|
const roleIdsFromArray = normalizeIdArray((data as any).roleIds)
|
||||||
|
const roleIdsFromRoles = Array.isArray((data as any).roles)
|
||||||
|
? (data as any).roles.map((r: any) => String(r.roleId ?? r.id ?? r.role_id)).filter(Boolean)
|
||||||
|
: []
|
||||||
|
const roleIdsFromList = normalizeIdArray((data as any).roleIdList)
|
||||||
|
|
||||||
|
let mergedRoleIds = roleIdsFromArray.length
|
||||||
|
? roleIdsFromArray
|
||||||
|
: (roleIdsFromRoles.length ? roleIdsFromRoles : roleIdsFromList)
|
||||||
|
|
||||||
|
// 如果没有返回角色ID,但返回了角色名称(英文逗号分隔),尝试依据名称映射为ID
|
||||||
|
if (!mergedRoleIds.length) {
|
||||||
|
const namesStr = (data as any).roleName || (data as any).roleNames
|
||||||
|
const roleNames: string[] = Array.isArray(namesStr)
|
||||||
|
? (namesStr as any[]).map(n => String(n))
|
||||||
|
: (typeof namesStr === 'string' ? namesStr.split(/,|,/).map(s => s.trim()).filter(Boolean) : [])
|
||||||
|
if (roleNames.length && Array.isArray(roleList.value)) {
|
||||||
|
const labelToId = new Map<string, string>((roleList.value || []).map(opt => [String(opt.label), String(opt.value)]))
|
||||||
|
const ids = roleNames.map(n => labelToId.get(n)).filter((v): v is string => !!v)
|
||||||
|
if (ids.length) mergedRoleIds = ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.roleIds = mergedRoleIds
|
||||||
|
|
||||||
|
// 确保下拉选项中包含当前已分配的角色(避免因选项缺失或类型不一致导致无法预选)
|
||||||
|
try {
|
||||||
|
const optionValSet = new Set((roleList.value || []).map(o => String(o.value)))
|
||||||
|
const fromRolesMap = new Map<string, string>()
|
||||||
|
if (Array.isArray((data as any).roles)) {
|
||||||
|
;(data as any).roles.forEach((r: any) => {
|
||||||
|
const id = String(r.roleId ?? r.id ?? r.role_id)
|
||||||
|
const name = r.roleName ?? r.name ?? r.role_key ?? id
|
||||||
|
if (id) fromRolesMap.set(id, String(name))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const toAppend: any[] = []
|
||||||
|
mergedRoleIds.forEach(id => {
|
||||||
|
const sId = String(id)
|
||||||
|
if (!optionValSet.has(sId)) {
|
||||||
|
toAppend.push({ label: fromRolesMap.get(sId) || sId, value: sId, disabled: false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (toAppend.length) {
|
||||||
|
// 直接追加到选项列表,确保能展示已选值
|
||||||
|
roleList.value = [...(roleList.value || []), ...toAppend]
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
visible.value = true
|
visible.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户详情失败', error)
|
console.error('获取用户详情失败', error)
|
||||||
|
|
Loading…
Reference in New Issue