This commit is contained in:
马诗敏 2025-08-10 20:46:42 +08:00
commit a80a23fd74
22 changed files with 3503 additions and 1152 deletions

View File

@ -3,8 +3,8 @@
VITE_API_PREFIX = '/dev-api'
# 接口地址
# VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
VITE_API_BASE_URL = 'http://localhost:8888/'
VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
# VITE_API_BASE_URL = 'http://localhost:8888/'
# 接口地址 (WebSocket)
VITE_API_WS_URL = 'ws://localhost:8000'
@ -13,7 +13,7 @@ VITE_API_WS_URL = 'ws://localhost:8000'
VITE_BASE = '/'
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = false
VITE_OPEN_DEVTOOLS = true
# 应用配置面板
VITE_APP_SETTING = true

88
PREVIEW_FEATURE_GUIDE.md Normal file
View File

@ -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. 支持文档的分页预览功能

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,49 +1,70 @@
import http from '@/utils/http'
import type { EquipmentApprovalReq, EquipmentApprovalResp, EquipmentApprovalListReq } from './type'
import type { EquipmentApprovalListReq, EquipmentApprovalResp } from './type'
/**
* API
* API
*/
export const equipmentApprovalApi = {
/**
*
*/
getPendingApprovals: (params: EquipmentApprovalListReq) => {
return http.get<ApiRes<PageRes<EquipmentApprovalResp>>>('/equipment/approval/pending', { params })
getPendingApprovals(params: EquipmentApprovalListReq) {
return http.get<EquipmentApprovalResp[]>('/equipment/approval/pending', params)
},
/**
*
*/
getApprovedApprovals: (params: EquipmentApprovalListReq) => {
return http.get<ApiRes<PageRes<EquipmentApprovalResp>>>('/equipment/approval/approved', { params })
getApprovedApprovals(params: EquipmentApprovalListReq) {
return http.get<EquipmentApprovalResp[]>('/equipment/approval/approved', params)
},
/**
*
*/
approve: (approvalId: string, data: EquipmentApprovalReq) => {
return http.post<ApiRes<null>>(`/equipment/approval/${approvalId}/approve`, data)
approve(approvalId: string, data: any) {
return http.post(`/equipment/approval/${approvalId}/approve`, data)
},
/**
*
*/
reject: (approvalId: string, data: EquipmentApprovalReq) => {
return http.post<ApiRes<null>>(`/equipment/approval/${approvalId}/reject`, data)
reject(approvalId: string, data: any) {
return http.post(`/equipment/approval/${approvalId}/reject`, data)
},
/**
*
*/
getApprovalDetail: (approvalId: string) => {
return http.get<ApiRes<EquipmentApprovalResp>>(`/equipment/approval/${approvalId}`)
getApprovalDetail(approvalId: string) {
return http.get<EquipmentApprovalResp>(`/equipment/approval/${approvalId}`)
},
/**
*
*/
getApprovalStats: () => {
return http.get<ApiRes<unknown>>('/equipment/approval/stats')
getApprovalStats() {
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`)
}
}

View File

@ -19,8 +19,13 @@
:content-style="{ marginTop: '-5px', padding: 0, border: 'none' }"
:arrow-style="{ width: 0, height: 0 }"
>
<a-badge :count="unreadMessageCount" dot>
<a-button size="mini" class="gi_hover_btn">
<a-badge
:count="unreadMessageCount"
:dot="unreadMessageCount > 0"
:show-zero="false"
class="notification-badge"
>
<a-button size="mini" class="gi_hover_btn notification-btn">
<template #icon>
<icon-notification :size="18" />
</template>
@ -74,9 +79,9 @@
</template>
<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 { onMounted, ref, nextTick } from 'vue'
import { onMounted, ref, nextTick, onBeforeUnmount } from 'vue'
import Message from './Message.vue'
import SettingDrawer from './SettingDrawer.vue'
import Search from './Search.vue'
@ -97,10 +102,148 @@ onBeforeUnmount(() => {
socket.close()
socket = null
}
//
if (titleFlashInterval) {
clearInterval(titleFlashInterval)
titleFlashInterval = null
}
})
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 - 使
let initTimer: NodeJS.Timeout | null = null
const initWebSocket = (token: string) => {
@ -116,29 +259,70 @@ const initWebSocket = (token: string) => {
}
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 = () => {
// console.log('WebSocket connection opened')
console.log('WebSocket连接成功')
}
socket.onmessage = (event) => {
const count = Number.parseInt(event.data)
if (!isNaN(count)) {
unreadMessageCount.value = count
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)
if (!isNaN(count)) {
unreadMessageCount.value = count
}
}
}
socket.onerror = () => {
// console.error('WebSocket error:', error)
socket.onerror = (error) => {
console.error('WebSocket连接错误:', error)
}
socket.onclose = () => {
// console.log('WebSocket connection closed')
socket.onclose = (event) => {
console.log('WebSocket连接关闭:', event.code, event.reason)
socket = null
}
} catch (error) {
console.error('Failed to create WebSocket connection:', error)
console.error('创建WebSocket连接失败:', error)
}
initTimer = null
@ -149,10 +333,17 @@ const initWebSocket = (token: string) => {
const getMessageCount = async () => {
try {
const token = getToken()
console.log('获取到token:', token ? '存在' : '不存在')
if (token && !socket) {
console.log('准备初始化WebSocket连接')
nextTick(() => {
initWebSocket(token)
})
} else if (!token) {
console.warn('Token不存在无法建立WebSocket连接')
} else if (socket) {
console.log('WebSocket连接已存在')
}
} catch (error) {
console.error('Failed to get message count:', error)
@ -186,9 +377,32 @@ const logout = () => {
onMounted(() => {
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>
<style scoped lang="scss">
@ -210,4 +424,56 @@ onMounted(() => {
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>

1
src/types/env.d.ts vendored
View File

@ -4,6 +4,7 @@
interface ImportMetaEnv {
readonly VITE_API_PREFIX: string
readonly VITE_API_BASE_URL: string
readonly VITE_API_WS_URL: string
readonly VITE_BASE: string
readonly VITE_APP_SETTING: string
readonly VITE_CLIENT_ID: string

View File

@ -225,7 +225,3 @@ export default {
requestRaw,
download,
}
export const updateContract = (contractId, contractData) => {
return http.put(`/contract/${contractId}`, contractData)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,125 @@
<template>
<div>
<a-tab-pane key="props" tap="形变" title="形变原数据">
<div class="tab-content">
<raw-data>
</raw-data>
</div>
</a-tab-pane>
</div>
</template>
<script setup>
import rawData from './raw-data.vue';
</script>
<style lang="scss" scoped>
.data-storage-container {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
}
.page-header {
margin-bottom: 16px;
.page-title {
font-size: 20px;
font-weight: 500;
color: #262626;
margin: 0;
}
}
.tabs-section {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.tab-content {
margin-top: 16px;
}
.filter-section {
margin-bottom: 16px;
padding: 12px;
background: #fafafa;
border-radius: 4px;
.filter-item {
display: inline-flex;
align-items: center;
.filter-label {
font-size: 14px;
color: #595959;
margin-right: 8px;
}
}
}
.uploaded-files-section {
.section-title {
font-size: 16px;
font-weight: 500;
color: #262626;
margin: 0 0 16px 0;
}
}
.preview-container {
text-align: center;
.image-preview,
.video-preview {
max-height: 500px;
overflow: hidden;
}
.file-info {
text-align: left;
p {
margin: 8px 0;
font-size: 14px;
color: #595959;
}
}
}
:deep(.arco-tabs-nav) {
margin-bottom: 0;
}
:deep(.arco-tabs-tab) {
font-size: 14px;
padding: 8px 16px;
}
:deep(.arco-table-th) {
background-color: #fafafa;
color: #8c8c8c;
font-weight: 500;
}
:deep(.arco-table-td) {
padding: 12px 16px;
}
:deep(.arco-tag) {
border-radius: 4px;
font-size: 12px;
}
:deep(.arco-btn-size-small) {
padding: 2px 8px;
font-size: 12px;
}
:deep(.arco-upload-drag:hover) {
border-color: #1890ff;
}
</style>

View File

@ -0,0 +1,590 @@
<template>
<GiPageLayout>
<div class="raw-data-container">
<!-- <div class="page-header">
<div class="page-title">原始数据管理</div>
<div class="page-subtitle">管理和分析原始视频数据</div>
</div> -->
<div class="action-bar">
<div class="action-buttons">
<a-button type="primary" @click="showUploadModal = true">
<template #icon>
<IconUpload />
</template>
上传视频
</a-button>
<a-button type="primary" @click="handleBatchAnalysis">
<template #icon>
<IconPlayCircle />
</template>
批量分析
</a-button>
<a-button type="primary" @click="handleExportData">
<template #icon>
<IconDownload />
</template>
导出数据
</a-button>
</div>
<div class="filter-section">
<a-form :model="filterForm" layout="inline">
<a-form-item label="项目">
<a-select v-model="filterForm.projectId" placeholder="请选择项目">
<a-option value="project-1">风电场A区</a-option>
<a-option value="project-2">风电场B区</a-option>
<a-option value="project-3">风电场C区</a-option>
</a-select>
</a-form-item>
<a-form-item label="机组号">
<a-input v-model="filterForm.unitNumber" placeholder="请输入机组号" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model="filterForm.status" placeholder="请选择状态">
<a-option value="completed">已完成</a-option>
<a-option value="pending">待分析</a-option>
<a-option value="analyzing">分析中</a-option>
<a-option value="failed">失败</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleFilterChange">查询</a-button>
</a-form-item>
</a-form>
</div>
</div>
<div class="project-sections">
<div v-for="project in filteredProjects" :key="project.id" class="project-section">
<div class="project-header">
<div class="project-title">{{ project.name }}</div>
<div class="project-stats">
<div class="stat-item">
<IconVideoCamera />
{{ project.totalVideos }} 个视频
</div>
<div class="stat-item">
<IconCheckCircle />
{{ project.completedCount }} 个已完成
</div>
<div class="stat-item">
<IconClockCircle />
{{ project.pendingCount }} 个待分析
</div>
</div>
</div>
<div class="units-grid">
<div v-for="unit in project.units" :key="unit.id" class="unit-card">
<div class="unit-header">
<div class="unit-title">{{ unit.number }}</div>
<div class="unit-actions">
<a-button type="primary" size="small" @click="handleViewUnitVideos(unit)">查看全部</a-button>
<a-button type="primary" size="small" @click="handleAnalyzeUnit(unit)">{{
getAnalysisButtonText(unit.status)
}}</a-button>
</div>
</div>
<div class="videos-list">
<div v-for="video in unit.videos" :key="video.id" class="video-item">
<div class="video-thumbnail">
<img :src="video.thumbnail" alt="Video Thumbnail" />
<div class="video-overlay" @click="handlePlayVideo(video)">
<IconPlayArrowFill style="font-size: 24px; color: #fff;" />
</div>
</div>
<div class="video-info">
<div class="video-name">{{ video.name }}</div>
<div class="video-meta">
<span>{{ video.duration }}</span>
<span>{{ video.angle }}°</span>
</div>
<div class="video-status">
<a-tag :color="getStatusColor(video.status)">{{ getStatusText(video.status) }}</a-tag>
</div>
</div>
</div>
</div>
<div class="analysis-progress">
<div class="progress-info">
<span>分析进度</span>
<span>{{ unit.progress }}%</span>
</div>
<a-progress :percent="unit.progress" :show-text="false" status="active" />
</div>
</div>
</div>
</div>
</div>
<!-- 视频播放模态框 -->
<a-modal v-model:visible="videoModalVisible" title="原始视频播放" width="900px" @ok="videoModalVisible = false"
@cancel="videoModalVisible = false">
<video v-if="selectedVideo" :src="selectedVideo.url" controls
style="width: 100%; height: 480px; border-radius: 8px; background: #000;"></video>
<div v-if="selectedVideo" class="video-meta-info">
<p>项目{{ selectedVideo.projectName }}</p>
<p>机组号{{ selectedVideo.unitNumber }}</p>
<p>采集人{{ selectedVideo.collector }}</p>
<p>风速{{ selectedVideo.windSpeed }} m/s</p>
<p>转速{{ selectedVideo.rpm }} rpm</p>
<p>采集时间{{ selectedVideo.time }}</p>
<p>角度{{ selectedVideo.angle }}°</p>
</div>
</a-modal>
<!-- 上传视频模态框 -->
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" @ok="handleUpload"
@cancel="showUploadModal = false">
<a-form :model="uploadForm" layout="vertical">
<a-form-item label="项目" required>
<a-select v-model="uploadForm.projectId" placeholder="请选择项目">
<a-option value="project-1">风电场A区</a-option>
<a-option value="project-2">风电场B区</a-option>
<a-option value="project-3">风电场C区</a-option>
</a-select>
</a-form-item>
<a-form-item label="机组号" required>
<a-input v-model="uploadForm.unitNumber" placeholder="请输入机组号" />
</a-form-item>
<a-form-item label="采集人" required>
<a-input v-model="uploadForm.collector" placeholder="请输入采集人姓名" />
</a-form-item>
<a-form-item label="风速 (m/s)">
<a-input-number v-model="uploadForm.windSpeed" :min="0" />
</a-form-item>
<a-form-item label="转速 (rpm)">
<a-input-number v-model="uploadForm.rpm" :min="0" />
</a-form-item>
<a-form-item label="采集时间" required>
<a-date-picker v-model="uploadForm.time" show-time format="YYYY-MM-DD HH:mm" style="width: 100%;" />
</a-form-item>
<a-form-item label="视频文件可多选建议3个角度" required>
<a-upload v-model:file-list="uploadForm.fileList" :multiple="true" :limit="3" accept="video/*"
:auto-upload="false" list-type="picture-card">
<template #upload-button>
<a-button>选择视频</a-button>
</template>
</a-upload>
</a-form-item>
</a-form>
</a-modal>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconUpload,
IconPlayCircle,
IconDownload,
IconVideoCamera,
IconCheckCircle,
IconClockCircle,
IconPlayArrowFill
} from '@arco-design/web-vue/es/icon'
const showUploadModal = ref(false)
const videoModalVisible = ref(false)
const selectedVideo = ref<any>(null)
const filterForm = reactive({
projectId: '',
unitNumber: '',
status: ''
})
const uploadForm = reactive({
projectId: '',
unitNumber: '',
collector: '',
windSpeed: null,
rpm: null,
time: '',
fileList: []
})
//
const projects = ref([
{
id: 'project-1',
name: '风电场A区',
totalVideos: 6,
completedCount: 4,
pendingCount: 2,
units: [
{
id: 'A-001',
number: 'A-001',
status: 'completed',
progress: 100,
videos: [
{
id: 'v1',
name: 'A-001-正面',
url: '/videos/A-001-front.mp4',
thumbnail: '/images/A-001-front.jpg',
angle: 0,
duration: '00:30',
status: 'completed',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
},
{
id: 'v2',
name: 'A-001-侧面',
url: '/videos/A-001-side.mp4',
thumbnail: '/images/A-001-side.jpg',
angle: 90,
duration: '00:30',
status: 'completed',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
},
{
id: 'v3',
name: 'A-001-背面',
url: '/videos/A-001-back.mp4',
thumbnail: '/images/A-001-back.jpg',
angle: 180,
duration: '00:30',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
}
]
},
{
id: 'A-002',
number: 'A-002',
status: 'analyzing',
progress: 60,
videos: [
{
id: 'v4',
name: 'A-002-正面',
url: '/videos/A-002-front.mp4',
thumbnail: '/images/A-002-front.jpg',
angle: 0,
duration: '00:28',
status: 'analyzing',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
},
{
id: 'v5',
name: 'A-002-侧面',
url: '/videos/A-002-side.mp4',
thumbnail: '/images/A-002-side.jpg',
angle: 90,
duration: '00:28',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
},
{
id: 'v6',
name: 'A-002-背面',
url: '/videos/A-002-back.mp4',
thumbnail: '/images/A-002-back.jpg',
angle: 180,
duration: '00:28',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
}
]
}
]
}
// ...
])
const filteredProjects = computed(() => {
//
return projects.value
.filter(p => !filterForm.projectId || p.id === filterForm.projectId)
.map(project => ({
...project,
units: project.units
.filter(u => !filterForm.unitNumber || u.number === filterForm.unitNumber)
.map(unit => ({
...unit,
videos: unit.videos.filter(v => !filterForm.status || v.status === filterForm.status)
}))
.filter(u => u.videos.length > 0)
}))
.filter(p => p.units.length > 0)
})
function handleFilterChange() {
// API
}
function handlePlayVideo(video: any) {
selectedVideo.value = video
videoModalVisible.value = true
}
function handleViewUnitVideos(unit: any) {
//
Message.info(`查看机组 ${unit.number} 的所有视频`)
}
function handleAnalyzeUnit(unit: any) {
//
Message.success(`已提交机组 ${unit.number} 的分析任务`)
// API
}
function getStatusColor(status: string) {
switch (status) {
case 'completed': return 'green'
case 'pending': return 'gray'
case 'analyzing': return 'blue'
case 'failed': return 'red'
default: return 'gray'
}
}
function getStatusText(status: string) {
switch (status) {
case 'completed': return '已完成'
case 'pending': return '待分析'
case 'analyzing': return '分析中'
case 'failed': return '失败'
default: return '未知'
}
}
function getAnalysisButtonText(status: string) {
switch (status) {
case 'completed': return '重新分析'
case 'pending': return '分析'
case 'analyzing': return '分析中...'
case 'failed': return '重新分析'
default: return '分析'
}
}
function handleBatchAnalysis() {
Message.success('批量分析任务已提交')
}
function handleExportData() {
Message.success('数据导出成功')
}
function handleUpload() {
Message.success('上传成功')
showUploadModal.value = false
}
</script>
<style scoped lang="scss">
.raw-data-container {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
margin: 0 0 8px 0;
color: #1d2129;
}
.page-subtitle {
font-size: 14px;
color: #86909c;
margin: 0;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.filter-section {
margin-left: 24px;
}
}
.project-sections {
.project-section {
margin-bottom: 32px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px #f0f1f2;
padding: 20px;
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.project-title {
font-size: 20px;
font-weight: 600;
}
.project-stats {
display: flex;
gap: 16px;
.stat-item {
display: flex;
align-items: center;
gap: 4px;
color: #86909c;
}
}
}
.units-grid {
display: flex;
gap: 24px;
flex-wrap: wrap;
.unit-card {
background: #fafbfc;
border-radius: 8px;
padding: 16px;
width: 360px;
margin-bottom: 16px;
.unit-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.unit-title {
font-size: 16px;
font-weight: 600;
}
.unit-actions {
display: flex;
gap: 8px;
}
}
.videos-list {
display: flex;
gap: 12px;
margin-bottom: 8px;
.video-item {
width: 100px;
.video-thumbnail {
position: relative;
width: 100px;
height: 60px;
border-radius: 6px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.2);
opacity: 0;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}
}
.video-info {
margin-top: 4px;
.video-name {
font-size: 12px;
font-weight: 500;
color: #1d2129;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.video-meta {
font-size: 11px;
color: #86909c;
display: flex;
gap: 4px;
}
.video-status {
margin-top: 2px;
}
}
}
}
.analysis-progress {
margin-top: 8px;
.progress-info {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #86909c;
margin-bottom: 2px;
}
}
}
}
}
}
.video-meta-info {
margin-top: 16px;
font-size: 13px;
color: #4e5969;
p {
margin: 2px 0;
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<GiPageLayout>
<!-- 页面标题 -->
<!-- <div class="page-header">
<!-- <div class="page-header">
<h2 class="page-title">图像音频关联查看</h2>
</div>-->
@ -17,62 +17,32 @@
<!-- 项目选择 -->
<div class="filter-item">
<span class="filter-label">项目</span>
<a-select
v-model="filterParams.project"
placeholder="请选择项目"
:options="projectOptions"
allow-search
allow-clear
:loading="loading.project"
style="width: 200px"
@change="handleFilterChange"
/>
<a-select v-model="filterParams.project" placeholder="请选择项目" :options="projectOptions" allow-search
allow-clear :loading="loading.project" style="width: 200px" @change="handleFilterChange" />
</div>
<!-- 机组选择 -->
<div class="filter-item">
<span class="filter-label">机组</span>
<a-select
v-model="filterParams.unit"
placeholder="请先选择项目"
:options="unitOptions"
allow-search
allow-clear
:disabled="!filterParams.project"
:loading="loading.unit"
style="width: 200px"
@change="handleFilterChange"
/>
<a-select v-model="filterParams.unit" placeholder="请先选择项目" :options="unitOptions" allow-search
allow-clear :disabled="!filterParams.project" :loading="loading.unit" style="width: 200px"
@change="handleFilterChange" />
</div>
<!-- 部件选择 -->
<div class="filter-item">
<span class="filter-label">部件</span>
<a-select
v-model="filterParams.component"
placeholder="请先选择机组"
:options="componentOptions"
allow-search
allow-clear
:disabled="!filterParams.unit"
:loading="loading.component"
style="width: 200px"
@change="handleFilterChange"
/>
<a-select v-model="filterParams.component" placeholder="请先选择机组" :options="componentOptions"
allow-search allow-clear :disabled="!filterParams.unit" :loading="loading.component"
style="width: 200px" @change="handleFilterChange" />
</div>
</a-space>
</div>
<!-- 已上传数据列表 -->
<div class="uploaded-files-section">
<a-table
:columns="fileColumns"
:data="imageList"
:pagination="false"
:scroll="{ x: '100%', y: 'calc(100vh - 380px)' }"
:loading="loading.image"
class="scrollable-table"
>
<a-table :columns="fileColumns" :data="imageList" :pagination="false"
:scroll="{ x: '100%', y: 'calc(100vh - 380px)' }" :loading="loading.image" class="scrollable-table">
<!-- 文件类型 -->
<template #type="{ record }">
<a-tag :color="getFileTypeColor(record.type)" size="small">
@ -82,7 +52,7 @@
<!-- 文件大小 -->
<template #size="{ record }">
<span>{{ record.imageTypeLabel}}</span>
<span>{{ record.imageTypeLabel }}</span>
</template>
<!-- 状态 -->
@ -103,20 +73,27 @@
</div>
</div>
</a-tab-pane>
<a-tab-pane key="props" tap="形变" title="形变原数据">
<div class="tab-content">
<raw-data>
</raw-data>
</div>
</a-tab-pane>
</a-tabs>
</div>
<!-- 文件预览模态框 -->
<PreviewModal ref="previewModal" />
<!-- 文件预览模态框待重新设计 -->
<!-- <PreviewModal ref="previewModal" /> -->
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed,onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import PreviewModal from './components/PreviewModal.vue'
import PreviewModal from './components/PreviewModal.vue'
import rawData from './components/raw-data.vue'
import {
getProjectList,
getTurbineList,
@ -128,8 +105,10 @@ import {
batchUploadImages,
uploadImageToPartV2
} from '@/apis/industrial-image'
import DeformationTap from './components/DeformationTap.vue'
const previewModal = ref()
//
// const previewModal = ref()
//
const activeTab = ref('image')
@ -141,9 +120,9 @@ const filterParams = reactive({
})
//
const projectOptions = ref<Array<{label: string, value: string}>>([])
const unitOptions = ref<Array<{label: string, value: string}>>([])
const componentOptions = ref<Array<{label: string, value: string}>>([])
const projectOptions = ref<Array<{ label: string, value: string }>>([])
const unitOptions = ref<Array<{ label: string, value: string }>>([])
const componentOptions = ref<Array<{ label: string, value: string }>>([])
//
const imageList = ref<Array<{
@ -251,37 +230,32 @@ const fetchPartList = async (projectId: string, turbineId: string) => {
//
const handleFilterChange = async () => {
// if (!filterParams.project) return
// if (!filterParams.project) return
loading.image = true
try {
let params = {
projectId: filterParams.project
const params: any = {
projectId: filterParams.project,
}
if(filterParams.unit){
params = {
projectId: filterParams.project,
turbineId: filterParams.unit
}
if (filterParams.unit) {
params.turbineId = filterParams.unit
}
if(filterParams.component){
params = {
projectId: filterParams.project,
turbineId: filterParams.unit,
partId: filterParams.component
}
if (filterParams.component) {
params.partId = filterParams.component
}
const res = await getImageList(params)
imageList.value = res.data.map((item: any) => ({
id: item.imageId,
name: item.imageName,
type: item.imageType?item.imageType:"未指定类型",
type: item.imageType ? item.imageType : "未指定类型",
imageTypeLabel: item.imageTypeLabel,
shootingTime: item.shootingTime,
preTreatment: item.preTreatment?"已审核":"未审核",
preTreatment: item.preTreatment ? "已审核" : "未审核",
imagePath: item.imagePath,
audioList:item.audioList
audioList: item.audioList
}))
Message.success(`获取到 ${imageList.value.length} 条图像数据`)
} catch (error) {
@ -313,14 +287,6 @@ const fileColumns: TableColumnData[] = [
{ title: '操作', slotName: 'action', width: 150, fixed: 'right' }
]
//
const filteredFiles = computed(() => {
return uploadedFiles.value.filter(file =>
(filterParams.project === null || file.project === filterParams.project) &&
(filterParams.unit === null || file.unit === filterParams.unit) &&
(filterParams.component === null || file.component === filterParams.component)
)
})
//
const getFileTypeColor = (type: string) => {
@ -358,28 +324,25 @@ const getImageUrl = (imagePath: string): string => {
return `${baseUrl}${imagePath}`
}
//
//
const previewFile = (file: any) => {
/* previewFileData.value = file
previewModalVisible.value = true*/
/* previewFileData.value = file
previewModalVisible.value = true*/
const fileObj ={
const fileObj = {
id: file.id,
name: file.name,
url: getImageUrl(file.imagePath),
audios: file.audioList
url: getImageUrl(file.imagePath)
}
previewModal.value.openPreview(fileObj)
// TODO:
// previewModal.value.openPreview(fileObj)
console.log('预览文件:', fileObj)
//
}
//
const deleteFile = (file: any) => {
console.log(index);
const index = uploadedFiles.value.findIndex(f => f.id === file.id)
if (index > -1) {
uploadedFiles.value.splice(index, 1)
Message.success('文件已删除')
}
}
</script>

View File

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

View File

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

View File

@ -1,28 +1,28 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="支出合同管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
row-key="id"
title="支出合同管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
@ -35,51 +35,113 @@
</a-button>
</a-space>
</template>
<!-- 合同状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
<a-tag :color="getStatusColor(record.contractStatus)">
{{ getStatusText(record.contractStatusLabel || record.contractStatus) }}
</a-tag>
</template>
<!-- 合同金额 -->
<template #contractAmount="{ record }">
<span class="font-medium text-red-600">{{ record.contractAmount.toLocaleString() }}</span>
<span class="font-medium text-green-600">{{ (record.amount || 0).toLocaleString() }}</span>
</template>
<!-- 款金额 -->
<template #paidAmount="{ record }">
<span class="font-medium text-orange-600">{{ record.paidAmount.toLocaleString() }}</span>
<!-- 款金额 -->
<template #receivedAmount="{ record }">
<span class="font-medium text-blue-600">{{ (record.receivedAmount || 0).toLocaleString() }}</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link @click="editRecord(record)" v-if="record.status === 'draft'">编辑</a-link>
<a-link @click="approveContract(record)" v-if="record.status === 'pending'">审批</a-link>
<a-link @click="viewPayment(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 @click="viewPayment(record)">收款记录</a-link>
<a-link v-if="record.contractStatus !== '已签署' && record.contractStatus !== '已完成'" @click="deleteContract(record)">删除</a-link>
</a-space>
</template>
</GiTable>
<!-- 合同详情弹窗 -->
<a-modal
v-model:visible="showDetailModal"
title="合同详情"
:width="800"
:footer="false"
@cancel="closeDetailModal"
>
<ContractDetail v-if="showDetailModal" :contract-id="selectedContractId" />
</a-modal>
<!-- 合同编辑弹窗 -->
<a-modal
v-model:visible="showEditModal"
title="编辑合同"
:width="800"
@cancel="closeEditModal"
@before-ok="handleEditSubmit"
>
<ContractEdit
v-if="showEditModal"
:contract-data="selectedContractData"
@update:contract-data="handleContractDataUpdate"
/>
</a-modal>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { onMounted, reactive, ref } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import ContractEdit from './ContractEdit.vue'
import ContractDetail from './ContractDetail.vue'
import http from '@/utils/http'
//
interface ContractItem {
contractId: string
customer: string
code: string
projectId: string
type: string
productService: string
paymentDate: string | null
performanceDeadline: string | null
paymentAddress: string
amount: number
accountNumber: string
notes: string
contractStatus: string
contractText: string | null
projectName: string
salespersonName: string | null
salespersonDeptName: string
settlementAmount: number | null
receivedAmount: number | null
contractStatusLabel: string | null
createBy: string | null
updateBy: string | null
createTime: string
updateTime: string
page: number
pageSize: number
signDate: string
duration: string
}
//
let searchForm = reactive({
const searchForm = reactive({
contractName: '',
contractCode: '',
supplier: '',
client: '',
status: '',
signDate: '',
page: 1,
size: 10
size: 10,
})
//
@ -89,16 +151,16 @@ const queryFormColumns = [
label: '合同名称',
type: 'input' as const,
props: {
placeholder: '请输入合同名称'
}
placeholder: '请输入合同名称',
},
},
{
field: 'supplier',
label: '供应商',
field: 'client',
label: '客户',
type: 'input' as const,
props: {
placeholder: '请输入供应商名称'
}
placeholder: '请输入客户名称',
},
},
{
field: 'status',
@ -107,147 +169,121 @@ const queryFormColumns = [
props: {
placeholder: '请选择合同状态',
options: [
{ label: '草稿', value: 'draft' },
{ label: '待审批', value: 'pending' },
{ label: '已签署', value: 'signed' },
{ label: '执行中', value: 'executing' },
{ label: '已完成', value: 'completed' },
{ label: '已终止', value: 'terminated' }
]
}
}
{ label: '未确认', value: '未确认' },
{ label: '待审批', value: '待审批' },
{ label: '已签署', value: '已签署' },
{ label: '执行中', value: '执行中' },
{ label: '已完成', value: '已完成' },
{ label: '已终止', value: '已终止' },
],
},
},
]
//
const tableColumns: TableColumnData[] = [
{ title: '合同编号', dataIndex: 'contractCode', width: 150 },
{ title: '合同名称', dataIndex: 'contractName', width: 250, ellipsis: true, tooltip: true },
{ title: '供应商名称', dataIndex: 'supplier', width: 200, ellipsis: true, tooltip: true },
{ title: '合同类型', dataIndex: 'contractType', width: 120 },
{ title: '合同金额', dataIndex: 'contractAmount', slotName: 'contractAmount', width: 120 },
{ title: '已付款金额', dataIndex: 'paidAmount', slotName: 'paidAmount', width: 120 },
{ title: '未付款金额', dataIndex: 'unpaidAmount', width: 120 },
{ title: '合同编号', dataIndex: 'code', width: 150 },
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
{ title: '客户名称', dataIndex: 'customer', width: 200, ellipsis: true, tooltip: true },
{ title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 },
{ title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 },
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 },
{ title: '签署日期', dataIndex: 'signDate', width: 120 },
{ title: '开始日期', dataIndex: 'startDate', width: 120 },
{ title: '结束日期', dataIndex: 'endDate', width: 120 },
{ title: '合同状态', dataIndex: 'status', slotName: 'status', width: 100 },
{ title: '项目关联', dataIndex: 'relatedProject', width: 200, ellipsis: true, tooltip: true },
{ title: '采购负责人', dataIndex: 'purchaseManager', width: 100 },
{ title: '付款方式', dataIndex: 'paymentMethod', width: 100 },
{ title: '备注', dataIndex: 'remark', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }
{ title: '履约期限', dataIndex: 'performanceDeadline', width: 120 },
{ title: '付款日期', dataIndex: 'paymentDate', width: 120 },
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 },
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
{ title: '产品服务', dataIndex: 'productService', width: 120, ellipsis: true, tooltip: true },
{ title: '备注', dataIndex: 'notes', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' },
]
//
const loading = ref(false)
const dataList = ref([
{
id: 1,
contractCode: 'EC2024001',
contractName: '风电检测设备采购合同',
supplier: '深圳市智能检测设备有限公司',
contractType: '设备采购',
contractAmount: 120,
paidAmount: 60,
unpaidAmount: 60,
signDate: '2024-02-25',
startDate: '2024-03-01',
endDate: '2024-03-31',
status: 'executing',
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 dataList = ref<ContractItem[]>([])
// API
const fetchContractList = async () => {
try {
loading.value = true
const params = {
page: searchForm.page,
pageSize: searchForm.size,
contractName: searchForm.contractName,
code: searchForm.contractCode,
customer: searchForm.client,
contractStatus: searchForm.status,
signDate: searchForm.signDate,
}
const response = await http.get('/contract/list', params)
if (response.code === 200) {
// ""
const allContracts = response.rows || []
const revenueContracts = allContracts.filter((item: ContractItem) => item.type === '支出合同')
//
dataList.value = revenueContracts.map((item: ContractItem) => ({
...item,
pendingAmount: (item.amount || 0) - (item.receivedAmount || 0),
}))
pagination.total = Number.parseInt(response.total) || 0
} else {
Message.error(response.msg || '获取合同列表失败')
dataList.value = []
}
} catch (error) {
console.error('获取合同列表失败:', error)
Message.error('获取合同列表失败')
dataList.value = []
} finally {
loading.value = false
}
])
}
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
total: 0,
showTotal: true,
showPageSize: true
showPageSize: true,
})
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'draft': 'gray',
'pending': 'orange',
'signed': 'blue',
'executing': 'cyan',
'completed': 'green',
'terminated': 'red'
未确认: 'gray',
待审批: 'orange',
已签署: 'blue',
执行中: 'cyan',
已完成: 'green',
已终止: 'red',
}
return colorMap[status] || 'gray'
}
//
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
'draft': '草稿',
'pending': '待审批',
'signed': '已签署',
'executing': '执行中',
'completed': '已完成',
'terminated': '已终止'
}
return textMap[status] || status
// contractStatusLabel使使contractStatus
return status || '未知状态'
}
//
const search = async () => {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1000)
await fetchContractList()
}
const reset = () => {
Object.assign(searchForm, {
contractName: '',
contractCode: '',
supplier: '',
client: '',
status: '',
signDate: '',
page: 1,
size: 10
size: 10,
})
pagination.current = 1
search()
@ -277,23 +313,131 @@ const exportContract = () => {
Message.info('导出合同功能开发中...')
}
const viewDetail = (record: any) => {
Message.info(`查看合同详情: ${record.contractName}`)
//
const showEditModal = ref(false)
const selectedContractData = ref<ContractItem | null>(null)
const editedContractData = ref<ContractItem | null>(null)
const editRecord = (record: ContractItem) => {
//
const completeRecord = {
...record,
amount: record.amount || 0,
projectId: record.projectId || '',
type: record.type || '收入合同',
contractStatus: record.contractStatus || '未确认',
}
selectedContractData.value = completeRecord
showEditModal.value = true
}
const editRecord = (record: any) => {
Message.info(`编辑合同: ${record.contractName}`)
const closeEditModal = () => {
showEditModal.value = false
selectedContractData.value = null
editedContractData.value = null
}
const approveContract = (record: any) => {
Message.info(`审批合同: ${record.contractName}`)
const handleContractDataUpdate = (data: ContractItem) => {
editedContractData.value = data
}
const viewPayment = (record: any) => {
Message.info(`查看付款记录: ${record.contractName}`)
const handleEditSubmit = async () => {
if (!editedContractData.value) return false;
try {
const requestData = {
...editedContractData.value,
accountNumber: editedContractData.value.accountNumber || '',
amount: editedContractData.value.amount || 0,
code: editedContractData.value.code || '',
contractId: editedContractData.value.contractId,
contractStatus: editedContractData.value.contractStatus || '',
contractText: editedContractData.value.contractText || '',
customer: editedContractData.value.customer || '',
departmentId: editedContractData.value.departmentId || '',
duration: editedContractData.value.duration || '',
notes: editedContractData.value.notes || '',
paymentAddress: editedContractData.value.paymentAddress || '',
paymentDate: editedContractData.value.paymentDate || null,
performanceDeadline: editedContractData.value.performanceDeadline || null,
productService: editedContractData.value.productService || '',
projectId: editedContractData.value.projectId || '',
salespersonId: editedContractData.value.salespersonId || '',
signDate: editedContractData.value.signDate || null,
type: editedContractData.value.type || '',
};
console.log('Edited Contract Data:', requestData); // 便
// /contract PUT
const response = await http.put('/contract', requestData);
//
if (response.status === 200 && response.code === 200) {
Message.success('合同编辑成功');
closeEditModal();
search(); //
return true;
} else {
Message.error(response.msg || '合同编辑失败');
return false;
}
} catch (error) {
console.error('合同编辑失败:', error);
Message.error('合同编辑失败: ' + (error.message || '请稍后再试'));
return false;
}
}
//
const deleteContract = async (record: ContractItem) => {
try {
await Modal.confirm({
title: '确认删除',
content: `确定要删除合同 "${record.projectName}" 吗?`,
})
const response = await http.delete(`/contract/${record.contractId}`)
if (response.code === 200) {
Message.success('合同删除成功')
search() //
} else {
Message.error(response.msg || '合同删除失败')
}
} catch (error) {
//
if (error !== 'cancel') {
console.error('合同删除失败:', error)
Message.error('合同删除失败')
}
}
}
//
const showDetailModal = ref(false)
const selectedContractId = ref<string | null>(null)
const viewDetail = (record: ContractItem) => {
selectedContractId.value = record.contractId
showDetailModal.value = true
}
const closeDetailModal = () => {
showDetailModal.value = false
selectedContractId.value = null
}
const approveContract = (record: ContractItem) => {
Message.info(`审批合同: ${record.projectName}`)
}
const viewPayment = (record: ContractItem) => {
Message.info(`查看收款记录: ${record.projectName}`)
}
onMounted(() => {
search()
fetchContractList()
})
</script>
</script>

View File

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

View File

@ -1,25 +1,25 @@
<template>
<GiPageLayout>
<GiTable
row-key="id"
title="收入合同管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
row-key="id"
title="收入合同管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
@ -53,36 +53,53 @@
<span class="font-medium text-blue-600">{{ (record.receivedAmount || 0).toLocaleString() }}</span>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">详情</a-link>
<a-link v-if="record.contractStatus === '未确认'" @click="editRecord(record)">编辑</a-link>
<a-link v-if="record.contractStatus === '待审批'" @click="approveContract(record)">审批</a-link>
<a-link @click="viewPayment(record)">收款记录</a-link>
<a-link v-if="record.contractStatus !== '已签署' && record.contractStatus !== '已完成'" @click="deleteContract(record)">删除</a-link>
</a-space>
</template>
</GiTable>
<!-- 合同详情弹窗 -->
<a-modal
v-model:visible="showDetailModal"
title="合同详情"
:width="800"
@cancel="closeDetailModal"
:footer="false"
v-model:visible="showDetailModal"
title="合同详情"
:width="800"
:footer="false"
@cancel="closeDetailModal"
>
<ContractDetail v-if="showDetailModal" :contract-id="selectedContractId" />
</a-modal>
<!-- 合同编辑弹窗 -->
<a-modal
v-model:visible="showEditModal"
title="编辑合同"
:width="800"
@cancel="closeEditModal"
@before-ok="handleEditSubmit"
>
<ContractEdit
v-if="showEditModal"
:contract-data="selectedContractData"
@update:contract-data="handleContractDataUpdate"
/>
</a-modal>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { onMounted, reactive, ref } from 'vue'
import { Message, Modal } 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 http from '@/utils/http'
//
interface ContractItem {
@ -181,7 +198,6 @@ const tableColumns: TableColumnData[] = [
{ title: '备注', dataIndex: 'notes', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' },
]
//
const loading = ref(false)
const dataList = ref<ContractItem[]>([])
@ -297,6 +313,108 @@ const exportContract = () => {
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 selectedContractId = ref<string | null>(null)
@ -311,10 +429,6 @@ const closeDetailModal = () => {
selectedContractId.value = null
}
const editRecord = (record: ContractItem) => {
Message.info(`编辑合同: ${record.projectName}`)
}
const approveContract = (record: ContractItem) => {
Message.info(`审批合同: ${record.projectName}`)
}

View File

@ -129,29 +129,18 @@ const debouncedSearch = debounce(() => {
const search = () => {
console.log('🔍 ApprovalSearch - 搜索按钮被点击')
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 = {
equipmentName: searchForm.equipmentName || undefined,
applicantName: searchForm.applicantName || undefined,
businessType: searchForm.businessType,
approvalStatus: searchForm.approvalStatus,
applyTimeStart: searchForm.applyTimeStart,
applyTimeEnd: searchForm.applyTimeEnd
businessType: searchForm.businessType || undefined,
approvalStatus: searchForm.approvalStatus || undefined,
applyTimeStart: searchForm.applyTimeStart || undefined,
applyTimeEnd: searchForm.applyTimeEnd || undefined,
}
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)
}

View File

@ -458,10 +458,10 @@ const loadData = async (searchParams?: EquipmentApprovalListReq) => {
loading.value = true
try {
// -
const params: EquipmentApprovalListReq = {
pageSize: pagination.pageSize || 10,
page: pagination.current || 1,
pageNum: pagination.current || 1, // pageNum
pageSize: pagination.pageSize,
page: pagination.current,
...(searchParams || {}),
}

View File

@ -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>

View File

@ -175,6 +175,19 @@
<a-button type="text" size="small" @click="handleEdit(record)">
编辑
</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
content="确定要删除这条采购记录吗?"
@ok="handleDelete(record)"
@ -195,6 +208,13 @@
:mode="modalMode"
@success="handleModalSuccess"
/>
<!-- 采购申请弹窗 -->
<ProcurementApplicationModal
v-model:visible="applicationModalVisible"
:equipment-data="currentApplicationData"
@success="handleApplicationSuccess"
/>
</div>
</template>
@ -213,7 +233,9 @@ import {
import message from '@arco-design/web-vue/es/message'
import ProcurementModal from './components/ProcurementModal.vue'
import ProcurementSearch from './components/ProcurementSearch.vue'
import ProcurementApplicationModal from './components/ProcurementApplicationModal.vue'
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
import { equipmentApprovalApi } from '@/apis/equipment/approval'
import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type'
defineOptions({ name: 'EquipmentProcurement' })
@ -243,6 +265,10 @@ const modalVisible = ref(false)
const currentProcurement = ref<EquipmentResp | null>(null)
const modalMode = ref<'add' | 'edit' | 'view'>('add')
//
const applicationModalVisible = ref(false)
const currentApplicationData = ref<EquipmentResp | null>(null)
//
const selectedRowKeys = ref<string[]>([])
const rowSelection = reactive({
@ -611,6 +637,12 @@ const handleEdit = (record: EquipmentResp) => {
modalVisible.value = true
}
//
const handleApplyProcurement = (record: EquipmentResp) => {
currentApplicationData.value = { ...record }
applicationModalVisible.value = true
}
//
const handleDelete = async (record: EquipmentResp) => {
try {
@ -629,6 +661,12 @@ const handleModalSuccess = () => {
loadData(currentSearchParams.value)
}
//
const handleApplicationSuccess = () => {
applicationModalVisible.value = false
loadData(currentSearchParams.value)
}
//
const refreshData = () => {
loadData(currentSearchParams.value)
@ -674,6 +712,36 @@ const getTotalAmount = () => {
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(() => {
loadData()
})

View File

@ -5,6 +5,11 @@ import createVitePlugins from './config/plugins'
export default defineConfig(({ command, mode }) => {
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 {
// 开发或生产环境服务的公共基础路径
base: env.VITE_BASE,