This commit is contained in:
马诗敏 2025-08-12 17:34:37 +08:00
commit 7c455f59eb
18 changed files with 1246 additions and 269 deletions

View File

@ -1,4 +1,4 @@
// @/apis/bussiness/index.ts - 商务数据库信息模块API // @/apis/bussiness/index.ts - 智能商务API
import http from '@/utils/http' import http from '@/utils/http'
import type { import type {
FolderInfo, FolderInfo,
@ -59,7 +59,9 @@ export function getFilesApi(params?: FileListParams) {
page: params?.page || 1, page: params?.page || 1,
pageSize: params?.pageSize || 10, pageSize: params?.pageSize || 10,
folderId: params?.folderId || '0', folderId: params?.folderId || '0',
fileName: params?.fileName fileName: params?.fileName,
sortField: params?.sortField,
sortOrder: params?.sortOrder
} }
}) })
} }

View File

@ -31,6 +31,8 @@ export interface FileListParams {
pageSize?: number pageSize?: number
folderId?: string folderId?: string
fileName?: string fileName?: string
sortField?: string
sortOrder?: string
} }
/** 文件夹列表响应 */ /** 文件夹列表响应 */

View File

@ -194,6 +194,12 @@ export interface EquipmentResp {
inventoryBasis?: string inventoryBasis?: string
/** 动态记录 */ /** 动态记录 */
dynamicRecord?: string dynamicRecord?: string
/** 采购状态 */
procurementStatus?: string
/** 审批状态 */
approvalStatus?: string
} }
/** /**
@ -294,6 +300,17 @@ export enum BusinessType {
RETURN = 'RETURN' RETURN = 'RETURN'
} }
/**
*
*/
export enum ProcurementStatus {
NOT_STARTED = 'NOT_STARTED',
PENDING_APPROVAL = 'PENDING_APPROVAL',
APPROVED = 'APPROVED',
REJECTED = 'REJECTED',
COMPLETED = 'COMPLETED'
}
/** /**
* *
*/ */

View File

@ -0,0 +1,297 @@
<template>
<div
class="approval-message-item"
:class="{
'unread': !notification.read,
'urgent': notification.priority === 'URGENT',
'high': notification.priority === 'HIGH',
'action-required': notification.actionRequired
}"
@click="handleClick"
>
<!-- 消息图标 -->
<div class="message-icon">
<component :is="getIconByType(notification.type)" />
</div>
<!-- 消息内容 -->
<div class="message-content">
<div class="message-header">
<div class="message-title">
{{ notification.title }}
<a-tag
v-if="notification.actionRequired"
size="small"
color="red"
>
需操作
</a-tag>
<a-tag
v-if="notification.priority === 'URGENT'"
size="small"
color="red"
>
紧急
</a-tag>
<a-tag
v-if="notification.priority === 'HIGH'"
size="small"
color="orange"
>
重要
</a-tag>
</div>
<div class="message-time">
{{ formatTime(notification.createTime) }}
</div>
</div>
<div class="message-body">
{{ notification.content }}
</div>
<div class="message-footer">
<div class="message-meta">
<span class="category">{{ notification.category }}</span>
<span v-if="notification.source" class="source">{{ notification.source }}</span>
</div>
<div class="message-actions">
<a-button
type="text"
size="small"
@click.stop="handleView"
>
查看详情
</a-button>
<a-button
type="text"
size="small"
@click.stop="handleApprove"
v-if="canApprove"
>
审批
</a-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import {
IconCheckCircle,
IconClockCircle,
IconApps,
IconSettings,
IconExclamationCircle
} from '@arco-design/web-vue/es/icon'
interface Props {
notification: {
id: string
type: string
title: string
content: string
priority?: string
category: string
source?: string
createTime: string
read: boolean
actionRequired: boolean
targetUrl?: string
metadata?: any
reminderTime?: string
reminderType?: string
lastReminderTime?: string
}
}
const props = defineProps<Props>()
const router = useRouter()
//
const canApprove = computed(() => {
return props.notification.type.includes('APPROVAL') ||
props.notification.type === 'PENDING' ||
props.notification.type === 'PROCUREMENT'
})
//
const getIconByType = (type: string) => {
const iconMap: Record<string, any> = {
'APPROVAL': IconCheckCircle,
'PENDING': IconClockCircle,
'PROCUREMENT': IconApps,
'EQUIPMENT_BORROW': IconApps,
'EQUIPMENT_RETURN': IconApps,
'EQUIPMENT_MAINTENANCE': IconSettings,
'EQUIPMENT_ALERT': IconExclamationCircle,
'PROCUREMENT_APPROVAL': IconApps,
'BORROW_APPROVAL': IconApps,
'RETURN_APPROVAL': IconApps
}
return iconMap[type] || IconCheckCircle
}
//
const formatTime = (time: string) => {
if (!time) return '-'
const date = new Date(time)
const now = new Date()
const diff = now.getTime() - date.getTime()
if (diff < 60000) return '刚刚'
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
if (diff < 2592000000) return `${Math.floor(diff / 86400000)}天前`
return date.toLocaleDateString()
}
//
const handleClick = () => {
if (props.notification.targetUrl) {
router.push(props.notification.targetUrl)
}
}
//
const handleView = () => {
if (props.notification.targetUrl) {
router.push(props.notification.targetUrl)
}
}
//
const handleApprove = () => {
if (props.notification.targetUrl) {
router.push(props.notification.targetUrl)
}
}
</script>
<style scoped lang="scss">
.approval-message-item {
display: flex;
align-items: flex-start;
padding: 20px;
border: 1px solid var(--color-border-2);
border-radius: 8px;
margin-bottom: 16px;
cursor: pointer;
transition: all 0.2s;
background: var(--color-bg-2);
&:hover {
background-color: var(--color-fill-2);
border-color: var(--color-primary-light-3);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&.unread {
background-color: var(--color-primary-light-1);
border-color: var(--color-primary-light-3);
&:hover {
background-color: var(--color-primary-light-2);
}
}
&.urgent {
border-left: 4px solid var(--color-danger);
background-color: var(--color-danger-light-1);
}
&.high {
border-left: 4px solid var(--color-warning);
background-color: var(--color-warning-light-1);
}
&.action-required {
border-left: 4px solid var(--color-primary);
background-color: var(--color-primary-light-1);
}
.message-icon {
margin-right: 16px;
margin-top: 2px;
color: var(--color-text-2);
font-size: 20px;
}
.message-content {
flex: 1;
min-width: 0;
.message-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
.message-title {
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
color: var(--color-text-1);
font-size: 16px;
flex: 1;
margin-right: 16px;
}
.message-time {
font-size: 12px;
color: var(--color-text-3);
white-space: nowrap;
}
}
.message-body {
font-size: 14px;
color: var(--color-text-2);
margin-bottom: 16px;
line-height: 1.5;
}
.message-footer {
display: flex;
justify-content: space-between;
align-items: center;
.message-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-3);
.category {
background: var(--color-fill-3);
padding: 2px 8px;
border-radius: 4px;
font-weight: 500;
}
.source {
color: var(--color-text-4);
}
}
.message-actions {
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.2s;
}
}
&:hover .message-actions {
opacity: 1;
}
}
}
</style>

View File

@ -1,15 +1,13 @@
<template> <template>
<div class="notification-center"> <div class="notification-center">
<!-- 消息中心图标和徽章 --> <!-- 聊天信息图标 -->
<div class="notification-trigger" @click="toggleDropdown"> <div class="notification-trigger">
<a-badge :count="unreadCount" :dot="hasUrgentNotifications"> <a-button type="text" class="notification-btn" title="聊天信息">
<a-button type="text" class="notification-btn" title="消息中心"> <template #icon>
<template #icon> <IconNotification />
<IconNotification /> </template>
</template> <span class="notification-text">聊天信息</span>
<span class="notification-text">消息中心</span> </a-button>
</a-button>
</a-badge>
</div> </div>
<!-- 消息中心弹窗 --> <!-- 消息中心弹窗 -->
@ -229,16 +227,37 @@ const reminderForm = ref({
// //
const notifications = computed(() => notificationService.getAllNotifications()) const notifications = computed(() => notificationService.getAllNotifications())
const unreadCount = computed(() => notificationService.unreadCount.value) const unreadCount = computed(() => {
const count = notificationService.unreadCount.value
// NaN
if (typeof count === 'number' && !isNaN(count) && isFinite(count)) {
return count
}
return 0
})
const totalCount = computed(() => notifications.value.length) const totalCount = computed(() => notifications.value.length)
const pendingCount = computed(() => notificationService.pendingCount.value) const pendingCount = computed(() => {
const equipmentCount = computed(() => const count = notificationService.pendingCount.value
notificationService.equipmentBorrowCount.value + if (typeof count === 'number' && !isNaN(count) && isFinite(count)) {
notificationService.equipmentReturnCount.value + return count
notificationService.equipmentMaintenanceCount.value + }
notificationService.equipmentAlertCount.value return 0
) })
const urgentCount = computed(() => notificationService.urgentCount.value) const equipmentCount = computed(() => {
const borrowCount = notificationService.equipmentBorrowCount.value || 0
const returnCount = notificationService.equipmentReturnCount.value || 0
const maintenanceCount = notificationService.equipmentMaintenanceCount.value || 0
const alertCount = notificationService.equipmentAlertCount.value || 0
return borrowCount + returnCount + maintenanceCount + alertCount
})
const urgentCount = computed(() => {
const count = notificationService.urgentCount.value
if (typeof count === 'number' && !isNaN(count) && isFinite(count)) {
return count
}
return 0
})
const hasUrgentNotifications = computed(() => urgentCount.value > 0) const hasUrgentNotifications = computed(() => urgentCount.value > 0)
// //
@ -644,6 +663,11 @@ onUnmounted(() => {
clearInterval(reminderCheckInterval) clearInterval(reminderCheckInterval)
} }
}) })
//
defineExpose({
toggleDropdown
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -34,7 +34,9 @@
<a-dropdown trigger="hover"> <a-dropdown trigger="hover">
<a-row align="center" :wrap="false" class="user"> <a-row align="center" :wrap="false" class="user">
<!-- 管理员头像 --> <!-- 管理员头像 -->
<Avatar :src="userStore.avatar" :name="userStore.nickname" :size="32" /> <a-badge :count="unreadMessageCount > 0 ? unreadMessageCount : undefined" :dot="false">
<Avatar :src="userStore.avatar" :name="userStore.nickname" :size="32" />
</a-badge>
<span class="username">{{ userStore.nickname }}</span> <span class="username">{{ userStore.nickname }}</span>
<icon-down /> <icon-down />
</a-row> </a-row>
@ -42,8 +44,14 @@
<a-doption @click="router.push('/user/profile')"> <a-doption @click="router.push('/user/profile')">
<span>个人中心</span> <span>个人中心</span>
</a-doption> </a-doption>
<a-doption @click="router.push('/user/message')"> <a-doption @click="showNotificationCenter">
<span>消息中心</span> <span>消息中心</span>
<a-badge
v-if="unreadMessageCount > 0"
:count="unreadMessageCount"
:dot="false"
class="dropdown-notification-badge"
/>
</a-doption> </a-doption>
<a-divider :margin="0" /> <a-divider :margin="0" />
<a-doption @click="logout"> <a-doption @click="logout">
@ -78,7 +86,14 @@ const { breakpoint } = useBreakpoint()
const notificationCenterRef = ref() const notificationCenterRef = ref()
// 使 // 使
const unreadMessageCount = computed(() => notificationService.unreadCount.value) const unreadMessageCount = computed(() => {
const count = notificationService.unreadCount.value
// NaN
if (typeof count === 'number' && !isNaN(count) && isFinite(count)) {
return count
}
return 0
})
// //
const playNotificationSound = () => { const playNotificationSound = () => {
@ -224,6 +239,27 @@ const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
const SettingDrawerRef = ref<InstanceType<typeof SettingDrawer>>() const SettingDrawerRef = ref<InstanceType<typeof SettingDrawer>>()
//
const showNotificationCenter = () => {
console.log('showNotificationCenter 被调用')
console.log('notificationCenterRef.value:', notificationCenterRef.value)
if (notificationCenterRef.value) {
console.log('调用 toggleDropdown 方法')
try {
notificationCenterRef.value.toggleDropdown()
} catch (error) {
console.error('调用 toggleDropdown 失败:', error)
//
if (notificationCenterRef.value.modalVisible !== undefined) {
notificationCenterRef.value.modalVisible = true
}
}
} else {
console.error('notificationCenterRef.value 为空')
}
}
// 退 // 退
const logout = () => { const logout = () => {
Modal.warning({ Modal.warning({
@ -262,22 +298,6 @@ onMounted(() => {
transform: rotate(180deg); transform: rotate(180deg);
} }
.user {
cursor: pointer;
color: var(--color-text-1);
.username {
margin-left: 10px;
white-space: nowrap;
}
.arco-icon-down {
transition: all 0.3s;
margin-left: 2px;
}
}
//
.notification-badge { .notification-badge {
.arco-badge-dot { .arco-badge-dot {
background-color: #f53f3f; background-color: #f53f3f;
@ -328,4 +348,49 @@ onMounted(() => {
transform: translate3d(0, -2px, 0); transform: translate3d(0, -2px, 0);
} }
} }
//
.dropdown-notification-badge {
margin-left: 8px;
:deep(.arco-badge-count) {
background-color: #f53f3f;
font-weight: bold;
font-size: 12px;
min-width: 18px;
height: 18px;
line-height: 18px;
border-radius: 9px;
animation: bounce 0.6s ease-in-out;
}
}
//
.user {
cursor: pointer;
color: var(--color-text-1);
.username {
margin-left: 10px;
white-space: nowrap;
}
.arco-icon-down {
transition: all 0.3s;
margin-left: 2px;
}
//
:deep(.arco-badge-count) {
background-color: #f53f3f;
font-weight: bold;
font-size: 11px;
min-width: 16px;
height: 16px;
line-height: 16px;
border-radius: 8px;
animation: bounce 0.6s ease-in-out;
box-shadow: 0 2px 4px rgba(245, 63, 63, 0.3);
}
}
</style> </style>

View File

@ -1104,7 +1104,7 @@ export const systemRoutes: RouteRecordRaw[] = [
}, },
// ], // ],
// }, // },
// 商务数据库信息模块 // 智能商务模块
{ {
path: '/bussiness-knowledge', path: '/bussiness-knowledge',
name: 'bussinesskonwledge', name: 'bussinesskonwledge',
@ -1117,7 +1117,7 @@ export const systemRoutes: RouteRecordRaw[] = [
name: 'bussiness-knowledge', name: 'bussiness-knowledge',
component: () => import('@/views/bussiness-data/bussiness.vue'), component: () => import('@/views/bussiness-data/bussiness.vue'),
meta: { meta: {
title: '商务数据库信息', title: '智能商务',
icon: 'info-circle', icon: 'info-circle',
hidden: false, hidden: false,
}, },

View File

@ -8,6 +8,7 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default'] ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default']
ApprovalMessageItem: typeof import('./../components/NotificationCenter/ApprovalMessageItem.vue')['default']
Avatar: typeof import('./../components/Avatar/index.vue')['default'] Avatar: typeof import('./../components/Avatar/index.vue')['default']
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default'] Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
CellCopy: typeof import('./../components/CellCopy/index.vue')['default'] CellCopy: typeof import('./../components/CellCopy/index.vue')['default']

View File

@ -4,10 +4,9 @@
<a-layout-sider <a-layout-sider
width="260" width="260"
:collapsed-width="80" :collapsed-width="80"
theme="light" theme="dark"
class="folder-sidebar" class="folder-sidebar"
:collapsed="sidebarCollapsed" :collapsed="sidebarCollapsed"
collapsible
@collapse="handleSidebarCollapse" @collapse="handleSidebarCollapse"
@expand="handleSidebarExpand" @expand="handleSidebarExpand"
> >
@ -102,29 +101,7 @@
</div> </div>
</div> </div>
<!-- 侧边栏底部分页控件 -->
<div class="sidebar-footer" v-if="!sidebarCollapsed && folderList.length > 0">
<div class="pagination-info">
<a-typography-text type="secondary" size="small">
{{ totalFolders }} 个文件夹
</a-typography-text>
</div>
<!-- 隐藏分页控件因为现在获取所有文件夹 -->
<!-- <div class="pagination-controls">
<a-pagination
:current="currentPage"
:page-size="pageSize"
:total="totalFolders"
:show-size-changer="true"
:page-size-options="['10', '20', '50', '100']"
@change="handlePageChange"
@showSizeChange="handlePageSizeChange"
size="small"
show-total
/>
</div> -->
</div>
</a-layout-sider> </a-layout-sider>
<a-layout> <a-layout>
@ -242,10 +219,42 @@
<div class="file-grid-container" v-if="currentFolderId && !loading"> <div class="file-grid-container" v-if="currentFolderId && !loading">
<!-- 表头行 --> <!-- 表头行 -->
<a-row class="table-header-row"> <a-row class="table-header-row">
<a-col :span="10" class="table-column name-column">文件名</a-col> <a-col :span="10" class="table-column name-column">
<a-col :span="4" class="table-column type-column">类型</a-col> <div class="sortable-header" @click="handleSortChange('fileName')">
<a-col :span="3" class="table-column size-column">大小</a-col> <span>文件名</span>
<a-col :span="5" class="table-column time-column">修改时间</a-col> <div class="sort-indicator">
<div class="sort-arrow up" :class="{ active: sortField === 'file_name' && sortOrder === 'asc' }"></div>
<div class="sort-arrow down" :class="{ active: sortField === 'file_name' && sortOrder === 'desc' }"></div>
</div>
</div>
</a-col>
<a-col :span="4" class="table-column type-column">
<div class="sortable-header" @click="handleSortChange('fileType')">
<span>类型</span>
<div class="sort-indicator">
<div class="sort-arrow up" :class="{ active: sortField === 'file_type' && sortOrder === 'asc' }"></div>
<div class="sort-arrow down" :class="{ active: sortField === 'file_type' && sortOrder === 'desc' }"></div>
</div>
</div>
</a-col>
<a-col :span="3" class="table-column size-column">
<div class="sortable-header" @click="handleSortChange('fileSize')">
<span>大小</span>
<div class="sort-indicator">
<div class="sort-arrow up" :class="{ active: sortField === 'file_size' && sortOrder === 'asc' }"></div>
<div class="sort-arrow down" :class="{ active: sortField === 'file_size' && sortOrder === 'desc' }"></div>
</div>
</div>
</a-col>
<a-col :span="5" class="table-column time-column">
<div class="sortable-header" @click="handleSortChange('uploadTime')">
<span>修改时间</span>
<div class="sort-indicator">
<div class="sort-arrow up" :class="{ active: sortField === 'upload_time' && sortOrder === 'asc' }"></div>
<div class="sort-arrow down" :class="{ active: sortField === 'upload_time' && sortOrder === 'desc' }"></div>
</div>
</div>
</a-col>
<a-col :span="2" class="table-column action-column">操作</a-col> <a-col :span="2" class="table-column action-column">操作</a-col>
</a-row> </a-row>
@ -273,7 +282,7 @@
<!-- 大小列 --> <!-- 大小列 -->
<a-col :span="3" class="table-column size-column"> <a-col :span="3" class="table-column size-column">
<div class="cell-content">{{ formatFileSize(file.fileSize || file.size) }}</div> <div class="cell-content">{{ formatFileListSize(file.fileSize || file.size) }}</div>
</a-col> </a-col>
<!-- 时间列 --> <!-- 时间列 -->
@ -613,6 +622,18 @@ const fileCurrentPage = ref(1);
const filePageSize = ref(10); const filePageSize = ref(10);
const totalFiles = ref(0); const totalFiles = ref(0);
//
const sortField = ref('');
const sortOrder = ref('');
// ->
const sortFieldMap = {
'fileName': 'file_name',
'fileType': 'file_type',
'fileSize': 'file_size',
'uploadTime': 'upload_time'
};
// //
const folderForm = reactive({ const folderForm = reactive({
id: '', id: '',
@ -638,7 +659,7 @@ const fileListTemp = ref([]);
const folderFormRef = ref(null); const folderFormRef = ref(null);
const uploadFormRef = ref(null); const uploadFormRef = ref(null);
const uploadRef = ref(null); const uploadRef = ref(null);
const folderColor = '#165DFF'; const folderColor = 'var(--color-primary)';
const refreshing = ref(false); const refreshing = ref(false);
const folderSubmitting = ref(false); const folderSubmitting = ref(false);
const uploading = ref(false); const uploading = ref(false);
@ -941,6 +962,9 @@ const handleFileSearch = () => {
console.log('文件搜索关键词:', fileSearchKeyword.value); console.log('文件搜索关键词:', fileSearchKeyword.value);
// //
fileCurrentPage.value = 1; fileCurrentPage.value = 1;
//
sortField.value = '';
sortOrder.value = '';
console.log('重置文件页码为:', fileCurrentPage.value); console.log('重置文件页码为:', fileCurrentPage.value);
if (currentFolderId.value) { if (currentFolderId.value) {
loadFiles(currentFolderId.value); loadFiles(currentFolderId.value);
@ -960,6 +984,9 @@ const handleFileSearchInput = (value) => {
searchTimeout.value = setTimeout(() => { searchTimeout.value = setTimeout(() => {
console.log('=== 防抖文件搜索执行 ==='); console.log('=== 防抖文件搜索执行 ===');
fileCurrentPage.value = 1; fileCurrentPage.value = 1;
//
sortField.value = '';
sortOrder.value = '';
console.log('重置文件页码为:', fileCurrentPage.value); console.log('重置文件页码为:', fileCurrentPage.value);
if (currentFolderId.value) { if (currentFolderId.value) {
loadFiles(currentFolderId.value); loadFiles(currentFolderId.value);
@ -976,6 +1003,9 @@ const handleFileSearchClear = () => {
console.log('清除文件搜索定时器'); console.log('清除文件搜索定时器');
} }
fileCurrentPage.value = 1; fileCurrentPage.value = 1;
//
sortField.value = '';
sortOrder.value = '';
console.log('重置文件页码为:', fileCurrentPage.value); console.log('重置文件页码为:', fileCurrentPage.value);
if (currentFolderId.value) { if (currentFolderId.value) {
loadFiles(currentFolderId.value); loadFiles(currentFolderId.value);
@ -985,12 +1015,20 @@ const handleFileSearchClear = () => {
const loadFiles = async (folderId) => { const loadFiles = async (folderId) => {
try { try {
loading.value = true; loading.value = true;
const res = await getFilesApi({ const apiParams = {
folderId: folderId, folderId: folderId,
page: fileCurrentPage.value, page: fileCurrentPage.value,
pageSize: filePageSize.value, pageSize: filePageSize.value,
fileName: fileSearchKeyword.value || undefined fileName: fileSearchKeyword.value || undefined
}); };
//
if (sortField.value && sortOrder.value) {
apiParams.sortField = sortField.value;
apiParams.sortOrder = sortOrder.value;
}
const res = await getFilesApi(apiParams);
// //
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
@ -1012,6 +1050,26 @@ const loadFiles = async (folderId) => {
} }
}; };
//
const handleSortChange = (field) => {
const backendField = sortFieldMap[field];
if (!backendField) return;
//
if (sortField.value === backendField) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
} else {
//
sortField.value = backendField;
sortOrder.value = 'desc';
}
//
if (currentFolderId.value) {
loadFiles(currentFolderId.value);
}
};
// //
// const handleFolderClick = (folderId) => { // const handleFolderClick = (folderId) => {
@ -1030,8 +1088,10 @@ const handleFolderSelect = (selectedKeys, info) => {
const folderId = selectedKeys[0]; const folderId = selectedKeys[0];
if (currentFolderId.value !== folderId) { if (currentFolderId.value !== folderId) {
fileCurrentPage.value = 1; fileCurrentPage.value = 1;
// //
fileSearchKeyword.value = ''; fileSearchKeyword.value = '';
sortField.value = '';
sortOrder.value = '';
} }
currentFolderId.value = folderId; currentFolderId.value = folderId;
loadFiles(folderId); loadFiles(folderId);
@ -1136,6 +1196,9 @@ const handleBreadcrumbClick = (index) => {
if (index === 0) { if (index === 0) {
// "" // ""
currentFolderId.value = '0'; currentFolderId.value = '0';
//
sortField.value = '';
sortOrder.value = '';
loadFiles('0'); loadFiles('0');
} else { } else {
// ID // ID
@ -1146,6 +1209,9 @@ const handleBreadcrumbClick = (index) => {
const targetFolder = folderList.value.find(folder => folder.name === targetFolderName); const targetFolder = folderList.value.find(folder => folder.name === targetFolderName);
if (targetFolder) { if (targetFolder) {
currentFolderId.value = targetFolder.id; currentFolderId.value = targetFolder.id;
//
sortField.value = '';
sortOrder.value = '';
loadFiles(targetFolder.id); loadFiles(targetFolder.id);
} }
} }
@ -1307,6 +1373,9 @@ const refreshData = async () => {
// //
searchKeyword.value = ''; searchKeyword.value = '';
currentPage.value = 1; currentPage.value = 1;
//
sortField.value = '';
sortOrder.value = '';
await initData(); await initData();
if (currentFolderId.value) { if (currentFolderId.value) {
@ -1532,7 +1601,7 @@ const fileColor = (extension) => {
bmp: '#722ed1', bmp: '#722ed1',
webp: '#13c2c2' webp: '#13c2c2'
}; };
return colorMap[extension.toLowerCase()] || '#8c8c8c'; return colorMap[extension.toLowerCase()] || 'var(--color-text-3)';
}; };
@ -1789,8 +1858,8 @@ const showTextPreview = async (blob, fileName) => {
maxWidth: '100%', maxWidth: '100%',
maxHeight: '70vh', maxHeight: '70vh',
overflow: 'auto', overflow: 'auto',
backgroundColor: '#f8f9fa', backgroundColor: 'var(--color-fill-1)',
border: '1px solid #e9ecef', border: '1px solid var(--color-border)',
borderRadius: '8px', borderRadius: '8px',
padding: '20px', padding: '20px',
fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace", fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace",
@ -1798,7 +1867,7 @@ const showTextPreview = async (blob, fileName) => {
lineHeight: '1.6', lineHeight: '1.6',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
wordBreak: 'break-word', wordBreak: 'break-word',
color: '#333', color: 'var(--color-text-1)',
textAlign: 'left' textAlign: 'left'
} }
}, text) }, text)
@ -2244,6 +2313,21 @@ const formatFileSize = (fileSize) => {
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`; return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}; };
// KB
const formatFileListSize = (fileSize) => {
const size = Number(fileSize);
if (isNaN(size) || size < 0) return '未知';
// KB
if (size < 1024) {
return `${size} KB`;
} else if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(1)} MB`;
} else {
return `${(size / (1024 * 1024)).toFixed(1)} GB`;
}
};
const fileTypeText = (type) => { const fileTypeText = (type) => {
@ -2366,7 +2450,7 @@ onMounted(() => {
<style scoped> <style scoped>
.knowledge-container { .knowledge-container {
height: 100vh; height: 100vh;
background-color: var(--color-bg-2); background-color: var(--color-bg-1);
} }
@ -2378,13 +2462,15 @@ onMounted(() => {
transition: all 0.3s ease; transition: all 0.3s ease;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%); background: var(--color-bg-1);
display: flex;
flex-direction: column;
} }
.sidebar-header { .sidebar-header {
padding: 20px 16px; padding: 20px 16px;
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); background: var(--color-bg-1);
position: relative; position: relative;
&::after { &::after {
@ -2400,12 +2486,18 @@ onMounted(() => {
.folder-content { .folder-content {
padding: 16px 0; padding: 16px 0;
height: calc(100vh - 320px); /* 为底部分页控件留出更多空间,因为文件夹项现在更高 */ flex: 1;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin; scrollbar-width: thin;
background: rgba(255, 255, 255, 0.6); background: var(--color-bg-1);
display: flex;
flex-direction: column;
min-height: 0;
max-height: calc(100vh - 200px);
} }
.folder-content::-webkit-scrollbar { .folder-content::-webkit-scrollbar {
width: 8px; width: 8px;
} }
@ -2445,7 +2537,7 @@ onMounted(() => {
border: 1px solid transparent; border: 1px solid transparent;
&:hover { &:hover {
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); background: linear-gradient(135deg, var(--color-fill-2) 0%, var(--color-fill-3) 100%);
border-color: var(--color-primary-light-2); border-color: var(--color-primary-light-2);
transform: translateX(2px); transform: translateX(2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
@ -2514,7 +2606,7 @@ onMounted(() => {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 24px; padding: 0 24px;
background: var(--color-bg-2); background: var(--color-bg-1);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
height: 64px; height: 64px;
} }
@ -2531,10 +2623,12 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 24px; padding: 24px;
overflow: auto; overflow: hidden;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
background: var(--color-bg-2); background: var(--color-bg-1);
min-height: 0;
max-height: calc(100vh - 120px);
} }
.file-card { .file-card {
@ -2545,6 +2639,7 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
position: relative; position: relative;
height: 100%; height: 100%;
overflow: hidden;
} }
/* 表格容器 */ /* 表格容器 */
@ -2557,7 +2652,9 @@ onMounted(() => {
overflow-y: auto; overflow-y: auto;
background-color: var(--color-bg-1); background-color: var(--color-bg-1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
padding-bottom: 80px; /* 为分页留出空间 */ margin-bottom: 0;
min-height: 300px;
max-height: calc(100vh - 300px);
} }
/* 表头行样式 */ /* 表头行样式 */
@ -2637,8 +2734,8 @@ onMounted(() => {
} }
.folder-icon { .folder-icon {
color: #165DFF; color: var(--color-primary);
background-color: #E8F3FF; background-color: var(--color-primary-light-1);
} }
.file-name { .file-name {
@ -2652,7 +2749,7 @@ onMounted(() => {
} }
.table-data-row:hover .file-name { .table-data-row:hover .file-name {
color: #165DFF; color: var(--color-primary);
} }
.type-column, .size-column, .time-column { .type-column, .size-column, .time-column {
@ -2697,7 +2794,7 @@ onMounted(() => {
white-space: nowrap; white-space: nowrap;
.table-data-row:hover & { .table-data-row:hover & {
color: #165DFF; color: var(--color-primary);
} }
} }
@ -2830,6 +2927,35 @@ onMounted(() => {
} }
} }
/* 浏览器缩放调整 */
@media (max-height: 800px) {
.folder-content {
max-height: calc(100vh - 180px);
}
.file-content {
max-height: calc(100vh - 100px);
}
.file-grid-container {
max-height: calc(100vh - 280px);
}
}
@media (max-height: 600px) {
.folder-content {
max-height: calc(100vh - 160px);
}
.file-content {
max-height: calc(100vh - 80px);
}
.file-grid-container {
max-height: calc(100vh - 260px);
}
}
/* 空状态样式 */ /* 空状态样式 */
.initial-state, .empty-state { .initial-state, .empty-state {
display: flex; display: flex;
@ -2838,7 +2964,7 @@ onMounted(() => {
justify-content: center; justify-content: center;
padding: 64px 0; padding: 64px 0;
color: var(--color-text-3); color: var(--color-text-3);
background-color: #fafafa; background-color: var(--color-fill-1);
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
} }
@ -2852,7 +2978,7 @@ onMounted(() => {
:deep(.empty-state .arco-btn) { :deep(.empty-state .arco-btn) {
margin-top: 16px; margin-top: 16px;
padding: 8px 16px; padding: 8px 16px;
background-color: #165DFF; background-color: var(--color-primary);
color: white; color: white;
border-radius: 4px; border-radius: 4px;
border: none; border: none;
@ -2862,7 +2988,7 @@ onMounted(() => {
} }
:deep(.empty-state .arco-btn:hover) { :deep(.empty-state .arco-btn:hover) {
background-color: #0E42D2; background-color: var(--color-primary-dark-1);
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
@ -2984,50 +3110,7 @@ onMounted(() => {
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
} }
/* 侧边栏底部分页样式 */
.sidebar-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
border-top: 1px solid var(--color-border);
padding: 20px 16px;
z-index: 10;
backdrop-filter: blur(10px);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.pagination-info {
margin-bottom: 16px;
text-align: center;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.8);
border-radius: 8px;
border: 1px solid var(--color-border);
}
.pagination-controls {
display: flex;
justify-content: center;
:deep(.arco-pagination) {
.arco-pagination-item {
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
}
}
/* 确保文件夹内容区域不被底部分页遮挡 */
.folder-content {
/* 高度已调整无需额外padding */
}
/* 动画效果 */ /* 动画效果 */
:deep(.arco-icon-refresh.spin) { :deep(.arco-icon-refresh.spin) {
@ -3158,8 +3241,8 @@ onMounted(() => {
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
&:hover { &:hover {
background: #e2e8f0; background: var(--color-fill-2);
color: #165DFF; color: var(--color-primary);
} }
} }
@ -3168,8 +3251,8 @@ onMounted(() => {
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
border-color: #165DFF; border-color: var(--color-primary);
color: #165DFF; color: var(--color-primary);
} }
&:active { &:active {
@ -3278,7 +3361,7 @@ onMounted(() => {
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
transition: all 0.3s ease; transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.9); background: rgba(var(--color-bg-1-rgb), 0.9);
&:hover { &:hover {
border-color: var(--color-primary-light-2); border-color: var(--color-primary-light-2);
@ -3296,7 +3379,7 @@ onMounted(() => {
.search-result-tip { .search-result-tip {
padding: 12px 16px; padding: 12px 16px;
margin: 12px 16px; margin: 12px 16px;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); background: linear-gradient(135deg, var(--color-primary-light-1) 0%, var(--color-primary-light-2) 100%);
border-radius: 8px; border-radius: 8px;
border-left: 4px solid var(--color-primary); border-left: 4px solid var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
@ -3310,7 +3393,7 @@ onMounted(() => {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%); background: linear-gradient(45deg, transparent 30%, rgba(var(--color-bg-1-rgb), 0.1) 50%, transparent 70%);
animation: shimmer 2s infinite; animation: shimmer 2s infinite;
} }
} }
@ -3348,7 +3431,7 @@ onMounted(() => {
margin-top: 16px; margin-top: 16px;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 8px; border-radius: 8px;
background: var(--color-bg-2); background: var(--color-bg-1);
max-height: 300px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
} }
@ -3464,19 +3547,74 @@ onMounted(() => {
} }
} }
/* 可排序表头样式 */
.sortable-header {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s ease;
user-select: none;
}
.sortable-header:hover {
background: var(--color-fill-2);
color: var(--color-primary);
}
.sort-indicator {
display: flex;
flex-direction: column;
gap: 1px;
margin-left: 4px;
}
.sort-arrow {
width: 0;
height: 0;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
transition: all 0.2s ease;
}
.sort-arrow.up {
border-bottom: 3px solid var(--color-text-4);
}
.sort-arrow.down {
border-top: 3px solid var(--color-text-4);
}
.sort-arrow.active {
border-bottom-color: var(--color-primary);
border-top-color: var(--color-primary);
}
.sortable-header:hover .sort-arrow.up {
border-bottom-color: var(--color-primary);
}
.sortable-header:hover .sort-arrow.down {
border-top-color: var(--color-primary);
}
/* 文件分页样式 */ /* 文件分页样式 */
.file-pagination { .file-pagination {
position: absolute; position: sticky;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
z-index: 10; margin-top: 16px;
margin-top: 0;
padding: 16px 0; padding: 16px 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
background: var(--color-bg-1); background: var(--color-bg-1);
flex-shrink: 0;
z-index: 10;
.arco-pagination { .arco-pagination {
.arco-pagination-total { .arco-pagination-total {
@ -3516,7 +3654,7 @@ onMounted(() => {
/* 树形文件夹结构 */ /* 树形文件夹结构 */
.folder-tree-container { .folder-tree-container {
padding: 8px; padding: 8px;
background: var(--color-bg-2); background: var(--color-bg-1);
border-radius: 6px; border-radius: 6px;
margin: 8px; margin: 8px;
overflow: hidden; overflow: hidden;

View File

@ -50,6 +50,11 @@
<a-descriptions-item label="备注"> <a-descriptions-item label="备注">
{{ contractDetail.notes }} {{ contractDetail.notes }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="合同内容">
<a-typography-paragraph :ellipsis="{ rows: 5, expandable: true, collapseText: '收起', suffix: '' }">
{{ contractDetail.contractText || '—' }}
</a-typography-paragraph>
</a-descriptions-item>
</a-descriptions> </a-descriptions>
</div> </div>
<div v-else-if="!loading" class="empty-container"> <div v-else-if="!loading" class="empty-container">

View File

@ -7,16 +7,8 @@
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item field="projectId" label="项目"> <a-form-item field="projectName" label="项目">
<a-select v-model="contractData.projectId" <a-input v-model="contractData.projectName" placeholder="请输入项目名称" allow-clear />
:options="projectOptions"
:loading="projectLoading"
:virtual-list-props="virtualListProps"
placeholder="请选择项目"
allow-search allow-clear
@focus="handleProjectFocus"
@dropdown-visible-change="handleProjectDropdown"
@search="handleProjectSearch" />
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -43,11 +35,11 @@
<a-col :span="12"> <a-col :span="12">
<a-form-item field="contractStatus" label="合同状态"> <a-form-item field="contractStatus" label="合同状态">
<a-select v-model="contractData.contractStatus"> <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-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-select>
</a-form-item> </a-form-item>
@ -99,6 +91,11 @@
</a-col> </a-col>
</a-row> </a-row>
<a-form-item field="productService" label="产品或服务">
<a-input v-model="contractData.productService" />
</a-form-item>
<a-form-item field="paymentAddress" label="付款地址"> <a-form-item field="paymentAddress" label="付款地址">
<a-input v-model="contractData.paymentAddress" /> <a-input v-model="contractData.paymentAddress" />
</a-form-item> </a-form-item>

View File

@ -48,18 +48,23 @@
<span class="font-medium text-green-600">{{ (record.amount || 0).toLocaleString() }}</span> <span class="font-medium text-green-600">{{ (record.amount || 0).toLocaleString() }}</span>
</template> </template>
<!-- 收款金额 --> <!-- 结算金额支出合同 -->
<template #receivedAmount="{ record }"> <template #settlementAmount="{ record }">
<span class="font-medium text-blue-600">{{ (record.receivedAmount || 0).toLocaleString() }}</span> <span class="font-medium text-blue-600">{{ (record.settlementAmount || record.receivedAmount || 0).toLocaleString() }}</span>
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
<a-space> <a-space>
<a-link @click="viewDetail(record)">查看</a-link> <a-link @click="viewDetail(record)">查看</a-link>
<a-link @click="editRecord(record)">编辑</a-link> <a-link @click="editRecord(record)">编辑</a-link>
<a-link :disabled="record.contractStatus === '已结算'" @click="!(record.contractStatus === '已结算') && openSettlement(record)">结算</a-link>
<a-link status="danger" @click="deleteContract(record)">删除</a-link> <a-link status="danger" @click="deleteContract(record)">删除</a-link>
</a-space> </a-space>
</template> </template>
<!-- 日期展示仅年月日放在 action 模板之外作为独立列插槽 -->
<template #signDate="{ record }">{{ (record.signDate || '').slice(0,10) }}</template>
<template #performanceDeadline="{ record }">{{ (record.performanceDeadline || '').slice(0,10) }}</template>
<template #paymentDate="{ record }">{{ (record.paymentDate || '').slice(0,10) }}</template>
</GiTable> </GiTable>
<!-- 合同详情弹窗 --> <!-- 合同详情弹窗 -->
@ -102,6 +107,15 @@
@update:contract-data="handleNewContractDataUpdate" @update:contract-data="handleNewContractDataUpdate"
/> />
</a-modal> </a-modal>
<!-- 合同结算弹窗 -->
<a-modal v-model:visible="showSettlementModal" title="合同结算" :width="520" @before-ok="submitSettlement">
<a-form :model="settlementForm" layout="vertical">
<a-form-item field="amount" label="结算金额"><a-input-number v-model="settlementForm.amount" style="width:100%" /></a-form-item>
<a-form-item field="paymentDate" label="付款日期"><a-date-picker v-model="settlementForm.paymentDate" show-time value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/></a-form-item>
<a-form-item field="paymentPeriod" label="账期"><a-date-picker v-model="settlementForm.paymentPeriod" show-time value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/></a-form-item>
<a-form-item field="notes" label="备注"><a-textarea v-model="settlementForm.notes" /></a-form-item>
</a-form>
</a-modal>
</GiPageLayout> </GiPageLayout>
</template> </template>
@ -173,11 +187,11 @@ const queryFormColumns = [
props: { props: {
placeholder: '请选择合同状态', placeholder: '请选择合同状态',
options: [ options: [
{ label: '未确认', value: '未确认' }, { label: '未执行', value: '未执行' },
{ label: '待审批', value: '待审批' },
{ label: '已签署', value: '已签署' },
{ label: '执行中', value: '执行中' }, { label: '执行中', value: '执行中' },
{ label: '已完成', value: '已完成' }, { label: '验收中', value: '验收中' },
{ label: '结算中', value: '结算中' },
{ label: '已结算', value: '已结算' },
{ label: '已终止', value: '已终止' }, { label: '已终止', value: '已终止' },
], ],
}, },
@ -187,9 +201,8 @@ const queryFormColumns = [
label: '签署时间', label: '签署时间',
type: 'range-picker' as const, type: 'range-picker' as const,
props: { props: {
placeholder: ['开始时间', '结束时间'], placeholder: ['开始日期', '结束日期'],
showTime: true, format: 'YYYY-MM-DD',
format: 'YYYY-MM-DD HH:mm:ss',
}, },
}, },
] ]
@ -200,11 +213,11 @@ const tableColumns: TableColumnData[] = [
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true }, { title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
{ title: '客户名称', dataIndex: 'customer', width: 200, ellipsis: true, tooltip: true }, { title: '客户名称', dataIndex: 'customer', width: 200, ellipsis: true, tooltip: true },
{ title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 }, { title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 },
{ title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 }, { title: '已结算金额', dataIndex: 'settlementAmount', slotName: 'settlementAmount', width: 120 },
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 }, { title: '未结算金额', dataIndex: 'pendingAmount', width: 120 },
{ title: '签署日期', dataIndex: 'signDate', width: 120 }, { title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
{ title: '履约期限', dataIndex: 'performanceDeadline', width: 120 }, { title: '履约期限', dataIndex: 'performanceDeadline', slotName: 'performanceDeadline', width: 120 },
{ title: '付款日期', dataIndex: 'paymentDate', width: 120 }, { title: '付款日期', dataIndex: 'paymentDate', slotName: 'paymentDate', width: 120 },
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 }, { title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 }, { title: '销售人员', dataIndex: 'salespersonName', width: 100 },
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 }, { title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
@ -252,10 +265,10 @@ const fetchContractList = async () => {
} }
} }
// //
dataList.value = filtered.map((item: ContractItem) => ({ dataList.value = filtered.map((item: ContractItem) => ({
...item, ...item,
pendingAmount: (item.amount || 0) - (item.receivedAmount || 0), pendingAmount: (item.amount || 0) - ((item.settlementAmount || item.receivedAmount || 0)),
})) }))
// //
@ -284,11 +297,11 @@ const pagination = reactive({
// //
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {
未确认: 'gray', 未执行: 'gray',
待审批: 'orange',
已签署: 'blue',
执行中: 'cyan', 执行中: 'cyan',
已完成: 'green', 验收中: 'arcoblue',
结算中: 'orange',
已结算: 'green',
已终止: 'red', 已终止: 'red',
} }
return colorMap[status] || 'gray' return colorMap[status] || 'gray'
@ -381,7 +394,7 @@ const openAddModal = () => {
amount: 0, amount: 0,
accountNumber: '', accountNumber: '',
notes: '', notes: '',
contractStatus: '未确认', contractStatus: '未执行',
contractText: '', contractText: '',
projectName: '', projectName: '',
salespersonName: null, salespersonName: null,
@ -426,7 +439,8 @@ const handleAddSubmit = async () => {
paymentDate: newContractData.value.paymentDate || null, paymentDate: newContractData.value.paymentDate || null,
performanceDeadline: newContractData.value.performanceDeadline || null, performanceDeadline: newContractData.value.performanceDeadline || null,
productService: newContractData.value.productService || '', productService: newContractData.value.productService || '',
projectId: newContractData.value.projectId || '', // projectId
projectName: newContractData.value.projectName || '',
salespersonId: (newContractData.value as any).salespersonId || '', salespersonId: (newContractData.value as any).salespersonId || '',
signDate: newContractData.value.signDate || null, signDate: newContractData.value.signDate || null,
type: newContractData.value.type || '支出合同', type: newContractData.value.type || '支出合同',
@ -476,8 +490,8 @@ const editRecord = (record: ContractItem) => {
...record, ...record,
amount: record.amount || 0, amount: record.amount || 0,
projectId: record.projectId || '', projectId: record.projectId || '',
type: record.type || '收入合同', type: record.type || '支出合同',
contractStatus: record.contractStatus || '未确认', contractStatus: record.contractStatus || '未执行',
} }
selectedContractData.value = completeRecord selectedContractData.value = completeRecord
@ -525,7 +539,7 @@ const handleEditSubmit = async () => {
type: editedContractData.value.type || '', type: editedContractData.value.type || '',
}; };
console.log('Edited Contract Data:', requestData); // 便 // console.log('Edited Contract Data:', requestData); // 便
// /contract PUT // /contract PUT
const response = await http.put('/contract', requestData); const response = await http.put('/contract', requestData);
@ -547,6 +561,91 @@ const handleEditSubmit = async () => {
} }
} }
//
const showSettlementModal = ref(false)
const settlementForm = reactive({
//
contractId: '',
amount: 0,
paymentDate: '',
paymentPeriod: '',
notes: '',
//
accountNumber: '',
code: '',
customer: '',
departmentId: '',
duration: '',
productService: '',
projectId: '',
salespersonId: '',
settlementId: '',
settlementStatus: '',
})
const settlementRecord = ref<ContractItem | null>(null)
const openSettlement = (record: ContractItem) => {
if (record.contractStatus === '已结算') return
settlementRecord.value = record
Object.assign(settlementForm, {
//
contractId: record.contractId,
amount: record.amount || 0,
paymentDate: record.paymentDate || '',
paymentPeriod: '',
notes: '',
// 便
accountNumber: (record as any).accountNumber || '',
code: (record as any).code || '',
customer: (record as any).customer || '',
departmentId: (record as any).departmentId || '',
duration: (record as any).duration || '',
productService: (record as any).productService || '',
projectId: (record as any).projectId || '',
salespersonId: (record as any).salespersonId || '',
settlementId: (record as any).settlementId || '',
settlementStatus: (record as any).settlementStatus || '',
})
showSettlementModal.value = true
}
const submitSettlement = async () => {
try {
const payload:any = {
accountNumber: settlementForm.accountNumber || '',
amount: settlementForm.amount,
code: settlementForm.code || '',
contractId: settlementForm.contractId,
customer: settlementForm.customer || '',
departmentId: settlementForm.departmentId || '',
duration: settlementForm.duration || '',
notes: settlementForm.notes || '',
paymentDate: settlementForm.paymentDate || null,
paymentPeriod: settlementForm.paymentPeriod || '',
productService: settlementForm.productService || '',
projectId: settlementForm.projectId || '',
salespersonId: settlementForm.salespersonId || '',
settlementId: settlementForm.settlementId || '',
settlementStatus: settlementForm.settlementStatus || '',
}
const res = await http.post('/contract-settlement', payload)
if (res.code === 200 || (res as any).status === 200) {
Message.success('合同结算成功')
// 0 0 => /
const origin = settlementRecord.value
const settledAmount = (origin?.settlementAmount ?? origin?.receivedAmount ?? 0) + (settlementForm.amount || 0)
const pendingAfter = Math.max((origin?.amount || 0) - settledAmount, 0)
const targetStatus = pendingAfter === 0 ? '已结算' : '结算中'
await http.put('/contract', { contractId: settlementForm.contractId, contractStatus: targetStatus, type: origin?.type || '支出合同' })
showSettlementModal.value = false
search()
return true
}
Message.error(res.msg || '合同结算失败')
return false
} catch (e:any) {
Message.error(e?.message || '合同结算失败')
return false
}
}
// //
const deleteContract = (record: ContractItem) => { const deleteContract = (record: ContractItem) => {

View File

@ -50,6 +50,11 @@
<a-descriptions-item label="备注"> <a-descriptions-item label="备注">
{{ contractDetail.notes }} {{ contractDetail.notes }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="合同内容">
<a-typography-paragraph :ellipsis="{ rows: 5, expandable: true, collapseText: '收起', suffix: '' }">
{{ contractDetail.contractText || '—' }}
</a-typography-paragraph>
</a-descriptions-item>
</a-descriptions> </a-descriptions>
</div> </div>
<div v-else-if="!loading" class="empty-container"> <div v-else-if="!loading" class="empty-container">

View File

@ -7,16 +7,8 @@
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item field="projectId" label="项目"> <a-form-item field="projectName" label="项目">
<a-select v-model="contractData.projectId" <a-input v-model="contractData.projectName" placeholder="请输入项目名称" allow-clear />
:options="projectOptions"
:loading="projectLoading"
:virtual-list-props="virtualListProps"
placeholder="请选择项目"
allow-search allow-clear
@focus="handleProjectFocus"
@dropdown-visible-change="handleProjectDropdown"
@search="handleProjectSearch" />
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -43,11 +35,11 @@
<a-col :span="12"> <a-col :span="12">
<a-form-item field="contractStatus" label="合同状态"> <a-form-item field="contractStatus" label="合同状态">
<a-select v-model="contractData.contractStatus"> <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-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-select>
</a-form-item> </a-form-item>
@ -99,6 +91,11 @@
</a-col> </a-col>
</a-row> </a-row>
<a-form-item field="productService" label="产品或服务">
<a-input v-model="contractData.productService" />
</a-form-item>
<a-form-item field="paymentAddress" label="付款地址"> <a-form-item field="paymentAddress" label="付款地址">
<a-input v-model="contractData.paymentAddress" /> <a-input v-model="contractData.paymentAddress" />
</a-form-item> </a-form-item>

View File

@ -48,18 +48,24 @@
<span class="font-medium text-green-600">{{ (record.amount || 0).toLocaleString() }}</span> <span class="font-medium text-green-600">{{ (record.amount || 0).toLocaleString() }}</span>
</template> </template>
<!-- 收款金额 --> <!-- 回款金额收入合同 -->
<template #receivedAmount="{ record }"> <template #receivedAmount="{ record }">
<span class="font-medium text-blue-600">{{ (record.receivedAmount || 0).toLocaleString() }}</span> <span class="font-medium text-blue-600">{{ (record.receivedAmount || record.settlementAmount || 0).toLocaleString() }}</span>
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
<a-space> <a-space>
<a-link @click="viewDetail(record)">查看</a-link> <a-link @click="viewDetail(record)">查看</a-link>
<a-link @click="editRecord(record)">编辑</a-link> <a-link @click="editRecord(record)">编辑</a-link>
<a-link :disabled="record.contractStatus === '已结算'" @click="!(record.contractStatus === '已结算') && openSettlement(record)">结算</a-link>
<a-link status="danger" @click="deleteContract(record)">删除</a-link> <a-link status="danger" @click="deleteContract(record)">删除</a-link>
</a-space> </a-space>
</template> </template>
<!-- 日期展示仅年月日放在 action 模板之外作为独立列插槽 -->
<template #signDate="{ record }">{{ (record.signDate || '').slice(0,10) }}</template>
<template #performanceDeadline="{ record }">{{ (record.performanceDeadline || '').slice(0,10) }}</template>
<template #paymentDate="{ record }">{{ (record.paymentDate || '').slice(0,10) }}</template>
</GiTable> </GiTable>
<!-- 合同详情弹窗 --> <!-- 合同详情弹窗 -->
@ -87,8 +93,6 @@
@update:contract-data="handleContractDataUpdate" @update:contract-data="handleContractDataUpdate"
/> />
</a-modal> </a-modal>
</GiPageLayout>
<!-- 新建合同弹窗 --> <!-- 新建合同弹窗 -->
<a-modal <a-modal
v-model:visible="showAddModal" v-model:visible="showAddModal"
@ -102,7 +106,18 @@
:contract-data="newContractData" :contract-data="newContractData"
@update:contract-data="handleNewContractDataUpdate" @update:contract-data="handleNewContractDataUpdate"
/> />
</a-modal>
<!-- 合同结算弹窗 -->
<a-modal v-model:visible="showSettlementModal" title="合同结算" :width="520" @before-ok="submitSettlement">
<a-form :model="settlementForm" layout="vertical">
<a-form-item field="amount" label="结算金额"><a-input-number v-model="settlementForm.amount" style="width:100%" /></a-form-item>
<a-form-item field="paymentDate" label="付款日期"><a-date-picker v-model="settlementForm.paymentDate" show-time value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/></a-form-item>
<a-form-item field="paymentPeriod" label="账期"><a-date-picker v-model="settlementForm.paymentPeriod" show-time value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/></a-form-item>
<a-form-item field="notes" label="备注"><a-textarea v-model="settlementForm.notes" /></a-form-item>
</a-form>
</a-modal> </a-modal>
</GiPageLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -148,49 +163,33 @@ interface ContractItem {
// //
const searchForm = reactive({ const searchForm = reactive({
contractName: '',
contractCode: '', contractCode: '',
client: '', client: '',
status: '', status: '',
signDate: '', signDateRange: [] as [string, string] | [],
page: 1, page: 1,
size: 10, size: 10,
}) })
// //
const queryFormColumns = [ const queryFormColumns = [
{ { field: 'client', label: '客户', type: 'input' as const, props: { placeholder: '请输入客户名称' } },
field: 'contractName', { field: 'status', label: '合同状态', type: 'select' as const, props: {
label: '合同名称',
type: 'input' as const,
props: {
placeholder: '请输入合同名称',
},
},
{
field: 'client',
label: '客户',
type: 'input' as const,
props: {
placeholder: '请输入客户名称',
},
},
{
field: 'status',
label: '合同状态',
type: 'select' as const,
props: {
placeholder: '请选择合同状态', placeholder: '请选择合同状态',
options: [ options: [
{ label: '未确认', value: '未确认' }, { label: '未执行', value: '未执行' },
{ label: '待审批', value: '待审批' },
{ label: '已签署', value: '已签署' },
{ label: '执行中', value: '执行中' }, { label: '执行中', value: '执行中' },
{ label: '已完成', value: '已完成' }, { label: '验收中', value: '验收中' },
{ label: '结算中', value: '结算中' },
{ label: '已结算', value: '已结算' },
{ label: '已终止', value: '已终止' }, { label: '已终止', value: '已终止' },
], ],
}, },
}, },
{ field: 'signDateRange', label: '签署时间', type: 'range-picker' as const, props: {
placeholder: ['开始日期', '结束日期'], format: 'YYYY-MM-DD',
}
},
] ]
// //
@ -201,16 +200,105 @@ const tableColumns: TableColumnData[] = [
{ title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 }, { title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 },
{ title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 }, { title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 },
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 }, { title: '未收款金额', dataIndex: 'pendingAmount', width: 120 },
{ title: '签署日期', dataIndex: 'signDate', width: 120 }, { title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
{ title: '履约期限', dataIndex: 'performanceDeadline', width: 120 }, { title: '履约期限', dataIndex: 'performanceDeadline', slotName: 'performanceDeadline', width: 120 },
{ title: '付款日期', dataIndex: 'paymentDate', width: 120 }, { title: '付款日期', dataIndex: 'paymentDate', slotName: 'paymentDate', width: 120 },
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 }, { title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 120 },
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 }, { title: '销售人员', dataIndex: 'salespersonName', width: 100 },
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 }, { title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
{ title: '产品服务', dataIndex: 'productService', width: 120, ellipsis: true, tooltip: true }, { title: '产品服务', dataIndex: 'productService', width: 120, ellipsis: true, tooltip: true },
{ title: '备注', dataIndex: 'notes', width: 200, ellipsis: true, tooltip: true }, { title: '备注', dataIndex: 'notes', width: 220, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' }, { title: '操作', slotName: 'action', width: 240, fixed: 'right' },
] ]
//
const showSettlementModal = ref(false)
const settlementForm = reactive({
//
contractId: '',
amount: 0,
paymentDate: '',
paymentPeriod: '',
notes: '',
//
accountNumber: '',
code: '',
customer: '',
departmentId: '',
duration: '',
productService: '',
projectId: '',
salespersonId: '',
settlementId: '',
settlementStatus: '',
})
const settlementRecord = ref<ContractItem | null>(null)
const openSettlement = (record: ContractItem) => {
if (record.contractStatus === '已结算') return
settlementRecord.value = record
Object.assign(settlementForm, {
//
contractId: record.contractId,
amount: record.amount || 0,
paymentDate: record.paymentDate || '',
paymentPeriod: '',
notes: '',
// 便
accountNumber: (record as any).accountNumber || '',
code: (record as any).code || '',
customer: (record as any).customer || '',
departmentId: (record as any).departmentId || '',
duration: (record as any).duration || '',
productService: (record as any).productService || '',
projectId: (record as any).projectId || '',
salespersonId: (record as any).salespersonId || '',
settlementId: (record as any).settlementId || '',
settlementStatus: (record as any).settlementStatus || '',
})
showSettlementModal.value = true
}
const submitSettlement = async () => {
try {
const payload:any = {
accountNumber: settlementForm.accountNumber || '',
amount: settlementForm.amount,
code: settlementForm.code || '',
contractId: settlementForm.contractId,
customer: settlementForm.customer || '',
departmentId: settlementForm.departmentId || '',
duration: settlementForm.duration || '',
notes: settlementForm.notes || '',
paymentDate: settlementForm.paymentDate || null,
paymentPeriod: settlementForm.paymentPeriod || '',
productService: settlementForm.productService || '',
projectId: settlementForm.projectId || '',
salespersonId: settlementForm.salespersonId || '',
settlementId: settlementForm.settlementId || '',
settlementStatus: settlementForm.settlementStatus || '',
}
const res = await http.post('/contract-settlement', payload)
if (res.code === 200 || (res as any).status === 200) {
Message.success('合同结算成功')
// 0 0 =>
const origin = settlementRecord.value
const settledAmount = (origin?.settlementAmount ?? origin?.receivedAmount ?? 0) + (settlementForm.amount || 0)
const pendingAfter = Math.max((origin?.amount || 0) - settledAmount, 0)
const targetStatus = pendingAfter === 0 ? '已结算' : '结算中'
await http.put('/contract', { contractId: settlementForm.contractId, contractStatus: targetStatus, type: origin?.type || '收入合同' })
showSettlementModal.value = false
search()
return true
}
Message.error(res.msg || '合同结算失败')
return false
} catch (e:any) {
Message.error(e?.message || '合同结算失败')
return false
}
}
// //
const loading = ref(false) const loading = ref(false)
const dataList = ref<ContractItem[]>([]) const dataList = ref<ContractItem[]>([])
@ -222,27 +310,39 @@ const fetchContractList = async () => {
const params = { const params = {
page: searchForm.page, page: searchForm.page,
pageSize: searchForm.size, pageSize: searchForm.size,
contractName: searchForm.contractName,
code: searchForm.contractCode, code: searchForm.contractCode,
customer: searchForm.client, customer: searchForm.client,
contractStatus: searchForm.status, contractStatus: searchForm.status,
signDate: searchForm.signDate, signDateStart: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[0] : undefined,
signDateEnd: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[1] : undefined,
} }
const response = await http.get('/contract/list', params) const response = await http.get('/contract/list', params)
if (response.code === 200) { if (response.code === 200) {
// ""
const allContracts = response.rows || [] const allContracts = response.rows || []
const revenueContracts = allContracts.filter((item: ContractItem) => item.type === '收入合同') let revenueContracts = allContracts.filter((item: ContractItem) => item.type === '收入合同')
//
const range = Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange : null
if (range) {
const [start, end] = range
const s = new Date(start as any).getTime(), e = new Date(end as any).getTime()
if (!Number.isNaN(s) && !Number.isNaN(e)) {
revenueContracts = revenueContracts.filter((item: ContractItem) => {
if (!item.signDate) return false
const t = new Date(item.signDate as any).getTime()
return !Number.isNaN(t) && t >= s && t <= e
})
}
}
//
dataList.value = revenueContracts.map((item: ContractItem) => ({ dataList.value = revenueContracts.map((item: ContractItem) => ({
...item, ...item,
pendingAmount: (item.amount || 0) - (item.receivedAmount || 0), pendingAmount: (item.amount || 0) - ((item.receivedAmount || item.settlementAmount || 0)),
})) }))
pagination.total = Number.parseInt(response.total) || 0 pagination.total = dataList.value.length
} else { } else {
Message.error(response.msg || '获取合同列表失败') Message.error(response.msg || '获取合同列表失败')
dataList.value = [] dataList.value = []
@ -267,11 +367,11 @@ const pagination = reactive({
// //
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {
未确认: 'gray', 未执行: 'gray',
待审批: 'orange',
已签署: 'blue',
执行中: 'cyan', 执行中: 'cyan',
已完成: 'green', 验收中: 'arcoblue',
结算中: 'orange',
已结算: 'green',
已终止: 'red', 已终止: 'red',
} }
return colorMap[status] || 'gray' return colorMap[status] || 'gray'
@ -290,11 +390,10 @@ const search = async () => {
const reset = () => { const reset = () => {
Object.assign(searchForm, { Object.assign(searchForm, {
contractName: '',
contractCode: '', contractCode: '',
client: '', client: '',
status: '', status: '',
signDate: '', signDateRange: [],
page: 1, page: 1,
size: 10, size: 10,
}) })
@ -323,7 +422,7 @@ const newContractData = ref<ContractItem>({
contractId: '', customer: '', code: '', projectId: '', type: '收入合同', contractId: '', customer: '', code: '', projectId: '', type: '收入合同',
productService: '', paymentDate: null, performanceDeadline: null, productService: '', paymentDate: null, performanceDeadline: null,
paymentAddress: '', amount: 0, accountNumber: '', notes: '', paymentAddress: '', amount: 0, accountNumber: '', notes: '',
contractStatus: '未确认', contractText: '', projectName: '', contractStatus: '未执行', contractText: '', projectName: '',
salespersonName: null, salespersonDeptName: '', settlementAmount: null, salespersonName: null, salespersonDeptName: '', settlementAmount: null,
receivedAmount: null, contractStatusLabel: null, createBy: null, updateBy: null, receivedAmount: null, contractStatusLabel: null, createBy: null, updateBy: null,
createTime: '', updateTime: '', page: 1, pageSize: 10, signDate: '', duration: '', createTime: '', updateTime: '', page: 1, pageSize: 10, signDate: '', duration: '',
@ -334,7 +433,7 @@ const openAddModal = () => {
contractId: '', customer: '', code: '', projectId: '', type: '收入合同', contractId: '', customer: '', code: '', projectId: '', type: '收入合同',
productService: '', paymentDate: null, performanceDeadline: null, productService: '', paymentDate: null, performanceDeadline: null,
paymentAddress: '', amount: 0, accountNumber: '', notes: '', paymentAddress: '', amount: 0, accountNumber: '', notes: '',
contractStatus: '未确认', contractText: '', projectName: '', contractStatus: '未执行', contractText: '', projectName: '',
salespersonName: null, salespersonDeptName: '', settlementAmount: null, salespersonName: null, salespersonDeptName: '', settlementAmount: null,
receivedAmount: null, contractStatusLabel: null, createBy: null, updateBy: null, receivedAmount: null, contractStatusLabel: null, createBy: null, updateBy: null,
createTime: '', updateTime: '', page: 1, pageSize: 10, signDate: '', duration: '', createTime: '', updateTime: '', page: 1, pageSize: 10, signDate: '', duration: '',
@ -352,7 +451,7 @@ const handleAddSubmit = async () => {
accountNumber: newContractData.value.accountNumber || '', accountNumber: newContractData.value.accountNumber || '',
amount: newContractData.value.amount || 0, amount: newContractData.value.amount || 0,
code: newContractData.value.code || '', code: newContractData.value.code || '',
contractStatus: newContractData.value.contractStatus || '', contractStatus: newContractData.value.contractStatus || '未执行',
contractText: newContractData.value.contractText || '', contractText: newContractData.value.contractText || '',
customer: newContractData.value.customer || '', customer: newContractData.value.customer || '',
departmentId: (newContractData.value as any).departmentId || '', departmentId: (newContractData.value as any).departmentId || '',
@ -362,7 +461,8 @@ const handleAddSubmit = async () => {
paymentDate: newContractData.value.paymentDate || null, paymentDate: newContractData.value.paymentDate || null,
performanceDeadline: newContractData.value.performanceDeadline || null, performanceDeadline: newContractData.value.performanceDeadline || null,
productService: newContractData.value.productService || '', productService: newContractData.value.productService || '',
projectId: newContractData.value.projectId || '', // projectId
projectName: newContractData.value.projectName || '',
salespersonId: (newContractData.value as any).salespersonId || '', salespersonId: (newContractData.value as any).salespersonId || '',
signDate: newContractData.value.signDate || null, signDate: newContractData.value.signDate || null,
type: newContractData.value.type || '收入合同', type: newContractData.value.type || '收入合同',
@ -395,7 +495,7 @@ const editRecord = (record: ContractItem) => {
amount: record.amount || 0, amount: record.amount || 0,
projectId: record.projectId || '', projectId: record.projectId || '',
type: record.type || '收入合同', type: record.type || '收入合同',
contractStatus: record.contractStatus || '未确认', contractStatus: record.contractStatus || '未执行',
} }
selectedContractData.value = completeRecord selectedContractData.value = completeRecord
@ -422,7 +522,7 @@ const handleEditSubmit = async () => {
amount: editedContractData.value.amount || 0, amount: editedContractData.value.amount || 0,
code: editedContractData.value.code || '', code: editedContractData.value.code || '',
contractId: editedContractData.value.contractId, contractId: editedContractData.value.contractId,
contractStatus: editedContractData.value.contractStatus || '', contractStatus: editedContractData.value.contractStatus || '未执行',
contractText: editedContractData.value.contractText || '', contractText: editedContractData.value.contractText || '',
customer: editedContractData.value.customer || '', customer: editedContractData.value.customer || '',
departmentId: editedContractData.value.departmentId || '', departmentId: editedContractData.value.departmentId || '',

View File

@ -1,11 +1,163 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
type OrderStatus = '待支付' | '进行中' | '已完成' | '已取消'
interface OrderItem { id: string; orderNo: string; customer: string; amount: number; status: OrderStatus; createTime: string }
const StatusOptions: { label: string; value: OrderStatus }[] = [
{ label: '待支付', value: '待支付' },
{ label: '进行中', value: '进行中' },
{ label: '已完成', value: '已完成' },
{ label: '已取消', value: '已取消' },
]
//
const allOrders = ref<OrderItem[]>([])
const stats = computed(() => ({
total: allOrders.value.length,
pending: allOrders.value.filter(o=>o.status==='待支付').length,
progress: allOrders.value.filter(o=>o.status==='进行中').length,
done: allOrders.value.filter(o=>o.status==='已完成').length,
cancel: allOrders.value.filter(o=>o.status==='已取消').length,
}))
//
const statCards = computed(() => [
{ key: 'total', title: '订单总数', value: stats.value.total, color: '#2f54eb', iconChar: 'T' },
{ key: 'pending', title: '待支付', value: stats.value.pending, color: '#faad14', iconChar: 'P' },
{ key: 'progress', title: '进行中', value: stats.value.progress, color: '#1677ff', iconChar: 'R' },
{ key: 'done', title: '已完成', value: stats.value.done, color: '#52c41a', iconChar: 'D' },
{ key: 'cancel', title: '已取消', value: stats.value.cancel, color: '#8c8c8c', iconChar: 'C' },
])
//
const searchForm = reactive({ orderNo: '', status: '' as ''|OrderStatus, timeRange: [] as [string,string]|[], page: 1, size: 10 })
const filtered = computed(() => {
let list = allOrders.value.slice()
if (searchForm.orderNo) list = list.filter(o=>o.orderNo.includes(searchForm.orderNo.trim()))
if (searchForm.status) list = list.filter(o=>o.status===searchForm.status)
if (Array.isArray(searchForm.timeRange) && searchForm.timeRange.length===2) {
const [s,e] = searchForm.timeRange; const S=+new Date(s as any), E=+new Date(e as any)
list = list.filter(o=>{ const t=+new Date(o.createTime as any); return t>=S && t<=E })
}
return list
})
const paged = computed(() => {
const start = (searchForm.page-1)*searchForm.size
return filtered.value.slice(start, start+searchForm.size)
})
const pagination = reactive({ current: 1, pageSize: 10, total: 0, showTotal: true, showPageSize: true })
//
const columns: TableColumnData[] = [
{ title: '订单编号', dataIndex: 'orderNo', width: 180 },
{ title: '客户', dataIndex: 'customer', width: 160 },
{ title: '金额', dataIndex: 'amount', width: 120, render: ({record}:any)=>`${(record.amount||0).toLocaleString()}` },
{ title: '状态', dataIndex: 'status', width: 100, slotName: 'status' },
{ title: '下单时间', dataIndex: 'createTime', width: 180 },
{ title: '操作', slotName: 'action', width: 140, fixed: 'right' },
]
//
const loading = ref(false)
const search = () => { pagination.total = filtered.value.length }
const reset = () => { Object.assign(searchForm, { orderNo: '', status: '', timeRange: [], page: 1, size: 10 }); pagination.current=1; pagination.pageSize=10; search() }
const onPageChange = (p:number)=>{ searchForm.page=p; pagination.current=p }
const onPageSizeChange=(s:number)=>{ searchForm.size=s; searchForm.page=1; pagination.pageSize=s; pagination.current=1; search() }
const getStatusColor = (s:OrderStatus)=>({ '待支付':'orange','进行中':'blue','已完成':'green','已取消':'gray' }[s])
//
const showAdd = ref(false)
const newOrder = reactive<{orderNo:string;customer:string;amount:number;status:OrderStatus;createTime:string}>(
{ orderNo:'', customer:'', amount:0, status:'待支付', createTime:'' }
)
const openAdd = ()=>{ Object.assign(newOrder,{ orderNo:'',customer:'',amount:0,status:'待支付',createTime:new Date().toISOString().slice(0,19).replace('T',' ') }); showAdd.value=true }
const submitAdd = ()=>{
const id = Math.random().toString(36).slice(2)
allOrders.value.unshift({ id, ...newOrder })
showAdd.value=false; search()
}
//
const genMock = (n=60)=>{
const pick=<T,>(arr:T[])=>arr[Math.floor(Math.random()*arr.length)]
const now=Date.now()
return Array.from({length:n}).map((_,i)=>{
const offset = Math.floor(Math.random()*60)*86400000
const dt = new Date(now-offset).toISOString().slice(0,19).replace('T',' ')
const st = pick<OrderStatus>(['待支付','进行中','已完成','已取消'])
return { id:`O${i+1}`, orderNo:`NO${100000+i}`, customer:`客户${(i%20)+1}`, amount: Math.floor(Math.random()*50000)+1000, status: st, createTime: dt }
})
}
onMounted(()=>{ allOrders.value = genMock(88); search() })
</script> </script>
<template> <template>
<GiPageLayout>
<!-- 顶部统计美化 + 居中 + 独立容器 -->
<a-row :gutter="12" class="mb-3 stats-row">
<a-col :span="24">
<div class="stats-wrap">
<a-row :gutter="12" justify="center">
<a-col v-for="card in statCards" :key="card.key" :xs="12" :sm="12" :md="6" :lg="4">
<a-card hoverable class="stat-card">
<div class="stat-inner">
<div class="stat-icon" :style="{ background: card.color }">
<span>{{ card.iconChar }}</span>
</div>
<div class="stat-value">{{ card.value }}</div>
<div class="stat-title">{{ card.title }}</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
</a-col>
</a-row>
<!-- 查询表单 + 新增按钮 -->
<GiForm :columns="[
{ field:'orderNo', label:'订单编号', type:'input', props:{placeholder:'支持模糊查询'} },
{ field:'status', label:'订单状态', type:'select', props:{ placeholder:'全部', options: StatusOptions } },
{ field:'timeRange', label:'订单时间', type:'range-picker', props:{ showTime:true, format:'YYYY-MM-DD HH:mm:ss' } },
]" v-model="searchForm" search size="medium" @search="search" @reset="reset">
<template #extra>
<a-button type="primary" @click="openAdd"><icon-plus/> 新增订单</a-button>
</template>
</GiForm>
<!-- 订单列表 -->
<GiTable :data="paged" :columns="columns" :loading="loading" :pagination="pagination"
@page-change="onPageChange" @page-size-change="onPageSizeChange" @refresh="search">
<template #status="{ record }"><a-tag :color="getStatusColor(record.status)">{{ record.status }}</a-tag></template>
<template #action="{ record }"><a-space>
<a-link @click="()=>{}">查看</a-link>
<a-link @click="()=>{}">编辑</a-link>
</a-space></template>
</GiTable>
<!-- 新增订单弹窗本地 -->
<a-modal v-model:visible="showAdd" title="新增订单" :width="520" @before-ok="submitAdd">
<a-form :model="newOrder" layout="vertical">
<a-form-item field="orderNo" label="订单编号"><a-input v-model="newOrder.orderNo" placeholder="NO123456"/></a-form-item>
<a-form-item field="customer" label="客户"><a-input v-model="newOrder.customer"/></a-form-item>
<a-form-item field="amount" label="金额"><a-input-number v-model="newOrder.amount" style="width:100%"/></a-form-item>
<a-form-item field="status" label="状态"><a-select v-model="newOrder.status" :options="StatusOptions"/></a-form-item>
<a-form-item field="createTime" label="下单时间"><a-date-picker v-model="newOrder.createTime" show-time style="width:100%"/></a-form-item>
</a-form>
</a-modal>
</GiPageLayout>
</template> </template>
<style scoped lang="scss"> <style scoped>
.mb-3{ margin-bottom:12px; }
.stats-row{ }
.stats-wrap{ max-width: 1200px; margin: 0 auto; }
.stat-card{ border: 1px solid var(--color-border-2,#f0f0f0); background: linear-gradient(180deg,#ffffff 0%, #fafbff 100%); }
.stat-inner{ display:flex; flex-direction:column; align-items:center; justify-content:center; padding:18px 10px; text-align:center; }
.stat-icon{ width:44px; height:44px; border-radius:12px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-bottom:8px; box-shadow: 0 6px 16px rgba(0,0,0,0.08); }
.stat-value{ font-size:28px; font-weight:700; color:#1d2129; line-height:1.2; }
.stat-title{ margin-top:6px; color:#86909c; }
</style> </style>

View File

@ -237,6 +237,7 @@ const rules = {
// //
watch(() => props.equipmentData, (newData) => { watch(() => props.equipmentData, (newData) => {
if (newData) { if (newData) {
console.log('采购申请弹窗 - 接收到设备数据:', newData)
// //
Object.assign(formData, { Object.assign(formData, {
equipmentId: newData.equipmentId || '', // ID equipmentId: newData.equipmentId || '', // ID
@ -254,6 +255,7 @@ watch(() => props.equipmentData, (newData) => {
technicalRequirements: '', technicalRequirements: '',
businessJustification: '' businessJustification: ''
}) })
console.log('采购申请弹窗 - 填充后的表单数据:', formData)
} }
}, { immediate: true }) }, { immediate: true })
@ -291,10 +293,18 @@ const handleSubmit = async () => {
// //
console.log('提交的表单数据:', submitData) console.log('提交的表单数据:', submitData)
console.log('设备ID:', submitData.equipmentId)
console.log('设备名称:', submitData.equipmentName)
console.log('applyReason 值:', submitData.applyReason) console.log('applyReason 值:', submitData.applyReason)
console.log('applyReason 类型:', typeof submitData.applyReason) console.log('applyReason 类型:', typeof submitData.applyReason)
console.log('applyReason 长度:', submitData.applyReason?.length) console.log('applyReason 长度:', submitData.applyReason?.length)
// ID
if (!submitData.equipmentId) {
Message.error('设备ID不能为空请检查设备数据')
return
}
await equipmentApprovalApi.submitProcurementApplication(submitData) await equipmentApprovalApi.submitProcurementApplication(submitData)
// //

View File

@ -128,6 +128,17 @@
</a-tag> </a-tag>
</template> </template>
<!-- 采购状态 -->
<template #procurementStatus="{ record }">
<a-tag
v-if="record.procurementStatus && record.procurementStatus !== 'NOT_STARTED'"
:color="getProcurementStatusColor(record.procurementStatus)"
>
{{ getProcurementStatusText(record.procurementStatus) }}
</a-tag>
<a-tag v-else color="gray">未开始</a-tag>
</template>
<!-- 位置状态 --> <!-- 位置状态 -->
<template #locationStatus="{ record }"> <template #locationStatus="{ record }">
<a-tag :color="getLocationStatusColor(record.locationStatus)"> <a-tag :color="getLocationStatusColor(record.locationStatus)">
@ -175,7 +186,7 @@
<a-button type="text" size="small" @click="handleEdit(record)"> <a-button type="text" size="small" @click="handleEdit(record)">
编辑 编辑
</a-button> </a-button>
<!-- 申请采购按钮 --> <!-- 申请采购按钮 - 只在特定状态下显示 -->
<a-button <a-button
v-if="canApplyProcurement(record)" v-if="canApplyProcurement(record)"
type="primary" type="primary"
@ -184,12 +195,20 @@
> >
申请采购 申请采购
</a-button> </a-button>
<!-- 显示采购状态 - 优先显示采购状态 -->
<a-tag
v-if="record.procurementStatus && record.procurementStatus !== 'NOT_STARTED'"
:color="getProcurementStatusColor(record.procurementStatus)"
>
{{ getProcurementStatusText(record.procurementStatus) }}
</a-tag>
<!-- 显示审批状态 --> <!-- 显示审批状态 -->
<a-tag v-if="record.approvalStatus" :color="getApprovalStatusColor(record.approvalStatus)"> <a-tag v-if="record.approvalStatus" :color="getApprovalStatusColor(record.approvalStatus)">
{{ getApprovalStatusText(record.approvalStatus) }} {{ getApprovalStatusText(record.approvalStatus) }}
</a-tag> </a-tag>
<!-- 删除按钮 -->
<a-popconfirm <a-popconfirm
content="确定要删除这条采购记录吗?" content="确定要删除这条记录吗?"
@ok="handleDelete(record)" @ok="handleDelete(record)"
> >
<a-button type="text" size="small" status="danger"> <a-button type="text" size="small" status="danger">
@ -345,6 +364,14 @@ const columns = [
slotName: 'equipmentStatus', slotName: 'equipmentStatus',
width: 120, width: 120,
}, },
{
title: '采购状态',
dataIndex: 'procurementStatus',
key: 'procurementStatus',
slotName: 'procurementStatus',
width: 120,
fixed: false,
},
{ {
title: '位置状态', title: '位置状态',
dataIndex: 'locationStatus', dataIndex: 'locationStatus',
@ -403,6 +430,30 @@ const getEquipmentStatusText = (status: string) => {
return textMap[status] || '未知' return textMap[status] || '未知'
} }
//
const getProcurementStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
NOT_STARTED: 'gray',
PENDING_APPROVAL: 'blue',
APPROVED: 'green',
REJECTED: 'red',
COMPLETED: 'purple',
}
return colorMap[status] || 'blue'
}
//
const getProcurementStatusText = (status: string) => {
const textMap: Record<string, string> = {
NOT_STARTED: '未开始',
PENDING_APPROVAL: '待审批',
APPROVED: '已通过',
REJECTED: '已拒绝',
COMPLETED: '已完成',
}
return textMap[status] || '未知'
}
// //
const getLocationStatusColor = (status: string) => { const getLocationStatusColor = (status: string) => {
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {
@ -530,6 +581,7 @@ const transformBackendData = (data: any[]): EquipmentResp[] => {
totalPrice: item.totalPrice, totalPrice: item.totalPrice,
inventoryBasis: item.inventoryBasis, inventoryBasis: item.inventoryBasis,
dynamicRecord: item.dynamicRecord, dynamicRecord: item.dynamicRecord,
procurementStatus: item.procurementStatus,
})) }))
} }
@ -572,6 +624,10 @@ const loadData = async (searchParams?: EquipmentListReq) => {
if (dataList.length > 0) { if (dataList.length > 0) {
const transformedData = transformBackendData(dataList) const transformedData = transformBackendData(dataList)
console.log('转换后的数据:', transformedData) console.log('转换后的数据:', transformedData)
//
transformedData.forEach((item, index) => {
console.log(`设备 ${index + 1} - 名称: ${item.equipmentName}, 采购状态: ${item.procurementStatus}`)
})
tableData.value = transformedData tableData.value = transformedData
} else { } else {
tableData.value = [] tableData.value = []
@ -662,9 +718,15 @@ const handleModalSuccess = () => {
} }
// //
const handleApplicationSuccess = () => { const handleApplicationSuccess = async () => {
applicationModalVisible.value = false applicationModalVisible.value = false
loadData(currentSearchParams.value) console.log('采购申请成功,准备刷新数据...')
//
setTimeout(async () => {
console.log('开始刷新数据...')
await loadData(currentSearchParams.value)
message.success('采购申请已提交,请等待审批')
}, 1000)
} }
// //
@ -714,8 +776,12 @@ const getTotalAmount = () => {
// //
const canApplyProcurement = (record: EquipmentResp) => { const canApplyProcurement = (record: EquipmentResp) => {
// //
return !record.approvalStatus || record.approvalStatus === 'PENDING_APPLICATION' //
const allowedStatuses = ['NOT_STARTED', 'REJECTED', 'COMPLETED', null, undefined]
const canApply = allowedStatuses.includes(record.procurementStatus)
console.log(`设备 ${record.equipmentName} 采购状态: ${record.procurementStatus}, 可申请: ${canApply}`)
return canApply
} }
// //