Merge branch 'devlopment' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web into devlopment
This commit is contained in:
commit
ff2c80e1e8
|
@ -0,0 +1,26 @@
|
|||
# 环境变量 (命名必须以 VITE_ 开头)
|
||||
# 接口前缀
|
||||
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://10.18.34.163:8888/'
|
||||
# VITE_API_BASE_URL = 'http://10.18.34.213:8888/'
|
||||
|
||||
# 接口地址 (WebSocket)
|
||||
# VITE_API_WS_URL = 'ws://localhost:8000'
|
||||
VITE_API_WS_URL = 'ws://10.18.34.163:8000'
|
||||
# VITE_API_WS_URL = 'ws://10.18.34.213:8000'
|
||||
|
||||
# 地址前缀
|
||||
VITE_BASE = '/'
|
||||
|
||||
# 是否开启开发者工具
|
||||
VITE_OPEN_DEVTOOLS = false
|
||||
|
||||
# 应用配置面板
|
||||
VITE_APP_SETTING = true
|
||||
|
||||
# 客户端ID
|
||||
VITE_CLIENT_ID = 'ef51c9a3e9046c4f2ea45142c8a8344a'
|
Binary file not shown.
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 17 KiB |
|
@ -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>
|
||||
|
@ -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,10 +259,12 @@ const initWebSocket = (token: string) => {
|
|||
}
|
||||
|
||||
try {
|
||||
// 修复WebSocket URL,确保使用正确的端口
|
||||
const wsUrl = import.meta.env.VITE_API_WS_URL || 'ws://localhost:8888'
|
||||
console.log('正在连接WebSocket:', `${wsUrl}/websocket?token=${token}`)
|
||||
const wsEndpoint = wsUrl.replace('8000', '8888') // 确保使用8888端口
|
||||
console.log('正在连接WebSocket:', `${wsEndpoint}/websocket?token=${token}`)
|
||||
|
||||
socket = new WebSocket(`${wsUrl}/websocket?token=${token}`)
|
||||
socket = new WebSocket(`${wsEndpoint}/websocket?token=${token}`)
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log('WebSocket连接成功')
|
||||
|
@ -133,6 +278,10 @@ const initWebSocket = (token: string) => {
|
|||
// 处理通知消息
|
||||
if (data.type && data.title && data.content) {
|
||||
console.log('处理通知消息:', data)
|
||||
|
||||
// 播放语音提示
|
||||
playNotificationSound()
|
||||
|
||||
// 显示通知
|
||||
Notification.info({
|
||||
title: data.title,
|
||||
|
@ -144,6 +293,9 @@ const initWebSocket = (token: string) => {
|
|||
|
||||
// 增加未读消息计数
|
||||
unreadMessageCount.value++
|
||||
|
||||
// 触发页面标题闪烁
|
||||
flashPageTitle()
|
||||
} else {
|
||||
// 处理简单的数字消息(兼容旧版本)
|
||||
const count = Number.parseInt(event.data)
|
||||
|
@ -181,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)
|
||||
|
@ -218,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">
|
||||
|
@ -242,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>
|
||||
|
|
|
@ -70,6 +70,6 @@ declare global {
|
|||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,6 +73,13 @@
|
|||
</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>
|
||||
|
||||
|
@ -112,10 +89,11 @@
|
|||
</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 rawData from './components/raw-data.vue'
|
||||
import {
|
||||
getProjectList,
|
||||
getTurbineList,
|
||||
|
@ -127,6 +105,7 @@ import {
|
|||
batchUploadImages,
|
||||
uploadImageToPartV2
|
||||
} from '@/apis/industrial-image'
|
||||
import DeformationTap from './components/DeformationTap.vue'
|
||||
|
||||
// 预览弹窗引用(待重新设计)
|
||||
// const previewModal = ref()
|
||||
|
@ -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) => {
|
||||
|
@ -360,6 +326,9 @@ const getImageUrl = (imagePath: string): string => {
|
|||
|
||||
// 预览文件(待重新设计预览弹窗)
|
||||
const previewFile = (file: any) => {
|
||||
/* previewFileData.value = file
|
||||
previewModalVisible.value = true*/
|
||||
|
||||
const fileObj = {
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
|
@ -373,12 +342,7 @@ const previewFile = (file: any) => {
|
|||
|
||||
// 删除文件
|
||||
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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
@ -38,48 +38,110 @@
|
|||
|
||||
<!-- 合同状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
<a-tag :color="getStatusColor(record.contractStatus)">
|
||||
{{ getStatusText(record.contractStatusLabel || record.contractStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 合同金额 -->
|
||||
<template #contractAmount="{ record }">
|
||||
<span class="font-medium text-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>
|
|
@ -8,7 +8,7 @@
|
|||
5. 导入导出团队成员数据
|
||||
-->
|
||||
<template>
|
||||
<GiPageLayout>
|
||||
<GiPageLayout class="construction-personnel-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
|
@ -828,15 +828,22 @@ onMounted(() => {
|
|||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 24px;
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow: hidden;
|
||||
|
||||
.arco-table-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 350px);
|
||||
}
|
||||
|
||||
.arco-table {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.arco-table-body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -917,6 +924,28 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
// 页面滚动修复
|
||||
.construction-personnel-page {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.gi-page-layout) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.gi-page-layout-content) {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
|
|
|
@ -237,31 +237,37 @@
|
|||
|
||||
<!-- 团队成员明细 -->
|
||||
<div class="detail-section">
|
||||
<div class="section-header">
|
||||
<h3>团队成员明细</h3>
|
||||
<a-button size="small" @click="openPersonnelManagement">
|
||||
<div class="section-header team-members-header">
|
||||
<div class="header-left">
|
||||
<h3>团队成员明细</h3>
|
||||
<span class="member-count" v-if="currentProject.teamMembers && currentProject.teamMembers.length > 0">
|
||||
({{ currentProject.teamMembers.length }}人)
|
||||
</span>
|
||||
</div>
|
||||
<a-button size="small" @click="openPersonnelManagement" class="add-member-btn">
|
||||
<template #icon><icon-user-group /></template>
|
||||
添加成员
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="team-members">
|
||||
<div
|
||||
v-for="member in currentProject.teamMembers"
|
||||
v-for="(member, index) in currentProject.teamMembers"
|
||||
:key="member.id"
|
||||
class="member-item"
|
||||
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||
@click="editMemberPosition(member)"
|
||||
>
|
||||
<div class="member-avatar">
|
||||
<icon-user />
|
||||
</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">{{ member.name }}</div>
|
||||
<div class="member-position">{{ member.position }}</div>
|
||||
<div class="member-name">{{ member.name || '未设置姓名' }}</div>
|
||||
<div class="member-position">{{ member.position || '未设置岗位' }}</div>
|
||||
<div class="member-details">
|
||||
<span class="member-status" :class="member.status">
|
||||
{{ member.status === 'available' ? '在线' : '离线' }}
|
||||
</span>
|
||||
<span class="member-date">入职: {{ member.joinDate }}</span>
|
||||
<span class="member-date">入职: {{ member.joinDate || '未设置' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-actions">
|
||||
|
@ -453,15 +459,18 @@ const mapProjectRespToProjectCard = (projectResp: any): any => {
|
|||
// 处理团队成员数据 - 使用后端返回的真实数据
|
||||
const teamMembers = projectResp.teamMembers ? projectResp.teamMembers.map((member: any) => {
|
||||
console.log('处理团队成员数据:', member) // 添加调试日志
|
||||
console.log('成员userName字段:', member.userName)
|
||||
console.log('成员name字段:', member.name)
|
||||
|
||||
const mappedMember = {
|
||||
id: member.memberId,
|
||||
name: member.name, // 修复:使用正确的姓名字段
|
||||
position: member.roleTypeDesc || member.jobCodeDesc,
|
||||
name: member.userName || member.name || '未设置姓名', // 修复:优先使用userName字段
|
||||
position: member.roleTypeDesc || member.jobCodeDesc || '未设置岗位',
|
||||
phone: member.phone || '', // 后端数据中的电话字段
|
||||
email: member.email || '', // 后端数据中的邮箱字段
|
||||
status: member.status === 'ACTIVE' ? 'available' : 'offline',
|
||||
skills: [], // 后端数据中没有技能字段
|
||||
joinDate: member.joinDate,
|
||||
joinDate: member.joinDate || '未设置',
|
||||
remark: member.remark || member.jobDesc || '',
|
||||
// 保留原始数据用于后续处理
|
||||
originalData: member
|
||||
|
@ -533,18 +542,32 @@ const loadKanbanData = async () => {
|
|||
...(response.data.pendingProjects || [])
|
||||
]
|
||||
|
||||
console.log('后端返回的所有项目数据:', allProjects)
|
||||
console.log('后端返回的preparingProjects:', response.data.preparingProjects)
|
||||
console.log('后端返回的ongoingProjects:', response.data.ongoingProjects)
|
||||
console.log('后端返回的inProgressProjects:', response.data.inProgressProjects)
|
||||
console.log('后端返回的pendingProjects:', response.data.pendingProjects)
|
||||
|
||||
// 根据项目状态分类
|
||||
allProjects.forEach(project => {
|
||||
console.log('处理项目:', project.projectName || project.name)
|
||||
console.log('项目团队成员:', project.teamMembers)
|
||||
console.log('项目状态:', project.status, typeof project.status)
|
||||
|
||||
const mappedProject = mapProjectRespToProjectCard(project)
|
||||
console.log('映射后的项目:', mappedProject.name)
|
||||
console.log('映射后的团队成员:', mappedProject.teamMembers)
|
||||
|
||||
// 确保状态比较时类型一致,将字符串转换为数字进行比较
|
||||
const status = typeof project.status === 'string' ? parseInt(project.status) : project.status
|
||||
|
||||
if (status === 0) {
|
||||
// status: 0 表示准备中
|
||||
console.log('添加到准备中项目:', mappedProject.name)
|
||||
preparingProjects.value.push(mappedProject)
|
||||
} else if (status === 1) {
|
||||
// status: 1 表示进行中
|
||||
console.log('添加到进行中项目:', mappedProject.name)
|
||||
ongoingProjects.value.push(mappedProject)
|
||||
}
|
||||
// status: 其他值表示未开工,我们不需要显示
|
||||
|
@ -629,10 +652,12 @@ const openProjectDetail = async (project: any) => {
|
|||
try {
|
||||
loading.value = true
|
||||
console.log('正在获取项目详情,项目ID:', project.id)
|
||||
console.log('传入的项目数据:', project)
|
||||
|
||||
// 首先检查传入的项目数据是否已经包含团队成员信息
|
||||
if (project.teamMembers && project.teamMembers.length > 0) {
|
||||
console.log('项目数据已包含团队成员信息,直接使用')
|
||||
console.log('团队成员数据:', project.teamMembers)
|
||||
currentProject.value = project
|
||||
projectDetailVisible.value = true
|
||||
return
|
||||
|
@ -643,7 +668,9 @@ const openProjectDetail = async (project: any) => {
|
|||
|
||||
if (response.data) {
|
||||
// 使用映射函数处理后端数据
|
||||
console.log('API返回的原始数据:', response.data)
|
||||
currentProject.value = mapProjectRespToProjectCard(response.data)
|
||||
console.log('映射后的项目数据:', currentProject.value)
|
||||
projectDetailVisible.value = true
|
||||
} else {
|
||||
// 如果API没有返回数据,使用传入的项目数据作为后备
|
||||
|
@ -1135,98 +1162,238 @@ onMounted(async () => {
|
|||
}
|
||||
}
|
||||
|
||||
.team-members {
|
||||
.member-item {
|
||||
.team-members-header {
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
gap: 8px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #1d2129;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.member-count {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.add-member-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f8f9fa;
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.team-members {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
|
||||
// 自定义滚动条样式
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #e8f2ff 100%);
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #f0f0f0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #86909c;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #52c41a;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.member-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.member-name {
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
margin-bottom: 2px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-position {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(135deg, #f0f2f5 0%, #e5e6eb 100%);
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.member-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
|
||||
.member-status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
&.available {
|
||||
background: #e3f2fd;
|
||||
color: #2196f3;
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||
color: #1890ff;
|
||||
border: 1px solid #91d5ff;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background: #f8f9fa;
|
||||
color: #868e96;
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
|
||||
color: #8c8c8c;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.member-date {
|
||||
background: #f7f8fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e5e6eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.member-actions {
|
||||
.arco-btn {
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
padding: 60px 20px;
|
||||
color: #86909c;
|
||||
background: linear-gradient(135deg, #fafbfc 0%, #f0f2f5 100%);
|
||||
border-radius: 12px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
margin: 20px 0;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
color: #bfbfbf;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue