Merge branch 'devlopment' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web into devlopment
This commit is contained in:
commit
7c455f59eb
|
@ -1,4 +1,4 @@
|
|||
// @/apis/bussiness/index.ts - 商务数据库信息模块API
|
||||
// @/apis/bussiness/index.ts - 智能商务API
|
||||
import http from '@/utils/http'
|
||||
import type {
|
||||
FolderInfo,
|
||||
|
@ -59,7 +59,9 @@ export function getFilesApi(params?: FileListParams) {
|
|||
page: params?.page || 1,
|
||||
pageSize: params?.pageSize || 10,
|
||||
folderId: params?.folderId || '0',
|
||||
fileName: params?.fileName
|
||||
fileName: params?.fileName,
|
||||
sortField: params?.sortField,
|
||||
sortOrder: params?.sortOrder
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ export interface FileListParams {
|
|||
pageSize?: number
|
||||
folderId?: string
|
||||
fileName?: string
|
||||
sortField?: string
|
||||
sortOrder?: string
|
||||
}
|
||||
|
||||
/** 文件夹列表响应 */
|
||||
|
|
|
@ -194,6 +194,12 @@ export interface EquipmentResp {
|
|||
inventoryBasis?: string
|
||||
/** 动态记录 */
|
||||
dynamicRecord?: string
|
||||
|
||||
/** 采购状态 */
|
||||
procurementStatus?: string
|
||||
|
||||
/** 审批状态 */
|
||||
approvalStatus?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -294,6 +300,17 @@ export enum BusinessType {
|
|||
RETURN = 'RETURN'
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备采购状态枚举
|
||||
*/
|
||||
export enum ProcurementStatus {
|
||||
NOT_STARTED = 'NOT_STARTED',
|
||||
PENDING_APPROVAL = 'PENDING_APPROVAL',
|
||||
APPROVED = 'APPROVED',
|
||||
REJECTED = 'REJECTED',
|
||||
COMPLETED = 'COMPLETED'
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备审批列表查询请求
|
||||
*/
|
||||
|
|
|
@ -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>
|
|
@ -1,15 +1,13 @@
|
|||
<template>
|
||||
<div class="notification-center">
|
||||
<!-- 消息中心图标和徽章 -->
|
||||
<div class="notification-trigger" @click="toggleDropdown">
|
||||
<a-badge :count="unreadCount" :dot="hasUrgentNotifications">
|
||||
<a-button type="text" class="notification-btn" title="消息中心">
|
||||
<template #icon>
|
||||
<IconNotification />
|
||||
</template>
|
||||
<span class="notification-text">消息中心</span>
|
||||
</a-button>
|
||||
</a-badge>
|
||||
<!-- 聊天信息图标 -->
|
||||
<div class="notification-trigger">
|
||||
<a-button type="text" class="notification-btn" title="聊天信息">
|
||||
<template #icon>
|
||||
<IconNotification />
|
||||
</template>
|
||||
<span class="notification-text">聊天信息</span>
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 消息中心弹窗 -->
|
||||
|
@ -229,16 +227,37 @@ const reminderForm = ref({
|
|||
|
||||
// 计算属性
|
||||
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 pendingCount = computed(() => notificationService.pendingCount.value)
|
||||
const equipmentCount = computed(() =>
|
||||
notificationService.equipmentBorrowCount.value +
|
||||
notificationService.equipmentReturnCount.value +
|
||||
notificationService.equipmentMaintenanceCount.value +
|
||||
notificationService.equipmentAlertCount.value
|
||||
)
|
||||
const urgentCount = computed(() => notificationService.urgentCount.value)
|
||||
const pendingCount = computed(() => {
|
||||
const count = notificationService.pendingCount.value
|
||||
if (typeof count === 'number' && !isNaN(count) && isFinite(count)) {
|
||||
return count
|
||||
}
|
||||
return 0
|
||||
})
|
||||
const equipmentCount = computed(() => {
|
||||
const borrowCount = notificationService.equipmentBorrowCount.value || 0
|
||||
const returnCount = notificationService.equipmentReturnCount.value || 0
|
||||
const maintenanceCount = notificationService.equipmentMaintenanceCount.value || 0
|
||||
const alertCount = notificationService.equipmentAlertCount.value || 0
|
||||
|
||||
return borrowCount + returnCount + maintenanceCount + alertCount
|
||||
})
|
||||
const urgentCount = computed(() => {
|
||||
const count = notificationService.urgentCount.value
|
||||
if (typeof count === 'number' && !isNaN(count) && isFinite(count)) {
|
||||
return count
|
||||
}
|
||||
return 0
|
||||
})
|
||||
const hasUrgentNotifications = computed(() => urgentCount.value > 0)
|
||||
|
||||
// 过滤后的消息列表
|
||||
|
@ -644,6 +663,11 @@ onUnmounted(() => {
|
|||
clearInterval(reminderCheckInterval)
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
toggleDropdown
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
@ -34,7 +34,9 @@
|
|||
<a-dropdown trigger="hover">
|
||||
<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>
|
||||
<icon-down />
|
||||
</a-row>
|
||||
|
@ -42,8 +44,14 @@
|
|||
<a-doption @click="router.push('/user/profile')">
|
||||
<span>个人中心</span>
|
||||
</a-doption>
|
||||
<a-doption @click="router.push('/user/message')">
|
||||
<a-doption @click="showNotificationCenter">
|
||||
<span>消息中心</span>
|
||||
<a-badge
|
||||
v-if="unreadMessageCount > 0"
|
||||
:count="unreadMessageCount"
|
||||
:dot="false"
|
||||
class="dropdown-notification-badge"
|
||||
/>
|
||||
</a-doption>
|
||||
<a-divider :margin="0" />
|
||||
<a-doption @click="logout">
|
||||
|
@ -78,7 +86,14 @@ const { breakpoint } = useBreakpoint()
|
|||
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 = () => {
|
||||
|
@ -224,6 +239,27 @@ const router = useRouter()
|
|||
const userStore = useUserStore()
|
||||
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 = () => {
|
||||
Modal.warning({
|
||||
|
@ -262,22 +298,6 @@ onMounted(() => {
|
|||
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 {
|
||||
.arco-badge-dot {
|
||||
background-color: #f53f3f;
|
||||
|
@ -328,4 +348,49 @@ onMounted(() => {
|
|||
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>
|
||||
|
|
|
@ -1104,7 +1104,7 @@ export const systemRoutes: RouteRecordRaw[] = [
|
|||
},
|
||||
// ],
|
||||
// },
|
||||
// 商务数据库信息模块
|
||||
// 智能商务模块
|
||||
{
|
||||
path: '/bussiness-knowledge',
|
||||
name: 'bussinesskonwledge',
|
||||
|
@ -1117,7 +1117,7 @@ export const systemRoutes: RouteRecordRaw[] = [
|
|||
name: 'bussiness-knowledge',
|
||||
component: () => import('@/views/bussiness-data/bussiness.vue'),
|
||||
meta: {
|
||||
title: '商务数据库信息',
|
||||
title: '智能商务',
|
||||
icon: 'info-circle',
|
||||
hidden: false,
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@ export {}
|
|||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default']
|
||||
ApprovalMessageItem: typeof import('./../components/NotificationCenter/ApprovalMessageItem.vue')['default']
|
||||
Avatar: typeof import('./../components/Avatar/index.vue')['default']
|
||||
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
|
||||
CellCopy: typeof import('./../components/CellCopy/index.vue')['default']
|
||||
|
|
|
@ -4,10 +4,9 @@
|
|||
<a-layout-sider
|
||||
width="260"
|
||||
:collapsed-width="80"
|
||||
theme="light"
|
||||
theme="dark"
|
||||
class="folder-sidebar"
|
||||
:collapsed="sidebarCollapsed"
|
||||
collapsible
|
||||
@collapse="handleSidebarCollapse"
|
||||
@expand="handleSidebarExpand"
|
||||
>
|
||||
|
@ -102,29 +101,7 @@
|
|||
</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>
|
||||
|
@ -242,10 +219,42 @@
|
|||
<div class="file-grid-container" v-if="currentFolderId && !loading">
|
||||
<!-- 表头行 -->
|
||||
<a-row class="table-header-row">
|
||||
<a-col :span="10" class="table-column name-column">文件名</a-col>
|
||||
<a-col :span="4" class="table-column type-column">类型</a-col>
|
||||
<a-col :span="3" class="table-column size-column">大小</a-col>
|
||||
<a-col :span="5" class="table-column time-column">修改时间</a-col>
|
||||
<a-col :span="10" class="table-column name-column">
|
||||
<div class="sortable-header" @click="handleSortChange('fileName')">
|
||||
<span>文件名</span>
|
||||
<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-row>
|
||||
|
||||
|
@ -273,7 +282,7 @@
|
|||
|
||||
<!-- 大小列 -->
|
||||
<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>
|
||||
|
||||
<!-- 时间列 -->
|
||||
|
@ -613,6 +622,18 @@ const fileCurrentPage = ref(1);
|
|||
const filePageSize = ref(10);
|
||||
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({
|
||||
id: '',
|
||||
|
@ -638,7 +659,7 @@ const fileListTemp = ref([]);
|
|||
const folderFormRef = ref(null);
|
||||
const uploadFormRef = ref(null);
|
||||
const uploadRef = ref(null);
|
||||
const folderColor = '#165DFF';
|
||||
const folderColor = 'var(--color-primary)';
|
||||
const refreshing = ref(false);
|
||||
const folderSubmitting = ref(false);
|
||||
const uploading = ref(false);
|
||||
|
@ -941,6 +962,9 @@ const handleFileSearch = () => {
|
|||
console.log('文件搜索关键词:', fileSearchKeyword.value);
|
||||
// 重置到第一页并搜索
|
||||
fileCurrentPage.value = 1;
|
||||
// 搜索时重置排序状态
|
||||
sortField.value = '';
|
||||
sortOrder.value = '';
|
||||
console.log('重置文件页码为:', fileCurrentPage.value);
|
||||
if (currentFolderId.value) {
|
||||
loadFiles(currentFolderId.value);
|
||||
|
@ -960,6 +984,9 @@ const handleFileSearchInput = (value) => {
|
|||
searchTimeout.value = setTimeout(() => {
|
||||
console.log('=== 防抖文件搜索执行 ===');
|
||||
fileCurrentPage.value = 1;
|
||||
// 搜索时重置排序状态
|
||||
sortField.value = '';
|
||||
sortOrder.value = '';
|
||||
console.log('重置文件页码为:', fileCurrentPage.value);
|
||||
if (currentFolderId.value) {
|
||||
loadFiles(currentFolderId.value);
|
||||
|
@ -976,6 +1003,9 @@ const handleFileSearchClear = () => {
|
|||
console.log('清除文件搜索定时器');
|
||||
}
|
||||
fileCurrentPage.value = 1;
|
||||
// 清除搜索时重置排序状态
|
||||
sortField.value = '';
|
||||
sortOrder.value = '';
|
||||
console.log('重置文件页码为:', fileCurrentPage.value);
|
||||
if (currentFolderId.value) {
|
||||
loadFiles(currentFolderId.value);
|
||||
|
@ -985,12 +1015,20 @@ const handleFileSearchClear = () => {
|
|||
const loadFiles = async (folderId) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await getFilesApi({
|
||||
const apiParams = {
|
||||
folderId: folderId,
|
||||
page: fileCurrentPage.value,
|
||||
pageSize: filePageSize.value,
|
||||
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) {
|
||||
|
@ -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) => {
|
||||
|
@ -1030,8 +1088,10 @@ const handleFolderSelect = (selectedKeys, info) => {
|
|||
const folderId = selectedKeys[0];
|
||||
if (currentFolderId.value !== folderId) {
|
||||
fileCurrentPage.value = 1;
|
||||
// 切换文件夹时清空文件搜索关键词
|
||||
// 切换文件夹时清空文件搜索关键词和排序状态
|
||||
fileSearchKeyword.value = '';
|
||||
sortField.value = '';
|
||||
sortOrder.value = '';
|
||||
}
|
||||
currentFolderId.value = folderId;
|
||||
loadFiles(folderId);
|
||||
|
@ -1136,6 +1196,9 @@ const handleBreadcrumbClick = (index) => {
|
|||
if (index === 0) {
|
||||
// 点击"知识库",回到根目录
|
||||
currentFolderId.value = '0';
|
||||
// 重置排序状态
|
||||
sortField.value = '';
|
||||
sortOrder.value = '';
|
||||
loadFiles('0');
|
||||
} else {
|
||||
// 点击其他路径项,需要找到对应的文件夹ID
|
||||
|
@ -1146,6 +1209,9 @@ const handleBreadcrumbClick = (index) => {
|
|||
const targetFolder = folderList.value.find(folder => folder.name === targetFolderName);
|
||||
if (targetFolder) {
|
||||
currentFolderId.value = targetFolder.id;
|
||||
// 重置排序状态
|
||||
sortField.value = '';
|
||||
sortOrder.value = '';
|
||||
loadFiles(targetFolder.id);
|
||||
}
|
||||
}
|
||||
|
@ -1307,6 +1373,9 @@ const refreshData = async () => {
|
|||
// 强制清空搜索关键词,确保显示所有文件夹
|
||||
searchKeyword.value = '';
|
||||
currentPage.value = 1;
|
||||
// 刷新时重置排序状态
|
||||
sortField.value = '';
|
||||
sortOrder.value = '';
|
||||
|
||||
await initData();
|
||||
if (currentFolderId.value) {
|
||||
|
@ -1532,7 +1601,7 @@ const fileColor = (extension) => {
|
|||
bmp: '#722ed1',
|
||||
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%',
|
||||
maxHeight: '70vh',
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#f8f9fa',
|
||||
border: '1px solid #e9ecef',
|
||||
backgroundColor: 'var(--color-fill-1)',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '8px',
|
||||
padding: '20px',
|
||||
fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace",
|
||||
|
@ -1798,7 +1867,7 @@ const showTextPreview = async (blob, fileName) => {
|
|||
lineHeight: '1.6',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
color: '#333',
|
||||
color: 'var(--color-text-1)',
|
||||
textAlign: 'left'
|
||||
}
|
||||
}, text)
|
||||
|
@ -2244,6 +2313,21 @@ const formatFileSize = (fileSize) => {
|
|||
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) => {
|
||||
|
@ -2366,7 +2450,7 @@ onMounted(() => {
|
|||
<style scoped>
|
||||
.knowledge-container {
|
||||
height: 100vh;
|
||||
background-color: var(--color-bg-2);
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
|
||||
|
@ -2378,13 +2462,15 @@ onMounted(() => {
|
|||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%);
|
||||
background: var(--color-bg-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
background: var(--color-bg-1);
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
|
@ -2400,12 +2486,18 @@ onMounted(() => {
|
|||
|
||||
.folder-content {
|
||||
padding: 16px 0;
|
||||
height: calc(100vh - 320px); /* 为底部分页控件留出更多空间,因为文件夹项现在更高 */
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
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 {
|
||||
width: 8px;
|
||||
}
|
||||
|
@ -2445,7 +2537,7 @@ onMounted(() => {
|
|||
border: 1px solid transparent;
|
||||
|
||||
&: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);
|
||||
transform: translateX(2px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
|
@ -2514,7 +2606,7 @@ onMounted(() => {
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
background: var(--color-bg-2);
|
||||
background: var(--color-bg-1);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
height: 64px;
|
||||
}
|
||||
|
@ -2531,10 +2623,12 @@ onMounted(() => {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
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 {
|
||||
|
@ -2545,6 +2639,7 @@ onMounted(() => {
|
|||
flex-direction: column;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
|
@ -2557,7 +2652,9 @@ onMounted(() => {
|
|||
overflow-y: auto;
|
||||
background-color: var(--color-bg-1);
|
||||
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 {
|
||||
color: #165DFF;
|
||||
background-color: #E8F3FF;
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-primary-light-1);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
|
@ -2652,7 +2749,7 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
.table-data-row:hover .file-name {
|
||||
color: #165DFF;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.type-column, .size-column, .time-column {
|
||||
|
@ -2697,7 +2794,7 @@ onMounted(() => {
|
|||
white-space: nowrap;
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
|
@ -2838,7 +2964,7 @@ onMounted(() => {
|
|||
justify-content: center;
|
||||
padding: 64px 0;
|
||||
color: var(--color-text-3);
|
||||
background-color: #fafafa;
|
||||
background-color: var(--color-fill-1);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -2852,7 +2978,7 @@ onMounted(() => {
|
|||
:deep(.empty-state .arco-btn) {
|
||||
margin-top: 16px;
|
||||
padding: 8px 16px;
|
||||
background-color: #165DFF;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
|
@ -2862,7 +2988,7 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
:deep(.empty-state .arco-btn:hover) {
|
||||
background-color: #0E42D2;
|
||||
background-color: var(--color-primary-dark-1);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
@ -2984,50 +3110,7 @@ onMounted(() => {
|
|||
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) {
|
||||
|
@ -3158,8 +3241,8 @@ onMounted(() => {
|
|||
border: 1px solid #e2e8f0;
|
||||
|
||||
&:hover {
|
||||
background: #e2e8f0;
|
||||
color: #165DFF;
|
||||
background: var(--color-fill-2);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3168,8 +3251,8 @@ onMounted(() => {
|
|||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #165DFF;
|
||||
color: #165DFF;
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
|
@ -3278,7 +3361,7 @@ onMounted(() => {
|
|||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(var(--color-bg-1-rgb), 0.9);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary-light-2);
|
||||
|
@ -3296,7 +3379,7 @@ onMounted(() => {
|
|||
.search-result-tip {
|
||||
padding: 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-left: 4px solid var(--color-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
|
@ -3310,7 +3393,7 @@ onMounted(() => {
|
|||
left: 0;
|
||||
right: 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;
|
||||
}
|
||||
}
|
||||
|
@ -3348,7 +3431,7 @@ onMounted(() => {
|
|||
margin-top: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-2);
|
||||
background: var(--color-bg-1);
|
||||
max-height: 300px;
|
||||
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 {
|
||||
position: absolute;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
margin-top: 0;
|
||||
margin-top: 16px;
|
||||
padding: 16px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: var(--color-bg-1);
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
|
||||
.arco-pagination {
|
||||
.arco-pagination-total {
|
||||
|
@ -3516,7 +3654,7 @@ onMounted(() => {
|
|||
/* 树形文件夹结构 */
|
||||
.folder-tree-container {
|
||||
padding: 8px;
|
||||
background: var(--color-bg-2);
|
||||
background: var(--color-bg-1);
|
||||
border-radius: 6px;
|
||||
margin: 8px;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -50,6 +50,11 @@
|
|||
<a-descriptions-item label="备注">
|
||||
{{ contractDetail.notes }}
|
||||
</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>
|
||||
</div>
|
||||
<div v-else-if="!loading" class="empty-container">
|
||||
|
|
|
@ -7,16 +7,8 @@
|
|||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="projectId" label="项目">
|
||||
<a-select v-model="contractData.projectId"
|
||||
:options="projectOptions"
|
||||
:loading="projectLoading"
|
||||
:virtual-list-props="virtualListProps"
|
||||
placeholder="请选择项目"
|
||||
allow-search allow-clear
|
||||
@focus="handleProjectFocus"
|
||||
@dropdown-visible-change="handleProjectDropdown"
|
||||
@search="handleProjectSearch" />
|
||||
<a-form-item field="projectName" label="项目">
|
||||
<a-input v-model="contractData.projectName" placeholder="请输入项目名称" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
@ -43,11 +35,11 @@
|
|||
<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-option value="验收中">验收中</a-option>
|
||||
<a-option value="结算中">结算中</a-option>
|
||||
<a-option value="已结算">已结算</a-option>
|
||||
<a-option value="已终止">已终止</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
@ -99,6 +91,11 @@
|
|||
</a-col>
|
||||
</a-row>
|
||||
|
||||
|
||||
<a-form-item field="productService" label="产品或服务">
|
||||
<a-input v-model="contractData.productService" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="paymentAddress" label="付款地址">
|
||||
<a-input v-model="contractData.paymentAddress" />
|
||||
</a-form-item>
|
||||
|
|
|
@ -48,18 +48,23 @@
|
|||
<span class="font-medium text-green-600">¥{{ (record.amount || 0).toLocaleString() }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 已收款金额 -->
|
||||
<template #receivedAmount="{ record }">
|
||||
<span class="font-medium text-blue-600">¥{{ (record.receivedAmount || 0).toLocaleString() }}</span>
|
||||
<!-- 已结算金额(支出合同) -->
|
||||
<template #settlementAmount="{ record }">
|
||||
<span class="font-medium text-blue-600">¥{{ (record.settlementAmount || record.receivedAmount || 0).toLocaleString() }}</span>
|
||||
</template>
|
||||
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-link @click="viewDetail(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-space>
|
||||
</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>
|
||||
|
||||
<!-- 合同详情弹窗 -->
|
||||
|
@ -102,6 +107,15 @@
|
|||
@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>
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
|
@ -173,11 +187,11 @@ const queryFormColumns = [
|
|||
props: {
|
||||
placeholder: '请选择合同状态',
|
||||
options: [
|
||||
{ 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: '签署时间',
|
||||
type: 'range-picker' as const,
|
||||
props: {
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
showTime: true,
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
placeholder: ['开始日期', '结束日期'],
|
||||
format: 'YYYY-MM-DD',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
@ -200,11 +213,11 @@ const tableColumns: TableColumnData[] = [
|
|||
{ 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: 'performanceDeadline', width: 120 },
|
||||
{ title: '付款日期', dataIndex: 'paymentDate', width: 120 },
|
||||
{ title: '已结算金额', dataIndex: 'settlementAmount', slotName: 'settlementAmount', width: 120 },
|
||||
{ title: '未结算金额', dataIndex: 'pendingAmount', width: 120 },
|
||||
{ title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
|
||||
{ title: '履约期限', dataIndex: 'performanceDeadline', slotName: 'performanceDeadline', width: 120 },
|
||||
{ title: '付款日期', dataIndex: 'paymentDate', slotName: 'paymentDate', width: 120 },
|
||||
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
|
||||
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 },
|
||||
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
|
||||
|
@ -252,10 +265,10 @@ const fetchContractList = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 计算未收款金额
|
||||
// 计算未结算金额(支出合同)
|
||||
dataList.value = filtered.map((item: ContractItem) => ({
|
||||
...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 colorMap: Record<string, string> = {
|
||||
未确认: 'gray',
|
||||
待审批: 'orange',
|
||||
已签署: 'blue',
|
||||
未执行: 'gray',
|
||||
执行中: 'cyan',
|
||||
已完成: 'green',
|
||||
验收中: 'arcoblue',
|
||||
结算中: 'orange',
|
||||
已结算: 'green',
|
||||
已终止: 'red',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
|
@ -381,7 +394,7 @@ const openAddModal = () => {
|
|||
amount: 0,
|
||||
accountNumber: '',
|
||||
notes: '',
|
||||
contractStatus: '未确认',
|
||||
contractStatus: '未执行',
|
||||
contractText: '',
|
||||
projectName: '',
|
||||
salespersonName: null,
|
||||
|
@ -426,7 +439,8 @@ const handleAddSubmit = async () => {
|
|||
paymentDate: newContractData.value.paymentDate || null,
|
||||
performanceDeadline: newContractData.value.performanceDeadline || null,
|
||||
productService: newContractData.value.productService || '',
|
||||
projectId: newContractData.value.projectId || '',
|
||||
// 新建时不传 projectId,而是传项目名称,后续立项再建项目
|
||||
projectName: newContractData.value.projectName || '',
|
||||
salespersonId: (newContractData.value as any).salespersonId || '',
|
||||
signDate: newContractData.value.signDate || null,
|
||||
type: newContractData.value.type || '支出合同',
|
||||
|
@ -476,8 +490,8 @@ const editRecord = (record: ContractItem) => {
|
|||
...record,
|
||||
amount: record.amount || 0,
|
||||
projectId: record.projectId || '',
|
||||
type: record.type || '收入合同',
|
||||
contractStatus: record.contractStatus || '未确认',
|
||||
type: record.type || '支出合同',
|
||||
contractStatus: record.contractStatus || '未执行',
|
||||
}
|
||||
|
||||
selectedContractData.value = completeRecord
|
||||
|
@ -525,7 +539,7 @@ const handleEditSubmit = async () => {
|
|||
type: editedContractData.value.type || '',
|
||||
};
|
||||
|
||||
console.log('Edited Contract Data:', requestData); // 打印请求数据以便调试
|
||||
// console.log('Edited Contract Data:', requestData); // 打印请求数据以便调试
|
||||
|
||||
// 修改此处,直接向 /contract 发送 PUT 请求
|
||||
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) => {
|
||||
|
|
|
@ -50,6 +50,11 @@
|
|||
<a-descriptions-item label="备注">
|
||||
{{ contractDetail.notes }}
|
||||
</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>
|
||||
</div>
|
||||
<div v-else-if="!loading" class="empty-container">
|
||||
|
|
|
@ -7,16 +7,8 @@
|
|||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="projectId" label="项目">
|
||||
<a-select v-model="contractData.projectId"
|
||||
:options="projectOptions"
|
||||
:loading="projectLoading"
|
||||
:virtual-list-props="virtualListProps"
|
||||
placeholder="请选择项目"
|
||||
allow-search allow-clear
|
||||
@focus="handleProjectFocus"
|
||||
@dropdown-visible-change="handleProjectDropdown"
|
||||
@search="handleProjectSearch" />
|
||||
<a-form-item field="projectName" label="项目">
|
||||
<a-input v-model="contractData.projectName" placeholder="请输入项目名称" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
@ -43,11 +35,11 @@
|
|||
<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-option value="验收中">验收中</a-option>
|
||||
<a-option value="结算中">结算中</a-option>
|
||||
<a-option value="已结算">已结算</a-option>
|
||||
<a-option value="已终止">已终止</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
@ -99,6 +91,11 @@
|
|||
</a-col>
|
||||
</a-row>
|
||||
|
||||
|
||||
<a-form-item field="productService" label="产品或服务">
|
||||
<a-input v-model="contractData.productService" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="paymentAddress" label="付款地址">
|
||||
<a-input v-model="contractData.paymentAddress" />
|
||||
</a-form-item>
|
||||
|
|
|
@ -48,18 +48,24 @@
|
|||
<span class="font-medium text-green-600">¥{{ (record.amount || 0).toLocaleString() }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 已收款金额 -->
|
||||
<!-- 已回款金额(收入合同) -->
|
||||
<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 #action="{ record }">
|
||||
<a-space>
|
||||
<a-link @click="viewDetail(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-space>
|
||||
</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>
|
||||
|
||||
<!-- 合同详情弹窗 -->
|
||||
|
@ -87,8 +93,6 @@
|
|||
@update:contract-data="handleContractDataUpdate"
|
||||
/>
|
||||
</a-modal>
|
||||
</GiPageLayout>
|
||||
|
||||
<!-- 新建合同弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="showAddModal"
|
||||
|
@ -102,7 +106,18 @@
|
|||
:contract-data="newContractData"
|
||||
@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>
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -148,49 +163,33 @@ interface ContractItem {
|
|||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
contractName: '',
|
||||
contractCode: '',
|
||||
client: '',
|
||||
status: '',
|
||||
signDate: '',
|
||||
signDateRange: [] as [string, string] | [],
|
||||
page: 1,
|
||||
size: 10,
|
||||
})
|
||||
|
||||
// 查询条件配置
|
||||
const queryFormColumns = [
|
||||
{
|
||||
field: 'contractName',
|
||||
label: '合同名称',
|
||||
type: 'input' as const,
|
||||
props: {
|
||||
placeholder: '请输入合同名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'client',
|
||||
label: '客户',
|
||||
type: 'input' as const,
|
||||
props: {
|
||||
placeholder: '请输入客户名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '合同状态',
|
||||
type: 'select' as const,
|
||||
props: {
|
||||
{ field: 'client', label: '客户', type: 'input' as const, props: { placeholder: '请输入客户名称' } },
|
||||
{ field: 'status', label: '合同状态', type: 'select' as const, props: {
|
||||
placeholder: '请选择合同状态',
|
||||
options: [
|
||||
{ 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: 'receivedAmount', slotName: 'receivedAmount', width: 120 },
|
||||
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 },
|
||||
{ title: '签署日期', dataIndex: 'signDate', width: 120 },
|
||||
{ title: '履约期限', dataIndex: 'performanceDeadline', width: 120 },
|
||||
{ title: '付款日期', dataIndex: 'paymentDate', width: 120 },
|
||||
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
|
||||
{ title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
|
||||
{ title: '履约期限', dataIndex: 'performanceDeadline', slotName: 'performanceDeadline', width: 120 },
|
||||
{ title: '付款日期', dataIndex: 'paymentDate', slotName: 'paymentDate', width: 120 },
|
||||
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 120 },
|
||||
{ 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' },
|
||||
{ title: '备注', dataIndex: 'notes', width: 220, ellipsis: true, tooltip: true },
|
||||
{ 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 dataList = ref<ContractItem[]>([])
|
||||
|
@ -222,27 +310,39 @@ const fetchContractList = async () => {
|
|||
const params = {
|
||||
page: searchForm.page,
|
||||
pageSize: searchForm.size,
|
||||
contractName: searchForm.contractName,
|
||||
code: searchForm.contractCode,
|
||||
customer: searchForm.client,
|
||||
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)
|
||||
|
||||
if (response.code === 200) {
|
||||
// 过滤出类型为"收入合同"的数据
|
||||
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) => ({
|
||||
...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 {
|
||||
Message.error(response.msg || '获取合同列表失败')
|
||||
dataList.value = []
|
||||
|
@ -267,11 +367,11 @@ const pagination = reactive({
|
|||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
未确认: 'gray',
|
||||
待审批: 'orange',
|
||||
已签署: 'blue',
|
||||
未执行: 'gray',
|
||||
执行中: 'cyan',
|
||||
已完成: 'green',
|
||||
验收中: 'arcoblue',
|
||||
结算中: 'orange',
|
||||
已结算: 'green',
|
||||
已终止: 'red',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
|
@ -290,11 +390,10 @@ const search = async () => {
|
|||
|
||||
const reset = () => {
|
||||
Object.assign(searchForm, {
|
||||
contractName: '',
|
||||
contractCode: '',
|
||||
client: '',
|
||||
status: '',
|
||||
signDate: '',
|
||||
signDateRange: [],
|
||||
page: 1,
|
||||
size: 10,
|
||||
})
|
||||
|
@ -323,7 +422,7 @@ const newContractData = ref<ContractItem>({
|
|||
contractId: '', customer: '', code: '', projectId: '', type: '收入合同',
|
||||
productService: '', paymentDate: null, performanceDeadline: null,
|
||||
paymentAddress: '', amount: 0, accountNumber: '', notes: '',
|
||||
contractStatus: '未确认', contractText: '', projectName: '',
|
||||
contractStatus: '未执行', contractText: '', projectName: '',
|
||||
salespersonName: null, salespersonDeptName: '', settlementAmount: null,
|
||||
receivedAmount: null, contractStatusLabel: null, createBy: null, updateBy: null,
|
||||
createTime: '', updateTime: '', page: 1, pageSize: 10, signDate: '', duration: '',
|
||||
|
@ -334,7 +433,7 @@ const openAddModal = () => {
|
|||
contractId: '', customer: '', code: '', projectId: '', type: '收入合同',
|
||||
productService: '', paymentDate: null, performanceDeadline: null,
|
||||
paymentAddress: '', amount: 0, accountNumber: '', notes: '',
|
||||
contractStatus: '未确认', contractText: '', projectName: '',
|
||||
contractStatus: '未执行', contractText: '', projectName: '',
|
||||
salespersonName: null, salespersonDeptName: '', settlementAmount: null,
|
||||
receivedAmount: null, contractStatusLabel: null, createBy: null, updateBy: null,
|
||||
createTime: '', updateTime: '', page: 1, pageSize: 10, signDate: '', duration: '',
|
||||
|
@ -352,7 +451,7 @@ const handleAddSubmit = async () => {
|
|||
accountNumber: newContractData.value.accountNumber || '',
|
||||
amount: newContractData.value.amount || 0,
|
||||
code: newContractData.value.code || '',
|
||||
contractStatus: newContractData.value.contractStatus || '',
|
||||
contractStatus: newContractData.value.contractStatus || '未执行',
|
||||
contractText: newContractData.value.contractText || '',
|
||||
customer: newContractData.value.customer || '',
|
||||
departmentId: (newContractData.value as any).departmentId || '',
|
||||
|
@ -362,7 +461,8 @@ const handleAddSubmit = async () => {
|
|||
paymentDate: newContractData.value.paymentDate || null,
|
||||
performanceDeadline: newContractData.value.performanceDeadline || null,
|
||||
productService: newContractData.value.productService || '',
|
||||
projectId: newContractData.value.projectId || '',
|
||||
// 新建时不传 projectId,而是传项目名称
|
||||
projectName: newContractData.value.projectName || '',
|
||||
salespersonId: (newContractData.value as any).salespersonId || '',
|
||||
signDate: newContractData.value.signDate || null,
|
||||
type: newContractData.value.type || '收入合同',
|
||||
|
@ -395,7 +495,7 @@ const editRecord = (record: ContractItem) => {
|
|||
amount: record.amount || 0,
|
||||
projectId: record.projectId || '',
|
||||
type: record.type || '收入合同',
|
||||
contractStatus: record.contractStatus || '未确认',
|
||||
contractStatus: record.contractStatus || '未执行',
|
||||
}
|
||||
|
||||
selectedContractData.value = completeRecord
|
||||
|
@ -422,7 +522,7 @@ const handleEditSubmit = async () => {
|
|||
amount: editedContractData.value.amount || 0,
|
||||
code: editedContractData.value.code || '',
|
||||
contractId: editedContractData.value.contractId,
|
||||
contractStatus: editedContractData.value.contractStatus || '',
|
||||
contractStatus: editedContractData.value.contractStatus || '未执行',
|
||||
contractText: editedContractData.value.contractText || '',
|
||||
customer: editedContractData.value.customer || '',
|
||||
departmentId: editedContractData.value.departmentId || '',
|
||||
|
|
|
@ -1,11 +1,163 @@
|
|||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
|
@ -237,6 +237,7 @@ const rules = {
|
|||
// 监听设备数据变化,填充表单
|
||||
watch(() => props.equipmentData, (newData) => {
|
||||
if (newData) {
|
||||
console.log('采购申请弹窗 - 接收到设备数据:', newData)
|
||||
// 填充设备数据到表单
|
||||
Object.assign(formData, {
|
||||
equipmentId: newData.equipmentId || '', // 添加设备ID
|
||||
|
@ -254,6 +255,7 @@ watch(() => props.equipmentData, (newData) => {
|
|||
technicalRequirements: '',
|
||||
businessJustification: ''
|
||||
})
|
||||
console.log('采购申请弹窗 - 填充后的表单数据:', formData)
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
|
@ -291,10 +293,18 @@ const handleSubmit = async () => {
|
|||
|
||||
// 调试信息:打印提交的数据
|
||||
console.log('提交的表单数据:', submitData)
|
||||
console.log('设备ID:', submitData.equipmentId)
|
||||
console.log('设备名称:', submitData.equipmentName)
|
||||
console.log('applyReason 值:', submitData.applyReason)
|
||||
console.log('applyReason 类型:', typeof submitData.applyReason)
|
||||
console.log('applyReason 长度:', submitData.applyReason?.length)
|
||||
|
||||
// 确保设备ID存在
|
||||
if (!submitData.equipmentId) {
|
||||
Message.error('设备ID不能为空,请检查设备数据')
|
||||
return
|
||||
}
|
||||
|
||||
await equipmentApprovalApi.submitProcurementApplication(submitData)
|
||||
|
||||
// 添加采购申请通知
|
||||
|
|
|
@ -128,6 +128,17 @@
|
|||
</a-tag>
|
||||
</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 }">
|
||||
<a-tag :color="getLocationStatusColor(record.locationStatus)">
|
||||
|
@ -175,7 +186,7 @@
|
|||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<!-- 申请采购按钮 -->
|
||||
<!-- 申请采购按钮 - 只在特定状态下显示 -->
|
||||
<a-button
|
||||
v-if="canApplyProcurement(record)"
|
||||
type="primary"
|
||||
|
@ -184,12 +195,20 @@
|
|||
>
|
||||
申请采购
|
||||
</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)">
|
||||
{{ getApprovalStatusText(record.approvalStatus) }}
|
||||
</a-tag>
|
||||
<!-- 删除按钮 -->
|
||||
<a-popconfirm
|
||||
content="确定要删除这条采购记录吗?"
|
||||
content="确定要删除这条记录吗?"
|
||||
@ok="handleDelete(record)"
|
||||
>
|
||||
<a-button type="text" size="small" status="danger">
|
||||
|
@ -345,6 +364,14 @@ const columns = [
|
|||
slotName: 'equipmentStatus',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '采购状态',
|
||||
dataIndex: 'procurementStatus',
|
||||
key: 'procurementStatus',
|
||||
slotName: 'procurementStatus',
|
||||
width: 120,
|
||||
fixed: false,
|
||||
},
|
||||
{
|
||||
title: '位置状态',
|
||||
dataIndex: 'locationStatus',
|
||||
|
@ -403,6 +430,30 @@ const getEquipmentStatusText = (status: string) => {
|
|||
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 colorMap: Record<string, string> = {
|
||||
|
@ -530,6 +581,7 @@ const transformBackendData = (data: any[]): EquipmentResp[] => {
|
|||
totalPrice: item.totalPrice,
|
||||
inventoryBasis: item.inventoryBasis,
|
||||
dynamicRecord: item.dynamicRecord,
|
||||
procurementStatus: item.procurementStatus,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -572,6 +624,10 @@ const loadData = async (searchParams?: EquipmentListReq) => {
|
|||
if (dataList.length > 0) {
|
||||
const transformedData = transformBackendData(dataList)
|
||||
console.log('转换后的数据:', transformedData)
|
||||
// 调试采购状态字段
|
||||
transformedData.forEach((item, index) => {
|
||||
console.log(`设备 ${index + 1} - 名称: ${item.equipmentName}, 采购状态: ${item.procurementStatus}`)
|
||||
})
|
||||
tableData.value = transformedData
|
||||
} else {
|
||||
tableData.value = []
|
||||
|
@ -662,9 +718,15 @@ const handleModalSuccess = () => {
|
|||
}
|
||||
|
||||
// 采购申请成功回调
|
||||
const handleApplicationSuccess = () => {
|
||||
const handleApplicationSuccess = async () => {
|
||||
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) => {
|
||||
// 检查是否有审批状态,如果没有或者状态为待申请,则可以申请
|
||||
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
|
||||
}
|
||||
|
||||
// 获取审批状态颜色
|
||||
|
|
Loading…
Reference in New Issue