Merge branch 'devlopment' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web into devlopment
This commit is contained in:
commit
b3778ec7c2
|
@ -0,0 +1,88 @@
|
||||||
|
# 商务数据库文件预览功能说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
商务数据库信息模块的文件预览功能已经实现,支持多种文件类型的在线预览,为用户提供便捷的文件查看体验。
|
||||||
|
|
||||||
|
## 支持的文件类型
|
||||||
|
|
||||||
|
### 1. 图片文件
|
||||||
|
- **支持格式**: JPG, JPEG, PNG, GIF, BMP, WebP
|
||||||
|
- **预览方式**: 在模态窗口中直接显示图片
|
||||||
|
- **特性**:
|
||||||
|
- 自适应大小,最大高度为70vh
|
||||||
|
- 圆角和阴影效果提升视觉体验
|
||||||
|
- 显示文件名信息
|
||||||
|
|
||||||
|
### 2. PDF文件
|
||||||
|
- **支持格式**: PDF
|
||||||
|
- **预览方式**: 在新窗口中打开PDF文件
|
||||||
|
- **特性**: 利用浏览器原生PDF查看器
|
||||||
|
|
||||||
|
### 3. 文本文件
|
||||||
|
- **支持格式**: TXT, MD, JSON, XML, CSV
|
||||||
|
- **预览方式**: 在模态窗口中显示文本内容
|
||||||
|
- **特性**:
|
||||||
|
- 等宽字体显示,保持格式
|
||||||
|
- 400px高度的滚动区域
|
||||||
|
- 保留原文本格式和换行
|
||||||
|
|
||||||
|
### 4. Office文档
|
||||||
|
- **支持格式**: DOC, DOCX, XLS, XLSX, PPT, PPTX
|
||||||
|
- **预览方式**: 提示用户下载查看
|
||||||
|
- **说明**: 由于Office文档需要特殊渲染,建议下载后使用相应软件打开
|
||||||
|
|
||||||
|
### 5. 其他文件类型
|
||||||
|
- **处理方式**: 询问用户是否下载查看
|
||||||
|
- **说明**: 对于不支持在线预览的文件类型,系统会友好提示用户
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 用户体验优化
|
||||||
|
1. **加载提示**: 预览过程中显示"正在加载预览..."提示
|
||||||
|
2. **错误处理**: 针对不同错误状态提供详细的错误信息
|
||||||
|
3. **资源管理**: 自动清理预览使用的内存资源
|
||||||
|
|
||||||
|
### 错误处理机制
|
||||||
|
- **404错误**: "文件不存在或已被删除"
|
||||||
|
- **403错误**: "没有权限访问该文件"
|
||||||
|
- **500错误**: "服务器内部错误,请稍后重试"
|
||||||
|
- **网络错误**: "预览文件失败,请检查网络连接"
|
||||||
|
|
||||||
|
### 安全性
|
||||||
|
- 使用Blob URL进行文件预览,确保安全性
|
||||||
|
- 10秒后自动释放URL资源,防止内存泄漏
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
1. 在文件列表中找到要预览的文件
|
||||||
|
2. 点击文件行中的"眼睛"图标按钮
|
||||||
|
3. 系统会根据文件类型自动选择合适的预览方式
|
||||||
|
4. 对于不支持的文件类型,系统会提供下载选项
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### API接口
|
||||||
|
- **接口地址**: `/businessData/file/preview`
|
||||||
|
- **请求方式**: GET
|
||||||
|
- **参数**: `fileId` (必需)
|
||||||
|
- **响应类型**: Blob
|
||||||
|
|
||||||
|
### 核心函数
|
||||||
|
- `handlePreview(file)`: 主预览函数
|
||||||
|
- `showImagePreview(url, fileName)`: 图片预览组件
|
||||||
|
- `showTextPreview(blob, fileName)`: 文本预览组件
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **文件大小限制**: 大文件可能需要较长加载时间
|
||||||
|
2. **浏览器兼容性**: 确保浏览器支持Blob和URL.createObjectURL
|
||||||
|
3. **网络要求**: 稳定的网络连接确保预览效果
|
||||||
|
4. **权限控制**: 需要有相应的文件访问权限
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
1. 添加文件预览缓存机制
|
||||||
|
2. 支持更多文件格式的在线预览
|
||||||
|
3. 添加图片预览的缩放和旋转功能
|
||||||
|
4. 支持文档的分页预览功能
|
|
@ -1,49 +1,70 @@
|
||||||
import http from '@/utils/http'
|
import http from '@/utils/http'
|
||||||
import type { EquipmentApprovalReq, EquipmentApprovalResp, EquipmentApprovalListReq } from './type'
|
import type { EquipmentApprovalListReq, EquipmentApprovalResp } from './type'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设备审批管理API
|
* 设备审批API
|
||||||
*/
|
*/
|
||||||
export const equipmentApprovalApi = {
|
export const equipmentApprovalApi = {
|
||||||
/**
|
/**
|
||||||
* 分页查询待审批的设备采购申请
|
* 分页查询待审批的设备采购申请
|
||||||
*/
|
*/
|
||||||
getPendingApprovals: (params: EquipmentApprovalListReq) => {
|
getPendingApprovals(params: EquipmentApprovalListReq) {
|
||||||
return http.get<ApiRes<PageRes<EquipmentApprovalResp>>>('/equipment/approval/pending', { params })
|
return http.get<EquipmentApprovalResp[]>('/equipment/approval/pending', params)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询已审批的设备采购申请
|
* 分页查询已审批的设备采购申请
|
||||||
*/
|
*/
|
||||||
getApprovedApprovals: (params: EquipmentApprovalListReq) => {
|
getApprovedApprovals(params: EquipmentApprovalListReq) {
|
||||||
return http.get<ApiRes<PageRes<EquipmentApprovalResp>>>('/equipment/approval/approved', { params })
|
return http.get<EquipmentApprovalResp[]>('/equipment/approval/approved', params)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批通过
|
* 审批通过
|
||||||
*/
|
*/
|
||||||
approve: (approvalId: string, data: EquipmentApprovalReq) => {
|
approve(approvalId: string, data: any) {
|
||||||
return http.post<ApiRes<null>>(`/equipment/approval/${approvalId}/approve`, data)
|
return http.post(`/equipment/approval/${approvalId}/approve`, data)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批拒绝
|
* 审批拒绝
|
||||||
*/
|
*/
|
||||||
reject: (approvalId: string, data: EquipmentApprovalReq) => {
|
reject(approvalId: string, data: any) {
|
||||||
return http.post<ApiRes<null>>(`/equipment/approval/${approvalId}/reject`, data)
|
return http.post(`/equipment/approval/${approvalId}/reject`, data)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取审批详情
|
* 获取审批详情
|
||||||
*/
|
*/
|
||||||
getApprovalDetail: (approvalId: string) => {
|
getApprovalDetail(approvalId: string) {
|
||||||
return http.get<ApiRes<EquipmentApprovalResp>>(`/equipment/approval/${approvalId}`)
|
return http.get<EquipmentApprovalResp>(`/equipment/approval/${approvalId}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取审批统计信息
|
* 获取审批统计信息
|
||||||
*/
|
*/
|
||||||
getApprovalStats: () => {
|
getApprovalStats() {
|
||||||
return http.get<ApiRes<unknown>>('/equipment/approval/stats')
|
return http.get('/equipment/approval/stats')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交采购申请
|
||||||
|
*/
|
||||||
|
submitProcurementApplication(data: any) {
|
||||||
|
return http.post('/equipment/approval/procurement/apply', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的采购申请
|
||||||
|
*/
|
||||||
|
getMyProcurementApplications(params: EquipmentApprovalListReq) {
|
||||||
|
return http.get<EquipmentApprovalResp[]>('/equipment/approval/procurement/my-applications', params)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤回采购申请
|
||||||
|
*/
|
||||||
|
withdrawProcurementApplication(approvalId: string) {
|
||||||
|
return http.post(`/equipment/approval/procurement/${approvalId}/withdraw`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,13 @@
|
||||||
:content-style="{ marginTop: '-5px', padding: 0, border: 'none' }"
|
:content-style="{ marginTop: '-5px', padding: 0, border: 'none' }"
|
||||||
:arrow-style="{ width: 0, height: 0 }"
|
:arrow-style="{ width: 0, height: 0 }"
|
||||||
>
|
>
|
||||||
<a-badge :count="unreadMessageCount" dot>
|
<a-badge
|
||||||
<a-button size="mini" class="gi_hover_btn">
|
:count="unreadMessageCount"
|
||||||
|
:dot="unreadMessageCount > 0"
|
||||||
|
:show-zero="false"
|
||||||
|
class="notification-badge"
|
||||||
|
>
|
||||||
|
<a-button size="mini" class="gi_hover_btn notification-btn">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-notification :size="18" />
|
<icon-notification :size="18" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -74,9 +79,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Modal } from '@arco-design/web-vue'
|
import { Modal, Notification } from '@arco-design/web-vue'
|
||||||
import { useFullscreen } from '@vueuse/core'
|
import { useFullscreen } from '@vueuse/core'
|
||||||
import { onMounted, ref, nextTick } from 'vue'
|
import { onMounted, ref, nextTick, onBeforeUnmount } from 'vue'
|
||||||
import Message from './Message.vue'
|
import Message from './Message.vue'
|
||||||
import SettingDrawer from './SettingDrawer.vue'
|
import SettingDrawer from './SettingDrawer.vue'
|
||||||
import Search from './Search.vue'
|
import Search from './Search.vue'
|
||||||
|
@ -97,10 +102,148 @@ onBeforeUnmount(() => {
|
||||||
socket.close()
|
socket.close()
|
||||||
socket = null
|
socket = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理标题闪烁
|
||||||
|
if (titleFlashInterval) {
|
||||||
|
clearInterval(titleFlashInterval)
|
||||||
|
titleFlashInterval = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const unreadMessageCount = ref(0)
|
const unreadMessageCount = ref(0)
|
||||||
|
|
||||||
|
// 语音提示功能
|
||||||
|
const playNotificationSound = () => {
|
||||||
|
try {
|
||||||
|
// 创建音频上下文
|
||||||
|
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
|
||||||
|
const oscillator = audioContext.createOscillator()
|
||||||
|
const gainNode = audioContext.createGain()
|
||||||
|
|
||||||
|
oscillator.connect(gainNode)
|
||||||
|
gainNode.connect(audioContext.destination)
|
||||||
|
|
||||||
|
// 设置音频参数
|
||||||
|
oscillator.frequency.setValueAtTime(800, audioContext.currentTime) // 800Hz
|
||||||
|
oscillator.type = 'sine'
|
||||||
|
|
||||||
|
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime)
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5)
|
||||||
|
|
||||||
|
// 播放音频
|
||||||
|
oscillator.start(audioContext.currentTime)
|
||||||
|
oscillator.stop(audioContext.currentTime + 0.5)
|
||||||
|
|
||||||
|
console.log('播放语音提示')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('播放语音提示失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面标题闪烁功能
|
||||||
|
let titleFlashInterval: NodeJS.Timeout | null = null
|
||||||
|
const flashPageTitle = () => {
|
||||||
|
const originalTitle = document.title
|
||||||
|
let flashCount = 0
|
||||||
|
const maxFlashes = 6 // 闪烁3次(开-关-开-关-开-关)
|
||||||
|
|
||||||
|
// 清除之前的闪烁
|
||||||
|
if (titleFlashInterval) {
|
||||||
|
clearInterval(titleFlashInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
titleFlashInterval = setInterval(() => {
|
||||||
|
if (flashCount >= maxFlashes) {
|
||||||
|
document.title = originalTitle
|
||||||
|
if (titleFlashInterval) {
|
||||||
|
clearInterval(titleFlashInterval)
|
||||||
|
titleFlashInterval = null
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
document.title = flashCount % 2 === 0 ? '🔔 新的采购申请' : originalTitle
|
||||||
|
flashCount++
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露测试函数到全局,方便在控制台测试
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as any).testNotification = {
|
||||||
|
playSound: playNotificationSound,
|
||||||
|
flashTitle: flashPageTitle,
|
||||||
|
showNotification: () => {
|
||||||
|
Notification.info({
|
||||||
|
title: '测试通知',
|
||||||
|
content: '这是一个测试通知,用于验证通知功能是否正常工作。',
|
||||||
|
duration: 5000,
|
||||||
|
closable: true,
|
||||||
|
position: 'topRight'
|
||||||
|
})
|
||||||
|
unreadMessageCount.value++
|
||||||
|
},
|
||||||
|
testAll: () => {
|
||||||
|
playNotificationSound()
|
||||||
|
flashPageTitle()
|
||||||
|
Notification.info({
|
||||||
|
title: '测试通知',
|
||||||
|
content: '这是一个测试通知,用于验证通知功能是否正常工作。',
|
||||||
|
duration: 5000,
|
||||||
|
closable: true,
|
||||||
|
position: 'topRight'
|
||||||
|
})
|
||||||
|
unreadMessageCount.value++
|
||||||
|
},
|
||||||
|
// 添加调试函数
|
||||||
|
debugWebSocket: () => {
|
||||||
|
console.log('=== WebSocket 调试信息 ===')
|
||||||
|
console.log('Socket对象:', socket)
|
||||||
|
console.log('Socket状态:', socket ? socket.readyState : '未连接')
|
||||||
|
console.log('Token:', getToken())
|
||||||
|
console.log('环境变量:', import.meta.env.VITE_API_WS_URL)
|
||||||
|
console.log('未读消息计数:', unreadMessageCount.value)
|
||||||
|
console.log('用户Token:', userStore.token)
|
||||||
|
},
|
||||||
|
// 手动触发WebSocket消息处理
|
||||||
|
simulateWebSocketMessage: () => {
|
||||||
|
const mockMessage = {
|
||||||
|
type: "PROCUREMENT_APPLICATION",
|
||||||
|
title: "新的采购申请",
|
||||||
|
content: "收到来自 测试用户 的设备采购申请:测试设备"
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = new MessageEvent('message', {
|
||||||
|
data: JSON.stringify(mockMessage)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (socket && socket.onmessage) {
|
||||||
|
console.log('模拟WebSocket消息:', mockMessage)
|
||||||
|
socket.onmessage(event)
|
||||||
|
} else {
|
||||||
|
console.error('WebSocket连接不存在或onmessage未设置')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 强制重新连接WebSocket
|
||||||
|
reconnectWebSocket: () => {
|
||||||
|
console.log('强制重新连接WebSocket')
|
||||||
|
const token = getToken()
|
||||||
|
if (token) {
|
||||||
|
if (socket) {
|
||||||
|
socket.close()
|
||||||
|
socket = null
|
||||||
|
}
|
||||||
|
initWebSocket(token)
|
||||||
|
} else {
|
||||||
|
console.error('Token不存在,无法重新连接')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露socket对象到全局,方便调试
|
||||||
|
;(window as any).socket = socket
|
||||||
|
;(window as any).unreadMessageCount = unreadMessageCount
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化 WebSocket - 使用防抖避免重复连接
|
// 初始化 WebSocket - 使用防抖避免重复连接
|
||||||
let initTimer: NodeJS.Timeout | null = null
|
let initTimer: NodeJS.Timeout | null = null
|
||||||
const initWebSocket = (token: string) => {
|
const initWebSocket = (token: string) => {
|
||||||
|
@ -116,29 +259,70 @@ const initWebSocket = (token: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
socket = new WebSocket(`${import.meta.env.VITE_API_WS_URL}/websocket?token=${token}`)
|
// 修复WebSocket URL,确保使用正确的端口
|
||||||
|
const wsUrl = import.meta.env.VITE_API_WS_URL || 'ws://localhost:8888'
|
||||||
|
const wsEndpoint = wsUrl.replace('8000', '8888') // 确保使用8888端口
|
||||||
|
console.log('正在连接WebSocket:', `${wsEndpoint}/websocket?token=${token}`)
|
||||||
|
|
||||||
|
socket = new WebSocket(`${wsEndpoint}/websocket?token=${token}`)
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
// console.log('WebSocket connection opened')
|
console.log('WebSocket连接成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
|
console.log('收到WebSocket消息:', event.data)
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
|
||||||
|
// 处理通知消息
|
||||||
|
if (data.type && data.title && data.content) {
|
||||||
|
console.log('处理通知消息:', data)
|
||||||
|
|
||||||
|
// 播放语音提示
|
||||||
|
playNotificationSound()
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
Notification.info({
|
||||||
|
title: data.title,
|
||||||
|
content: data.content,
|
||||||
|
duration: 5000,
|
||||||
|
closable: true,
|
||||||
|
position: 'topRight'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 增加未读消息计数
|
||||||
|
unreadMessageCount.value++
|
||||||
|
|
||||||
|
// 触发页面标题闪烁
|
||||||
|
flashPageTitle()
|
||||||
|
} else {
|
||||||
|
// 处理简单的数字消息(兼容旧版本)
|
||||||
|
const count = Number.parseInt(event.data)
|
||||||
|
if (!isNaN(count)) {
|
||||||
|
unreadMessageCount.value = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析WebSocket消息失败:', error)
|
||||||
|
// 尝试解析为数字(兼容旧版本)
|
||||||
const count = Number.parseInt(event.data)
|
const count = Number.parseInt(event.data)
|
||||||
if (!isNaN(count)) {
|
if (!isNaN(count)) {
|
||||||
unreadMessageCount.value = count
|
unreadMessageCount.value = count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.onerror = () => {
|
|
||||||
// console.error('WebSocket error:', error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.onclose = () => {
|
socket.onerror = (error) => {
|
||||||
// console.log('WebSocket connection closed')
|
console.error('WebSocket连接错误:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onclose = (event) => {
|
||||||
|
console.log('WebSocket连接关闭:', event.code, event.reason)
|
||||||
socket = null
|
socket = null
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create WebSocket connection:', error)
|
console.error('创建WebSocket连接失败:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
initTimer = null
|
initTimer = null
|
||||||
|
@ -149,10 +333,17 @@ const initWebSocket = (token: string) => {
|
||||||
const getMessageCount = async () => {
|
const getMessageCount = async () => {
|
||||||
try {
|
try {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
|
console.log('获取到token:', token ? '存在' : '不存在')
|
||||||
|
|
||||||
if (token && !socket) {
|
if (token && !socket) {
|
||||||
|
console.log('准备初始化WebSocket连接')
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initWebSocket(token)
|
initWebSocket(token)
|
||||||
})
|
})
|
||||||
|
} else if (!token) {
|
||||||
|
console.warn('Token不存在,无法建立WebSocket连接')
|
||||||
|
} else if (socket) {
|
||||||
|
console.log('WebSocket连接已存在')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get message count:', error)
|
console.error('Failed to get message count:', error)
|
||||||
|
@ -186,9 +377,32 @@ const logout = () => {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// getMessageCount()
|
// 立即尝试初始化WebSocket
|
||||||
|
getMessageCount()
|
||||||
|
|
||||||
|
// 如果第一次失败,1秒后重试
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!socket) {
|
||||||
|
console.log('首次连接失败,重试WebSocket连接')
|
||||||
|
getMessageCount()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听用户登录状态变化
|
||||||
|
watch(() => userStore.token, (newToken, oldToken) => {
|
||||||
|
console.log('Token变化:', { oldToken: oldToken ? '存在' : '不存在', newToken: newToken ? '存在' : '不存在' })
|
||||||
|
|
||||||
|
if (newToken && !socket) {
|
||||||
|
console.log('用户登录,初始化WebSocket连接')
|
||||||
|
getMessageCount()
|
||||||
|
} else if (!newToken && socket) {
|
||||||
|
console.log('用户登出,关闭WebSocket连接')
|
||||||
|
socket.close()
|
||||||
|
socket = null
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@ -210,4 +424,56 @@ onMounted(() => {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知徽章样式
|
||||||
|
.notification-badge {
|
||||||
|
.arco-badge-dot {
|
||||||
|
background-color: #f53f3f;
|
||||||
|
box-shadow: 0 0 0 2px rgba(245, 63, 63, 0.2);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-badge-count {
|
||||||
|
background-color: #f53f3f;
|
||||||
|
font-weight: bold;
|
||||||
|
animation: bounce 0.6s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-btn {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脉冲动画
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(245, 63, 63, 0.7);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 10px rgba(245, 63, 63, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(245, 63, 63, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹跳动画
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 20%, 53%, 80%, 100% {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
40%, 43% {
|
||||||
|
transform: translate3d(0, -8px, 0);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: translate3d(0, -4px, 0);
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
transform: translate3d(0, -2px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_PREFIX: string
|
readonly VITE_API_PREFIX: string
|
||||||
readonly VITE_API_BASE_URL: string
|
readonly VITE_API_BASE_URL: string
|
||||||
|
readonly VITE_API_WS_URL: string
|
||||||
readonly VITE_BASE: string
|
readonly VITE_BASE: string
|
||||||
readonly VITE_APP_SETTING: string
|
readonly VITE_APP_SETTING: string
|
||||||
readonly VITE_CLIENT_ID: string
|
readonly VITE_CLIENT_ID: string
|
||||||
|
|
|
@ -225,7 +225,3 @@ export default {
|
||||||
requestRaw,
|
requestRaw,
|
||||||
download,
|
download,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateContract = (contractId, contractData) => {
|
|
||||||
return http.put(`/contract/${contractId}`, contractData)
|
|
||||||
}
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -83,8 +83,8 @@
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文件预览模态框 -->
|
<!-- 文件预览模态框(待重新设计) -->
|
||||||
<PreviewModal ref="previewModal" />
|
<!-- <PreviewModal ref="previewModal" /> -->
|
||||||
</GiPageLayout>
|
</GiPageLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -92,8 +92,12 @@
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import type { TableColumnData } from '@arco-design/web-vue'
|
import type { TableColumnData } from '@arco-design/web-vue'
|
||||||
|
<<<<<<< HEAD
|
||||||
import PreviewModal from './components/PreviewModal.vue'
|
import PreviewModal from './components/PreviewModal.vue'
|
||||||
import rawData from './components/raw-data.vue'
|
import rawData from './components/raw-data.vue'
|
||||||
|
=======
|
||||||
|
|
||||||
|
>>>>>>> 16743cfc7f885acdff70e08f20ddb7795d638a83
|
||||||
import {
|
import {
|
||||||
getProjectList,
|
getProjectList,
|
||||||
getTurbineList,
|
getTurbineList,
|
||||||
|
@ -107,7 +111,8 @@ import {
|
||||||
} from '@/apis/industrial-image'
|
} from '@/apis/industrial-image'
|
||||||
import DeformationTap from './components/DeformationTap.vue'
|
import DeformationTap from './components/DeformationTap.vue'
|
||||||
|
|
||||||
const previewModal = ref()
|
// 预览弹窗引用(待重新设计)
|
||||||
|
// const previewModal = ref()
|
||||||
// 活动选项卡
|
// 活动选项卡
|
||||||
const activeTab = ref('image')
|
const activeTab = ref('image')
|
||||||
|
|
||||||
|
@ -323,18 +328,23 @@ const getImageUrl = (imagePath: string): string => {
|
||||||
return `${baseUrl}${imagePath}`
|
return `${baseUrl}${imagePath}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预览文件
|
// 预览文件(待重新设计预览弹窗)
|
||||||
const previewFile = (file: any) => {
|
const previewFile = (file: any) => {
|
||||||
|
<<<<<<< HEAD
|
||||||
/* previewFileData.value = file
|
/* previewFileData.value = file
|
||||||
previewModalVisible.value = true*/
|
previewModalVisible.value = true*/
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> 16743cfc7f885acdff70e08f20ddb7795d638a83
|
||||||
const fileObj = {
|
const fileObj = {
|
||||||
id: file.id,
|
id: file.id,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
url: getImageUrl(file.imagePath),
|
url: getImageUrl(file.imagePath)
|
||||||
audios: file.audioList
|
|
||||||
}
|
}
|
||||||
previewModal.value.openPreview(fileObj)
|
// TODO: 重新设计预览弹窗后取消注释
|
||||||
|
// previewModal.value.openPreview(fileObj)
|
||||||
|
console.log('预览文件:', fileObj)
|
||||||
|
// 暂时禁用预览功能,等待重新设计
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除文件
|
// 删除文件
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
<template>
|
||||||
|
<a-spin :loading="loading">
|
||||||
|
<div v-if="contractDetail">
|
||||||
|
<a-descriptions
|
||||||
|
:column="1"
|
||||||
|
size="medium"
|
||||||
|
:label-style="{ width: '120px' }"
|
||||||
|
>
|
||||||
|
<a-descriptions-item label="合同编号">
|
||||||
|
{{ contractDetail.code }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="项目名称">
|
||||||
|
{{ contractDetail.projectName }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="客户名称">
|
||||||
|
{{ contractDetail.customer }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="合同金额">
|
||||||
|
<span class="font-medium text-green-600">¥{{ (contractDetail.amount || 0).toLocaleString() }}</span>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="已收款金额">
|
||||||
|
<span class="font-medium text-blue-600">¥{{ (contractDetail.receivedAmount || 0).toLocaleString() }}</span>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="未收款金额">
|
||||||
|
<span class="font-medium text-orange-600">¥{{ (contractDetail.pendingAmount || 0).toLocaleString() }}</span>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="签署日期">
|
||||||
|
{{ contractDetail.signDate }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="履约期限">
|
||||||
|
{{ contractDetail.performanceDeadline }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="付款日期">
|
||||||
|
{{ contractDetail.paymentDate }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="合同状态">
|
||||||
|
<a-tag :color="getStatusColor(contractDetail.contractStatus)">
|
||||||
|
{{ getStatusText(contractDetail.contractStatusLabel || contractDetail.contractStatus) }}
|
||||||
|
</a-tag>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="销售人员">
|
||||||
|
{{ contractDetail.salespersonName }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="销售部门">
|
||||||
|
{{ contractDetail.salespersonDeptName }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="产品服务">
|
||||||
|
{{ contractDetail.productService }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item label="备注">
|
||||||
|
{{ contractDetail.notes }}
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!loading" class="empty-container">
|
||||||
|
<a-empty description="暂无信息" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import http from '@/utils/http'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
|
||||||
|
interface ContractDetail {
|
||||||
|
contractId: string
|
||||||
|
customer: string
|
||||||
|
code: string
|
||||||
|
projectId: string
|
||||||
|
type: string
|
||||||
|
productService: string
|
||||||
|
paymentDate: string | null
|
||||||
|
performanceDeadline: string | null
|
||||||
|
paymentAddress: string
|
||||||
|
amount: number
|
||||||
|
accountNumber: string
|
||||||
|
notes: string
|
||||||
|
contractStatus: string
|
||||||
|
contractText: string | null
|
||||||
|
projectName: string
|
||||||
|
salespersonName: string | null
|
||||||
|
salespersonDeptName: string
|
||||||
|
settlementAmount: number | null
|
||||||
|
receivedAmount: number | null
|
||||||
|
contractStatusLabel: string | null
|
||||||
|
createBy: string | null
|
||||||
|
updateBy: string | null
|
||||||
|
createTime: string
|
||||||
|
updateTime: string
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
signDate: string
|
||||||
|
duration: string
|
||||||
|
pendingAmount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
contractId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const contractDetail = ref<ContractDetail | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
未确认: 'gray',
|
||||||
|
待审批: 'orange',
|
||||||
|
已签署: 'blue',
|
||||||
|
执行中: 'cyan',
|
||||||
|
已完成: 'green',
|
||||||
|
已终止: 'red'
|
||||||
|
}
|
||||||
|
return colorMap[status] || 'gray'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
return status || '未知状态'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchContractDetail = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await http.get(`/contract/${props.contractId}`)
|
||||||
|
if (response.code === 200) {
|
||||||
|
contractDetail.value = response.data
|
||||||
|
// 计算未收款金额
|
||||||
|
if (contractDetail.value) {
|
||||||
|
contractDetail.value.pendingAmount = (contractDetail.value.amount || 0) - (contractDetail.value.receivedAmount || 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Message.error(response.msg || '获取合同详情失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取合同详情失败:', error)
|
||||||
|
Message.error('获取合同详情失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchContractDetail()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.empty-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,122 @@
|
||||||
|
<template>
|
||||||
|
<a-form :model="contractData" layout="vertical">
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="code" label="合同编号">
|
||||||
|
<a-input v-model="contractData.code" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="projectName" label="项目名称">
|
||||||
|
<a-input v-model="contractData.projectName" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="customer" label="客户名称">
|
||||||
|
<a-input v-model="contractData.customer" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="amount" label="合同金额">
|
||||||
|
<a-input-number v-model="contractData.amount" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="accountNumber" label="收款账号">
|
||||||
|
<a-input v-model="contractData.accountNumber" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="contractStatus" label="合同状态">
|
||||||
|
<a-select v-model="contractData.contractStatus">
|
||||||
|
<a-option value="未确认">未确认</a-option>
|
||||||
|
<a-option value="待审批">待审批</a-option>
|
||||||
|
<a-option value="已签署">已签署</a-option>
|
||||||
|
<a-option value="执行中">执行中</a-option>
|
||||||
|
<a-option value="已完成">已完成</a-option>
|
||||||
|
<a-option value="已终止">已终止</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="signDate" label="签订日期">
|
||||||
|
<a-date-picker v-model="contractData.signDate" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="performanceDeadline" label="履约期限">
|
||||||
|
<a-date-picker v-model="contractData.performanceDeadline" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="paymentDate" label="付款日期">
|
||||||
|
<a-date-picker v-model="contractData.paymentDate" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="productService" label="产品或服务">
|
||||||
|
<a-input v-model="contractData.productService" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item field="paymentAddress" label="付款地址">
|
||||||
|
<a-input v-model="contractData.paymentAddress" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="notes" label="备注">
|
||||||
|
<a-textarea v-model="contractData.notes" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="contractText" label="合同内容">
|
||||||
|
<a-textarea v-model="contractData.contractText" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import type { ContractItem } from './index.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
contractData: ContractItem
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:contractData', data: ContractItem): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const contractData = ref({ ...props.contractData })
|
||||||
|
|
||||||
|
// 监听props变化更新内部数据
|
||||||
|
watch(
|
||||||
|
() => props.contractData,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
contractData.value = { ...newVal }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听内部数据变化并触发更新事件
|
||||||
|
watch(
|
||||||
|
contractData,
|
||||||
|
(newVal) => {
|
||||||
|
emit('update:contractData', newVal)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
</script>
|
|
@ -38,48 +38,110 @@
|
||||||
|
|
||||||
<!-- 合同状态 -->
|
<!-- 合同状态 -->
|
||||||
<template #status="{ record }">
|
<template #status="{ record }">
|
||||||
<a-tag :color="getStatusColor(record.status)">
|
<a-tag :color="getStatusColor(record.contractStatus)">
|
||||||
{{ getStatusText(record.status) }}
|
{{ getStatusText(record.contractStatusLabel || record.contractStatus) }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 合同金额 -->
|
<!-- 合同金额 -->
|
||||||
<template #contractAmount="{ record }">
|
<template #contractAmount="{ record }">
|
||||||
<span class="font-medium text-red-600">¥{{ record.contractAmount.toLocaleString() }}万</span>
|
<span class="font-medium text-green-600">¥{{ (record.amount || 0).toLocaleString() }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 已付款金额 -->
|
<!-- 已收款金额 -->
|
||||||
<template #paidAmount="{ record }">
|
<template #receivedAmount="{ record }">
|
||||||
<span class="font-medium text-orange-600">¥{{ record.paidAmount.toLocaleString() }}万</span>
|
<span class="font-medium text-blue-600">¥{{ (record.receivedAmount || 0).toLocaleString() }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 操作列 -->
|
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-link @click="viewDetail(record)">详情</a-link>
|
<a-link @click="viewDetail(record)">详情</a-link>
|
||||||
<a-link @click="editRecord(record)" v-if="record.status === 'draft'">编辑</a-link>
|
<a-link v-if="record.contractStatus === '未确认'" @click="editRecord(record)">编辑</a-link>
|
||||||
<a-link @click="approveContract(record)" v-if="record.status === 'pending'">审批</a-link>
|
<a-link v-if="record.contractStatus === '待审批'" @click="approveContract(record)">审批</a-link>
|
||||||
<a-link @click="viewPayment(record)">付款记录</a-link>
|
<a-link @click="viewPayment(record)">收款记录</a-link>
|
||||||
|
<a-link v-if="record.contractStatus !== '已签署' && record.contractStatus !== '已完成'" @click="deleteContract(record)">删除</a-link>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</GiTable>
|
</GiTable>
|
||||||
|
|
||||||
|
<!-- 合同详情弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="showDetailModal"
|
||||||
|
title="合同详情"
|
||||||
|
:width="800"
|
||||||
|
:footer="false"
|
||||||
|
@cancel="closeDetailModal"
|
||||||
|
>
|
||||||
|
<ContractDetail v-if="showDetailModal" :contract-id="selectedContractId" />
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 合同编辑弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="showEditModal"
|
||||||
|
title="编辑合同"
|
||||||
|
:width="800"
|
||||||
|
@cancel="closeEditModal"
|
||||||
|
@before-ok="handleEditSubmit"
|
||||||
|
>
|
||||||
|
<ContractEdit
|
||||||
|
v-if="showEditModal"
|
||||||
|
:contract-data="selectedContractData"
|
||||||
|
@update:contract-data="handleContractDataUpdate"
|
||||||
|
/>
|
||||||
|
</a-modal>
|
||||||
</GiPageLayout>
|
</GiPageLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
import { Message } 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 ContractEdit from './ContractEdit.vue'
|
||||||
|
|
||||||
|
import ContractDetail from './ContractDetail.vue'
|
||||||
|
import http from '@/utils/http'
|
||||||
|
|
||||||
|
// 接口数据类型定义
|
||||||
|
interface ContractItem {
|
||||||
|
contractId: string
|
||||||
|
customer: string
|
||||||
|
code: string
|
||||||
|
projectId: string
|
||||||
|
type: string
|
||||||
|
productService: string
|
||||||
|
paymentDate: string | null
|
||||||
|
performanceDeadline: string | null
|
||||||
|
paymentAddress: string
|
||||||
|
amount: number
|
||||||
|
accountNumber: string
|
||||||
|
notes: string
|
||||||
|
contractStatus: string
|
||||||
|
contractText: string | null
|
||||||
|
projectName: string
|
||||||
|
salespersonName: string | null
|
||||||
|
salespersonDeptName: string
|
||||||
|
settlementAmount: number | null
|
||||||
|
receivedAmount: number | null
|
||||||
|
contractStatusLabel: string | null
|
||||||
|
createBy: string | null
|
||||||
|
updateBy: string | null
|
||||||
|
createTime: string
|
||||||
|
updateTime: string
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
signDate: string
|
||||||
|
duration: string
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
let searchForm = reactive({
|
const searchForm = reactive({
|
||||||
contractName: '',
|
contractName: '',
|
||||||
contractCode: '',
|
contractCode: '',
|
||||||
supplier: '',
|
client: '',
|
||||||
status: '',
|
status: '',
|
||||||
signDate: '',
|
signDate: '',
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10
|
size: 10,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 查询条件配置
|
// 查询条件配置
|
||||||
|
@ -89,16 +151,16 @@ const queryFormColumns = [
|
||||||
label: '合同名称',
|
label: '合同名称',
|
||||||
type: 'input' as const,
|
type: 'input' as const,
|
||||||
props: {
|
props: {
|
||||||
placeholder: '请输入合同名称'
|
placeholder: '请输入合同名称',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'supplier',
|
field: 'client',
|
||||||
label: '供应商',
|
label: '客户',
|
||||||
type: 'input' as const,
|
type: 'input' as const,
|
||||||
props: {
|
props: {
|
||||||
placeholder: '请输入供应商名称'
|
placeholder: '请输入客户名称',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'status',
|
||||||
|
@ -107,147 +169,121 @@ const queryFormColumns = [
|
||||||
props: {
|
props: {
|
||||||
placeholder: '请选择合同状态',
|
placeholder: '请选择合同状态',
|
||||||
options: [
|
options: [
|
||||||
{ label: '草稿', value: 'draft' },
|
{ label: '未确认', value: '未确认' },
|
||||||
{ label: '待审批', value: 'pending' },
|
{ label: '待审批', value: '待审批' },
|
||||||
{ label: '已签署', value: 'signed' },
|
{ label: '已签署', value: '已签署' },
|
||||||
{ label: '执行中', value: 'executing' },
|
{ label: '执行中', value: '执行中' },
|
||||||
{ label: '已完成', value: 'completed' },
|
{ label: '已完成', value: '已完成' },
|
||||||
{ label: '已终止', value: 'terminated' }
|
{ label: '已终止', value: '已终止' },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// 表格列配置
|
// 表格列配置
|
||||||
const tableColumns: TableColumnData[] = [
|
const tableColumns: TableColumnData[] = [
|
||||||
{ title: '合同编号', dataIndex: 'contractCode', width: 150 },
|
{ title: '合同编号', dataIndex: 'code', width: 150 },
|
||||||
{ title: '合同名称', dataIndex: 'contractName', width: 250, ellipsis: true, tooltip: true },
|
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
|
||||||
{ title: '供应商名称', dataIndex: 'supplier', width: 200, ellipsis: true, tooltip: true },
|
{ title: '客户名称', dataIndex: 'customer', width: 200, ellipsis: true, tooltip: true },
|
||||||
{ title: '合同类型', dataIndex: 'contractType', width: 120 },
|
{ title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 },
|
||||||
{ title: '合同金额', dataIndex: 'contractAmount', slotName: 'contractAmount', width: 120 },
|
{ title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 },
|
||||||
{ title: '已付款金额', dataIndex: 'paidAmount', slotName: 'paidAmount', width: 120 },
|
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 },
|
||||||
{ title: '未付款金额', dataIndex: 'unpaidAmount', width: 120 },
|
|
||||||
{ title: '签署日期', dataIndex: 'signDate', width: 120 },
|
{ title: '签署日期', dataIndex: 'signDate', width: 120 },
|
||||||
{ title: '开始日期', dataIndex: 'startDate', width: 120 },
|
{ title: '履约期限', dataIndex: 'performanceDeadline', width: 120 },
|
||||||
{ title: '结束日期', dataIndex: 'endDate', width: 120 },
|
{ title: '付款日期', dataIndex: 'paymentDate', width: 120 },
|
||||||
{ title: '合同状态', dataIndex: 'status', slotName: 'status', width: 100 },
|
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
|
||||||
{ title: '项目关联', dataIndex: 'relatedProject', width: 200, ellipsis: true, tooltip: true },
|
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 },
|
||||||
{ title: '采购负责人', dataIndex: 'purchaseManager', width: 100 },
|
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
|
||||||
{ title: '付款方式', dataIndex: 'paymentMethod', width: 100 },
|
{ title: '产品服务', dataIndex: 'productService', width: 120, ellipsis: true, tooltip: true },
|
||||||
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
|
{ title: '备注', dataIndex: 'notes', width: 200, ellipsis: true, tooltip: true },
|
||||||
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
|
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 数据状态
|
// 数据状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const dataList = ref([
|
const dataList = ref<ContractItem[]>([])
|
||||||
{
|
|
||||||
id: 1,
|
// API调用函数
|
||||||
contractCode: 'EC2024001',
|
const fetchContractList = async () => {
|
||||||
contractName: '风电检测设备采购合同',
|
try {
|
||||||
supplier: '深圳市智能检测设备有限公司',
|
loading.value = true
|
||||||
contractType: '设备采购',
|
const params = {
|
||||||
contractAmount: 120,
|
page: searchForm.page,
|
||||||
paidAmount: 60,
|
pageSize: searchForm.size,
|
||||||
unpaidAmount: 60,
|
contractName: searchForm.contractName,
|
||||||
signDate: '2024-02-25',
|
code: searchForm.contractCode,
|
||||||
startDate: '2024-03-01',
|
customer: searchForm.client,
|
||||||
endDate: '2024-03-31',
|
contractStatus: searchForm.status,
|
||||||
status: 'executing',
|
signDate: searchForm.signDate,
|
||||||
relatedProject: '华能新能源风电场叶片检测服务项目',
|
|
||||||
purchaseManager: '李采购经理',
|
|
||||||
paymentMethod: '银行转账',
|
|
||||||
remark: '按合同约定分期付款'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
contractCode: 'EC2024002',
|
|
||||||
contractName: '无人机检测服务外包合同',
|
|
||||||
supplier: '北京航天无人机技术有限公司',
|
|
||||||
contractType: '服务外包',
|
|
||||||
contractAmount: 85,
|
|
||||||
paidAmount: 25.5,
|
|
||||||
unpaidAmount: 59.5,
|
|
||||||
signDate: '2024-03-02',
|
|
||||||
startDate: '2024-03-05',
|
|
||||||
endDate: '2024-04-05',
|
|
||||||
status: 'executing',
|
|
||||||
relatedProject: '大唐风电场防雷检测项目',
|
|
||||||
purchaseManager: '王采购经理',
|
|
||||||
paymentMethod: '分期付款',
|
|
||||||
remark: '服务外包,按进度付款'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
contractCode: 'EC2024003',
|
|
||||||
contractName: '检测车辆租赁合同',
|
|
||||||
supplier: '上海专业车辆租赁有限公司',
|
|
||||||
contractType: '车辆租赁',
|
|
||||||
contractAmount: 15,
|
|
||||||
paidAmount: 15,
|
|
||||||
unpaidAmount: 0,
|
|
||||||
signDate: '2024-01-20',
|
|
||||||
startDate: '2024-01-25',
|
|
||||||
endDate: '2024-02-25',
|
|
||||||
status: 'completed',
|
|
||||||
relatedProject: '中广核风电场设备维护服务项目',
|
|
||||||
purchaseManager: '刘采购经理',
|
|
||||||
paymentMethod: '月付',
|
|
||||||
remark: '租赁合同已完成'
|
|
||||||
}
|
}
|
||||||
])
|
|
||||||
|
const response = await http.get('/contract/list', params)
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
// 过滤出类型为"收入合同"的数据
|
||||||
|
const allContracts = response.rows || []
|
||||||
|
const revenueContracts = allContracts.filter((item: ContractItem) => item.type === '支出合同')
|
||||||
|
|
||||||
|
// 计算未收款金额
|
||||||
|
dataList.value = revenueContracts.map((item: ContractItem) => ({
|
||||||
|
...item,
|
||||||
|
pendingAmount: (item.amount || 0) - (item.receivedAmount || 0),
|
||||||
|
}))
|
||||||
|
|
||||||
|
pagination.total = Number.parseInt(response.total) || 0
|
||||||
|
} else {
|
||||||
|
Message.error(response.msg || '获取合同列表失败')
|
||||||
|
dataList.value = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取合同列表失败:', error)
|
||||||
|
Message.error('获取合同列表失败')
|
||||||
|
dataList.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 3,
|
total: 0,
|
||||||
showTotal: true,
|
showTotal: true,
|
||||||
showPageSize: true
|
showPageSize: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取状态颜色
|
// 获取状态颜色
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
'draft': 'gray',
|
未确认: 'gray',
|
||||||
'pending': 'orange',
|
待审批: 'orange',
|
||||||
'signed': 'blue',
|
已签署: 'blue',
|
||||||
'executing': 'cyan',
|
执行中: 'cyan',
|
||||||
'completed': 'green',
|
已完成: 'green',
|
||||||
'terminated': 'red'
|
已终止: 'red',
|
||||||
}
|
}
|
||||||
return colorMap[status] || 'gray'
|
return colorMap[status] || 'gray'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取状态文本
|
// 获取状态文本
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
const textMap: Record<string, string> = {
|
// 直接返回后端返回的状态文本,如果有contractStatusLabel则使用,否则使用contractStatus
|
||||||
'draft': '草稿',
|
return status || '未知状态'
|
||||||
'pending': '待审批',
|
|
||||||
'signed': '已签署',
|
|
||||||
'executing': '执行中',
|
|
||||||
'completed': '已完成',
|
|
||||||
'terminated': '已终止'
|
|
||||||
}
|
|
||||||
return textMap[status] || status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索和重置
|
// 搜索和重置
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
loading.value = true
|
await fetchContractList()
|
||||||
setTimeout(() => {
|
|
||||||
loading.value = false
|
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
Object.assign(searchForm, {
|
Object.assign(searchForm, {
|
||||||
contractName: '',
|
contractName: '',
|
||||||
contractCode: '',
|
contractCode: '',
|
||||||
supplier: '',
|
client: '',
|
||||||
status: '',
|
status: '',
|
||||||
signDate: '',
|
signDate: '',
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10
|
size: 10,
|
||||||
})
|
})
|
||||||
pagination.current = 1
|
pagination.current = 1
|
||||||
search()
|
search()
|
||||||
|
@ -277,23 +313,131 @@ const exportContract = () => {
|
||||||
Message.info('导出合同功能开发中...')
|
Message.info('导出合同功能开发中...')
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewDetail = (record: any) => {
|
// 显示合同编辑弹窗
|
||||||
Message.info(`查看合同详情: ${record.contractName}`)
|
const showEditModal = ref(false)
|
||||||
|
const selectedContractData = ref<ContractItem | null>(null)
|
||||||
|
const editedContractData = ref<ContractItem | null>(null)
|
||||||
|
|
||||||
|
const editRecord = (record: ContractItem) => {
|
||||||
|
// 确保所有必要的字段都存在
|
||||||
|
const completeRecord = {
|
||||||
|
...record,
|
||||||
|
amount: record.amount || 0,
|
||||||
|
projectId: record.projectId || '',
|
||||||
|
type: record.type || '收入合同',
|
||||||
|
contractStatus: record.contractStatus || '未确认',
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedContractData.value = completeRecord
|
||||||
|
showEditModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const editRecord = (record: any) => {
|
const closeEditModal = () => {
|
||||||
Message.info(`编辑合同: ${record.contractName}`)
|
showEditModal.value = false
|
||||||
|
selectedContractData.value = null
|
||||||
|
editedContractData.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const approveContract = (record: any) => {
|
const handleContractDataUpdate = (data: ContractItem) => {
|
||||||
Message.info(`审批合同: ${record.contractName}`)
|
editedContractData.value = data
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewPayment = (record: any) => {
|
const handleEditSubmit = async () => {
|
||||||
Message.info(`查看付款记录: ${record.contractName}`)
|
if (!editedContractData.value) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestData = {
|
||||||
|
...editedContractData.value,
|
||||||
|
accountNumber: editedContractData.value.accountNumber || '',
|
||||||
|
amount: editedContractData.value.amount || 0,
|
||||||
|
code: editedContractData.value.code || '',
|
||||||
|
contractId: editedContractData.value.contractId,
|
||||||
|
contractStatus: editedContractData.value.contractStatus || '',
|
||||||
|
contractText: editedContractData.value.contractText || '',
|
||||||
|
customer: editedContractData.value.customer || '',
|
||||||
|
departmentId: editedContractData.value.departmentId || '',
|
||||||
|
duration: editedContractData.value.duration || '',
|
||||||
|
notes: editedContractData.value.notes || '',
|
||||||
|
paymentAddress: editedContractData.value.paymentAddress || '',
|
||||||
|
paymentDate: editedContractData.value.paymentDate || null,
|
||||||
|
performanceDeadline: editedContractData.value.performanceDeadline || null,
|
||||||
|
productService: editedContractData.value.productService || '',
|
||||||
|
projectId: editedContractData.value.projectId || '',
|
||||||
|
salespersonId: editedContractData.value.salespersonId || '',
|
||||||
|
signDate: editedContractData.value.signDate || null,
|
||||||
|
type: editedContractData.value.type || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Edited Contract Data:', requestData); // 打印请求数据以便调试
|
||||||
|
|
||||||
|
// 修改此处,直接向 /contract 发送 PUT 请求
|
||||||
|
const response = await http.put('/contract', requestData);
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if (response.status === 200 && response.code === 200) {
|
||||||
|
Message.success('合同编辑成功');
|
||||||
|
closeEditModal();
|
||||||
|
search(); // 刷新列表
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Message.error(response.msg || '合同编辑失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('合同编辑失败:', error);
|
||||||
|
Message.error('合同编辑失败: ' + (error.message || '请稍后再试'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 删除合同
|
||||||
|
const deleteContract = async (record: ContractItem) => {
|
||||||
|
try {
|
||||||
|
await Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除合同 "${record.projectName}" 吗?`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await http.delete(`/contract/${record.contractId}`)
|
||||||
|
if (response.code === 200) {
|
||||||
|
Message.success('合同删除成功')
|
||||||
|
search() // 刷新列表
|
||||||
|
} else {
|
||||||
|
Message.error(response.msg || '合同删除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消删除或请求失败
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('合同删除失败:', error)
|
||||||
|
Message.error('合同删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示合同详情弹窗
|
||||||
|
const showDetailModal = ref(false)
|
||||||
|
const selectedContractId = ref<string | null>(null)
|
||||||
|
|
||||||
|
const viewDetail = (record: ContractItem) => {
|
||||||
|
selectedContractId.value = record.contractId
|
||||||
|
showDetailModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDetailModal = () => {
|
||||||
|
showDetailModal.value = false
|
||||||
|
selectedContractId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const approveContract = (record: ContractItem) => {
|
||||||
|
Message.info(`审批合同: ${record.projectName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewPayment = (record: ContractItem) => {
|
||||||
|
Message.info(`查看收款记录: ${record.projectName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
search()
|
fetchContractList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
|
@ -0,0 +1,122 @@
|
||||||
|
<template>
|
||||||
|
<a-form :model="contractData" layout="vertical">
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="code" label="合同编号">
|
||||||
|
<a-input v-model="contractData.code" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="projectName" label="项目名称">
|
||||||
|
<a-input v-model="contractData.projectName" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="customer" label="客户名称">
|
||||||
|
<a-input v-model="contractData.customer" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="amount" label="合同金额">
|
||||||
|
<a-input-number v-model="contractData.amount" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="accountNumber" label="收款账号">
|
||||||
|
<a-input v-model="contractData.accountNumber" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="contractStatus" label="合同状态">
|
||||||
|
<a-select v-model="contractData.contractStatus">
|
||||||
|
<a-option value="未确认">未确认</a-option>
|
||||||
|
<a-option value="待审批">待审批</a-option>
|
||||||
|
<a-option value="已签署">已签署</a-option>
|
||||||
|
<a-option value="执行中">执行中</a-option>
|
||||||
|
<a-option value="已完成">已完成</a-option>
|
||||||
|
<a-option value="已终止">已终止</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="signDate" label="签订日期">
|
||||||
|
<a-date-picker v-model="contractData.signDate" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="performanceDeadline" label="履约期限">
|
||||||
|
<a-date-picker v-model="contractData.performanceDeadline" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="paymentDate" label="付款日期">
|
||||||
|
<a-date-picker v-model="contractData.paymentDate" style="width: 100%" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item field="productService" label="产品或服务">
|
||||||
|
<a-input v-model="contractData.productService" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item field="paymentAddress" label="付款地址">
|
||||||
|
<a-input v-model="contractData.paymentAddress" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="notes" label="备注">
|
||||||
|
<a-textarea v-model="contractData.notes" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item field="contractText" label="合同内容">
|
||||||
|
<a-textarea v-model="contractData.contractText" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import type { ContractItem } from './index.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
contractData: ContractItem
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:contractData', data: ContractItem): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const contractData = ref({ ...props.contractData })
|
||||||
|
|
||||||
|
// 监听props变化更新内部数据
|
||||||
|
watch(
|
||||||
|
() => props.contractData,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
contractData.value = { ...newVal }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听内部数据变化并触发更新事件
|
||||||
|
watch(
|
||||||
|
contractData,
|
||||||
|
(newVal) => {
|
||||||
|
emit('update:contractData', newVal)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
</script>
|
|
@ -53,13 +53,13 @@
|
||||||
<span class="font-medium text-blue-600">¥{{ (record.receivedAmount || 0).toLocaleString() }}</span>
|
<span class="font-medium text-blue-600">¥{{ (record.receivedAmount || 0).toLocaleString() }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 操作列 -->
|
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-link @click="viewDetail(record)">详情</a-link>
|
<a-link @click="viewDetail(record)">详情</a-link>
|
||||||
<a-link v-if="record.contractStatus === '未确认'" @click="editRecord(record)">编辑</a-link>
|
<a-link v-if="record.contractStatus === '未确认'" @click="editRecord(record)">编辑</a-link>
|
||||||
<a-link v-if="record.contractStatus === '待审批'" @click="approveContract(record)">审批</a-link>
|
<a-link v-if="record.contractStatus === '待审批'" @click="approveContract(record)">审批</a-link>
|
||||||
<a-link @click="viewPayment(record)">收款记录</a-link>
|
<a-link @click="viewPayment(record)">收款记录</a-link>
|
||||||
|
<a-link v-if="record.contractStatus !== '已签署' && record.contractStatus !== '已完成'" @click="deleteContract(record)">删除</a-link>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</GiTable>
|
</GiTable>
|
||||||
|
@ -69,20 +69,37 @@
|
||||||
v-model:visible="showDetailModal"
|
v-model:visible="showDetailModal"
|
||||||
title="合同详情"
|
title="合同详情"
|
||||||
:width="800"
|
:width="800"
|
||||||
@cancel="closeDetailModal"
|
|
||||||
:footer="false"
|
:footer="false"
|
||||||
|
@cancel="closeDetailModal"
|
||||||
>
|
>
|
||||||
<ContractDetail v-if="showDetailModal" :contract-id="selectedContractId" />
|
<ContractDetail v-if="showDetailModal" :contract-id="selectedContractId" />
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 合同编辑弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="showEditModal"
|
||||||
|
title="编辑合同"
|
||||||
|
:width="800"
|
||||||
|
@cancel="closeEditModal"
|
||||||
|
@before-ok="handleEditSubmit"
|
||||||
|
>
|
||||||
|
<ContractEdit
|
||||||
|
v-if="showEditModal"
|
||||||
|
:contract-data="selectedContractData"
|
||||||
|
@update:contract-data="handleContractDataUpdate"
|
||||||
|
/>
|
||||||
|
</a-modal>
|
||||||
</GiPageLayout>
|
</GiPageLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
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 http from '@/utils/http'
|
import ContractEdit from './ContractEdit.vue'
|
||||||
|
|
||||||
import ContractDetail from './ContractDetail.vue'
|
import ContractDetail from './ContractDetail.vue'
|
||||||
|
import http from '@/utils/http'
|
||||||
|
|
||||||
// 接口数据类型定义
|
// 接口数据类型定义
|
||||||
interface ContractItem {
|
interface ContractItem {
|
||||||
|
@ -181,7 +198,6 @@ const tableColumns: TableColumnData[] = [
|
||||||
{ title: '备注', dataIndex: 'notes', width: 200, ellipsis: true, tooltip: true },
|
{ title: '备注', dataIndex: 'notes', width: 200, ellipsis: true, tooltip: true },
|
||||||
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' },
|
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 数据状态
|
// 数据状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const dataList = ref<ContractItem[]>([])
|
const dataList = ref<ContractItem[]>([])
|
||||||
|
@ -297,6 +313,108 @@ const exportContract = () => {
|
||||||
Message.info('导出合同功能开发中...')
|
Message.info('导出合同功能开发中...')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示合同编辑弹窗
|
||||||
|
const showEditModal = ref(false)
|
||||||
|
const selectedContractData = ref<ContractItem | null>(null)
|
||||||
|
const editedContractData = ref<ContractItem | null>(null)
|
||||||
|
|
||||||
|
const editRecord = (record: ContractItem) => {
|
||||||
|
// 确保所有必要的字段都存在
|
||||||
|
const completeRecord = {
|
||||||
|
...record,
|
||||||
|
amount: record.amount || 0,
|
||||||
|
projectId: record.projectId || '',
|
||||||
|
type: record.type || '收入合同',
|
||||||
|
contractStatus: record.contractStatus || '未确认',
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedContractData.value = completeRecord
|
||||||
|
showEditModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeEditModal = () => {
|
||||||
|
showEditModal.value = false
|
||||||
|
selectedContractData.value = null
|
||||||
|
editedContractData.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleContractDataUpdate = (data: ContractItem) => {
|
||||||
|
editedContractData.value = data
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditSubmit = async () => {
|
||||||
|
if (!editedContractData.value) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestData = {
|
||||||
|
...editedContractData.value,
|
||||||
|
accountNumber: editedContractData.value.accountNumber || '',
|
||||||
|
amount: editedContractData.value.amount || 0,
|
||||||
|
code: editedContractData.value.code || '',
|
||||||
|
contractId: editedContractData.value.contractId,
|
||||||
|
contractStatus: editedContractData.value.contractStatus || '',
|
||||||
|
contractText: editedContractData.value.contractText || '',
|
||||||
|
customer: editedContractData.value.customer || '',
|
||||||
|
departmentId: editedContractData.value.departmentId || '',
|
||||||
|
duration: editedContractData.value.duration || '',
|
||||||
|
notes: editedContractData.value.notes || '',
|
||||||
|
paymentAddress: editedContractData.value.paymentAddress || '',
|
||||||
|
paymentDate: editedContractData.value.paymentDate || null,
|
||||||
|
performanceDeadline: editedContractData.value.performanceDeadline || null,
|
||||||
|
productService: editedContractData.value.productService || '',
|
||||||
|
projectId: editedContractData.value.projectId || '',
|
||||||
|
salespersonId: editedContractData.value.salespersonId || '',
|
||||||
|
signDate: editedContractData.value.signDate || null,
|
||||||
|
type: editedContractData.value.type || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Edited Contract Data:', requestData); // 打印请求数据以便调试
|
||||||
|
|
||||||
|
// 修改此处,直接向 /contract 发送 PUT 请求
|
||||||
|
const response = await http.put('/contract', requestData);
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if (response.status === 200 && response.code === 200) {
|
||||||
|
Message.success('合同编辑成功');
|
||||||
|
closeEditModal();
|
||||||
|
search(); // 刷新列表
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Message.error(response.msg || '合同编辑失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('合同编辑失败:', error);
|
||||||
|
Message.error('合同编辑失败: ' + (error.message || '请稍后再试'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 删除合同
|
||||||
|
const deleteContract = async (record: ContractItem) => {
|
||||||
|
try {
|
||||||
|
await Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除合同 "${record.projectName}" 吗?`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await http.delete(`/contract/${record.contractId}`)
|
||||||
|
if (response.code === 200) {
|
||||||
|
Message.success('合同删除成功')
|
||||||
|
search() // 刷新列表
|
||||||
|
} else {
|
||||||
|
Message.error(response.msg || '合同删除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消删除或请求失败
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('合同删除失败:', error)
|
||||||
|
Message.error('合同删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 显示合同详情弹窗
|
// 显示合同详情弹窗
|
||||||
const showDetailModal = ref(false)
|
const showDetailModal = ref(false)
|
||||||
const selectedContractId = ref<string | null>(null)
|
const selectedContractId = ref<string | null>(null)
|
||||||
|
@ -311,10 +429,6 @@ const closeDetailModal = () => {
|
||||||
selectedContractId.value = null
|
selectedContractId.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const editRecord = (record: ContractItem) => {
|
|
||||||
Message.info(`编辑合同: ${record.projectName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const approveContract = (record: ContractItem) => {
|
const approveContract = (record: ContractItem) => {
|
||||||
Message.info(`审批合同: ${record.projectName}`)
|
Message.info(`审批合同: ${record.projectName}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -669,7 +669,7 @@ const closeProjectDetail = () => {
|
||||||
const openPersonnelManagement = () => {
|
const openPersonnelManagement = () => {
|
||||||
if (currentProject.value && currentProject.value.id) {
|
if (currentProject.value && currentProject.value.id) {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/project-management/projects/personnel-dispatch/construction-personnel',
|
path: '/project-management/personnel-dispatch/construction-personnel',
|
||||||
query: { projectId: currentProject.value.id }
|
query: { projectId: currentProject.value.id }
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -129,29 +129,18 @@ const debouncedSearch = debounce(() => {
|
||||||
const search = () => {
|
const search = () => {
|
||||||
console.log('🔍 ApprovalSearch - 搜索按钮被点击')
|
console.log('🔍 ApprovalSearch - 搜索按钮被点击')
|
||||||
console.log('🔍 ApprovalSearch - 搜索表单数据:', searchForm)
|
console.log('🔍 ApprovalSearch - 搜索表单数据:', searchForm)
|
||||||
console.log('🔍 ApprovalSearch - 搜索表单数据类型:', typeof searchForm)
|
|
||||||
console.log('🔍 ApprovalSearch - 搜索表单的键值对:')
|
|
||||||
Object.entries(searchForm).forEach(([key, value]) => {
|
|
||||||
console.log(` ${key}: ${value} (${typeof value})`)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 构建搜索参数
|
// 构建搜索参数 - 参考设备采购功能的实现
|
||||||
const searchParams: EquipmentApprovalListReq = {
|
const searchParams: EquipmentApprovalListReq = {
|
||||||
equipmentName: searchForm.equipmentName || undefined,
|
equipmentName: searchForm.equipmentName || undefined,
|
||||||
applicantName: searchForm.applicantName || undefined,
|
applicantName: searchForm.applicantName || undefined,
|
||||||
businessType: searchForm.businessType,
|
businessType: searchForm.businessType || undefined,
|
||||||
approvalStatus: searchForm.approvalStatus,
|
approvalStatus: searchForm.approvalStatus || undefined,
|
||||||
applyTimeStart: searchForm.applyTimeStart,
|
applyTimeStart: searchForm.applyTimeStart || undefined,
|
||||||
applyTimeEnd: searchForm.applyTimeEnd
|
applyTimeEnd: searchForm.applyTimeEnd || undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔍 ApprovalSearch - 发送的搜索参数:', searchParams)
|
console.log('🔍 ApprovalSearch - 发送的搜索参数:', searchParams)
|
||||||
console.log('🔍 ApprovalSearch - 发送参数的类型:', typeof searchParams)
|
|
||||||
console.log('🔍 ApprovalSearch - 发送参数的键值对:')
|
|
||||||
Object.entries(searchParams).forEach(([key, value]) => {
|
|
||||||
console.log(` ${key}: ${value} (${typeof value})`)
|
|
||||||
})
|
|
||||||
|
|
||||||
emit('search', searchParams)
|
emit('search', searchParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -458,10 +458,10 @@ const loadData = async (searchParams?: EquipmentApprovalListReq) => {
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
|
// 构建完整的请求参数 - 参考设备采购功能的实现
|
||||||
const params: EquipmentApprovalListReq = {
|
const params: EquipmentApprovalListReq = {
|
||||||
pageSize: pagination.pageSize || 10,
|
pageSize: pagination.pageSize,
|
||||||
page: pagination.current || 1,
|
page: pagination.current,
|
||||||
pageNum: pagination.current || 1, // 添加 pageNum 参数,以防后端期望这个名称
|
|
||||||
...(searchParams || {}),
|
...(searchParams || {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,340 @@
|
||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:visible="visible"
|
||||||
|
:title="modalTitle"
|
||||||
|
width="800px"
|
||||||
|
@ok="handleSubmit"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
@update:visible="(value) => emit('update:visible', value)"
|
||||||
|
:confirm-loading="submitLoading"
|
||||||
|
>
|
||||||
|
<a-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
layout="vertical"
|
||||||
|
class="procurement-application-form"
|
||||||
|
>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="设备名称" field="equipmentName">
|
||||||
|
<a-input
|
||||||
|
v-model="formData.equipmentName"
|
||||||
|
placeholder="请输入设备名称"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="设备类型" field="equipmentType">
|
||||||
|
<a-select
|
||||||
|
v-model="formData.equipmentType"
|
||||||
|
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-option value="显示设备">显示设备</a-option>
|
||||||
|
<a-option value="测试设备">测试设备</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="设备型号" field="equipmentModel">
|
||||||
|
<a-input
|
||||||
|
v-model="formData.equipmentModel"
|
||||||
|
placeholder="请输入设备型号"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="品牌" field="brand">
|
||||||
|
<a-input
|
||||||
|
v-model="formData.brand"
|
||||||
|
placeholder="请输入品牌"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="供应商名称" field="supplierName">
|
||||||
|
<a-input
|
||||||
|
v-model="formData.supplierName"
|
||||||
|
placeholder="请输入供应商名称"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="数量" field="quantity">
|
||||||
|
<a-input-number
|
||||||
|
v-model="formData.quantity"
|
||||||
|
placeholder="请输入数量"
|
||||||
|
:min="1"
|
||||||
|
:precision="0"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="预算金额" field="budgetAmount">
|
||||||
|
<a-input-number
|
||||||
|
v-model="formData.budgetAmount"
|
||||||
|
placeholder="请输入预算金额"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="紧急程度" field="urgencyLevel">
|
||||||
|
<a-select
|
||||||
|
v-model="formData.urgencyLevel"
|
||||||
|
placeholder="请选择紧急程度"
|
||||||
|
allow-clear
|
||||||
|
>
|
||||||
|
<a-option value="LOW">低</a-option>
|
||||||
|
<a-option value="NORMAL">普通</a-option>
|
||||||
|
<a-option value="HIGH">高</a-option>
|
||||||
|
<a-option value="URGENT">紧急</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="采购类型" field="procurementType">
|
||||||
|
<a-select
|
||||||
|
v-model="formData.procurementType"
|
||||||
|
placeholder="请选择采购类型"
|
||||||
|
allow-clear
|
||||||
|
>
|
||||||
|
<a-option value="NEW_PURCHASE">新采购</a-option>
|
||||||
|
<a-option value="REPLACEMENT">更换</a-option>
|
||||||
|
<a-option value="UPGRADE">升级</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="预期交付日期" field="expectedDeliveryDate">
|
||||||
|
<a-date-picker
|
||||||
|
v-model="formData.expectedDeliveryDate"
|
||||||
|
placeholder="请选择预期交付日期"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item label="申请原因" field="applyReason">
|
||||||
|
<a-textarea
|
||||||
|
v-model="formData.applyReason"
|
||||||
|
placeholder="请详细说明采购原因"
|
||||||
|
:rows="4"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="技术要求" field="technicalRequirements">
|
||||||
|
<a-textarea
|
||||||
|
v-model="formData.technicalRequirements"
|
||||||
|
placeholder="请描述设备的技术要求(可选)"
|
||||||
|
:rows="3"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="业务合理性说明" field="businessJustification">
|
||||||
|
<a-textarea
|
||||||
|
v-model="formData.businessJustification"
|
||||||
|
placeholder="请说明采购的业务合理性(可选)"
|
||||||
|
:rows="3"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { equipmentApprovalApi } from '@/apis/equipment/approval'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
equipmentData?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:visible', value: boolean): void
|
||||||
|
(e: 'success'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const modalTitle = ref('申请采购')
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = reactive({
|
||||||
|
equipmentId: '', // 添加设备ID字段
|
||||||
|
equipmentName: '',
|
||||||
|
equipmentType: '',
|
||||||
|
equipmentModel: '',
|
||||||
|
brand: '',
|
||||||
|
supplierName: '',
|
||||||
|
quantity: 1,
|
||||||
|
budgetAmount: undefined,
|
||||||
|
urgencyLevel: 'NORMAL',
|
||||||
|
procurementType: 'NEW_PURCHASE',
|
||||||
|
expectedDeliveryDate: null,
|
||||||
|
applyReason: '',
|
||||||
|
technicalRequirements: '',
|
||||||
|
businessJustification: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const rules = {
|
||||||
|
equipmentId: [{ required: true, message: '设备ID不能为空' }],
|
||||||
|
equipmentName: [{ required: true, message: '请输入设备名称' }],
|
||||||
|
equipmentType: [{ required: true, message: '请选择设备类型' }],
|
||||||
|
quantity: [{ required: true, message: '请输入数量' }],
|
||||||
|
budgetAmount: [{ required: true, message: '请输入预算金额' }],
|
||||||
|
urgencyLevel: [{ required: true, message: '请选择紧急程度' }],
|
||||||
|
procurementType: [{ required: true, message: '请选择采购类型' }],
|
||||||
|
applyReason: [
|
||||||
|
{ required: true, message: '请输入申请原因' },
|
||||||
|
{
|
||||||
|
validator: (value: string) => {
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
return new Error('申请原因不能为空')
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听设备数据变化,填充表单
|
||||||
|
watch(() => props.equipmentData, (newData) => {
|
||||||
|
if (newData) {
|
||||||
|
// 填充设备数据到表单
|
||||||
|
Object.assign(formData, {
|
||||||
|
equipmentId: newData.equipmentId || '', // 添加设备ID
|
||||||
|
equipmentName: newData.equipmentName || '',
|
||||||
|
equipmentType: newData.equipmentType || '',
|
||||||
|
equipmentModel: newData.equipmentModel || '',
|
||||||
|
brand: newData.brand || '',
|
||||||
|
supplierName: newData.supplierName || '',
|
||||||
|
quantity: newData.quantity || 1,
|
||||||
|
budgetAmount: newData.unitPrice || undefined,
|
||||||
|
urgencyLevel: 'NORMAL',
|
||||||
|
procurementType: 'NEW_PURCHASE',
|
||||||
|
expectedDeliveryDate: null,
|
||||||
|
applyReason: '', // 申请原因需要用户填写
|
||||||
|
technicalRequirements: '',
|
||||||
|
businessJustification: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 监听模态框显示状态
|
||||||
|
watch(() => props.visible, (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
// 模态框打开时,如果有设备数据则填充,否则重置表单
|
||||||
|
if (props.equipmentData) {
|
||||||
|
// 设备数据会在上面的 watch 中处理
|
||||||
|
} else {
|
||||||
|
// 没有设备数据时重置表单
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
// 手动验证 applyReason 字段
|
||||||
|
if (!formData.applyReason || formData.applyReason.trim() === '') {
|
||||||
|
Message.error('请输入申请原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await formRef.value.validate()
|
||||||
|
submitLoading.value = true
|
||||||
|
|
||||||
|
const submitData = {
|
||||||
|
...formData,
|
||||||
|
applyReason: formData.applyReason.trim(), // 确保去除空格
|
||||||
|
expectedDeliveryDate: formData.expectedDeliveryDate ?
|
||||||
|
(formData.expectedDeliveryDate as any).format('YYYY-MM-DD') : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调试信息:打印提交的数据
|
||||||
|
console.log('提交的表单数据:', submitData)
|
||||||
|
console.log('applyReason 值:', submitData.applyReason)
|
||||||
|
console.log('applyReason 类型:', typeof submitData.applyReason)
|
||||||
|
console.log('applyReason 长度:', submitData.applyReason?.length)
|
||||||
|
|
||||||
|
await equipmentApprovalApi.submitProcurementApplication(submitData)
|
||||||
|
|
||||||
|
Message.success('采购申请提交成功')
|
||||||
|
emit('success')
|
||||||
|
handleCancel()
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('提交采购申请失败:', error)
|
||||||
|
Message.error(error?.message || '提交失败')
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('update:visible', false)
|
||||||
|
// 清空用户输入的数据,保留设备基本信息
|
||||||
|
formData.equipmentId = ''
|
||||||
|
formData.applyReason = ''
|
||||||
|
formData.technicalRequirements = ''
|
||||||
|
formData.businessJustification = ''
|
||||||
|
formData.expectedDeliveryDate = null
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.procurement-application-form {
|
||||||
|
.arco-form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-input,
|
||||||
|
.arco-select,
|
||||||
|
.arco-input-number,
|
||||||
|
.arco-date-picker {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-textarea {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -175,6 +175,19 @@
|
||||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||||
编辑
|
编辑
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<!-- 申请采购按钮 -->
|
||||||
|
<a-button
|
||||||
|
v-if="canApplyProcurement(record)"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleApplyProcurement(record)"
|
||||||
|
>
|
||||||
|
申请采购
|
||||||
|
</a-button>
|
||||||
|
<!-- 显示审批状态 -->
|
||||||
|
<a-tag v-if="record.approvalStatus" :color="getApprovalStatusColor(record.approvalStatus)">
|
||||||
|
{{ getApprovalStatusText(record.approvalStatus) }}
|
||||||
|
</a-tag>
|
||||||
<a-popconfirm
|
<a-popconfirm
|
||||||
content="确定要删除这条采购记录吗?"
|
content="确定要删除这条采购记录吗?"
|
||||||
@ok="handleDelete(record)"
|
@ok="handleDelete(record)"
|
||||||
|
@ -195,6 +208,13 @@
|
||||||
:mode="modalMode"
|
:mode="modalMode"
|
||||||
@success="handleModalSuccess"
|
@success="handleModalSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 采购申请弹窗 -->
|
||||||
|
<ProcurementApplicationModal
|
||||||
|
v-model:visible="applicationModalVisible"
|
||||||
|
:equipment-data="currentApplicationData"
|
||||||
|
@success="handleApplicationSuccess"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -213,7 +233,9 @@ import {
|
||||||
import message from '@arco-design/web-vue/es/message'
|
import message from '@arco-design/web-vue/es/message'
|
||||||
import ProcurementModal from './components/ProcurementModal.vue'
|
import ProcurementModal from './components/ProcurementModal.vue'
|
||||||
import ProcurementSearch from './components/ProcurementSearch.vue'
|
import ProcurementSearch from './components/ProcurementSearch.vue'
|
||||||
|
import ProcurementApplicationModal from './components/ProcurementApplicationModal.vue'
|
||||||
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
|
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
|
||||||
|
import { equipmentApprovalApi } from '@/apis/equipment/approval'
|
||||||
import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type'
|
import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type'
|
||||||
|
|
||||||
defineOptions({ name: 'EquipmentProcurement' })
|
defineOptions({ name: 'EquipmentProcurement' })
|
||||||
|
@ -243,6 +265,10 @@ const modalVisible = ref(false)
|
||||||
const currentProcurement = ref<EquipmentResp | null>(null)
|
const currentProcurement = ref<EquipmentResp | null>(null)
|
||||||
const modalMode = ref<'add' | 'edit' | 'view'>('add')
|
const modalMode = ref<'add' | 'edit' | 'view'>('add')
|
||||||
|
|
||||||
|
// 采购申请弹窗控制
|
||||||
|
const applicationModalVisible = ref(false)
|
||||||
|
const currentApplicationData = ref<EquipmentResp | null>(null)
|
||||||
|
|
||||||
// 表格选择
|
// 表格选择
|
||||||
const selectedRowKeys = ref<string[]>([])
|
const selectedRowKeys = ref<string[]>([])
|
||||||
const rowSelection = reactive({
|
const rowSelection = reactive({
|
||||||
|
@ -611,6 +637,12 @@ const handleEdit = (record: EquipmentResp) => {
|
||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 申请采购
|
||||||
|
const handleApplyProcurement = (record: EquipmentResp) => {
|
||||||
|
currentApplicationData.value = { ...record }
|
||||||
|
applicationModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 删除
|
// 删除
|
||||||
const handleDelete = async (record: EquipmentResp) => {
|
const handleDelete = async (record: EquipmentResp) => {
|
||||||
try {
|
try {
|
||||||
|
@ -629,6 +661,12 @@ const handleModalSuccess = () => {
|
||||||
loadData(currentSearchParams.value)
|
loadData(currentSearchParams.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 采购申请成功回调
|
||||||
|
const handleApplicationSuccess = () => {
|
||||||
|
applicationModalVisible.value = false
|
||||||
|
loadData(currentSearchParams.value)
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
const refreshData = () => {
|
const refreshData = () => {
|
||||||
loadData(currentSearchParams.value)
|
loadData(currentSearchParams.value)
|
||||||
|
@ -674,6 +712,36 @@ const getTotalAmount = () => {
|
||||||
return formatPrice(total)
|
return formatPrice(total)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否可以申请采购
|
||||||
|
const canApplyProcurement = (record: EquipmentResp) => {
|
||||||
|
// 检查是否有审批状态,如果没有或者状态为待申请,则可以申请
|
||||||
|
return !record.approvalStatus || record.approvalStatus === 'PENDING_APPLICATION'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取审批状态颜色
|
||||||
|
const getApprovalStatusColor = (status: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
'PENDING_APPLICATION': 'blue',
|
||||||
|
'PENDING': 'orange',
|
||||||
|
'APPROVED': 'green',
|
||||||
|
'REJECTED': 'red',
|
||||||
|
'WITHDRAWN': 'gray'
|
||||||
|
}
|
||||||
|
return colorMap[status] || 'blue'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取审批状态文本
|
||||||
|
const getApprovalStatusText = (status: string) => {
|
||||||
|
const textMap: Record<string, string> = {
|
||||||
|
'PENDING_APPLICATION': '待申请',
|
||||||
|
'PENDING': '待审批',
|
||||||
|
'APPROVED': '已通过',
|
||||||
|
'REJECTED': '已拒绝',
|
||||||
|
'WITHDRAWN': '已撤回'
|
||||||
|
}
|
||||||
|
return textMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData()
|
loadData()
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,6 +5,11 @@ import createVitePlugins from './config/plugins'
|
||||||
export default defineConfig(({ command, mode }) => {
|
export default defineConfig(({ command, mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd()) as ImportMetaEnv
|
const env = loadEnv(mode, process.cwd()) as ImportMetaEnv
|
||||||
|
|
||||||
|
// 设置默认的WebSocket URL
|
||||||
|
if (!env.VITE_API_WS_URL) {
|
||||||
|
env.VITE_API_WS_URL = 'ws://localhost:8888'
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 开发或生产环境服务的公共基础路径
|
// 开发或生产环境服务的公共基础路径
|
||||||
base: env.VITE_BASE,
|
base: env.VITE_BASE,
|
||||||
|
|
Loading…
Reference in New Issue