Merge branch 'devlopment' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web into devlopment
This commit is contained in:
commit
cee78d71c9
|
@ -0,0 +1,188 @@
|
|||
// @/apis/data/index.ts - 数据管理API
|
||||
import http from '@/utils/http'
|
||||
import type {
|
||||
FolderInfo,
|
||||
FileInfo,
|
||||
FolderListParams,
|
||||
FileListParams,
|
||||
FolderListResponse,
|
||||
FileListResponse,
|
||||
CreateFolderParams,
|
||||
RenameFolderParams,
|
||||
DeleteFolderParams,
|
||||
UploadFileParams,
|
||||
DownloadFileParams,
|
||||
DeleteFileParams,
|
||||
PreviewFileParams,
|
||||
RenameFileParams
|
||||
} from './type'
|
||||
|
||||
const { request, requestRaw } = http
|
||||
|
||||
// 导出类型定义
|
||||
export type {
|
||||
FolderInfo,
|
||||
FileInfo,
|
||||
FolderListParams,
|
||||
FileListParams,
|
||||
FolderListResponse,
|
||||
FileListResponse,
|
||||
CreateFolderParams,
|
||||
RenameFolderParams,
|
||||
DeleteFolderParams,
|
||||
UploadFileParams,
|
||||
DownloadFileParams,
|
||||
DeleteFileParams,
|
||||
PreviewFileParams,
|
||||
RenameFileParams
|
||||
}
|
||||
|
||||
// 获取文件夹列表(分页)
|
||||
export function getFolderListApi(params?: FolderListParams) {
|
||||
return request<FolderListResponse>({
|
||||
url: '/data/folder/list',
|
||||
method: 'get',
|
||||
params: {
|
||||
page: params?.page || 1,
|
||||
pageSize: params?.pageSize || 10,
|
||||
folderName: params?.folderName
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取文件列表(分页)
|
||||
export function getFilesApi(params?: FileListParams) {
|
||||
return request<FileListResponse>({
|
||||
url: '/data/file/list',
|
||||
method: 'get',
|
||||
params: {
|
||||
page: params?.page || 1,
|
||||
pageSize: params?.pageSize || 10,
|
||||
folderId: params?.folderId || '0',
|
||||
fileName: params?.fileName,
|
||||
sortField: params?.sortField,
|
||||
sortOrder: params?.sortOrder
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建文件夹
|
||||
export function createFolderApi(data: CreateFolderParams) {
|
||||
return request({
|
||||
url: '/data/folder/creatFolder',
|
||||
method: 'post',
|
||||
data: {
|
||||
name: data.name,
|
||||
parentId: data.parentId || '0'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重命名文件夹
|
||||
export function updateFolderApi(folderId: string, newName: string) {
|
||||
return request({
|
||||
url: '/data/folder/rename',
|
||||
method: 'put',
|
||||
params: {
|
||||
folderId: folderId,
|
||||
newName: newName
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除文件夹
|
||||
export function deleteFolderApi(folderId: string) {
|
||||
return request({
|
||||
url: '/data/folder/delete',
|
||||
method: 'delete',
|
||||
params: {
|
||||
folderId: folderId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
export function uploadFileApi(
|
||||
file: File,
|
||||
folderId: string,
|
||||
onUploadProgress?: (progressEvent: any) => void,
|
||||
cancelToken?: any
|
||||
) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return requestRaw({
|
||||
url: '/data/file/add',
|
||||
method: 'post',
|
||||
params: {
|
||||
folderId: folderId
|
||||
},
|
||||
data: formData,
|
||||
onUploadProgress,
|
||||
cancelToken,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}).then(response => response.data)
|
||||
.catch(error => {
|
||||
// 确保错误不会抛出,而是返回一个错误对象
|
||||
console.error('上传文件API错误:', error)
|
||||
return {
|
||||
code: 500,
|
||||
msg: error.message || '上传失败',
|
||||
success: false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
export function downloadFileApi(fileId: string) {
|
||||
return request({
|
||||
url: '/data/file/download',
|
||||
method: 'get',
|
||||
params: {
|
||||
fileId: fileId
|
||||
},
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
export function deleteFileApi(fileId: string) {
|
||||
return request({
|
||||
url: '/data/file/delete',
|
||||
method: 'delete',
|
||||
params: {
|
||||
fileId: fileId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 预览文件
|
||||
export function previewFileApi(fileId: string) {
|
||||
return request({
|
||||
url: '/data/file/preview',
|
||||
method: 'get',
|
||||
params: {
|
||||
fileId: fileId
|
||||
},
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 重命名文件
|
||||
export function renameFileApi(fileId: string, newFileName: string) {
|
||||
return request({
|
||||
url: '/data/file/rename',
|
||||
method: 'put',
|
||||
params: {
|
||||
fileId: fileId,
|
||||
newFileName: newFileName
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重命名文件(兼容旧接口)
|
||||
export function updateFileNameApi(fileId: string, data: RenameFileParams) {
|
||||
return renameFileApi(fileId, data.newFileName)
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/** 文件夹信息接口 */
|
||||
export interface FolderInfo {
|
||||
folderId: string
|
||||
name: string
|
||||
parentId: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
/** 文件信息接口 */
|
||||
export interface FileInfo {
|
||||
fileId: string
|
||||
fileName: string
|
||||
fileSize: number
|
||||
fileType: string
|
||||
folderId: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
/** 文件夹列表查询参数 */
|
||||
export interface FolderListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
folderName?: string
|
||||
}
|
||||
|
||||
/** 文件列表查询参数 */
|
||||
export interface FileListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
folderId?: string
|
||||
fileName?: string
|
||||
sortField?: string
|
||||
sortOrder?: string
|
||||
}
|
||||
|
||||
/** 文件夹列表响应 */
|
||||
export interface FolderListResponse {
|
||||
data: FolderInfo[]
|
||||
total: number
|
||||
current: number
|
||||
size: number
|
||||
}
|
||||
|
||||
/** 文件列表响应 */
|
||||
export interface FileListResponse {
|
||||
data: FileInfo[]
|
||||
total: number
|
||||
current: number
|
||||
size: number
|
||||
}
|
||||
|
||||
/** 创建文件夹请求参数 */
|
||||
export interface CreateFolderParams {
|
||||
name: string
|
||||
parentId?: string
|
||||
}
|
||||
|
||||
/** 重命名文件夹请求参数 */
|
||||
export interface RenameFolderParams {
|
||||
folderId: string
|
||||
newName: string
|
||||
}
|
||||
|
||||
/** 删除文件夹请求参数 */
|
||||
export interface DeleteFolderParams {
|
||||
folderId: string
|
||||
}
|
||||
|
||||
/** 上传文件请求参数 */
|
||||
export interface UploadFileParams {
|
||||
file: File
|
||||
folderId: string
|
||||
onUploadProgress?: (progressEvent: any) => void
|
||||
cancelToken?: any
|
||||
}
|
||||
|
||||
/** 下载文件请求参数 */
|
||||
export interface DownloadFileParams {
|
||||
fileId: string
|
||||
}
|
||||
|
||||
/** 删除文件请求参数 */
|
||||
export interface DeleteFileParams {
|
||||
fileId: string
|
||||
}
|
||||
|
||||
/** 预览文件请求参数 */
|
||||
export interface PreviewFileParams {
|
||||
fileId: string
|
||||
}
|
||||
|
||||
/** 重命名文件请求参数 */
|
||||
export interface RenameFileParams {
|
||||
fileId: string
|
||||
newFileName: string
|
||||
}
|
|
@ -457,6 +457,7 @@ export interface ReceiptRequest {
|
|||
useStatus?: string
|
||||
healthStatus?: string
|
||||
receiptStatus?: string
|
||||
paymentStatus?: string
|
||||
|
||||
// 其他管理信息
|
||||
depreciationMethod?: string
|
||||
|
|
|
@ -20,6 +20,7 @@ export * as RegulationAPI from './regulation'
|
|||
export * as TrainingAPI from './training'
|
||||
export * as EquipmentAPI from './equipment'
|
||||
export * as BussinessAPI from './bussiness/bussiness'
|
||||
export * as DataAPI from './data'
|
||||
|
||||
export * from './area/type'
|
||||
export * from './auth/type'
|
||||
|
|
|
@ -15,6 +15,12 @@ export function getProject(id: string | number) {
|
|||
return http.get<T.ProjectResp>(`${BASE_URL}/${id}`)
|
||||
}
|
||||
|
||||
/** @desc 获取项目详情(标准详情接口) */
|
||||
export function getProjectDetail(id: string | number) {
|
||||
return http.get<T.ProjectResp>(`${BASE_URL}/detail/${id}`)
|
||||
}
|
||||
|
||||
|
||||
/** @desc 新增项目 */
|
||||
export function addProject(data: any) {
|
||||
return http.post(`${BASE_URL}`, data)
|
||||
|
@ -49,4 +55,4 @@ export function importProject(file: File) {
|
|||
/** @desc 导出项目 */
|
||||
export function exportProject(query: T.ProjectQuery) {
|
||||
return http.download(`${BASE_URL}/export`, query)
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
:mask-closable="true"
|
||||
:closable="true"
|
||||
:destroy-on-close="false"
|
||||
:z-index="999999"
|
||||
:z-index="1000"
|
||||
class="notification-modal"
|
||||
>
|
||||
<!-- 消息中心头部 -->
|
||||
|
@ -673,7 +673,7 @@ defineExpose({
|
|||
<style scoped lang="scss">
|
||||
.notification-center {
|
||||
position: relative;
|
||||
z-index: 999999;
|
||||
z-index: 1000;
|
||||
|
||||
.notification-trigger {
|
||||
cursor: pointer;
|
||||
|
@ -699,15 +699,15 @@ defineExpose({
|
|||
// 消息中心弹窗样式
|
||||
.notification-modal {
|
||||
:deep(.arco-modal) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-modal-mask) {
|
||||
z-index: 999998 !important;
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-modal-wrapper) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -858,37 +858,37 @@ defineExpose({
|
|||
padding: 16px;
|
||||
}
|
||||
|
||||
// 确保弹窗在最上层
|
||||
// 确保弹窗在普通层级
|
||||
:deep(.arco-modal) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-modal-mask) {
|
||||
z-index: 999998 !important;
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-modal-wrapper) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
// 针对Arco Design v2的样式
|
||||
:deep(.arco-overlay) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-overlay-container) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
// 确保消息中心弹窗不被其他元素遮挡
|
||||
// 确保消息中心弹窗在普通层级
|
||||
:deep(.arco-modal) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
// 强制设置最高优先级
|
||||
// 强制设置普通优先级
|
||||
:deep(.arco-modal-wrapper) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -906,12 +906,61 @@ export const systemRoutes: RouteRecordRaw[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
// start
|
||||
|
||||
{
|
||||
path: '/construction-operation-platform',
|
||||
name: 'ConstructionOperationPlatform',
|
||||
component: Layout,
|
||||
redirect: '/construction-operation-platform/implementation-workflow/field-construction',
|
||||
meta: { title: '我的工作台', icon: 'tool', hidden: false, sort: 5 },
|
||||
children: [
|
||||
// {
|
||||
// path: '/construction-operation-platform/implementation-workflow',
|
||||
// name: 'ImplementationWorkflow',
|
||||
// component: () => import('@/components/ParentView/index.vue'),
|
||||
// redirect: '/construction-operation-platform/implementation-workflow/field-construction',
|
||||
// meta: { title: '项目实施工作流程', icon: 'fork', hidden: false },
|
||||
// children: [
|
||||
{
|
||||
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/attachment',
|
||||
name: 'AttachmentManagement',
|
||||
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-storage/index.vue'),
|
||||
meta: { title: '附件管理', icon: 'attachment', hidden: false },
|
||||
},
|
||||
{
|
||||
path: '/construction-operation-platform/implementation-workflow/data-processing/model-config',
|
||||
name: 'ModelConfig',
|
||||
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/model-config/index.vue'),
|
||||
meta: { title: '模型配置', icon: 'robot', hidden: false },
|
||||
},
|
||||
{
|
||||
path: '/construction-operation-platform/implementation-workflow/field-construction',
|
||||
name: 'FieldConstruction',
|
||||
component: () => import('@/components/ParentView/index.vue'),
|
||||
redirect: '/construction-operation-platform/implementation-workflow/field-construction/project-list',
|
||||
meta: { title: '我的项目', icon: 'construction', hidden: false },
|
||||
children: [
|
||||
{
|
||||
path: '/project-management/projects/list',
|
||||
name: 'ProjectList',
|
||||
component: () => import('@/views/project-management/projects/list/index.vue'),
|
||||
meta: {
|
||||
title: '项目列表',
|
||||
icon: 'unordered-list',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/construction-operation-platform/implementation-workflow/field-construction/technology',
|
||||
name: 'FieldConstructionTechnology',
|
||||
component: () => import('@/views/project-operation-platform/implementation-workflow/field-construction/project-list/index.vue'),
|
||||
meta: { title: '我的施工', icon: 'tool', hidden: false },
|
||||
},
|
||||
// start
|
||||
// 数据管理
|
||||
{
|
||||
path: '/data-management',
|
||||
name: 'DataManagement',
|
||||
component: Layout,
|
||||
redirect: '/data-management/project-management/project-template',
|
||||
meta: { title: '数据管理', icon: 'database', hidden: false, sort: 4 },
|
||||
children: [
|
||||
|
@ -1027,55 +1076,6 @@ export const systemRoutes: RouteRecordRaw[] = [
|
|||
],
|
||||
},
|
||||
// end
|
||||
{
|
||||
path: '/construction-operation-platform',
|
||||
name: 'ConstructionOperationPlatform',
|
||||
component: Layout,
|
||||
redirect: '/construction-operation-platform/implementation-workflow/field-construction',
|
||||
meta: { title: '我的工作台', icon: 'tool', hidden: false, sort: 5 },
|
||||
children: [
|
||||
// {
|
||||
// path: '/construction-operation-platform/implementation-workflow',
|
||||
// name: 'ImplementationWorkflow',
|
||||
// component: () => import('@/components/ParentView/index.vue'),
|
||||
// redirect: '/construction-operation-platform/implementation-workflow/field-construction',
|
||||
// meta: { title: '项目实施工作流程', icon: 'fork', hidden: false },
|
||||
// children: [
|
||||
{
|
||||
path: '/construction-operation-platform/implementation-workflow/data-processing/data-storage/attachment',
|
||||
name: 'AttachmentManagement',
|
||||
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/data-storage/index.vue'),
|
||||
meta: { title: '附件管理', icon: 'attachment', hidden: false },
|
||||
},
|
||||
{
|
||||
path: '/construction-operation-platform/implementation-workflow/data-processing/model-config',
|
||||
name: 'ModelConfig',
|
||||
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/model-config/index.vue'),
|
||||
meta: { title: '模型配置', icon: 'robot', hidden: false },
|
||||
},
|
||||
{
|
||||
path: '/construction-operation-platform/implementation-workflow/field-construction',
|
||||
name: 'FieldConstruction',
|
||||
component: () => import('@/components/ParentView/index.vue'),
|
||||
redirect: '/construction-operation-platform/implementation-workflow/field-construction/project-list',
|
||||
meta: { title: '我的项目', icon: 'construction', hidden: false },
|
||||
children: [
|
||||
{
|
||||
path: '/project-management/projects/list',
|
||||
name: 'ProjectList',
|
||||
component: () => import('@/views/project-management/projects/list/index.vue'),
|
||||
meta: {
|
||||
title: '项目列表',
|
||||
icon: 'unordered-list',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/construction-operation-platform/implementation-workflow/field-construction/technology',
|
||||
name: 'FieldConstructionTechnology',
|
||||
component: () => import('@/views/project-operation-platform/implementation-workflow/field-construction/project-list/index.vue'),
|
||||
meta: { title: '我的施工', icon: 'tool', hidden: false },
|
||||
},
|
||||
{
|
||||
path: '/construction-operation-platform/implementation-workflow/project-delivery',
|
||||
name: 'ProjectDelivery',
|
||||
|
@ -1184,7 +1184,7 @@ export const systemRoutes: RouteRecordRaw[] = [
|
|||
name: 'bussinesskonwledge',
|
||||
component: Layout,
|
||||
redirect: '/bussiness-knowledge/data',
|
||||
meta: { title: '商务资料知识库', icon: 'database', hidden: false, sort: 5.5 },
|
||||
meta: { title: '智能商务', icon: 'database', hidden: false, sort: 5.5 },
|
||||
children: [
|
||||
{
|
||||
path: '/bussiness-konwledge/data',
|
||||
|
@ -1198,47 +1198,162 @@ export const systemRoutes: RouteRecordRaw[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
// 数据管理模块
|
||||
{
|
||||
path: '/chat-platform',
|
||||
name: 'ChatPlatform',
|
||||
path: '/data-management',
|
||||
name: 'dataManagement',
|
||||
component: Layout,
|
||||
redirect: '/chat-platform/options',
|
||||
meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 },
|
||||
redirect: '/data-management/data',
|
||||
meta: { title: '数据管理', icon: 'database', hidden: false, sort: 5.6 },
|
||||
children: [
|
||||
// {
|
||||
// path: '/chat-platform/options',
|
||||
// name: 'ChatOptions',
|
||||
// component: () => import('@/views/default/redirect/index.vue'), // 临时使用一个组件,实际开发中需要替换
|
||||
// meta: {
|
||||
// title: '二级选项1',
|
||||
// icon: 'setting',
|
||||
// hidden: false
|
||||
// }
|
||||
// }
|
||||
{
|
||||
path: '/data-management/data',
|
||||
name: 'data-management',
|
||||
component: () => import('@/views/data/data.vue'),
|
||||
meta: {
|
||||
title: '数据管理',
|
||||
icon: 'database',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/image-detection',
|
||||
name: 'ImageDetection',
|
||||
component: Layout,
|
||||
redirect: '/Image-detection/tower-monitoring/clearance-monitoring',
|
||||
meta: {
|
||||
title: '图像检测',
|
||||
icon: 'monitor',
|
||||
hidden: false,
|
||||
sort: 6.5,
|
||||
},
|
||||
|
||||
children: [
|
||||
{
|
||||
path: '/image-detection/image-analysis',
|
||||
name: 'ImageAnalysis',
|
||||
component: () => import('@/components/ParentView/index.vue'),
|
||||
meta: {
|
||||
title: '检查图像分析',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
},
|
||||
redirect: '/image-detection/image-analysis/defect-detection',
|
||||
children: [
|
||||
{path: '/image-detection/image-analysis/defect-detection',
|
||||
name: 'DefectDetection',
|
||||
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/defect-algorithm/index.vue'),
|
||||
meta: {
|
||||
title: '缺陷检测',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
}
|
||||
},
|
||||
{path: '/image-detection/image-analysis/defect-edit',
|
||||
name: 'DefectEdit',
|
||||
component: () => import('@/components/ParentView/index.vue'),
|
||||
meta: {
|
||||
title: '缺陷编辑',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
}
|
||||
},
|
||||
{path: '/image-detection/image-analysis/generate-reports',
|
||||
name: 'GenerateReports',
|
||||
component: () => import('@/views/project-operation-platform/data-processing/report-generation/index.vue'),
|
||||
meta: {
|
||||
title: '生成报告',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
}
|
||||
},
|
||||
{path: '/image-detection/image-analysis/defect-base',
|
||||
name: 'DefectBase',
|
||||
component: () => import('@/views/project-operation-platform/data-processing/standard-info/index.vue'),
|
||||
meta: {
|
||||
title: '缺陷标准数据信息库',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/tower-monitoring',
|
||||
name: 'TowerMonitoring',
|
||||
component: () => import('@/components/ParentView/index.vue'),
|
||||
redirect: '/tower-monitoring/clearance-monitoring',
|
||||
meta: {
|
||||
title: '音视频检测',
|
||||
icon: 'monitor',
|
||||
hidden: false,
|
||||
sort: 6.5,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/tower-monitoring/clearance-monitoring',
|
||||
name: 'ClearanceMonitoring',
|
||||
component: () => import('@/views/tower-monitoring/deformation.vue'),
|
||||
meta: {
|
||||
title: '净空监测',
|
||||
icon: 'fullscreen',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tower-monitoring/deformation-monitoring',
|
||||
name: 'DeformationMonitoring',
|
||||
component: () => import('@/views/tower-monitoring/clearance.vue'),
|
||||
meta: {
|
||||
title: '形变监测',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tower-monitoring/whistle-monitoring',
|
||||
name: 'WhistleMonitoring',
|
||||
component: () => import('@/views/tower-monitoring/whistle.vue'),
|
||||
meta: {
|
||||
title: '哨声监测',
|
||||
icon: 'sound',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tower-monitoring/vibration-monitoring',
|
||||
name: 'VibrationMonitoring',
|
||||
component: () => import('@/views/tower-monitoring/vibration.vue'),
|
||||
meta: {
|
||||
title: '振动监测',
|
||||
icon: 'shake',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/image-detection/reporting-center',
|
||||
name: 'ReportingCenter',
|
||||
component: () => import('@/views/tower-monitoring/vibration.vue'),
|
||||
meta: {
|
||||
title: '报告中心',
|
||||
icon: 'shake',
|
||||
hidden: false,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// {
|
||||
// path: '/user/profile',
|
||||
// name: 'UserProfile',
|
||||
// path: '/chat-platform',
|
||||
// name: 'ChatPlatform',
|
||||
// component: Layout,
|
||||
// redirect: '/user/profile',
|
||||
// meta: {
|
||||
// title: '个人中心',
|
||||
// icon: 'user',
|
||||
// hidden: false,
|
||||
// sort: 100,
|
||||
// },
|
||||
// redirect: '/chat-platform/options',
|
||||
// meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 },
|
||||
// children: [
|
||||
// {
|
||||
// path: '/user/profile',
|
||||
// name: 'UsersProfile',
|
||||
// component: () => import('@/views/user/profile/index.vue'),
|
||||
// meta: {
|
||||
// title: '个人中心',
|
||||
// icon: 'user',
|
||||
// hidden: false,
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,105 +0,0 @@
|
|||
<template>
|
||||
<div class="file-header">
|
||||
<div class="breadcrumbs">
|
||||
<a-breadcrumb>
|
||||
<a-breadcrumb-item
|
||||
v-for="(item, index) in breadcrumbPath"
|
||||
:key="index"
|
||||
:class="{ 'clickable': index < breadcrumbPath.length - 1 }"
|
||||
@click="handleBreadcrumbClick(index)"
|
||||
>
|
||||
{{ item }}
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
@click="handleRefresh"
|
||||
:loading="refreshing"
|
||||
tooltip="刷新数据"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-refresh :spin="refreshing" />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button type="outline" @click="handleUpload">
|
||||
<template #icon><icon-upload /></template>
|
||||
上传文件
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleCreateFolder">
|
||||
<template #icon><icon-plus /></template>
|
||||
新建文件夹
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { IconRefresh, IconUpload, IconPlus } from '@arco-design/web-vue/es/icon';
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
breadcrumbPath: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
refreshing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits(['breadcrumb-click', 'refresh', 'upload', 'create-folder']);
|
||||
|
||||
// 处理面包屑点击
|
||||
const handleBreadcrumbClick = (index) => {
|
||||
emit('breadcrumb-click', index);
|
||||
};
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = () => {
|
||||
emit('refresh');
|
||||
};
|
||||
|
||||
// 处理上传文件
|
||||
const handleUpload = () => {
|
||||
emit('upload');
|
||||
};
|
||||
|
||||
// 处理新建文件夹
|
||||
const handleCreateFolder = () => {
|
||||
emit('create-folder');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
height: 64px;
|
||||
background: var(--color-bg-1);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
color: var(--color-primary);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.clickable:hover {
|
||||
color: var(--color-primary-light-1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
|
@ -1,669 +0,0 @@
|
|||
<template>
|
||||
<div class="file-list-container">
|
||||
<!-- 文件列表标题和搜索框在同一行 -->
|
||||
<div v-if="currentFolderId" class="file-header-container">
|
||||
<div class="file-title">
|
||||
<span class="file-list-title">文件列表 ({{ files.length }})</span>
|
||||
</div>
|
||||
<div class="file-search-container">
|
||||
<a-input-search
|
||||
v-model="fileSearchKeyword"
|
||||
placeholder="搜索文件名..."
|
||||
class="file-search-input"
|
||||
@search="handleFileSearch"
|
||||
@input="handleFileSearchInput"
|
||||
@clear="handleFileSearchClear"
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-divider size="small" v-if="currentFolderId" />
|
||||
|
||||
<template v-if="!currentFolderId">
|
||||
<div class="initial-state">
|
||||
<icon-folder-add class="initial-icon" />
|
||||
<div class="initial-text">请从左侧选择一个文件夹</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 文件列表加载状态 -->
|
||||
<a-skeleton
|
||||
:loading="loading && currentFolderId"
|
||||
:rows="8"
|
||||
v-if="loading && currentFolderId"
|
||||
animation="pulse"
|
||||
>
|
||||
<template #skeleton>
|
||||
<a-row class="table-data-row" v-for="i in 8" :key="i">
|
||||
<a-col :span="10" class="table-column name-column">
|
||||
<div class="file-main">
|
||||
<div class="w-8 h-8 rounded bg-gray-200 mr-3"></div>
|
||||
<div class="file-name-wrap">
|
||||
<div class="h-5 bg-gray-200 rounded w-1/2 mb-1"></div>
|
||||
<div class="h-4 bg-gray-200 rounded w-1/3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="4" class="table-column type-column">
|
||||
<div class="h-4 bg-gray-200 rounded w-1/3"></div>
|
||||
</a-col>
|
||||
<a-col :span="3" class="table-column size-column">
|
||||
<div class="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
</a-col>
|
||||
<a-col :span="5" class="table-column time-column">
|
||||
<div class="h-4 bg-gray-200 rounded w-2/3"></div>
|
||||
</a-col>
|
||||
<a-col :span="2" class="table-column action-column">
|
||||
<div class="flex gap-2">
|
||||
<div class="w-6 h-6 rounded bg-gray-200"></div>
|
||||
<div class="w-6 h-6 rounded bg-gray-200"></div>
|
||||
<div class="w-6 h-6 rounded bg-gray-200"></div>
|
||||
<div class="w-6 h-6 rounded bg-gray-200"></div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-skeleton>
|
||||
|
||||
<!-- 文件表格 -->
|
||||
<div class="file-grid-container" v-if="currentFolderId && !loading">
|
||||
<!-- 表头行 -->
|
||||
<a-row class="table-header-row">
|
||||
<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: props.sortField === 'file_name' && props.sortOrder === 'asc' }"></div>
|
||||
<div class="sort-arrow down" :class="{ active: props.sortField === 'file_name' && props.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: props.sortField === 'file_type' && props.sortOrder === 'asc' }"></div>
|
||||
<div class="sort-arrow down" :class="{ active: props.sortField === 'file_type' && props.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: props.sortField === 'file_size' && props.sortOrder === 'asc' }"></div>
|
||||
<div class="sort-arrow down" :class="{ active: props.sortField === 'file_size' && props.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: props.sortField === 'upload_time' && props.sortOrder === 'asc' }"></div>
|
||||
<div class="sort-arrow down" :class="{ active: props.sortField === 'upload_time' && props.sortOrder === 'desc' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="2" class="table-column action-column">操作</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 数据行 -->
|
||||
<a-row
|
||||
v-for="file in files"
|
||||
:key="file.fileId"
|
||||
class="table-data-row"
|
||||
>
|
||||
<!-- 文件名列 -->
|
||||
<a-col :span="10" class="table-column name-column">
|
||||
<div class="file-main">
|
||||
<icon-file :style="{ color: fileColor(getFileExtension(file.fileName || file.name)) }" class="file-icon-large" />
|
||||
<div class="file-name-wrap">
|
||||
<a-typography-title :heading="6" class="file-name">{{ file.fileName || file.name }}</a-typography-title>
|
||||
<div class="file-name-small">{{ file.fileName || file.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 类型列 -->
|
||||
<a-col :span="4" class="table-column type-column">
|
||||
<div class="cell-content">{{ fileTypeText(getFileExtension(file.fileName || file.name)) }}</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 大小列 -->
|
||||
<a-col :span="3" class="table-column size-column">
|
||||
<div class="cell-content">{{ formatFileListSize(file.fileSize || file.size) }}</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 时间列 -->
|
||||
<a-col :span="5" class="table-column time-column">
|
||||
<div class="cell-content">{{ formatUploadTime(file.uploadTime || file.uploadTime) }}</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<a-col :span="2" class="table-column action-column">
|
||||
<div class="file-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
tooltip="预览"
|
||||
@click="handlePreview(file)"
|
||||
>
|
||||
<icon-eye />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
tooltip="下载"
|
||||
@click="handleDownload(file)"
|
||||
>
|
||||
<icon-download />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
tooltip="重命名"
|
||||
@click="handleEditFile(file)"
|
||||
>
|
||||
<icon-edit />
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
tooltip="删除"
|
||||
@click="handleDelete(file)"
|
||||
class="action-btn delete-btn"
|
||||
>
|
||||
<icon-delete />
|
||||
</a-button>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty
|
||||
v-if="!loading && currentFolderId && files.length === 0"
|
||||
description="暂无文件"
|
||||
class="empty-state"
|
||||
>
|
||||
<template #image><icon-file /></template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="handleUpload">
|
||||
<template #icon><icon-upload /></template>
|
||||
上传文件
|
||||
</a-button>
|
||||
</template>
|
||||
</a-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 导入依赖
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import {
|
||||
IconFolder,
|
||||
IconFile,
|
||||
IconMore,
|
||||
IconDownload,
|
||||
IconDelete,
|
||||
IconEdit,
|
||||
IconEye,
|
||||
IconCopy,
|
||||
IconFolderAdd,
|
||||
IconUpload
|
||||
} from '@arco-design/web-vue/es/icon';
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentFolderId: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
sortField: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
sortOrder: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits([
|
||||
'file-click',
|
||||
'file-download',
|
||||
'file-delete',
|
||||
'file-edit',
|
||||
'file-preview',
|
||||
'file-copy',
|
||||
'file-more',
|
||||
'file-search',
|
||||
'file-search-input',
|
||||
'file-search-clear',
|
||||
'sort-change',
|
||||
'upload'
|
||||
]);
|
||||
|
||||
// 本地状态
|
||||
const fileSearchKeyword = ref('');
|
||||
|
||||
// 监听排序状态变化
|
||||
watch(() => props.sortField, (newVal, oldVal) => {
|
||||
console.log('👀 FileList组件 - sortField变化:', oldVal, '->', newVal);
|
||||
});
|
||||
|
||||
watch(() => props.sortOrder, (newVal, oldVal) => {
|
||||
console.log('👀 FileList组件 - sortOrder变化:', oldVal, '->', newVal);
|
||||
});
|
||||
|
||||
// 处理文件搜索
|
||||
const handleFileSearch = (value) => {
|
||||
emit('file-search', value);
|
||||
};
|
||||
|
||||
// 处理文件搜索输入
|
||||
const handleFileSearchInput = (value) => {
|
||||
emit('file-search-input', value);
|
||||
};
|
||||
|
||||
// 处理文件搜索清除
|
||||
const handleFileSearchClear = () => {
|
||||
emit('file-search-clear');
|
||||
};
|
||||
|
||||
// 处理排序变化
|
||||
const handleSortChange = (field) => {
|
||||
console.log('🎯 FileList组件 - 排序点击:', field);
|
||||
console.log('🎯 FileList组件 - 当前sortField:', props.sortField);
|
||||
console.log('🎯 FileList组件 - 当前sortOrder:', props.sortOrder);
|
||||
emit('sort-change', field);
|
||||
};
|
||||
|
||||
// 处理预览
|
||||
const handlePreview = (file) => {
|
||||
emit('file-preview', file);
|
||||
};
|
||||
|
||||
// 处理下载
|
||||
const handleDownload = (file) => {
|
||||
emit('file-download', file);
|
||||
};
|
||||
|
||||
// 处理编辑文件
|
||||
const handleEditFile = (file) => {
|
||||
emit('file-edit', file);
|
||||
};
|
||||
|
||||
// 处理删除
|
||||
const handleDelete = (file) => {
|
||||
emit('file-delete', file);
|
||||
};
|
||||
|
||||
// 处理上传
|
||||
const handleUpload = () => {
|
||||
emit('upload');
|
||||
};
|
||||
|
||||
// 工具函数 - 获取文件扩展名
|
||||
const getFileExtension = (filename) => {
|
||||
if (!filename) return '';
|
||||
return filename.split('.').pop().toLowerCase();
|
||||
};
|
||||
|
||||
// 工具函数 - 文件颜色
|
||||
const fileColor = (extension) => {
|
||||
const colorMap = {
|
||||
pdf: '#ff4d4f',
|
||||
doc: '#1890ff',
|
||||
docx: '#1890ff',
|
||||
xls: '#52c41a',
|
||||
xlsx: '#52c41a',
|
||||
ppt: '#fa8c16',
|
||||
pptx: '#fa8c16',
|
||||
zip: '#722ed1',
|
||||
rar: '#722ed1',
|
||||
txt: '#8c8c8c',
|
||||
jpg: '#fadb14',
|
||||
jpeg: '#fadb14',
|
||||
png: '#fadb14',
|
||||
gif: '#fadb14',
|
||||
bmp: '#fadb14',
|
||||
webp: '#fadb14'
|
||||
};
|
||||
return colorMap[extension] || '#8c8c8c';
|
||||
};
|
||||
|
||||
// 工具函数 - 文件类型文本
|
||||
const fileTypeText = (extension) => {
|
||||
const typeMap = {
|
||||
pdf: 'PDF文档',
|
||||
doc: 'Word文档',
|
||||
docx: 'Word文档',
|
||||
xls: 'Excel表格',
|
||||
xlsx: 'Excel表格',
|
||||
ppt: 'PPT演示',
|
||||
pptx: 'PPT演示',
|
||||
zip: '压缩文件',
|
||||
rar: '压缩文件',
|
||||
txt: '文本文件',
|
||||
jpg: '图片文件',
|
||||
jpeg: '图片文件',
|
||||
png: '图片文件',
|
||||
gif: '图片文件',
|
||||
bmp: '图片文件',
|
||||
webp: '图片文件'
|
||||
};
|
||||
return typeMap[extension] || '未知文件';
|
||||
};
|
||||
|
||||
// 工具函数 - 格式化文件大小
|
||||
const formatFileListSize = (bytes) => {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 工具函数 - 格式化上传时间
|
||||
const formatUploadTime = (time) => {
|
||||
if (!time) return '';
|
||||
const date = new Date(time);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-list-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.file-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-list-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.file-search-container {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-search-input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.initial-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 64px 0;
|
||||
color: var(--color-text-3);
|
||||
background-color: var(--color-fill-1);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.initial-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
|
||||
.file-grid-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-bg-1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 0;
|
||||
min-height: 300px;
|
||||
max-height: calc(100vh - 380px);
|
||||
}
|
||||
|
||||
.table-header-row {
|
||||
padding: 0 16px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
background-color: var(--color-fill-1);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.table-data-row {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
height: 64px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
transition: all 0.25s ease;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
.table-data-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-data-row:hover {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.table-column {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.name-column {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.type-column {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.size-column {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.time-column {
|
||||
flex: 1.5;
|
||||
}
|
||||
|
||||
.action-column {
|
||||
flex: 0.5;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sortable-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.sortable-header:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 4px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.sort-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 3px solid transparent;
|
||||
border-right: 3px solid transparent;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.sort-arrow.up {
|
||||
border-bottom: 3px solid var(--color-text-4);
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.file-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-icon-large {
|
||||
font-size: 24px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-name-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-name-small {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cell-content {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.table-data-row:hover .file-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.file-actions .action-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: var(--color-text-3);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.file-actions .action-btn:hover {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-fill-3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.file-actions .delete-btn:hover {
|
||||
color: var(--color-danger);
|
||||
background: var(--color-danger-light-1);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 64px 0;
|
||||
color: var(--color-text-3);
|
||||
background-color: var(--color-fill-1);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.empty-state .arco-btn) {
|
||||
margin-top: 16px;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.empty-state .arco-btn:hover) {
|
||||
background-color: var(--color-primary-dark-1);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:deep(.empty-state .arco-btn:active) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
|
@ -1,116 +0,0 @@
|
|||
<template>
|
||||
<div v-if="visible" class="pagination-container">
|
||||
<a-pagination
|
||||
:total="total"
|
||||
:current="current"
|
||||
:page-size="pageSize"
|
||||
:show-total="true"
|
||||
:show-page-size="true"
|
||||
:page-size-options="[10, 20, 50, 100]"
|
||||
:show-jumper="true"
|
||||
:hide-on-single-page="false"
|
||||
size="default"
|
||||
@change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
current: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits(['page-change', 'page-size-change']);
|
||||
|
||||
// 处理页码变化
|
||||
const handlePageChange = (page) => {
|
||||
emit('page-change', page);
|
||||
};
|
||||
|
||||
// 处理每页条数变化
|
||||
const handlePageSizeChange = (pageSize) => {
|
||||
emit('page-size-change', pageSize);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-bg-1);
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-item) {
|
||||
border-radius: 6px;
|
||||
margin: 0 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-item:hover) {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-item-active) {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-prev),
|
||||
.pagination-container :deep(.arco-pagination-next) {
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-prev:hover),
|
||||
.pagination-container :deep(.arco-pagination-next:hover) {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-size-changer) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-jumper) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.pagination-container :deep(.arco-pagination-total) {
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
|
@ -1,557 +0,0 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="上传文件"
|
||||
width="620px"
|
||||
:mask-closable="false"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:confirm-loading="uploading"
|
||||
:ok-disabled="!canUpload"
|
||||
@update:visible="(val) => emit('update:visible', val)"
|
||||
>
|
||||
<a-form :model="uploadForm" ref="uploadFormRef" layout="vertical">
|
||||
<!-- 选择文件 -->
|
||||
<a-form-item
|
||||
label="选择文件"
|
||||
:validate-status="!hasFiles ? 'error' : ''"
|
||||
:help="!hasFiles ? '请选择需要上传的文件' : ''"
|
||||
>
|
||||
<div class="upload-container">
|
||||
<!-- 上传按钮 -->
|
||||
<a-upload
|
||||
ref="uploadRef"
|
||||
:key="visible ? 'upload-open' : 'upload-closed'"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
@change="handleFileChange"
|
||||
:accept="allowedFileTypes"
|
||||
multiple
|
||||
>
|
||||
<a-button type="primary" class="upload-btn">
|
||||
<icon-upload />
|
||||
点击选择文件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
|
||||
<!-- 文件类型提示 -->
|
||||
<div class="upload-hint">
|
||||
支持 {{ allowedFileTypesText }} 等格式,单个文件不超过 {{ maxFileSizeText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<div class="upload-file-list" v-if="fileListTemp.length > 0">
|
||||
<div
|
||||
class="upload-file-item"
|
||||
v-for="file in fileListTemp"
|
||||
:key="file.uid"
|
||||
:class="{ 'file-error': file.error }"
|
||||
>
|
||||
<div class="file-info">
|
||||
<icon-file
|
||||
:style="{ color: fileColor(getFileExtension(file.name)) }"
|
||||
class="file-icon"
|
||||
/>
|
||||
<div class="file-details">
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="file-meta">
|
||||
{{ formatFileSize(file.size) }}
|
||||
<span v-if="file.error" class="error-text">{{ file.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="file-progress" v-if="file.status === 'uploading'">
|
||||
<a-progress
|
||||
:percent="file.percent || 0"
|
||||
size="small"
|
||||
:status="file.percent === 100 ? 'success' : 'processing'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="file-actions">
|
||||
<a-button
|
||||
v-if="file.status !== 'uploading'"
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
@click="removeFile(file)"
|
||||
class="remove-btn"
|
||||
>
|
||||
<icon-delete />
|
||||
</a-button>
|
||||
<a-button
|
||||
v-else
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="small"
|
||||
@click="cancelUpload(file)"
|
||||
class="cancel-btn"
|
||||
>
|
||||
<icon-stop />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 目标文件夹选择 -->
|
||||
<a-form-item
|
||||
label="上传至目录"
|
||||
field="folderId"
|
||||
:rules="[{ required: true, message: '请选择目标文件夹' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="uploadForm.folderId"
|
||||
placeholder="请选择目标文件夹"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="0">根目录</a-option>
|
||||
<a-option
|
||||
v-for="folder in folderList"
|
||||
:key="folder.id"
|
||||
:value="folder.id"
|
||||
>
|
||||
{{ folder.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconUpload, IconFile, IconDelete, IconStop } from '@arco-design/web-vue/es/icon'
|
||||
import { uploadFileApi } from '@/apis/bussiness'
|
||||
import axios from 'axios'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
folderList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
currentFolderId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits([
|
||||
'update:visible',
|
||||
'upload-success'
|
||||
])
|
||||
|
||||
// 响应式数据
|
||||
const uploadForm = reactive({
|
||||
folderId: ''
|
||||
})
|
||||
|
||||
const fileListTemp = ref([])
|
||||
const uploadFormRef = ref(null)
|
||||
const uploadRef = ref(null)
|
||||
const uploading = ref(false)
|
||||
const cancelTokens = ref({})
|
||||
|
||||
// 常量
|
||||
const allowedFileTypes = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.txt,.jpg,.jpeg,.png,.gif,.bmp,.webp'
|
||||
const allowedFileTypesText = 'PDF, Word, Excel, PPT, 压缩文件, 文本文件, 图片文件'
|
||||
const maxFileSize = 1000 * 1024 * 1024 // 1000MB
|
||||
const maxFileSizeText = '1000MB'
|
||||
|
||||
// 计算属性
|
||||
const hasFiles = computed(() => {
|
||||
const validFiles = fileListTemp.value.filter(file => {
|
||||
return !file.error && file.status !== 'removed' && file.status !== 'canceled'
|
||||
})
|
||||
return validFiles.length > 0
|
||||
})
|
||||
|
||||
const canUpload = computed(() => {
|
||||
return hasFiles.value && !uploading.value && uploadForm.folderId
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
// 重置表单
|
||||
uploadForm.folderId = props.currentFolderId || ''
|
||||
fileListTemp.value = []
|
||||
|
||||
// 重置上传组件
|
||||
if (uploadRef.value) {
|
||||
try {
|
||||
uploadRef.value.reset()
|
||||
} catch (error) {
|
||||
console.log('重置上传组件时出错:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = (info) => {
|
||||
if (!info || !Array.isArray(info) || !props.visible) {
|
||||
return
|
||||
}
|
||||
|
||||
const fileList = info
|
||||
if (fileList.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前已存在的文件UID列表,用于去重
|
||||
const existingUids = fileListTemp.value.map(f => f.uid)
|
||||
|
||||
// 处理新选择的文件
|
||||
fileList.forEach((file) => {
|
||||
// 检查文件是否已存在(去重)
|
||||
if (existingUids.includes(file.uid)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 确保文件对象有正确的属性
|
||||
const fileObj = {
|
||||
uid: file.uid,
|
||||
name: file.name,
|
||||
size: file.size || file.file?.size || 0,
|
||||
type: file.type || file.file?.type || '',
|
||||
status: 'ready',
|
||||
error: '',
|
||||
originFileObj: file.file || file
|
||||
}
|
||||
|
||||
// 验证文件
|
||||
const isValid = validateFile(fileObj)
|
||||
|
||||
if (isValid) {
|
||||
fileListTemp.value.push(fileObj)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 文件验证
|
||||
const validateFile = (file) => {
|
||||
file.error = ''
|
||||
|
||||
// 验证文件类型
|
||||
const ext = getFileExtension(file.name).toLowerCase()
|
||||
const allowedExts = allowedFileTypes
|
||||
.split(',')
|
||||
.map(type => type.toLowerCase().replace(/^\./, ''))
|
||||
|
||||
if (!allowedExts.includes(ext)) {
|
||||
file.error = `不支持的文件类型,支持: ${allowedFileTypesText}`
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if (file.size > maxFileSize) {
|
||||
file.error = `文件过大,最大支持 ${maxFileSizeText}`
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
const getFileExtension = (fileName) => {
|
||||
const lastDotIndex = fileName.lastIndexOf('.')
|
||||
return lastDotIndex > 0 ? fileName.slice(lastDotIndex + 1) : ''
|
||||
}
|
||||
|
||||
// 获取文件图标颜色
|
||||
const fileColor = (extension) => {
|
||||
const colorMap = {
|
||||
pdf: '#ff4d4f',
|
||||
doc: '#1890ff',
|
||||
docx: '#1890ff',
|
||||
xls: '#52c41a',
|
||||
xlsx: '#52c41a',
|
||||
ppt: '#faad14',
|
||||
pptx: '#faad14',
|
||||
zip: '#722ed1',
|
||||
txt: '#8c8c8c',
|
||||
jpg: '#52c41a',
|
||||
jpeg: '#52c41a',
|
||||
png: '#1890ff',
|
||||
gif: '#faad14',
|
||||
bmp: '#722ed1',
|
||||
webp: '#13c2c2'
|
||||
}
|
||||
return colorMap[extension.toLowerCase()] || 'var(--color-text-3)'
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (fileSize) => {
|
||||
if (fileSize === 0) return '0 B'
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = fileSize
|
||||
let unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
const removeFile = (file) => {
|
||||
fileListTemp.value = fileListTemp.value.filter(f => f.uid !== file.uid)
|
||||
|
||||
// 如果是正在上传的文件,取消请求
|
||||
if (file.status === 'uploading' && cancelTokens.value[file.uid]) {
|
||||
cancelTokens.value[file.uid].cancel('上传已取消')
|
||||
delete cancelTokens.value[file.uid]
|
||||
}
|
||||
}
|
||||
|
||||
// 取消上传
|
||||
const cancelUpload = (file) => {
|
||||
if (cancelTokens.value[file.uid]) {
|
||||
cancelTokens.value[file.uid].cancel('上传已取消')
|
||||
file.status = 'canceled'
|
||||
}
|
||||
}
|
||||
|
||||
// 提交上传
|
||||
const handleSubmit = async () => {
|
||||
// 过滤有效文件
|
||||
const validFiles = fileListTemp.value.filter(file =>
|
||||
!file.error && file.status !== 'removed' && file.status !== 'canceled'
|
||||
)
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
Message.warning('请选择有效的文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件夹ID
|
||||
if (!uploadForm.folderId) {
|
||||
Message.warning('请选择目标文件夹')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
let hasError = false
|
||||
let hasFileExists = false
|
||||
|
||||
for (const fileItem of validFiles) {
|
||||
// 获取原始File对象
|
||||
const realFile = fileItem.originFileObj || fileItem
|
||||
|
||||
if (!realFile) {
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
fileItem.status = 'uploading'
|
||||
fileItem.percent = 0
|
||||
|
||||
// 创建取消令牌
|
||||
const source = axios.CancelToken.source()
|
||||
cancelTokens.value[fileItem.uid] = source
|
||||
|
||||
// 调用API
|
||||
const result = await uploadFileApi(
|
||||
realFile,
|
||||
Number(uploadForm.folderId),
|
||||
(progressEvent) => {
|
||||
if (progressEvent.lengthComputable) {
|
||||
fileItem.percent = Math.round((progressEvent.loaded / progressEvent.total) * 100)
|
||||
}
|
||||
},
|
||||
source.token
|
||||
)
|
||||
|
||||
// 检查上传结果
|
||||
if (result.code === 200) {
|
||||
fileItem.status = 'success'
|
||||
fileItem.percent = 100
|
||||
} else if (result.code === 400 && result.msg && result.msg.includes('已存在')) {
|
||||
// 文件已存在的情况
|
||||
fileItem.status = 'error'
|
||||
fileItem.error = '文件已存在'
|
||||
hasFileExists = true
|
||||
} else {
|
||||
fileItem.status = 'error'
|
||||
fileItem.error = result.msg || '上传失败'
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
|
||||
// 根据结果显示相应的消息
|
||||
if (hasFileExists && !hasError) {
|
||||
Message.warning('文件已存在')
|
||||
} else if (hasError) {
|
||||
Message.error('上传失败')
|
||||
} else {
|
||||
Message.success('上传成功')
|
||||
emit('upload-success')
|
||||
}
|
||||
|
||||
handleCancel()
|
||||
}
|
||||
|
||||
// 取消上传
|
||||
const handleCancel = () => {
|
||||
// 取消所有正在进行的上传
|
||||
Object.values(cancelTokens.value).forEach(source => {
|
||||
source.cancel('上传已取消')
|
||||
})
|
||||
|
||||
// 重置所有状态
|
||||
emit('update:visible', false)
|
||||
uploadForm.folderId = props.currentFolderId || ''
|
||||
fileListTemp.value = []
|
||||
cancelTokens.value = {}
|
||||
uploading.value = false
|
||||
|
||||
// 清空上传组件
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.reset()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 上传文件相关样式 */
|
||||
.upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
align-self: flex-start;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
padding: 8px 12px;
|
||||
background: var(--color-fill-2);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--color-primary-light-3);
|
||||
}
|
||||
|
||||
.upload-file-list {
|
||||
margin-top: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-1);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.upload-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
|
||||
&.file-error {
|
||||
background: rgba(255, 77, 79, 0.05);
|
||||
border-left: 3px solid #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #ff4d4f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-progress {
|
||||
margin: 0 16px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
color: var(--color-text-3);
|
||||
|
||||
&:hover {
|
||||
color: #ff4d4f;
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
color: var(--color-text-3);
|
||||
|
||||
&:hover {
|
||||
color: #faad14;
|
||||
background: rgba(250, 173, 20, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
|
@ -73,18 +73,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="props" tap="形变" title="形变原数据">
|
||||
<div class="tab-content">
|
||||
<raw-data>
|
||||
|
||||
</raw-data>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 文件预览模态框(待重新设计) -->
|
||||
<!-- <PreviewModal ref="previewModal" /> -->
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
|
@ -107,8 +97,6 @@ import {
|
|||
} from '@/apis/industrial-image'
|
||||
import DeformationTap from './components/DeformationTap.vue'
|
||||
|
||||
// 预览弹窗引用(待重新设计)
|
||||
// const previewModal = ref()
|
||||
// 活动选项卡
|
||||
const activeTab = ref('image')
|
||||
|
||||
|
|
|
@ -3,22 +3,22 @@ import { computed } from 'vue'
|
|||
import WindTurbine from './icons/WindTurbine.vue'
|
||||
|
||||
interface Turbine {
|
||||
id: number
|
||||
turbineNo: string
|
||||
status: 0 | 1 | 2 // 0 待施工 1 施工中 2 已完成
|
||||
lat?: number
|
||||
lng?: number
|
||||
id: number
|
||||
turbineNo: string
|
||||
status: 0 | 1 | 2 // 0 待施工 1 施工中 2 已完成
|
||||
lat?: number
|
||||
lng?: number
|
||||
}
|
||||
|
||||
const props = defineProps<{ modelValue: Turbine }>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: Turbine): void
|
||||
(e: 'map'): void
|
||||
(e: 'update:modelValue', v: Turbine): void
|
||||
(e: 'map'): void
|
||||
}>()
|
||||
|
||||
const turbine = computed({
|
||||
get: () => props.modelValue,
|
||||
set: v => emit('update:modelValue', v)
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v),
|
||||
})
|
||||
|
||||
/* 状态文字 & 颜色 */
|
||||
|
@ -27,34 +27,36 @@ const statusColorMap = { 0: '#FF7D00', 1: '#165DFF', 2: '#00B42A' }
|
|||
|
||||
/* 点击循环切换 */
|
||||
function toggleStatus() {
|
||||
const next = ((turbine.value.status + 1) % 3) as 0 | 1 | 2
|
||||
turbine.value = { ...turbine.value, status: next }
|
||||
const next = ((turbine.value.status + 1) % 3) as 0 | 1 | 2
|
||||
turbine.value = { ...turbine.value, status: next }
|
||||
}
|
||||
|
||||
function updateNo(val: string) {
|
||||
turbine.value = { ...turbine.value, turbineNo: val }
|
||||
turbine.value = { ...turbine.value, turbineNo: val }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="turbine-card">
|
||||
<!-- 可点击的状态标签 -->
|
||||
<div class="status-tag" :style="{ backgroundColor: statusColorMap[turbine.status] }" @click="toggleStatus">
|
||||
{{ statusTextMap[turbine.status] }}
|
||||
</div>
|
||||
|
||||
<!-- 风机图标 -->
|
||||
<WindTurbine />
|
||||
|
||||
<!-- 机组编号输入框 -->
|
||||
<a-input :model-value="turbine.turbineNo" @update:model-value="updateNo" size="small" class="turbine-input"
|
||||
placeholder="编号" />
|
||||
|
||||
<!-- 地图选点按钮 -->
|
||||
<a-button size="mini" @click="$emit('map')">
|
||||
<template #icon><icon-location /></template>
|
||||
</a-button>
|
||||
<div class="turbine-card">
|
||||
<!-- 可点击的状态标签 -->
|
||||
<div class="status-tag" :style="{ backgroundColor: statusColorMap[turbine.status] }" @click="toggleStatus">
|
||||
{{ statusTextMap[turbine.status] }}
|
||||
</div>
|
||||
|
||||
<!-- 风机图标 -->
|
||||
<WindTurbine />
|
||||
|
||||
<!-- 机组编号输入框 -->
|
||||
<a-input
|
||||
:model-value="turbine.turbineNo" size="small" class="turbine-input" placeholder="编号"
|
||||
@update:model-value="updateNo"
|
||||
/>
|
||||
|
||||
<!-- 地图选点按钮 -->
|
||||
<a-button size="mini" @click="$emit('map')">
|
||||
<template #icon><icon-location /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
@ -85,4 +87,4 @@ function updateNo(val: string) {
|
|||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
<template #icon><icon-arrow-left /></template>
|
||||
</a-button>
|
||||
<h2 class="ml-2">{{ projectTitle }}</h2>
|
||||
<a-tag class="ml-2" :color="getStatusColor(projectData.status)" v-if="projectData.status">{{
|
||||
projectData.status
|
||||
}}</a-tag>
|
||||
<a-tag class="ml-2" :color="getStatusColor(projectData.statusLabel ?? projectData.status)" v-if="projectData.status !== undefined && projectData.status !== null">
|
||||
{{ projectData.statusLabel ?? projectData.status }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<a-button v-permission="['project:update']" type="primary" class="mr-2" @click="editProject">
|
||||
|
@ -198,8 +198,8 @@
|
|||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { getProject, deleteProject } from '@/apis/project'
|
||||
import { addTask, addTaskGroup, listTask, updateTaskProgress } from '@/apis/project/task'
|
||||
import { getProjectDetail, deleteProject } from '@/apis/project'
|
||||
import { addTask, addTaskGroup, updateTaskProgress } from '@/apis/project/task'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
defineOptions({ name: 'ProjectDetail' })
|
||||
|
@ -257,10 +257,15 @@ const projectTitle = computed(() => {
|
|||
|
||||
const projectInfos = computed(() => [
|
||||
{ label: '项目编号', value: projectData.value?.projectCode },
|
||||
{ label: '项目负责人', value: projectData.value?.projectManager },
|
||||
{ label: '参与人', value: projectData.value?.projectStaff?.join(', ') },
|
||||
{ label: '项目周期', value: projectData.value?.projectPeriod ? `${projectData.value.projectPeriod[0]} 至 ${projectData.value.projectPeriod[1]}` : '' },
|
||||
{ label: '客户', value: projectData.value?.commissionUnit },
|
||||
{ label: '项目负责人', value: projectData.value?.projectManagerName || projectData.value?.projectManager },
|
||||
{ label: '项目来源', value: projectData.value?.projectOrigin },
|
||||
{ label: '风场名称', value: projectData.value?.farmName },
|
||||
{ label: '风场地址', value: projectData.value?.farmAddress },
|
||||
{ label: '开始时间', value: projectData.value?.startDate },
|
||||
{ label: '结束时间', value: projectData.value?.endDate },
|
||||
{ label: '项目规模', value: projectData.value?.scale },
|
||||
{ label: '状态', value: (statusMap as any)[Number(projectData.value?.status)]?.label || projectData.value?.statusLabel },
|
||||
{ label: '客户', value: projectData.value?.client },
|
||||
{ label: '备注', value: projectData.value?.projectIntro || '无' }
|
||||
])
|
||||
|
||||
|
@ -284,6 +289,15 @@ const taskDetailInfos = computed(() => {
|
|||
{ label: '状态', value: currentTask.value.status },
|
||||
{ label: '描述', value: currentTask.value.description || '无' }
|
||||
]
|
||||
|
||||
const statusMap: Record<number, { label: string; color: string }> = {
|
||||
0: { label: '待施工', color: 'gray' },
|
||||
1: { label: '施工中', color: 'blue' },
|
||||
2: { label: '已完工', color: 'green' },
|
||||
3: { label: '已审核', color: 'orange' },
|
||||
4: { label: '已验收', color: 'arcoblue' },
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
|
@ -322,8 +336,14 @@ const formatDate = (date: string) => {
|
|||
const fetchProjectData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProject(projectId.value)
|
||||
projectData.value = res.data
|
||||
const res = await getProjectDetail(projectId.value)
|
||||
const detail = (res as any).data || res
|
||||
// 如果status是数字,补充statusLabel用于页面展示
|
||||
if (typeof detail.status === 'number' && !detail.statusLabel) {
|
||||
const mapper = (statusMap as any)[detail.status]
|
||||
if (mapper) detail.statusLabel = mapper.label
|
||||
}
|
||||
projectData.value = detail
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
Message.error('获取项目详情失败')
|
||||
|
@ -332,34 +352,39 @@ const fetchProjectData = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const fetchTaskData = async () => {
|
||||
try {
|
||||
const res = await listTask({
|
||||
projectId: projectId.value,
|
||||
page: 1,
|
||||
size: 100
|
||||
})
|
||||
// 若任务未返回状态,依据计划时间简单推断状态
|
||||
const inferTaskStatus = (task: any): string => {
|
||||
if (task.status) return task.status
|
||||
const now = dayjs()
|
||||
const start = task.planStartDate ? dayjs(task.planStartDate) : null
|
||||
const end = task.planEndDate ? dayjs(task.planEndDate) : null
|
||||
if (end && end.isBefore(now)) return '已完成'
|
||||
if (start && start.isAfter(now)) return '计划中'
|
||||
if (start && (!end || end.isAfter(now))) return '正在做'
|
||||
return '其他'
|
||||
}
|
||||
|
||||
// 重置任务列表
|
||||
taskColumns.value.forEach(column => {
|
||||
column.tasks = []
|
||||
})
|
||||
|
||||
const tasks = res.data?.list || []
|
||||
const fetchTaskData = () => {
|
||||
// 使用详情接口返回的任务列表
|
||||
const detail = projectData.value || {}
|
||||
const tasks = (detail.tasks || []) as any[]
|
||||
|
||||
// 分配任务到对应的列
|
||||
tasks.forEach((task: any) => {
|
||||
const column = taskColumns.value.find(col => col.status === task.status)
|
||||
if (column) {
|
||||
column.tasks.push(task)
|
||||
} else {
|
||||
taskColumns.value.find(col => col.status === '其他')?.tasks.push(task)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
Message.error('获取任务数据失败')
|
||||
}
|
||||
// 重置任务列表
|
||||
taskColumns.value.forEach(column => {
|
||||
column.tasks = []
|
||||
})
|
||||
|
||||
// 分配任务到对应的列(按状态或推断状态)
|
||||
tasks.forEach((task: any) => {
|
||||
const st = inferTaskStatus(task)
|
||||
const column = taskColumns.value.find(col => col.status === st)
|
||||
if (column) {
|
||||
column.tasks.push(task)
|
||||
} else {
|
||||
taskColumns.value.find(col => col.status === '其他')?.tasks.push(task)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
|
@ -491,8 +516,7 @@ const submitProgressUpdate = async () => {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjectData()
|
||||
fetchTaskData()
|
||||
fetchProjectData().then(() => fetchTaskData())
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -332,6 +332,249 @@
|
|||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 编辑项目弹窗(与新增分离) -->
|
||||
<a-modal
|
||||
v-model:visible="editModalVisible"
|
||||
title="编辑项目"
|
||||
:ok-button-props="{ loading: submitLoading }"
|
||||
width="800px"
|
||||
modal-class="project-form-modal"
|
||||
@cancel="() => { editModalVisible.value = false }"
|
||||
@ok="handleEditSubmit"
|
||||
>
|
||||
<a-form
|
||||
ref="editFormRef"
|
||||
:model="editForm"
|
||||
:rules="formRules"
|
||||
layout="vertical"
|
||||
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"
|
||||
>
|
||||
<a-divider orientation="left">基本信息</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="projectName" label="项目名称" required>
|
||||
<a-input v-model="editForm.projectName" placeholder="请输入项目名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="farmAddress" label="地址">
|
||||
<a-input v-model="editForm.farmAddress" placeholder="请输入地址" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="projectManagerId" label="项目经理" required>
|
||||
<a-select v-model="editForm.projectManagerId" placeholder="请选择项目经理" :loading="userLoading">
|
||||
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
|
||||
{{ user.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="inspectionUnit" label="业主">
|
||||
<a-input v-model="editForm.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (editForm.farmName = val)" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="inspectionContact" label="业主单位联系人">
|
||||
<a-input v-model="editForm.inspectionContact" placeholder="请输入联系人" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="inspectionPhone" label="业主单位联系电话">
|
||||
<a-input v-model="editForm.inspectionPhone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="client" label="委托单位">
|
||||
<a-input v-model="editForm.client" placeholder="请输入委托单位" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="clientContact" label="委托单位联系人">
|
||||
<a-input v-model="editForm.clientContact" placeholder="请输入联系人" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="clientPhone" label="委托单位联系电话">
|
||||
<a-input v-model="editForm.clientPhone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="projectOrigin" label="项目来源" :rules="[{ required: true, message: '请输入项目来源' }]">
|
||||
<a-input v-model="editForm.projectOrigin" placeholder="请输入项目来源" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-divider orientation="left">任务设置</a-divider>
|
||||
<div class="mb-2">
|
||||
<a-button type="dashed" size="small" @click="addEditTask">
|
||||
<template #icon><icon-plus /></template>
|
||||
新增任务
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-if="editForm.tasks.length === 0" class="text-gray-500 mb-2">暂无任务。</div>
|
||||
<a-space direction="vertical" fill>
|
||||
<a-card v-for="(task, tIndex) in editForm.tasks" :key="tIndex" size="small">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>任务 {{ tIndex + 1 }}</span>
|
||||
<a-space>
|
||||
<a-button size="mini" @click="addEditSubtask(tIndex)">新增子任务</a-button>
|
||||
<a-button size="mini" status="danger" @click="removeEditTask(tIndex)">删除</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.taskName`" label="任务名称" required>
|
||||
<a-input v-model="task.taskName" placeholder="请输入任务名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item :field="`tasks.${tIndex}.taskCode`" label="任务编号">
|
||||
<a-input v-model="task.taskCode" placeholder="编号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item :field="`tasks.${tIndex}.mainUserId`" label="负责人">
|
||||
<a-select v-model="task.mainUserId" placeholder="选择负责人" :loading="userLoading">
|
||||
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-form-item :field="`tasks.${tIndex}.scales`" label="工量">
|
||||
<a-input-number v-model="task.scales" :min="0" :max="9999" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.planStartDate`" label="计划开始">
|
||||
<a-date-picker v-model="task.planStartDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.planEndDate`" label="计划结束">
|
||||
<a-date-picker v-model="task.planEndDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.taskGroupId`" label="任务组">
|
||||
<a-input-number v-model="task.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 子任务 -->
|
||||
<div v-if="task.children && task.children.length">
|
||||
<a-divider orientation="left">子任务</a-divider>
|
||||
<a-card v-for="(sub, sIndex) in task.children" :key="sIndex" size="small" class="mb-2">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>子任务 {{ tIndex + 1 }}-{{ sIndex + 1 }}</span>
|
||||
<a-button size="mini" status="danger" @click="removeEditSubtask(tIndex, sIndex)">删除</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskName`" label="任务名称" required>
|
||||
<a-input v-model="sub.taskName" placeholder="请输入任务名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskCode`" label="任务编号">
|
||||
<a-input v-model="sub.taskCode" placeholder="编号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.mainUserId`" label="负责人">
|
||||
<a-select v-model="sub.mainUserId" placeholder="选择负责人" :loading="userLoading">
|
||||
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.scales`" label="工量">
|
||||
<a-input-number v-model="sub.scales" :min="0" :max="9999" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planStartDate`" label="计划开始">
|
||||
<a-date-picker v-model="sub.planStartDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planEndDate`" label="计划结束">
|
||||
<a-date-picker v-model="sub.planEndDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskGroupId`" label="任务组">
|
||||
<a-input-number v-model="sub.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-space>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="status" label="项目状态">
|
||||
<a-select v-model="editForm.status" placeholder="请选择状态">
|
||||
<a-option v-for="option in PROJECT_STATUS_OPTIONS" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="scale" label="项目规模">
|
||||
<a-input-number v-model="editForm.scale" placeholder="请输入项目规模" :min="0" :max="999" :step="1" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="startDate" label="开始时间">
|
||||
<a-date-picker v-model="editForm.startDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="endDate" label="结束时间">
|
||||
<a-date-picker v-model="editForm.endDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
|
||||
<!-- 导入项目弹窗 -->
|
||||
<a-modal v-model:visible="importModalVisible" title="导入文件" @cancel="handleCancelImport" @before-ok="handleImport">
|
||||
<div class="flex flex-col items-center justify-center p-8">
|
||||
|
@ -347,20 +590,136 @@
|
|||
</a-upload>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="detailVisible"
|
||||
title="项目详情"
|
||||
width="900px"
|
||||
:footer="false"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<a-spin :loading="detailLoading">
|
||||
<a-descriptions :column="2" bordered :label-style="{ width: '140px', fontWeight: 'bold' }">
|
||||
<a-descriptions-item label="项目名称" :span="2">{{ detailData.projectName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="项目编号">{{ detailData.projectCode || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag v-if="detailData.status !== undefined" :color="getStatusColor(detailData.status)">
|
||||
{{ PROJECT_STATUS_MAP[detailData.status] || detailData.statusLabel || '-' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="项目类型">{{ detailData.projectCategory || '-' }}</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="业主">{{ detailData.inspectionUnit || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="业主联系人">{{ detailData.inspectionContact || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="业主电话">{{ detailData.inspectionPhone || '-' }}</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="委托单位">{{ detailData.client || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="委托联系人">{{ detailData.clientContact || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="委托电话">{{ detailData.clientPhone || '-' }}</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="风场名称">{{ detailData.farmName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="风场地址">{{ detailData.farmAddress || '-' }}</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="项目经理">{{ detailData.projectManagerName || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="项目规模">{{ detailData.scale || detailData.projectScale || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="风机型号">{{ detailData.turbineModel || '-' }}</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="开始时间">{{ detailData.startDate || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="结束时间">{{ detailData.endDate || '-' }}</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="项目简介" :span="2">
|
||||
<a-typography-paragraph :ellipsis="{ rows: 3, expandable: true, collapseText: '收起', expandText: '展开' }">
|
||||
{{ detailData.projectIntro || '-' }}
|
||||
</a-typography-paragraph>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-divider orientation="left">任务列表</a-divider>
|
||||
<a-table
|
||||
:data="detailTasks"
|
||||
:columns="detailTaskColumns as any"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
/>
|
||||
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const editModalVisible = ref(false)
|
||||
const editFormRef = ref()
|
||||
|
||||
// 编辑表单
|
||||
const editForm = reactive({
|
||||
projectId: '',
|
||||
projectName: '',
|
||||
projectManagerId: '',
|
||||
client: '',
|
||||
clientContact: '',
|
||||
clientPhone: '',
|
||||
inspectionUnit: '',
|
||||
inspectionContact: '',
|
||||
inspectionPhone: '',
|
||||
farmName: '',
|
||||
farmAddress: '',
|
||||
projectOrigin: '',
|
||||
scale: '',
|
||||
turbineModel: '',
|
||||
status: 0,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
constructionTeamLeaderId: '',
|
||||
constructorIds: '',
|
||||
qualityOfficerId: '',
|
||||
auditorId: '',
|
||||
tasks: [] as Array<{
|
||||
taskName: string
|
||||
taskCode?: string
|
||||
mainUserId?: string | number
|
||||
planStartDate?: string
|
||||
planEndDate?: string
|
||||
scales?: number
|
||||
taskGroupId?: number | string
|
||||
children?: Array<{
|
||||
taskName: string
|
||||
taskCode?: string
|
||||
mainUserId?: string | number
|
||||
planStartDate?: string
|
||||
planEndDate?: string
|
||||
scales?: number
|
||||
taskGroupId?: number | string
|
||||
}>
|
||||
}>,
|
||||
turbineList: [] as { id: number, turbineNo: string, lat?: number, lng?: number, status: 0 | 1 | 2 }[],
|
||||
})
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import TurbineGrid from './TurbineGrid.vue'
|
||||
import { addProject, deleteProject, exportProject, importProject, listProject, updateProject } from '@/apis/project'
|
||||
import { addProject, deleteProject, exportProject, importProject, listProject, updateProject, getProjectDetail } from '@/apis/project'
|
||||
import { isMobile } from '@/utils'
|
||||
import http from '@/utils/http'
|
||||
import type { ColumnItem } from '@/components/GiForm'
|
||||
import type { ProjectPageQuery } from '@/apis/project/type'
|
||||
|
||||
// 详情任务表格
|
||||
const detailTasks = ref<any[]>([])
|
||||
const detailTaskColumns = [
|
||||
{ title: '任务名称', dataIndex: 'taskName', ellipsis: true, tooltip: true },
|
||||
{ title: '负责人', dataIndex: 'mainUserName', width: 120 },
|
||||
{ title: '计划开始', dataIndex: 'planStartDate', width: 120 },
|
||||
{ title: '计划结束', dataIndex: 'planEndDate', width: 120 },
|
||||
{ title: '工量', dataIndex: 'scales', width: 80 },
|
||||
{ title: '状态', dataIndex: 'statusLabel', width: 100 },
|
||||
]
|
||||
|
||||
import type * as T from '@/apis/project/type'
|
||||
|
||||
defineOptions({ name: 'ProjectManagement' })
|
||||
|
@ -412,6 +771,12 @@ const dataList = ref<T.ProjectResp[]>([])
|
|||
const userLoading = ref(false)
|
||||
const userOptions = ref<{ label: string, value: string }[]>([])
|
||||
|
||||
|
||||
// 详情弹窗状态
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailData = ref<any>({})
|
||||
|
||||
const searchForm = reactive<Partial<ProjectPageQuery>>({
|
||||
projectName: '',
|
||||
status: undefined,
|
||||
|
@ -681,6 +1046,94 @@ const fetchData = async () => {
|
|||
}
|
||||
} else {
|
||||
Message.error(res.msg || '获取数据失败')
|
||||
|
||||
const handleEditSubmit = async () => {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
// 验证
|
||||
await editFormRef.value?.validate()
|
||||
|
||||
const normalizeDate = (d: any) => (d ? (typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0]) : '')
|
||||
const pickTaskFields = (t: any) => ({
|
||||
mainUserId: t.mainUserId ?? '',
|
||||
planEndDate: normalizeDate(t.planEndDate),
|
||||
planStartDate: normalizeDate(t.planStartDate),
|
||||
scales: t.scales ?? 0,
|
||||
taskCode: t.taskCode ?? '',
|
||||
taskGroupId: t.taskGroupId ?? '',
|
||||
taskName: t.taskName ?? '',
|
||||
})
|
||||
const flattenTasks = (tasks: any[]) => {
|
||||
const result: any[] = []
|
||||
;(tasks || []).forEach((t) => {
|
||||
result.push(pickTaskFields(t))
|
||||
;(t.children || []).forEach((c: any) => result.push(pickTaskFields(c)))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const submitData = {
|
||||
auditorId: (editForm as any).auditorId || '',
|
||||
bonusProvision: (editForm as any).bonusProvision ?? 0,
|
||||
client: editForm.client || '',
|
||||
clientContact: editForm.clientContact || '',
|
||||
clientPhone: editForm.clientPhone || '',
|
||||
constructionTeamLeaderId: editForm.constructionTeamLeaderId || '',
|
||||
constructorIds: Array.isArray(editForm.constructorIds) ? editForm.constructorIds.join(',') : (editForm.constructorIds || ''),
|
||||
coverUrl: (editForm as any).coverUrl || '',
|
||||
duration: (editForm as any).duration ?? 0,
|
||||
endDate: normalizeDate(editForm.endDate),
|
||||
equipmentAmortization: (editForm as any).equipmentAmortization ?? 0,
|
||||
farmAddress: editForm.farmAddress || '',
|
||||
farmName: editForm.farmName || '',
|
||||
inspectionContact: editForm.inspectionContact || '',
|
||||
inspectionPhone: editForm.inspectionPhone || '',
|
||||
inspectionUnit: editForm.inspectionUnit || '',
|
||||
laborCost: (editForm as any).laborCost ?? 0,
|
||||
othersCost: (editForm as any).othersCost ?? 0,
|
||||
projectBudget: (editForm as any).projectBudget ?? 0,
|
||||
projectId: editForm.projectId || currentId.value || '',
|
||||
projectManagerId: editForm.projectManagerId || '',
|
||||
projectName: editForm.projectName,
|
||||
projectOrigin: (editForm as any).projectOrigin || '',
|
||||
qualityOfficerId: editForm.qualityOfficerId || '',
|
||||
scale: editForm.scale || '',
|
||||
startDate: normalizeDate(editForm.startDate),
|
||||
status: (editForm as any).status ?? 0,
|
||||
tasks: flattenTasks(editForm.tasks as any[]),
|
||||
transAccomMeals: (editForm as any).transAccomMeals ?? 0,
|
||||
turbineModel: editForm.turbineModel || '',
|
||||
}
|
||||
|
||||
if (!currentId.value && submitData.projectId) currentId.value = submitData.projectId
|
||||
if (!currentId.value) {
|
||||
Message.error('缺少项目ID,无法更新')
|
||||
submitLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const res = await updateProject(submitData, currentId.value)
|
||||
if (res && res.success === false) {
|
||||
Message.error(res.msg || '更新失败')
|
||||
submitLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
Message.success('更新成功')
|
||||
editModalVisible.value = false
|
||||
fetchData()
|
||||
} catch (e: any) {
|
||||
if (e && e.type === 'form') {
|
||||
// 表单校验失败
|
||||
} else {
|
||||
console.error('编辑提交失败', e)
|
||||
Message.error(e?.message || '编辑失败')
|
||||
}
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
dataList.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
|
@ -757,7 +1210,7 @@ const resetForm = () => {
|
|||
|
||||
const openAddModal = () => {
|
||||
resetForm()
|
||||
addModalVisible.value = true
|
||||
editModalVisible.value = true
|
||||
}
|
||||
// 任务增删改(仅在新增/编辑弹窗内部使用)
|
||||
const addTask = () => {
|
||||
|
@ -777,35 +1230,77 @@ const removeSubtask = (parentIndex: number, index: number) => {
|
|||
}
|
||||
|
||||
|
||||
const openEditModal = (record: T.ProjectResp) => {
|
||||
const openEditModal = async (record: T.ProjectResp) => {
|
||||
isEdit.value = true
|
||||
currentId.value = record.id || record.projectId || null
|
||||
|
||||
// 重置表单为默认(确保 tasks、turbineList 等为数组初始值)
|
||||
resetForm()
|
||||
|
||||
// 填充表单数据
|
||||
Object.keys(form).forEach((key) => {
|
||||
if (key in record && record[key as keyof T.ProjectResp] !== undefined) {
|
||||
// @ts-expect-error - 这里需要处理类型转换
|
||||
form[key] = record[key as keyof T.ProjectResp]
|
||||
}
|
||||
// 重置编辑表单
|
||||
Object.assign(editForm, {
|
||||
projectId: '', projectName: '', projectManagerId: '', client: '', clientContact: '', clientPhone: '',
|
||||
inspectionUnit: '', inspectionContact: '', inspectionPhone: '', farmName: '', farmAddress: '', projectOrigin: '',
|
||||
scale: '', turbineModel: '', status: 0, startDate: '', endDate: '', constructionTeamLeaderId: '', constructorIds: '',
|
||||
qualityOfficerId: '', auditorId: '', tasks: [], turbineList: [],
|
||||
})
|
||||
|
||||
// 处理特殊字段映射
|
||||
if (record.farmName) form.farmName = record.farmName
|
||||
if (record.farmAddress) form.farmAddress = record.farmAddress
|
||||
if (record.client) form.client = record.client
|
||||
if (record.clientContact) form.clientContact = record.clientContact
|
||||
if (record.clientPhone) form.clientPhone = record.clientPhone
|
||||
if (record.turbineModel) form.turbineModel = record.turbineModel
|
||||
if (record.scale) form.scale = record.scale
|
||||
try {
|
||||
if (currentId.value) {
|
||||
const res = await getProjectDetail(currentId.value as any)
|
||||
const detail = (res as any).data || res || {}
|
||||
|
||||
// 处理日期字段
|
||||
if (record.startDate) form.startDate = record.startDate
|
||||
if (record.endDate) form.endDate = record.endDate
|
||||
// 将详情基础字段回填到编辑表单
|
||||
Object.keys(editForm).forEach((key) => {
|
||||
if (key in detail && (detail as any)[key] !== undefined) {
|
||||
// @ts-expect-error 动态赋值
|
||||
editForm[key] = (detail as any)[key]
|
||||
}
|
||||
})
|
||||
|
||||
addModalVisible.value = true
|
||||
// 别名字段
|
||||
editForm.farmName = detail.farmName || record.farmName || editForm.farmName
|
||||
editForm.farmAddress = detail.farmAddress || record.farmAddress || editForm.farmAddress
|
||||
editForm.client = detail.client || record.client || editForm.client
|
||||
editForm.clientContact = detail.clientContact || record.clientContact || editForm.clientContact
|
||||
editForm.clientPhone = detail.clientPhone || record.clientPhone || editForm.clientPhone
|
||||
editForm.turbineModel = detail.turbineModel || record.turbineModel || editForm.turbineModel
|
||||
editForm.scale = detail.scale || record.scale || editForm.scale
|
||||
editForm.startDate = detail.startDate || record.startDate || editForm.startDate
|
||||
editForm.endDate = detail.endDate || record.endDate || editForm.endDate
|
||||
|
||||
// 任务映射
|
||||
const mapTask = (t: any): any => ({
|
||||
taskName: t.taskName ?? t.name ?? '',
|
||||
taskCode: t.taskCode ?? t.code ?? '',
|
||||
mainUserId: t.mainUserId ?? t.mainUser?.id ?? t.mainUser ?? t.ownerId ?? '',
|
||||
planStartDate: t.planStartDate ?? t.startDate ?? '',
|
||||
planEndDate: t.planEndDate ?? t.endDate ?? '',
|
||||
scales: t.scales ?? t.workload ?? 0,
|
||||
taskGroupId: t.taskGroupId ?? t.groupId ?? '',
|
||||
children: Array.isArray(t.children) ? t.children.map(mapTask) : [],
|
||||
})
|
||||
|
||||
const tasksSource: any[] = Array.isArray(detail.tasks)
|
||||
? detail.tasks
|
||||
: (Array.isArray((detail as any).taskList) ? (detail as any).taskList : [])
|
||||
if (Array.isArray(tasksSource) && tasksSource.length) {
|
||||
;(editForm.tasks as any[]) = tasksSource.map(mapTask)
|
||||
}
|
||||
|
||||
// projectId 保证存在
|
||||
editForm.projectId = (detail.projectId ?? currentId.value ?? record.projectId ?? '').toString()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取项目详情失败', e)
|
||||
// 回退:至少使用列表字段
|
||||
Object.keys(editForm).forEach((key) => {
|
||||
if (key in record && (record as any)[key] !== undefined) {
|
||||
// @ts-expect-error 动态赋值
|
||||
editForm[key] = (record as any)[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 打开编辑弹窗(与新增分离)
|
||||
editModalVisible.value = true
|
||||
}
|
||||
|
||||
// 添加表单验证规则
|
||||
|
@ -842,17 +1337,55 @@ const handleSubmit = async () => {
|
|||
}))
|
||||
}))
|
||||
|
||||
const pickTaskFields = (t: any) => ({
|
||||
mainUserId: t.mainUserId ?? '',
|
||||
planEndDate: normalizeDate(t.planEndDate),
|
||||
planStartDate: normalizeDate(t.planStartDate),
|
||||
scales: t.scales ?? 0,
|
||||
taskCode: t.taskCode ?? '',
|
||||
taskGroupId: t.taskGroupId ?? '',
|
||||
taskName: t.taskName ?? '',
|
||||
})
|
||||
const flattenTasks = (tasks: any[]) => {
|
||||
const result: any[] = []
|
||||
;(tasks || []).forEach((t) => {
|
||||
result.push(pickTaskFields(t))
|
||||
;(t.children || []).forEach((c: any) => result.push(pickTaskFields(c)))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const submitData = {
|
||||
...form,
|
||||
// 确保projectId字段正确
|
||||
projectId: isEdit.value && currentId.value ? currentId.value : form.projectId,
|
||||
// 处理日期格式 - 确保是字符串格式 YYYY-MM-DD
|
||||
startDate: normalizeDate(form.startDate),
|
||||
auditorId: (form as any).auditorId || '',
|
||||
bonusProvision: (form as any).bonusProvision ?? 0,
|
||||
client: form.client || '',
|
||||
clientContact: form.clientContact || '',
|
||||
clientPhone: form.clientPhone || '',
|
||||
constructionTeamLeaderId: form.constructionTeamLeaderId || '',
|
||||
constructorIds: Array.isArray(form.constructorIds) ? form.constructorIds.join(',') : (form.constructorIds || ''),
|
||||
coverUrl: (form as any).coverUrl || '',
|
||||
duration: (form as any).duration ?? 0,
|
||||
endDate: normalizeDate(form.endDate),
|
||||
// 处理施工人员ID - 如果是数组,转换为逗号分隔的字符串
|
||||
constructorIds: Array.isArray(form.constructorIds) ? form.constructorIds.join(',') : form.constructorIds,
|
||||
// 新字段:任务
|
||||
tasks: mapTasks(form.tasks as any[]),
|
||||
equipmentAmortization: (form as any).equipmentAmortization ?? 0,
|
||||
farmAddress: form.farmAddress || '',
|
||||
farmName: form.farmName || '',
|
||||
inspectionContact: form.inspectionContact || '',
|
||||
inspectionPhone: form.inspectionPhone || '',
|
||||
inspectionUnit: form.inspectionUnit || '',
|
||||
laborCost: (form as any).laborCost ?? 0,
|
||||
othersCost: (form as any).othersCost ?? 0,
|
||||
projectBudget: (form as any).projectBudget ?? 0,
|
||||
projectId: isEdit.value && currentId.value ? currentId.value : form.projectId,
|
||||
projectManagerId: form.projectManagerId || '',
|
||||
projectName: form.projectName,
|
||||
projectOrigin: (form as any).projectOrigin || '',
|
||||
qualityOfficerId: form.qualityOfficerId || '',
|
||||
scale: form.scale || '',
|
||||
startDate: normalizeDate(form.startDate),
|
||||
status: (form as any).status ?? 0,
|
||||
tasks: flattenTasks(form.tasks as any[]),
|
||||
transAccomMeals: (form as any).transAccomMeals ?? 0,
|
||||
turbineModel: form.turbineModel || '',
|
||||
}
|
||||
|
||||
console.log('提交数据:', submitData)
|
||||
|
@ -927,19 +1460,52 @@ const deleteItem = async (record: T.ProjectResp) => {
|
|||
}
|
||||
}
|
||||
|
||||
const viewDetail = (record: T.ProjectResp) => {
|
||||
const viewDetail = async (record: T.ProjectResp) => {
|
||||
const projectId = record.id || record.projectId
|
||||
if (!projectId) {
|
||||
Message.error('项目ID不存在')
|
||||
return
|
||||
}
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
try {
|
||||
// /project/detail/{projectId}
|
||||
const res = await getProjectDetail(projectId)
|
||||
const data = (res as any).data || res || {}
|
||||
// 若后端返回status为数字,补充状态文案
|
||||
if (typeof data.status === 'number' && !data.statusLabel) {
|
||||
data.statusLabel = PROJECT_STATUS_MAP[data.status]
|
||||
}
|
||||
detailData.value = data
|
||||
|
||||
router.push({
|
||||
name: 'ProjectDetail',
|
||||
params: {
|
||||
id: projectId.toString(),
|
||||
},
|
||||
})
|
||||
// 处理任务列表(支持后端直接返回 tasks 或 taskList)
|
||||
const rawTasks: any[] = Array.isArray(data.tasks) ? data.tasks : (Array.isArray(data.taskList) ? data.taskList : [])
|
||||
// 扁平化(包含子任务)并做字段兜底
|
||||
const flatten = (tasks: any[]): any[] => {
|
||||
const result: any[] = []
|
||||
;(tasks || []).forEach(t => {
|
||||
result.push({
|
||||
taskName: t.taskName ?? '-',
|
||||
mainUserName: t.mainUserName ?? t.mainUserId ?? '-',
|
||||
planStartDate: t.planStartDate ?? '',
|
||||
planEndDate: t.planEndDate ?? '',
|
||||
scales: t.scales ?? '',
|
||||
status: t.status,
|
||||
statusLabel: t.statusLabel ?? (typeof t.status === 'number' ? PROJECT_STATUS_MAP[t.status as 0|1|2] : t.status ?? '-')
|
||||
})
|
||||
if (Array.isArray(t.children) && t.children.length) {
|
||||
result.push(...flatten(t.children))
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
detailTasks.value = flatten(rawTasks)
|
||||
} catch (e) {
|
||||
console.error('获取项目详情失败:', e)
|
||||
Message.error('获取项目详情失败')
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openImportModal = () => {
|
||||
|
|
|
@ -0,0 +1,534 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="合同发票管理"
|
||||
width="800px"
|
||||
:footer="false"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<div class="contract-invoice-container">
|
||||
<!-- 设备基本信息 -->
|
||||
<a-card title="设备信息" class="info-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="设备名称">
|
||||
{{ equipmentData?.equipmentName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备型号">
|
||||
{{ equipmentData?.equipmentModel }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商">
|
||||
{{ equipmentData?.supplierName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购价格">
|
||||
¥{{ formatPrice(equipmentData?.purchasePrice || 0) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 合同信息 -->
|
||||
<a-card title="合同信息" class="info-card" :bordered="false">
|
||||
<a-form
|
||||
ref="contractFormRef"
|
||||
:model="contractForm"
|
||||
:rules="contractRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="合同编号" name="contractNumber">
|
||||
<a-input
|
||||
v-model="contractForm.contractNumber"
|
||||
placeholder="请输入合同编号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="合同签订日期" name="contractDate">
|
||||
<a-date-picker
|
||||
v-model="contractForm.contractDate"
|
||||
placeholder="请选择合同签订日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="合同金额" name="contractAmount">
|
||||
<a-input-number
|
||||
v-model="contractForm.contractAmount"
|
||||
placeholder="请输入合同金额"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="合同状态" name="contractStatus">
|
||||
<a-select
|
||||
v-model="contractForm.contractStatus"
|
||||
placeholder="请选择合同状态"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="DRAFT">草稿</a-option>
|
||||
<a-option value="SIGNED">已签订</a-option>
|
||||
<a-option value="EXECUTING">执行中</a-option>
|
||||
<a-option value="COMPLETED">已完成</a-option>
|
||||
<a-option value="TERMINATED">已终止</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="合同文件" name="contractFile">
|
||||
<a-upload
|
||||
v-model:file-list="contractForm.contractFile"
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="onContractUploadSuccess"
|
||||
:on-error="onUploadError"
|
||||
accept=".pdf,.doc,.docx"
|
||||
:max-count="1"
|
||||
>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传合同文件
|
||||
</a-button>
|
||||
<template #itemRender="{ file }">
|
||||
<a-space>
|
||||
<IconFile />
|
||||
<span>{{ file.name }}</span>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
status="danger"
|
||||
@click="removeContractFile(file)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
<a-form-item label="合同备注" name="contractRemark">
|
||||
<a-textarea
|
||||
v-model="contractForm.contractRemark"
|
||||
placeholder="请输入合同备注信息"
|
||||
:rows="3"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 发票信息 -->
|
||||
<a-card title="发票信息" class="info-card" :bordered="false">
|
||||
<a-form
|
||||
ref="invoiceFormRef"
|
||||
:model="invoiceForm"
|
||||
:rules="invoiceRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="发票号码" name="invoiceNumber">
|
||||
<a-input
|
||||
v-model="invoiceForm.invoiceNumber"
|
||||
placeholder="请输入发票号码"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="发票类型" name="invoiceType">
|
||||
<a-select
|
||||
v-model="invoiceForm.invoiceType"
|
||||
placeholder="请选择发票类型"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="VAT_SPECIAL">增值税专用发票</a-option>
|
||||
<a-option value="VAT_COMMON">增值税普通发票</a-option>
|
||||
<a-option value="ELECTRONIC">电子发票</a-option>
|
||||
<a-option value="OTHER">其他</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="发票金额" name="invoiceAmount">
|
||||
<a-input-number
|
||||
v-model="invoiceForm.invoiceAmount"
|
||||
placeholder="请输入发票金额"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="开票日期" name="invoiceDate">
|
||||
<a-date-picker
|
||||
v-model="invoiceForm.invoiceDate"
|
||||
placeholder="请选择开票日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="税率" name="taxRate">
|
||||
<a-input-number
|
||||
v-model="invoiceForm.taxRate"
|
||||
placeholder="请输入税率"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
addon-after="%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="税额" name="taxAmount">
|
||||
<a-input-number
|
||||
v-model="invoiceForm.taxAmount"
|
||||
placeholder="税额将自动计算"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
disabled
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="发票文件" name="invoiceFile">
|
||||
<a-upload
|
||||
v-model:file-list="invoiceForm.invoiceFile"
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="onInvoiceUploadSuccess"
|
||||
:on-error="onUploadError"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
:max-count="1"
|
||||
>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传发票文件
|
||||
</a-button>
|
||||
<template #itemRender="{ file }">
|
||||
<a-space>
|
||||
<IconFile />
|
||||
<span>{{ file.name }}</span>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
status="danger"
|
||||
@click="removeInvoiceFile(file)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
<a-form-item label="发票备注" name="invoiceRemark">
|
||||
<a-textarea
|
||||
v-model="invoiceForm.invoiceRemark"
|
||||
placeholder="请输入发票备注信息"
|
||||
:rows="3"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" @click="handleSave" :loading="saving">
|
||||
保存
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleComplete"
|
||||
:loading="saving"
|
||||
:disabled="!canComplete"
|
||||
>
|
||||
完成并进入收货阶段
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { IconUpload, IconFile } from '@arco-design/web-vue/es/icon'
|
||||
import message from '@arco-design/web-vue/es/message'
|
||||
import type { EquipmentResp } from '@/apis/equipment/type'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
equipmentData: EquipmentResp | null
|
||||
}
|
||||
|
||||
interface ContractForm {
|
||||
contractNumber: string
|
||||
contractDate: string | null
|
||||
contractAmount: number | null
|
||||
contractStatus: string
|
||||
contractFile: any[]
|
||||
contractRemark: string
|
||||
}
|
||||
|
||||
interface InvoiceForm {
|
||||
invoiceNumber: string
|
||||
invoiceType: string
|
||||
invoiceAmount: number | null
|
||||
invoiceDate: string | null
|
||||
taxRate: number | null
|
||||
taxAmount: number | null
|
||||
invoiceFile: any[]
|
||||
invoiceRemark: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
// 表单引用
|
||||
const contractFormRef = ref()
|
||||
const invoiceFormRef = ref()
|
||||
|
||||
// 保存状态
|
||||
const saving = ref(false)
|
||||
|
||||
// 合同表单
|
||||
const contractForm = reactive<ContractForm>({
|
||||
contractNumber: '',
|
||||
contractDate: null,
|
||||
contractAmount: null,
|
||||
contractStatus: 'DRAFT',
|
||||
contractFile: [],
|
||||
contractRemark: ''
|
||||
})
|
||||
|
||||
// 发票表单
|
||||
const invoiceForm = reactive<InvoiceForm>({
|
||||
invoiceNumber: '',
|
||||
invoiceType: '',
|
||||
invoiceAmount: null,
|
||||
invoiceDate: null,
|
||||
taxRate: null,
|
||||
taxAmount: null,
|
||||
invoiceFile: [],
|
||||
invoiceRemark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const contractRules = {
|
||||
contractNumber: [
|
||||
{ required: true, message: '请输入合同编号', trigger: 'blur' }
|
||||
],
|
||||
contractDate: [
|
||||
{ required: true, message: '请选择合同签订日期', trigger: 'change' }
|
||||
],
|
||||
contractAmount: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' }
|
||||
],
|
||||
contractStatus: [
|
||||
{ required: true, message: '请选择合同状态', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
const invoiceRules = {
|
||||
invoiceNumber: [
|
||||
{ required: true, message: '请输入发票号码', trigger: 'blur' }
|
||||
],
|
||||
invoiceType: [
|
||||
{ required: true, message: '请选择发票类型', trigger: 'change' }
|
||||
],
|
||||
invoiceAmount: [
|
||||
{ required: true, message: '请输入发票金额', trigger: 'blur' }
|
||||
],
|
||||
invoiceDate: [
|
||||
{ required: true, message: '请选择开票日期', trigger: 'change' }
|
||||
],
|
||||
taxRate: [
|
||||
{ required: true, message: '请输入税率', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 上传配置
|
||||
const uploadAction = '/api/file/upload'
|
||||
const uploadHeaders = {
|
||||
// 根据实际需求设置上传头
|
||||
}
|
||||
|
||||
// 计算是否可以完成
|
||||
const canComplete = computed(() => {
|
||||
return contractForm.contractStatus === 'SIGNED' &&
|
||||
invoiceForm.invoiceNumber &&
|
||||
invoiceForm.invoiceAmount
|
||||
})
|
||||
|
||||
// 监听发票金额和税率变化,自动计算税额
|
||||
watch([() => invoiceForm.invoiceAmount, () => invoiceForm.taxRate], ([amount, rate]) => {
|
||||
if (amount && rate) {
|
||||
invoiceForm.taxAmount = Number((amount * rate / 100).toFixed(2))
|
||||
} else {
|
||||
invoiceForm.taxAmount = null
|
||||
}
|
||||
})
|
||||
|
||||
// 格式化价格
|
||||
const formatPrice = (price: number) => {
|
||||
return price.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
}
|
||||
|
||||
// 上传前检查
|
||||
const beforeUpload = (file: File) => {
|
||||
const isValidSize = file.size / 1024 / 1024 < 10 // 10MB限制
|
||||
if (!isValidSize) {
|
||||
message.error('文件大小不能超过10MB')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 合同文件上传成功
|
||||
const onContractUploadSuccess = (response: any, file: any) => {
|
||||
console.log('合同文件上传成功:', response, file)
|
||||
message.success('合同文件上传成功')
|
||||
}
|
||||
|
||||
// 发票文件上传成功
|
||||
const onInvoiceUploadSuccess = (response: any, file: any) => {
|
||||
console.log('发票文件上传成功:', response, file)
|
||||
message.success('发票文件上传成功')
|
||||
}
|
||||
|
||||
// 上传失败
|
||||
const onUploadError = (error: any) => {
|
||||
console.error('文件上传失败:', error)
|
||||
message.error('文件上传失败')
|
||||
}
|
||||
|
||||
// 删除合同文件
|
||||
const removeContractFile = (file: any) => {
|
||||
const index = contractForm.contractFile.findIndex(f => f.uid === file.uid)
|
||||
if (index > -1) {
|
||||
contractForm.contractFile.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除发票文件
|
||||
const removeInvoiceFile = (file: any) => {
|
||||
const index = invoiceForm.invoiceFile.findIndex(f => f.uid === file.uid)
|
||||
if (index > -1) {
|
||||
invoiceForm.invoiceFile.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理 visible 变化
|
||||
const handleVisibleChange = (newVisible: boolean) => {
|
||||
emit('update:visible', newVisible)
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
saving.value = true
|
||||
|
||||
// 验证合同表单
|
||||
await contractFormRef.value?.validate()
|
||||
|
||||
// 验证发票表单
|
||||
await invoiceFormRef.value?.validate()
|
||||
|
||||
// 这里调用API保存数据
|
||||
console.log('保存合同发票信息:', {
|
||||
contract: contractForm,
|
||||
invoice: invoiceForm,
|
||||
equipmentId: props.equipmentData?.equipmentId
|
||||
})
|
||||
|
||||
message.success('保存成功')
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
message.error('保存失败,请检查表单信息')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 完成并进入收货阶段
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
saving.value = true
|
||||
|
||||
// 验证表单
|
||||
await contractFormRef.value?.validate()
|
||||
await invoiceFormRef.value?.validate()
|
||||
|
||||
// 这里调用API完成合同发票管理
|
||||
console.log('完成合同发票管理:', {
|
||||
contract: contractForm,
|
||||
invoice: invoiceForm,
|
||||
equipmentId: props.equipmentData?.equipmentId
|
||||
})
|
||||
|
||||
message.success('合同发票管理完成,可以进入收货阶段')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
message.error('操作失败,请检查表单信息')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.contract-invoice-container {
|
||||
.info-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,474 +1,380 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="设备付款"
|
||||
width="900px"
|
||||
:confirm-loading="loading"
|
||||
title="付款申请"
|
||||
width="800px"
|
||||
:footer="false"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleSubmit"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
layout="vertical"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<a-card title="设备信息" class="detail-card" :bordered="false">
|
||||
<div class="payment-application-container">
|
||||
<!-- 设备基本信息 -->
|
||||
<a-card title="设备信息" class="info-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="设备名称">
|
||||
{{ equipmentData?.equipmentName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备类型">
|
||||
{{ equipmentData?.equipmentType || '-' }}
|
||||
{{ equipmentData?.equipmentName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备型号">
|
||||
{{ equipmentData?.equipmentModel || '-' }}
|
||||
{{ equipmentData?.equipmentModel }}
|
||||
</a-descriptions-item>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="品牌">
|
||||
{{ equipmentData?.brand || '-' }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-descriptions-item label="供应商">
|
||||
{{ equipmentData?.supplierName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购订单">
|
||||
{{ equipmentData?.purchaseOrder || '-' }}
|
||||
{{ equipmentData?.supplierName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购价格">
|
||||
¥{{ equipmentData?.purchasePrice || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购数量">
|
||||
{{ equipmentData?.quantity || '-' }}
|
||||
¥{{ formatPrice(equipmentData?.purchasePrice || 0) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 支付信息 -->
|
||||
<a-card title="支付信息" class="detail-card" :bordered="false">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="支付方式" field="paymentMethod" required>
|
||||
<a-select
|
||||
v-model="formData.paymentMethod"
|
||||
placeholder="请选择支付方式"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="银行转账">银行转账</a-option>
|
||||
<a-option value="现金">现金</a-option>
|
||||
<a-option value="支票">支票</a-option>
|
||||
<a-option value="信用卡">信用卡</a-option>
|
||||
<a-option value="其他">其他</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="支付金额" field="paymentAmount" required>
|
||||
<a-input-number
|
||||
v-model="formData.paymentAmount"
|
||||
placeholder="请输入支付金额"
|
||||
:min="0.01"
|
||||
:max="999999.99"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 付款申请信息 -->
|
||||
<a-card title="付款申请信息" class="info-card" :bordered="false">
|
||||
<a-form
|
||||
ref="paymentFormRef"
|
||||
:model="paymentForm"
|
||||
:rules="paymentRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="申请日期" name="applicationDate">
|
||||
<a-date-picker
|
||||
v-model="paymentForm.applicationDate"
|
||||
placeholder="请选择申请日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="部门" name="department">
|
||||
<a-input
|
||||
v-model="paymentForm.department"
|
||||
placeholder="请输入部门名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="支付时间" field="paymentTime" required>
|
||||
<a-date-picker
|
||||
v-model="formData.paymentTime"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择支付时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="支付人" field="paymentPerson" required>
|
||||
<a-input
|
||||
v-model="formData.paymentPerson"
|
||||
placeholder="请输入支付人姓名"
|
||||
show-word-limit
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="归属项目" name="projectName">
|
||||
<a-input
|
||||
v-model="paymentForm.projectName"
|
||||
placeholder="请输入项目名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="支出类别" name="expenseCategory">
|
||||
<a-select
|
||||
v-model="paymentForm.expenseCategory"
|
||||
placeholder="请选择支出类别"
|
||||
allow-clear
|
||||
>
|
||||
<a-option value="EXPENSE_TRANSPORT">a.费用支出-交通费</a-option>
|
||||
<a-option value="EXPENSE_ACCOMMODATION">a.费用支出-住宿费</a-option>
|
||||
<a-option value="EXPENSE_TOOLS">a.费用支出-小工具</a-option>
|
||||
<a-option value="EXPENSE_VEHICLE_MAINTENANCE">a.费用支出-车辆保养</a-option>
|
||||
<a-option value="PROCUREMENT_DRONE">b.采购支出-设备采购-无人机</a-option>
|
||||
<a-option value="PROCUREMENT_VEHICLE">b.采购支出-设备采购-车</a-option>
|
||||
<a-option value="PROCUREMENT_COMPUTER">b.采购支出-设备采购-计算机</a-option>
|
||||
<a-option value="SALARY">c.工资支出</a-option>
|
||||
<a-option value="TAX">d.税款支出</a-option>
|
||||
<a-option value="OTHER">f.其他支出</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="支付备注" field="paymentRemark">
|
||||
<a-textarea
|
||||
v-model="formData.paymentRemark"
|
||||
placeholder="请输入支付备注"
|
||||
:rows="3"
|
||||
show-word-limit
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="事由" name="reason">
|
||||
<a-textarea
|
||||
v-model="paymentForm.reason"
|
||||
placeholder="请输入付款事由"
|
||||
:rows="3"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="经手人" name="handler">
|
||||
<a-input
|
||||
v-model="paymentForm.handler"
|
||||
placeholder="请输入经手人姓名"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="金额" name="amount">
|
||||
<a-input-number
|
||||
v-model="paymentForm.amount"
|
||||
placeholder="请输入付款金额"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
addon-before="¥"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="收款单位" name="payeeUnit">
|
||||
<a-input
|
||||
v-model="paymentForm.payeeUnit"
|
||||
placeholder="请输入收款单位名称"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="收款账号" name="payeeAccount">
|
||||
<a-input
|
||||
v-model="paymentForm.payeeAccount"
|
||||
placeholder="请输入收款账号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 发票附件及其他附件 -->
|
||||
<a-form-item label="发票附件" name="invoiceAttachments">
|
||||
<a-upload
|
||||
v-model:file-list="paymentForm.invoiceAttachments"
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="onInvoiceUploadSuccess"
|
||||
:on-error="onUploadError"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
:max-count="5"
|
||||
>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传发票附件
|
||||
</a-button>
|
||||
<template #itemRender="{ file }">
|
||||
<a-space>
|
||||
<IconFile />
|
||||
<span>{{ file.name }}</span>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
status="danger"
|
||||
@click="removeInvoiceAttachment(file)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 银行付款凭证 -->
|
||||
<a-form-item label="银行付款凭证" name="bankPaymentVouchers">
|
||||
<a-upload
|
||||
v-model:file-list="paymentForm.bankPaymentVouchers"
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="onBankVoucherUploadSuccess"
|
||||
:on-error="onUploadError"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
:max-count="3"
|
||||
>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传银行付款凭证
|
||||
</a-button>
|
||||
<template #itemRender="{ file }">
|
||||
<a-space>
|
||||
<IconFile />
|
||||
<span>{{ file.name }}</span>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
status="danger"
|
||||
@click="removeBankVoucher(file)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="remarks">
|
||||
<a-textarea
|
||||
v-model="paymentForm.remarks"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 发票信息 -->
|
||||
<a-card title="发票信息" class="detail-card" :bordered="false">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="发票类型" field="invoiceType" required>
|
||||
<a-select
|
||||
v-model="formData.invoiceType"
|
||||
placeholder="请选择发票类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="增值税专用发票">增值税专用发票</a-option>
|
||||
<a-option value="增值税普通发票">增值税普通发票</a-option>
|
||||
<a-option value="电子发票">电子发票</a-option>
|
||||
<a-option value="其他">其他</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="发票号码" field="invoiceNumber" required>
|
||||
<a-input
|
||||
v-model="formData.invoiceNumber"
|
||||
placeholder="请输入发票号码"
|
||||
show-word-limit
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="开票日期" field="invoiceDate" required>
|
||||
<a-date-picker
|
||||
v-model="formData.invoiceDate"
|
||||
format="YYYY-MM-DD"
|
||||
placeholder="请选择开票日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="发票金额" field="invoiceAmount" required>
|
||||
<a-input-number
|
||||
v-model="formData.invoiceAmount"
|
||||
placeholder="请输入发票金额"
|
||||
:min="0.01"
|
||||
:max="999999.99"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="税率(%)" field="taxRate" required>
|
||||
<a-input-number
|
||||
v-model="formData.taxRate"
|
||||
placeholder="请输入税率"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="税额" field="taxAmount" required>
|
||||
<a-input-number
|
||||
v-model="formData.taxAmount"
|
||||
placeholder="请输入税额"
|
||||
:min="0"
|
||||
:max="999999.99"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="不含税金额" field="amountWithoutTax" required>
|
||||
<a-input-number
|
||||
v-model="formData.amountWithoutTax"
|
||||
placeholder="请输入不含税金额"
|
||||
:min="0.01"
|
||||
:max="999999.99"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 合同信息 -->
|
||||
<a-card title="合同信息" class="detail-card" :bordered="false">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="合同编号" field="contractNumber" required>
|
||||
<a-input
|
||||
v-model="formData.contractNumber"
|
||||
placeholder="请输入合同编号"
|
||||
show-word-limit
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="合同金额" field="contractAmount" required>
|
||||
<a-input-number
|
||||
v-model="formData.contractAmount"
|
||||
placeholder="请输入合同金额"
|
||||
:min="0.01"
|
||||
:max="999999.99"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="签订日期" field="contractDate" required>
|
||||
<a-date-picker
|
||||
v-model="formData.contractDate"
|
||||
format="YYYY-MM-DD"
|
||||
placeholder="请选择签订日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="付款条件" field="paymentTerms" required>
|
||||
<a-input
|
||||
v-model="formData.paymentTerms"
|
||||
placeholder="请输入付款条件"
|
||||
show-word-limit
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="付款期限" field="paymentDeadline" required>
|
||||
<a-date-picker
|
||||
v-model="formData.paymentDeadline"
|
||||
format="YYYY-MM-DD"
|
||||
placeholder="请选择付款期限"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-form>
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
提交付款申请
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import type { EquipmentResp, PaymentRequest } from '@/apis/equipment/type'
|
||||
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { IconUpload, IconFile } from '@arco-design/web-vue/es/icon'
|
||||
import message from '@arco-design/web-vue/es/message'
|
||||
import type { EquipmentResp } from '@/apis/equipment/type'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
equipmentData?: EquipmentResp | null
|
||||
equipmentData: EquipmentResp | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
equipmentData: null,
|
||||
})
|
||||
interface PaymentForm {
|
||||
applicationDate: string | null
|
||||
department: string
|
||||
projectName: string
|
||||
expenseCategory: string
|
||||
reason: string
|
||||
handler: string
|
||||
amount: number | null
|
||||
payeeUnit: string
|
||||
payeeAccount: string
|
||||
invoiceAttachments: any[]
|
||||
bankPaymentVouchers: any[]
|
||||
remarks: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
// 表单引用
|
||||
const paymentFormRef = ref()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<PaymentRequest>({
|
||||
paymentMethod: '',
|
||||
paymentAmount: 0,
|
||||
paymentTime: '',
|
||||
paymentPerson: '',
|
||||
paymentRemark: '',
|
||||
invoiceType: '',
|
||||
invoiceNumber: '',
|
||||
invoiceDate: '',
|
||||
invoiceAmount: 0,
|
||||
taxRate: 13,
|
||||
taxAmount: 0,
|
||||
amountWithoutTax: 0,
|
||||
contractNumber: '',
|
||||
contractAmount: 0,
|
||||
contractDate: '',
|
||||
paymentTerms: '',
|
||||
paymentDeadline: '',
|
||||
})
|
||||
// 提交状态
|
||||
const submitting = ref(false)
|
||||
|
||||
// 计算税额和不含税金额
|
||||
const calculatedTaxAmount = computed(() => {
|
||||
if (formData.invoiceAmount && formData.taxRate) {
|
||||
return (formData.invoiceAmount * formData.taxRate) / 100
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const calculatedAmountWithoutTax = computed(() => {
|
||||
if (formData.invoiceAmount && formData.taxAmount) {
|
||||
return formData.invoiceAmount - formData.taxAmount
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
// 监听发票金额和税率变化,自动计算税额和不含税金额
|
||||
watch([() => formData.invoiceAmount, () => formData.taxRate], () => {
|
||||
formData.taxAmount = calculatedTaxAmount.value
|
||||
formData.amountWithoutTax = calculatedAmountWithoutTax.value
|
||||
// 付款申请表单
|
||||
const paymentForm = reactive<PaymentForm>({
|
||||
applicationDate: null,
|
||||
department: '',
|
||||
projectName: '',
|
||||
expenseCategory: '',
|
||||
reason: '',
|
||||
handler: '',
|
||||
amount: null,
|
||||
payeeUnit: '',
|
||||
payeeAccount: '',
|
||||
invoiceAttachments: [],
|
||||
bankPaymentVouchers: [],
|
||||
remarks: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
paymentMethod: [
|
||||
{ required: true, message: '请选择支付方式' }
|
||||
const paymentRules = {
|
||||
applicationDate: [
|
||||
{ required: true, message: '请选择申请日期', trigger: 'change' }
|
||||
],
|
||||
paymentAmount: [
|
||||
{ required: true, message: '请输入支付金额' },
|
||||
{ type: 'number', min: 0.01, message: '支付金额必须大于0' }
|
||||
department: [
|
||||
{ required: true, message: '请输入部门名称', trigger: 'blur' }
|
||||
],
|
||||
paymentTime: [
|
||||
{ required: true, message: '请选择支付时间' }
|
||||
projectName: [
|
||||
{ required: true, message: '请输入项目名称', trigger: 'blur' }
|
||||
],
|
||||
paymentPerson: [
|
||||
{ required: true, message: '请输入支付人姓名' },
|
||||
{ min: 2, max: 50, message: '支付人姓名长度应在2-50个字符之间' }
|
||||
expenseCategory: [
|
||||
{ required: true, message: '请选择支出类别', trigger: 'change' }
|
||||
],
|
||||
invoiceType: [
|
||||
{ required: true, message: '请选择发票类型' }
|
||||
reason: [
|
||||
{ required: true, message: '请输入付款事由', trigger: 'blur' }
|
||||
],
|
||||
invoiceNumber: [
|
||||
{ required: true, message: '请输入发票号码' },
|
||||
{ min: 2, max: 50, message: '发票号码长度应在2-50个字符之间' }
|
||||
handler: [
|
||||
{ required: true, message: '请输入经手人姓名', trigger: 'blur' }
|
||||
],
|
||||
invoiceDate: [
|
||||
{ required: true, message: '请选择开票日期' }
|
||||
amount: [
|
||||
{ required: true, message: '请输入付款金额', trigger: 'blur' }
|
||||
],
|
||||
invoiceAmount: [
|
||||
{ required: true, message: '请输入发票金额' },
|
||||
{ type: 'number', min: 0.01, message: '发票金额必须大于0' }
|
||||
],
|
||||
taxRate: [
|
||||
{ required: true, message: '请输入税率' },
|
||||
{ type: 'number', min: 0, max: 100, message: '税率应在0-100之间' }
|
||||
],
|
||||
contractNumber: [
|
||||
{ required: true, message: '请输入合同编号' },
|
||||
{ min: 2, max: 50, message: '合同编号长度应在2-50个字符之间' }
|
||||
],
|
||||
contractAmount: [
|
||||
{ required: true, message: '请输入合同金额' },
|
||||
{ type: 'number', min: 0.01, message: '合同金额必须大于0' }
|
||||
],
|
||||
contractDate: [
|
||||
{ required: true, message: '请选择签订日期' }
|
||||
],
|
||||
paymentTerms: [
|
||||
{ required: true, message: '请输入付款条件' },
|
||||
{ min: 2, max: 100, message: '付款条件长度应在2-100个字符之间' }
|
||||
],
|
||||
paymentDeadline: [
|
||||
{ required: true, message: '请选择付款期限' }
|
||||
payeeUnit: [
|
||||
{ required: true, message: '请输入收款单位名称', trigger: 'blur' }
|
||||
],
|
||||
payeeAccount: [
|
||||
{ required: true, message: '请输入收款账号', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听弹窗显示状态
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible && props.equipmentData) {
|
||||
// 重置表单并设置默认值
|
||||
Object.assign(formData, {
|
||||
paymentMethod: '',
|
||||
paymentAmount: props.equipmentData.purchasePrice || 0,
|
||||
paymentTime: '',
|
||||
paymentPerson: '',
|
||||
paymentRemark: '',
|
||||
invoiceType: '增值税专用发票',
|
||||
invoiceNumber: '',
|
||||
invoiceDate: '',
|
||||
invoiceAmount: props.equipmentData.purchasePrice || 0,
|
||||
taxRate: 13,
|
||||
taxAmount: 0,
|
||||
amountWithoutTax: 0,
|
||||
contractNumber: props.equipmentData.purchaseOrder || '',
|
||||
contractAmount: props.equipmentData.purchasePrice || 0,
|
||||
contractDate: '',
|
||||
paymentTerms: '货到付款',
|
||||
paymentDeadline: '',
|
||||
})
|
||||
|
||||
// 计算税额和不含税金额
|
||||
formData.taxAmount = calculatedTaxAmount.value
|
||||
formData.amountWithoutTax = calculatedAmountWithoutTax.value
|
||||
|
||||
formRef.value?.clearValidate()
|
||||
// 上传配置
|
||||
const uploadAction = '/api/file/upload'
|
||||
const uploadHeaders = {
|
||||
// 根据实际需求设置上传头
|
||||
}
|
||||
|
||||
// 格式化价格
|
||||
const formatPrice = (price: number) => {
|
||||
return price.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
}
|
||||
|
||||
// 上传前检查
|
||||
const beforeUpload = (file: File) => {
|
||||
const isValidSize = file.size / 1024 / 1024 < 10 // 10MB限制
|
||||
if (!isValidSize) {
|
||||
message.error('文件大小不能超过10MB')
|
||||
return false
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
if (!props.equipmentData?.equipmentId) {
|
||||
throw new Error('设备ID不能为空')
|
||||
}
|
||||
// 发票附件上传成功
|
||||
const onInvoiceUploadSuccess = (response: any, file: any) => {
|
||||
console.log('发票附件上传成功:', response, file)
|
||||
message.success('发票附件上传成功')
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const paymentTime = formData.paymentTime ? new Date(formData.paymentTime).toISOString() : new Date().toISOString()
|
||||
const invoiceDate = formData.invoiceDate ? new Date(formData.invoiceDate).toISOString() : new Date().toISOString()
|
||||
const contractDate = formData.contractDate ? new Date(formData.contractDate).toISOString() : new Date().toISOString()
|
||||
const paymentDeadline = formData.paymentDeadline ? new Date(formData.paymentDeadline).toISOString() : new Date().toISOString()
|
||||
|
||||
const requestData: PaymentRequest = {
|
||||
...formData,
|
||||
paymentTime,
|
||||
invoiceDate,
|
||||
contractDate,
|
||||
paymentDeadline,
|
||||
}
|
||||
// 银行付款凭证上传成功
|
||||
const onBankVoucherUploadSuccess = (response: any, file: any) => {
|
||||
console.log('银行付款凭证上传成功:', response, file)
|
||||
message.success('银行付款凭证上传成功')
|
||||
}
|
||||
|
||||
await equipmentProcurementApi.makePayment(props.equipmentData.equipmentId, requestData)
|
||||
|
||||
Message.success('付款成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error: any) {
|
||||
console.error('付款失败:', error)
|
||||
Message.error(error?.message || '付款失败,请检查表单信息')
|
||||
} finally {
|
||||
loading.value = false
|
||||
// 上传失败
|
||||
const onUploadError = (error: any) => {
|
||||
console.error('文件上传失败:', error)
|
||||
message.error('文件上传失败')
|
||||
}
|
||||
|
||||
// 删除发票附件
|
||||
const removeInvoiceAttachment = (file: any) => {
|
||||
const index = paymentForm.invoiceAttachments.findIndex(f => f.uid === file.uid)
|
||||
if (index > -1) {
|
||||
paymentForm.invoiceAttachments.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除银行付款凭证
|
||||
const removeBankVoucher = (file: any) => {
|
||||
const index = paymentForm.bankPaymentVouchers.findIndex(f => f.uid === file.uid)
|
||||
if (index > -1) {
|
||||
paymentForm.bankPaymentVouchers.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -476,30 +382,54 @@ const handleSubmit = async () => {
|
|||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理 visible 变化
|
||||
const handleVisibleChange = (newVisible: boolean) => {
|
||||
emit('update:visible', newVisible)
|
||||
}
|
||||
|
||||
// 提交付款申请
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
submitting.value = true
|
||||
|
||||
// 验证表单
|
||||
await paymentFormRef.value?.validate()
|
||||
|
||||
// 这里调用API提交付款申请
|
||||
console.log('提交付款申请:', {
|
||||
...paymentForm,
|
||||
equipmentId: props.equipmentData?.equipmentId
|
||||
})
|
||||
|
||||
message.success('付款申请提交成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
message.error('提交失败,请检查表单信息')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-card {
|
||||
margin-bottom: 16px;
|
||||
.payment-application-container {
|
||||
.info-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
.action-buttons {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.arco-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.arco-input,
|
||||
.arco-select,
|
||||
.arco-input-number,
|
||||
.arco-date-picker,
|
||||
.arco-textarea {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.arco-textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -87,11 +87,15 @@
|
|||
<a-form-item label="序列号" field="equipmentSn">
|
||||
<a-input
|
||||
v-model="formData.equipmentSn"
|
||||
placeholder="请输入序列号"
|
||||
:disabled="isView"
|
||||
placeholder="选择设备类型后自动生成"
|
||||
:disabled="true"
|
||||
show-word-limit
|
||||
:max-length="100"
|
||||
/>
|
||||
<div class="field-tip">
|
||||
<IconInfoCircle style="color: #1890ff; margin-right: 4px;" />
|
||||
选择设备类型后自动生成,格式:设备类型+顺序号+日期
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
@ -528,6 +532,7 @@ import { Message } from '@arco-design/web-vue'
|
|||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
|
||||
import type { EquipmentResp, EquipmentReq } from '@/apis/equipment/type'
|
||||
import { IconInfoCircle } from '@arco-design/web-vue/es/icon'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
|
@ -692,6 +697,40 @@ watch([() => formData.unitPrice, () => formData.quantity], ([newUnitPrice, newQu
|
|||
}
|
||||
})
|
||||
|
||||
// 生成设备序列号
|
||||
const generateEquipmentSn = (equipmentType: string) => {
|
||||
// 获取当前时间戳作为顺序号
|
||||
const timestamp = Date.now()
|
||||
const orderNumber = timestamp.toString().slice(-6) // 取后6位作为顺序号
|
||||
|
||||
// 获取设备类型简称
|
||||
const typeMap: Record<string, string> = {
|
||||
'detection': 'DET',
|
||||
'security': 'SEC',
|
||||
'office': 'OFF',
|
||||
'car': 'CAR',
|
||||
'other': 'OTH'
|
||||
}
|
||||
const typeCode = typeMap[equipmentType] || 'OTH'
|
||||
|
||||
// 格式化当前时间(年月日)
|
||||
const now = new Date()
|
||||
const dateStr = now.getFullYear().toString().slice(-2) +
|
||||
(now.getMonth() + 1).toString().padStart(2, '0') +
|
||||
now.getDate().toString().padStart(2, '0')
|
||||
|
||||
// 生成序列号:设备类型+顺序号+当前时间
|
||||
return `${typeCode}${orderNumber}${dateStr}`
|
||||
}
|
||||
|
||||
// 监听设备类型变化,自动生成序列号
|
||||
watch(() => formData.equipmentType, (newType) => {
|
||||
if (newType && !formData.equipmentSn) {
|
||||
// 只有在序列号为空时才自动生成
|
||||
formData.equipmentSn = generateEquipmentSn(newType)
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化表单数据
|
||||
const initFormData = () => {
|
||||
if (props.procurementData) {
|
||||
|
@ -841,7 +880,7 @@ const fillTestData = () => {
|
|||
const nextMaintenanceTime = nextMaintenanceDate.toISOString().slice(0, 19).replace('T', ' ')
|
||||
|
||||
// 生成随机序列号
|
||||
const randomSn = 'SN' + Math.random().toString(36).substr(2, 8).toUpperCase()
|
||||
const randomSn = generateEquipmentSn('detection')
|
||||
|
||||
// 生成随机资产编号
|
||||
const randomAssetCode = 'ZC' + Math.random().toString(36).substr(2, 6).toUpperCase()
|
||||
|
@ -949,6 +988,15 @@ const handleCancel = () => {
|
|||
color: var(--color-text-1);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.field-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-input,
|
||||
|
|
|
@ -27,9 +27,15 @@
|
|||
<a-descriptions-item label="设备型号">
|
||||
{{ equipmentData?.equipmentModel || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备序列号">
|
||||
{{ equipmentData?.equipmentSn || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="品牌">
|
||||
{{ equipmentData?.brand || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="配置规格">
|
||||
{{ equipmentData?.specification || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商">
|
||||
{{ equipmentData?.supplierName || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
@ -370,6 +376,32 @@ watch(() => props.equipmentData, () => {
|
|||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 生成设备序列号
|
||||
const generateEquipmentSn = (equipmentType: string, inStockTime: string) => {
|
||||
// 获取当前时间戳作为顺序号
|
||||
const timestamp = Date.now()
|
||||
const orderNumber = timestamp.toString().slice(-6) // 取后6位作为顺序号
|
||||
|
||||
// 获取设备类型简称
|
||||
const typeMap: Record<string, string> = {
|
||||
'detection': 'DET',
|
||||
'security': 'SEC',
|
||||
'office': 'OFF',
|
||||
'car': 'CAR',
|
||||
'other': 'OTH'
|
||||
}
|
||||
const typeCode = typeMap[equipmentType] || 'OTH'
|
||||
|
||||
// 格式化入库时间(年月日)
|
||||
const date = new Date(inStockTime)
|
||||
const dateStr = date.getFullYear().toString().slice(-2) +
|
||||
(date.getMonth() + 1).toString().padStart(2, '0') +
|
||||
date.getDate().toString().padStart(2, '0')
|
||||
|
||||
// 生成序列号:设备类型+顺序号+入库时间
|
||||
return `${typeCode}${orderNumber}${dateStr}`
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
|
@ -380,13 +412,37 @@ const handleSubmit = async () => {
|
|||
throw new Error('设备ID不能为空')
|
||||
}
|
||||
|
||||
console.log('📦 开始提交收货数据...')
|
||||
console.log('📦 设备数据:', props.equipmentData)
|
||||
console.log('📦 表单数据:', formData)
|
||||
|
||||
// 构建收货请求数据
|
||||
const receiptData: ReceiptRequest = {
|
||||
// 收货特有信息
|
||||
// 1. 获取采购阶段的设备信息
|
||||
const procurementData = props.equipmentData
|
||||
|
||||
// 2. 生成设备序列号
|
||||
const equipmentSn = generateEquipmentSn(
|
||||
procurementData.equipmentType || 'other',
|
||||
formData.receiptTime || new Date().toISOString()
|
||||
)
|
||||
|
||||
// 3. 合并收货信息和设备信息
|
||||
const equipmentData = {
|
||||
// 采购阶段的数据(已有)
|
||||
equipmentId: procurementData.equipmentId,
|
||||
equipmentName: procurementData.equipmentName,
|
||||
equipmentModel: procurementData.equipmentModel,
|
||||
equipmentType: procurementData.equipmentType,
|
||||
equipmentSn: equipmentSn, // 使用生成的序列号
|
||||
brand: procurementData.brand,
|
||||
specification: procurementData.specification,
|
||||
assetCode: procurementData.assetCode,
|
||||
|
||||
// 采购信息(已有)
|
||||
purchaseOrder: procurementData.purchaseOrder,
|
||||
supplierName: procurementData.supplierName,
|
||||
purchasePrice: procurementData.purchasePrice,
|
||||
purchaseTime: procurementData.purchaseTime,
|
||||
quantity: procurementData.quantity,
|
||||
unitPrice: procurementData.unitPrice,
|
||||
totalPrice: procurementData.totalPrice,
|
||||
|
||||
// 收货相关信息
|
||||
receiptTime: formData.receiptTime ? formatDateTime(formData.receiptTime) : formatDateTime(new Date()),
|
||||
receiptPerson: formData.receiptPerson,
|
||||
receiptQuantity: formData.receiptQuantity,
|
||||
|
@ -400,62 +456,31 @@ const handleSubmit = async () => {
|
|||
storageLocation: formData.storageLocation,
|
||||
storageManager: formData.storageManager,
|
||||
|
||||
// 设备基本信息(从采购数据继承)
|
||||
equipmentName: props.equipmentData.equipmentName,
|
||||
equipmentModel: props.equipmentData.equipmentModel,
|
||||
equipmentType: props.equipmentData.equipmentType,
|
||||
equipmentSn: props.equipmentData.equipmentSn,
|
||||
brand: props.equipmentData.brand,
|
||||
specification: props.equipmentData.specification,
|
||||
assetCode: props.equipmentData.assetCode,
|
||||
|
||||
// 采购信息(从采购数据继承)
|
||||
purchaseOrder: props.equipmentData.purchaseOrder,
|
||||
supplierName: props.equipmentData.supplierName,
|
||||
purchasePrice: props.equipmentData.purchasePrice,
|
||||
purchaseTime: props.equipmentData.purchaseTime,
|
||||
quantity: props.equipmentData.quantity,
|
||||
unitPrice: props.equipmentData.unitPrice,
|
||||
totalPrice: props.equipmentData.totalPrice,
|
||||
|
||||
// 入库信息
|
||||
inStockTime: formData.receiptTime ? formatDateTime(formData.receiptTime) : formatDateTime(new Date()),
|
||||
physicalLocation: formData.storageLocation,
|
||||
locationStatus: 'in_stock',
|
||||
responsiblePerson: formData.storageManager,
|
||||
inventoryBarcode: props.equipmentData.inventoryBarcode || generateInventoryBarcode(),
|
||||
|
||||
// 状态信息
|
||||
// 系统默认数据
|
||||
equipmentStatus: 'normal',
|
||||
useStatus: '0',
|
||||
healthStatus: 'good',
|
||||
receiptStatus: 'RECEIVED',
|
||||
|
||||
// 其他管理信息
|
||||
depreciationMethod: props.equipmentData.depreciationMethod || 'straight_line',
|
||||
depreciationYears: props.equipmentData.depreciationYears || 5,
|
||||
salvageValue: props.equipmentData.salvageValue || 0,
|
||||
currentNetValue: props.equipmentData.purchasePrice || 0,
|
||||
|
||||
// 系统字段
|
||||
createTime: formatDateTime(new Date()),
|
||||
updateTime: formatDateTime(new Date())
|
||||
useStatus: '0', // 空闲中
|
||||
locationStatus: 'in_stock', // 库存中
|
||||
healthStatus: 'excellent',
|
||||
responsiblePerson: formData.storageManager,
|
||||
physicalLocation: formData.storageLocation,
|
||||
inStockTime: formData.receiptTime ? formatDateTime(formData.receiptTime) : formatDateTime(new Date()),
|
||||
inventoryBarcode: `BC${Date.now()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`,
|
||||
depreciationMethod: 'straight_line',
|
||||
warrantyExpireDate: procurementData.warrantyExpireDate,
|
||||
assetRemark: `设备已收货入库,收货人:${formData.receiptPerson},入库时间:${formData.receiptTime}`
|
||||
}
|
||||
|
||||
console.log('📦 构建的收货数据:', receiptData)
|
||||
|
||||
// 调用收货API
|
||||
await equipmentProcurementApi.receiveGoods(
|
||||
props.equipmentData.equipmentId,
|
||||
receiptData
|
||||
)
|
||||
|
||||
Message.success('收货成功,设备已自动入库')
|
||||
console.log('📦 准备提交收货数据:', equipmentData)
|
||||
|
||||
// 4. 调用收货API
|
||||
await equipmentProcurementApi.receiveGoods(procurementData.equipmentId, equipmentData)
|
||||
|
||||
Message.success('收货成功!设备已自动入库')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('收货失败:', error)
|
||||
Message.error(error?.message || '收货失败,请检查表单信息')
|
||||
Message.error(error?.message || '收货失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -201,6 +201,8 @@
|
|||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<!-- 1. 采购相关操作 -->
|
||||
<!-- 申请采购按钮 - 只在特定状态下显示 -->
|
||||
<a-button
|
||||
v-if="canApplyProcurement(record)"
|
||||
|
@ -210,6 +212,38 @@
|
|||
>
|
||||
申请采购
|
||||
</a-button>
|
||||
|
||||
<!-- 2. 合同发票管理相关操作 -->
|
||||
<!-- 管理合同发票按钮 - 在采购审批通过后显示 -->
|
||||
<a-button
|
||||
v-if="canManageContractInvoice(record)"
|
||||
type="outline"
|
||||
size="small"
|
||||
@click="handleManageContractInvoice(record)"
|
||||
>
|
||||
管理合同发票
|
||||
</a-button>
|
||||
|
||||
<!-- 3. 支付相关操作 -->
|
||||
<!-- 申请付款按钮 -->
|
||||
<a-button
|
||||
v-if="canMakePayment(record)"
|
||||
type="outline"
|
||||
size="small"
|
||||
@click="handleMakePayment(record)"
|
||||
>
|
||||
申请付款
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.paymentStatus === 'PAID'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleViewPayment(record)"
|
||||
>
|
||||
查看支付详情
|
||||
</a-button>
|
||||
|
||||
<!-- 4. 收货相关操作 -->
|
||||
<!-- 收货操作按钮 -->
|
||||
<a-button
|
||||
v-if="canReceiveGoods(record)"
|
||||
|
@ -227,34 +261,56 @@
|
|||
>
|
||||
查看收货
|
||||
</a-button>
|
||||
<!-- 支付操作按钮 -->
|
||||
<a-button
|
||||
v-if="canMakePayment(record)"
|
||||
type="outline"
|
||||
size="small"
|
||||
@click="handleMakePayment(record)"
|
||||
>
|
||||
付款
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.paymentStatus === 'PAID'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleViewPayment(record)"
|
||||
>
|
||||
查看支付详情
|
||||
</a-button>
|
||||
<!-- 显示采购状态 - 优先显示采购状态 -->
|
||||
|
||||
|
||||
|
||||
<!-- 状态标签 - 按照要求的顺序排列 -->
|
||||
<!-- 1. 采购状态 -->
|
||||
<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) }}
|
||||
|
||||
<!-- 2. 合同发票管理状态 -->
|
||||
<a-tag
|
||||
v-if="hasContractInvoiceInfo(record)"
|
||||
color="green"
|
||||
>
|
||||
已完善
|
||||
</a-tag>
|
||||
<a-tag
|
||||
v-else-if="record.procurementStatus === 'APPROVED'"
|
||||
color="orange"
|
||||
>
|
||||
待完善
|
||||
</a-tag>
|
||||
<a-tag
|
||||
v-else
|
||||
color="gray"
|
||||
>
|
||||
未开始
|
||||
</a-tag>
|
||||
|
||||
<!-- 3. 支付状态 -->
|
||||
<a-tag
|
||||
v-if="record.paymentStatus && record.paymentStatus !== 'NOT_PAID'"
|
||||
:color="getPaymentStatusColor(record.paymentStatus)"
|
||||
>
|
||||
{{ getPaymentStatusText(record.paymentStatus) }}
|
||||
</a-tag>
|
||||
<a-tag v-else color="gray">未支付</a-tag>
|
||||
|
||||
<!-- 4. 收货状态 -->
|
||||
<a-tag
|
||||
v-if="record.receiptStatus && record.receiptStatus !== 'NOT_RECEIVED'"
|
||||
:color="getReceiptStatusColor(record.receiptStatus)"
|
||||
>
|
||||
{{ getReceiptStatusText(record.receiptStatus) }}
|
||||
</a-tag>
|
||||
<a-tag v-else color="gray">未收货</a-tag>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<a-popconfirm
|
||||
content="确定要删除这条记录吗?"
|
||||
|
@ -326,6 +382,13 @@
|
|||
:equipment-data="currentPaymentData"
|
||||
@success="handlePaymentSuccess"
|
||||
/>
|
||||
|
||||
<!-- 合同发票管理弹窗 -->
|
||||
<ContractInvoiceModal
|
||||
v-model:visible="contractInvoiceModalVisible"
|
||||
:equipment-data="currentContractInvoiceData"
|
||||
@success="handleContractInvoiceSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -349,6 +412,7 @@ import ReceiptDetailModal from './components/ReceiptDetailModal.vue'
|
|||
import PaymentDetailModal from './components/PaymentDetailModal.vue'
|
||||
import ReceiptModal from './components/ReceiptModal.vue'
|
||||
import PaymentModal from './components/PaymentModal.vue'
|
||||
import ContractInvoiceModal from './components/ContractInvoiceModal.vue'
|
||||
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
|
||||
import { equipmentApprovalApi } from '@/apis/equipment/approval'
|
||||
import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type'
|
||||
|
@ -397,6 +461,10 @@ const currentPaymentData = ref<EquipmentResp | null>(null)
|
|||
const receiptModalVisible = ref(false)
|
||||
const paymentModalVisible = ref(false)
|
||||
|
||||
// 合同发票管理弹窗控制
|
||||
const contractInvoiceModalVisible = ref(false)
|
||||
const currentContractInvoiceData = ref<EquipmentResp | null>(null)
|
||||
|
||||
// 表格选择
|
||||
const selectedRowKeys = ref<string[]>([])
|
||||
const rowSelection = reactive({
|
||||
|
@ -936,13 +1004,42 @@ const handleModalSuccess = () => {
|
|||
// 采购申请成功回调
|
||||
const handleApplicationSuccess = async () => {
|
||||
applicationModalVisible.value = false
|
||||
console.log('采购申请成功,准备刷新数据...')
|
||||
// 延迟刷新数据,确保后端状态更新完成
|
||||
console.log('✅ 采购申请成功,准备更新本地数据...')
|
||||
|
||||
// 立即更新本地数据,让申请采购按钮消失
|
||||
if (currentApplicationData.value) {
|
||||
const equipmentId = currentApplicationData.value.equipmentId
|
||||
|
||||
// 找到对应的记录并更新采购状态
|
||||
const recordIndex = tableData.value.findIndex(item => item.equipmentId === equipmentId)
|
||||
if (recordIndex !== -1) {
|
||||
// 立即更新本地状态为待审批
|
||||
tableData.value[recordIndex] = {
|
||||
...tableData.value[recordIndex],
|
||||
procurementStatus: 'PENDING_APPROVAL'
|
||||
}
|
||||
|
||||
console.log('✅ 本地数据已更新,申请采购按钮应该消失')
|
||||
console.log('🔍 更新后的记录:', tableData.value[recordIndex])
|
||||
|
||||
message.success('采购申请已提交,请等待审批')
|
||||
} else {
|
||||
console.warn('⚠️ 未找到对应的记录,无法更新本地状态')
|
||||
message.warning('状态更新失败,请手动刷新页面')
|
||||
}
|
||||
}
|
||||
|
||||
// 可选:延迟刷新数据以确保后端状态同步
|
||||
setTimeout(async () => {
|
||||
console.log('开始刷新数据...')
|
||||
await loadData(currentSearchParams.value)
|
||||
message.success('采购申请已提交,请等待审批')
|
||||
}, 1000)
|
||||
try {
|
||||
console.log('🔄 延迟刷新数据,确保后端状态同步...')
|
||||
await loadData(currentSearchParams.value)
|
||||
console.log('✅ 后端数据同步完成')
|
||||
} catch (error) {
|
||||
console.error('❌ 后端数据同步失败:', error)
|
||||
// 不显示错误提示,因为本地状态已经更新
|
||||
}
|
||||
}, 1000) // 1秒后刷新
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
|
@ -993,17 +1090,45 @@ const getTotalAmount = () => {
|
|||
// 检查是否可以申请采购
|
||||
const canApplyProcurement = (record: EquipmentResp) => {
|
||||
// 根据采购状态判断是否可以申请采购
|
||||
// 只有未开始、已拒绝、已完成的设备可以重新申请采购
|
||||
const allowedStatuses = ['NOT_STARTED', 'REJECTED', 'COMPLETED', null, undefined]
|
||||
// 只有未开始、已拒绝的设备可以申请采购
|
||||
// 待审批、已通过、已完成等状态不能重复申请
|
||||
const allowedStatuses = ['NOT_STARTED', 'REJECTED', null, undefined]
|
||||
const canApply = allowedStatuses.includes(record.procurementStatus)
|
||||
console.log(`设备 ${record.equipmentName} 采购状态: ${record.procurementStatus}, 可申请: ${canApply}`)
|
||||
|
||||
// 添加详细的调试日志
|
||||
console.log(`🔍 申请采购按钮显示检查 - 设备: ${record.equipmentName}`)
|
||||
console.log(`🔍 当前采购状态: ${record.procurementStatus}`)
|
||||
console.log(`🔍 允许申请的状态: ${allowedStatuses.join(', ')}`)
|
||||
console.log(`🔍 是否显示按钮: ${canApply}`)
|
||||
console.log(`🔍 完整记录:`, record)
|
||||
|
||||
return canApply
|
||||
}
|
||||
|
||||
// 检查是否可以收货
|
||||
const canReceiveGoods = (record: EquipmentResp) => {
|
||||
const receiptStatus = (record as any).receiptStatus
|
||||
return receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED'
|
||||
// 只有在采购状态为已通过且收货状态为未收货时才显示确认收货按钮
|
||||
// 同时需要确保合同发票信息已经完善
|
||||
const procurementStatus = record.procurementStatus
|
||||
const receiptStatus = record.receiptStatus
|
||||
|
||||
// 检查是否有合同发票信息(这里可以根据实际业务逻辑调整)
|
||||
// 暂时使用一个简单的判断,后续可以根据实际数据结构调整
|
||||
const hasContractInvoice = true // 暂时设为true,后续根据实际业务逻辑调整
|
||||
|
||||
console.log('🔍 canReceiveGoods 检查:', {
|
||||
equipmentName: record.equipmentName,
|
||||
procurementStatus,
|
||||
receiptStatus,
|
||||
hasContractInvoice,
|
||||
canReceive: procurementStatus === 'APPROVED' &&
|
||||
(receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED') &&
|
||||
hasContractInvoice
|
||||
})
|
||||
|
||||
return procurementStatus === 'APPROVED' &&
|
||||
(receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED') &&
|
||||
hasContractInvoice
|
||||
}
|
||||
|
||||
// 收货操作
|
||||
|
@ -1021,7 +1146,10 @@ const handleViewReceipt = (record: EquipmentResp) => {
|
|||
// 收货成功回调
|
||||
const handleReceiptSuccess = () => {
|
||||
receiptModalVisible.value = false
|
||||
// 收货成功后,采购状态应该更新为已收货
|
||||
// 重新加载数据以显示最新状态
|
||||
loadData(currentSearchParams.value)
|
||||
message.success('收货成功!设备已自动入库')
|
||||
}
|
||||
|
||||
// 检查是否可以付款
|
||||
|
@ -1072,6 +1200,57 @@ const getApprovalStatusText = (status: string) => {
|
|||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 手动刷新单条记录
|
||||
const handleRefreshRecord = async (record: EquipmentResp) => {
|
||||
console.log('🔄 手动刷新记录:', record.equipmentId)
|
||||
try {
|
||||
// 显示加载提示
|
||||
const loadingMessage = message.loading('正在刷新数据...')
|
||||
|
||||
// 重新加载整个表格数据,确保获取最新状态
|
||||
await loadData(currentSearchParams.value)
|
||||
|
||||
// 清除加载提示
|
||||
if (loadingMessage && typeof loadingMessage.close === 'function') {
|
||||
loadingMessage.close()
|
||||
}
|
||||
message.success('记录状态已刷新')
|
||||
|
||||
console.log('✅ 表格数据刷新完成')
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 刷新失败:', error)
|
||||
message.error(error?.message || '刷新失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以管理合同发票
|
||||
const canManageContractInvoice = (record: EquipmentResp) => {
|
||||
// 只有在采购状态为已通过时才显示管理合同发票按钮
|
||||
return record.procurementStatus === 'APPROVED'
|
||||
}
|
||||
|
||||
// 管理合同发票操作
|
||||
const handleManageContractInvoice = (record: EquipmentResp) => {
|
||||
currentContractInvoiceData.value = { ...record }
|
||||
contractInvoiceModalVisible.value = true
|
||||
}
|
||||
|
||||
// 合同发票管理成功回调
|
||||
const handleContractInvoiceSuccess = () => {
|
||||
contractInvoiceModalVisible.value = false
|
||||
loadData(currentSearchParams.value)
|
||||
}
|
||||
|
||||
// 检查是否有合同发票信息
|
||||
const hasContractInvoiceInfo = (record: EquipmentResp) => {
|
||||
// 这里可以根据实际业务逻辑调整判断条件
|
||||
// 暂时使用一个简单的判断,后续可以根据实际数据结构调整
|
||||
// 例如:检查是否有合同编号、发票号码等字段
|
||||
return record.procurementStatus === 'APPROVED' &&
|
||||
(record.receiptStatus === 'RECEIVED' || record.paymentStatus === 'PAID')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
@ -1258,6 +1437,8 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
|
|
|
@ -0,0 +1,498 @@
|
|||
<template>
|
||||
|
||||
<GiPageLayout>
|
||||
<div class="raw-data-container">
|
||||
<!-- 顶部按钮 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="openUploadModal">
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传视频
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-form :model="filterForm" layout="inline">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions"
|
||||
allow-clear :disabled="!filterForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
|
||||
:scroll="{ y: 'calc(100vh - 380px)' }">
|
||||
<template #type="{ record }">
|
||||
<a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.preTreatment ? 'green' : 'red'">
|
||||
{{ record.preTreatment ? '已处理' : '未处理' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="handlePreview(record)">预览</a-button>
|
||||
<a-button size="mini" @click="handleDownload(record)">下载</a-button>
|
||||
<a-popconfirm content="确认删除?" @ok="handleDelete(record)">
|
||||
<a-button size="mini" status="danger">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 上传弹窗 -->
|
||||
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading"
|
||||
@ok="handleUpload" @cancel="showUploadModal = false">
|
||||
<a-form :model="uploadForm" layout="vertical">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="onProjectChangeUpload" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload"
|
||||
allow-clear :disabled="!uploadForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" required>
|
||||
<a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="视频文件" required>
|
||||
<a-upload v-model:file-list="uploadForm.fileList" :multiple="uploadMode === 'batch'"
|
||||
:limit="uploadMode === 'batch' ? 10 : 1" accept="video/*" :auto-upload="false"
|
||||
list-type="picture-card" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model="uploadMode" type="button">
|
||||
<a-radio value="single">单文件</a-radio>
|
||||
<a-radio value="batch">批量</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</GiPageLayout>
|
||||
<!-- 视频预览弹窗 -->
|
||||
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false"
|
||||
@cancel="previewVisible = false">
|
||||
<a-tabs v-model:active-key="activePreviewTab" @change="activePreviewTab = $event as any">
|
||||
<!-- 原始视频 -->
|
||||
<a-tab-pane key="video" title="原始视频">
|
||||
<video v-if="previewUrl" :src="previewUrl" controls
|
||||
style="width: 100%; max-height: 60vh; border-radius: 4px"></video>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 处理结果 -->
|
||||
<a-tab-pane key="result" title="处理结果">
|
||||
<a-spin :loading="loadingResult">
|
||||
<a-space direction="vertical" size="medium" style="width: 100%">
|
||||
<!-- 图片 -->
|
||||
<img v-if="resultImgUrl" :src="resultImgUrl" style="max-width: 100%; border-radius: 4px"
|
||||
alt="last frame" />
|
||||
|
||||
<!-- JSON 预览 -->
|
||||
<a-card title="results.json" size="small">
|
||||
<pre>{{ JSON.stringify(resultJson, null, 2) }}</pre>
|
||||
</a-card>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import { IconUpload } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
getProjectList,
|
||||
getTurbineList
|
||||
} from '@/apis/industrial-image'
|
||||
import {
|
||||
getVideoPage,
|
||||
uploadBatchVideo,
|
||||
uploadSingleVideo,
|
||||
deleteVideo,
|
||||
downloadVideo
|
||||
} from '@/apis/video-monitor'
|
||||
|
||||
/* ---------------- 下拉 & 表单 ---------------- */
|
||||
const projectOptions = ref<{ label: string; value: string }[]>([])
|
||||
const turbineOptions = ref<{ label: string; value: string }[]>([]) // 筛选用
|
||||
const turbineOptionsUpload = ref<{ label: string; value: string }[]>([]) // 上传弹窗用
|
||||
const typeOptions = [
|
||||
{ label: '净空', value: 'clearance' },
|
||||
{ label: '形变', value: 'deformation' }
|
||||
]
|
||||
|
||||
const filterForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: ''
|
||||
})
|
||||
|
||||
const uploadForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: '',
|
||||
type: '',
|
||||
fileList: [] as any[]
|
||||
})
|
||||
|
||||
const uploadMode = ref<'single' | 'batch'>('single')
|
||||
|
||||
/* ---------------- 列表 ---------------- */
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
|
||||
// { title: '项目', dataIndex: 'projectName' },
|
||||
// { title: '机组', dataIndex: 'turbineName' },
|
||||
{ title: '类型', slotName: 'type' },
|
||||
{ title: '上传时间', dataIndex: 'uploadTime' },
|
||||
{ title: '状态', slotName: 'status' },
|
||||
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
|
||||
]
|
||||
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
|
||||
|
||||
/* ---------------- 控制弹窗 ---------------- */
|
||||
const showUploadModal = ref(false)
|
||||
const uploading = ref(false)
|
||||
|
||||
/* ---------------- 初始化 ---------------- */
|
||||
onMounted(async () => {
|
||||
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
|
||||
projectOptions.value = data.map((p: any) => ({ label: p.projectName, value: p.projectId }))
|
||||
handleQuery()
|
||||
})
|
||||
const activePreviewTab = ref<'video' | 'result'>('video') // 当前标签页
|
||||
const resultImgUrl = ref('')
|
||||
const resultJson = ref<Record<string, any>>({})
|
||||
const loadingResult = ref(false)
|
||||
const resutlVideoUrl = ref('')
|
||||
async function loadResultFiles(row: any) {
|
||||
if (!row.preTreatment) return
|
||||
loadingResult.value = true
|
||||
try {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
// 图片
|
||||
resultImgUrl.value = `${base}${row.preImagePath}/last_frame.jpg`
|
||||
resutlVideoUrl.value = `${base}${row.preImagePath}/annotated_video.mp4`
|
||||
// JSON
|
||||
const jsonUrl = `${base}${row.preImagePath}/results.json`
|
||||
const res = await fetch(jsonUrl)
|
||||
resultJson.value = await res.json()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
resultJson.value = {}
|
||||
} finally {
|
||||
loadingResult.value = false
|
||||
}
|
||||
console.log('result', resultImgUrl.value)
|
||||
}
|
||||
/* 项目 -> 机组(筛选) */
|
||||
watch(
|
||||
() => filterForm.projectId,
|
||||
async (val) => {
|
||||
filterForm.turbineId = ''
|
||||
turbineOptions.value = []
|
||||
if (!val) return
|
||||
const { data } = await getTurbineList({ projectId: val })
|
||||
turbineOptions.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
)
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
|
||||
function handlePreview(row: any) {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
|
||||
previewVisible.value = true
|
||||
activePreviewTab.value = 'video' // 默认先显示视频
|
||||
if (row.preTreatment) {
|
||||
loadResultFiles(row) // 预加载结果
|
||||
}
|
||||
}
|
||||
/* 项目 -> 机组(上传弹窗) */
|
||||
async function onProjectChangeUpload(projectId: string) {
|
||||
uploadForm.turbineId = ''
|
||||
turbineOptionsUpload.value = []
|
||||
if (!projectId) return
|
||||
const { data } = await getTurbineList({ projectId })
|
||||
turbineOptionsUpload.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
|
||||
/* ---------------- 查询 ---------------- */
|
||||
function handleQuery() {
|
||||
pagination.current = 1
|
||||
loadTable()
|
||||
}
|
||||
async function loadTable() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
projectId: filterForm.projectId,
|
||||
turbineId: filterForm.turbineId || undefined
|
||||
}
|
||||
const { data } = await getVideoPage(params)
|
||||
console.log(data)
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 上传 ---------------- */
|
||||
function openUploadModal() {
|
||||
uploadForm.projectId = ''
|
||||
uploadForm.turbineId = ''
|
||||
uploadForm.type = ''
|
||||
uploadForm.fileList = []
|
||||
showUploadModal.value = true
|
||||
}
|
||||
async function handleUpload() {
|
||||
if (!uploadForm.projectId || !uploadForm.type || !uploadForm.fileList.length) {
|
||||
Message.warning('请完整填写')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const files = uploadForm.fileList.map((f: any) => f.file)
|
||||
if (uploadMode.value === 'single') {
|
||||
await uploadSingleVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files[0]
|
||||
)
|
||||
} else {
|
||||
await uploadBatchVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files
|
||||
)
|
||||
}
|
||||
Message.success('上传成功')
|
||||
showUploadModal.value = false
|
||||
loadTable()
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 下载 / 删除 ---------------- */
|
||||
async function handleDownload(row: any) {
|
||||
const url = await downloadVideo(row.videoId)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
async function handleDelete(row: any) {
|
||||
await deleteVideo(row.videoId)
|
||||
Message.success('删除成功')
|
||||
loadTable()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.raw-data-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.filter-section {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-sections {
|
||||
.project-section {
|
||||
margin-bottom: 32px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px #f0f1f2;
|
||||
padding: 20px;
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.project-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.units-grid {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.unit-card {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: 360px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.unit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.unit-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.videos-list {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.video-item {
|
||||
width: 100px;
|
||||
|
||||
.video-thumbnail {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-top: 4px;
|
||||
|
||||
.video-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 11px;
|
||||
color: #86909c;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.video-status {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-progress {
|
||||
margin-top: 8px;
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-meta-info {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
|
||||
p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,665 @@
|
|||
<template>
|
||||
<GiPageLayout>
|
||||
<div class="raw-data-container">
|
||||
<!-- 顶部页签 -->
|
||||
<a-tabs v-model:active-key="topTab" type="rounded" destroy-on-hide>
|
||||
<!-- 原始数据 -->
|
||||
<a-tab-pane key="data" title="原始数据">
|
||||
<!-- 顶部按钮 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="openUploadModal">
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传视频
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-form :model="filterForm" layout="inline">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions"
|
||||
allow-clear :disabled="!filterForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
|
||||
:scroll="{ y: 'calc(100vh - 380px)' }">
|
||||
<template #type="{ record }">
|
||||
<a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.preTreatment ? 'green' : 'red'">
|
||||
{{ record.preTreatment ? '已处理' : '未处理' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="handlePreview(record)">预览</a-button>
|
||||
<a-button size="mini" @click="handleDownload(record)">下载</a-button>
|
||||
<a-popconfirm content="确认删除?" @ok="handleDelete(record)">
|
||||
<a-button size="mini" status="danger">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 净空报告 -->
|
||||
<a-tab-pane key="report" title="净空报告">
|
||||
<!-- 筛选 -->
|
||||
<a-form :model="reportFilterForm" layout="inline" style="margin-bottom: 16px">
|
||||
<a-form-item label="项目">
|
||||
<a-select v-model="reportFilterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="reportFilterForm.turbineId" placeholder="请选择机组"
|
||||
:options="turbineReportOptions" allow-clear :disabled="!reportFilterForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="loadReportGrid">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 网格 -->
|
||||
<a-row :gutter="[24, 24]">
|
||||
<a-col v-for="item in reportGrid" :key="item.videoId" :xs="24" :sm="12" :md="8" :lg="6">
|
||||
<!-- 卡片内部 -->
|
||||
<a-card hoverable>
|
||||
<div @click="openReportDetail(item)" style="cursor: pointer">
|
||||
<video :src="item.videoUrl"
|
||||
style="width: 100%; height: 120px; object-fit: cover; border-radius: 4px" />
|
||||
<div class="video-name">{{ item.videoName }}</div>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="8" style="margin-top: 8px">
|
||||
<a-col :span="8">
|
||||
<div class="stat-label">最小</div>
|
||||
<div class="stat-value">{{ item.min }} m</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="stat-label">最大</div>
|
||||
<div class="stat-value">{{ item.max }} m</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="stat-label">平均</div>
|
||||
<div class="stat-value">{{ item.avg }} m</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 查看详情按钮 -->
|
||||
<a-button size="mini" style="margin-top: 8px; width: 100%"
|
||||
@click="openReportDetail(item)">
|
||||
查看详情
|
||||
</a-button>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增:报告详情模态框 -->
|
||||
<a-modal v-model:visible="reportDetailVisible" title="净空报告详情" width="900px" :footer="false"
|
||||
@cancel="reportDetailVisible = false">
|
||||
<a-tabs size="small">
|
||||
<a-tab-pane key="trend" title="趋势数据">
|
||||
<a-table :columns="trendColumns" :data="currentReportDetail?.trend" size="small"
|
||||
:scroll="{ y: 400 }" row-key="frame" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="json" title="原始 JSON">
|
||||
<pre style="max-height: 400px; overflow: auto">{{
|
||||
JSON.stringify(currentReportDetail?.rawJson, null, 2)
|
||||
}}</pre>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 上传弹窗(已去掉处理结果) -->
|
||||
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading"
|
||||
@ok="handleUpload" @cancel="showUploadModal = false">
|
||||
<a-form :model="uploadForm" layout="vertical">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="onProjectChangeUpload" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload"
|
||||
allow-clear :disabled="!uploadForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" required>
|
||||
<a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="视频文件" required>
|
||||
<a-upload v-model:file-list="uploadForm.fileList" :multiple="uploadMode === 'batch'"
|
||||
:limit="uploadMode === 'batch' ? 10 : 1" accept="video/*" :auto-upload="false"
|
||||
list-type="picture-card" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model="uploadMode" type="button">
|
||||
<a-radio value="single">单文件</a-radio>
|
||||
<a-radio value="batch">批量</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览弹窗(仅保留原始视频) -->
|
||||
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false"
|
||||
@cancel="previewVisible = false">
|
||||
<a-tabs v-model:active-key="activePreviewTab">
|
||||
<a-tab-pane key="video" title="原始视频">
|
||||
<video v-if="previewUrl" :src="previewUrl" controls
|
||||
style="width: 100%; max-height: 60vh; border-radius: 4px"></video>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import { IconUpload } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
getProjectList,
|
||||
getTurbineList
|
||||
} from '@/apis/industrial-image'
|
||||
import {
|
||||
getVideoPage,
|
||||
uploadBatchVideo,
|
||||
uploadSingleVideo,
|
||||
deleteVideo,
|
||||
downloadVideo
|
||||
} from '@/apis/video-monitor'
|
||||
|
||||
/* ---------------- 下拉 & 表单 ---------------- */
|
||||
const projectOptions = ref<{ label: string; value: string }[]>([])
|
||||
const turbineOptions = ref<{ label: string; value: string }[]>([])
|
||||
const turbineOptionsUpload = ref<{ label: string; value: string }[]>([])
|
||||
const typeOptions = [
|
||||
{ label: '净空', value: 'clearance' },
|
||||
{ label: '形变', value: 'deformation' }
|
||||
]
|
||||
|
||||
const filterForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: ''
|
||||
})
|
||||
const uploadForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: '',
|
||||
type: '',
|
||||
fileList: [] as any[]
|
||||
})
|
||||
const uploadMode = ref<'single' | 'batch'>('single')
|
||||
const reportDetailVisible = ref(false)
|
||||
const currentReportDetail = ref<ReportItem | null>(null)
|
||||
|
||||
function openReportDetail(item: ReportItem) {
|
||||
currentReportDetail.value = item
|
||||
reportDetailVisible.value = true
|
||||
}
|
||||
/* ---------------- 列表 ---------------- */
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
|
||||
{ title: '类型', slotName: 'type' },
|
||||
{ title: '上传时间', dataIndex: 'uploadTime' },
|
||||
{ title: '状态', slotName: 'status' },
|
||||
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
|
||||
]
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
|
||||
|
||||
/* ---------------- 顶部 Tab ---------------- */
|
||||
const topTab = ref<'data' | 'report'>('data')
|
||||
|
||||
/* ---------------- 报告筛选 ---------------- */
|
||||
const reportFilterForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: ''
|
||||
})
|
||||
const turbineReportOptions = ref<{ label: string; value: string }[]>([])
|
||||
|
||||
watch(
|
||||
() => reportFilterForm.projectId,
|
||||
async (val) => {
|
||||
reportFilterForm.turbineId = ''
|
||||
turbineReportOptions.value = []
|
||||
if (!val) return
|
||||
const { data } = await getTurbineList({ projectId: val })
|
||||
turbineReportOptions.value = data.map((t: any) => ({
|
||||
label: t.turbineName,
|
||||
value: t.turbineId
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
/* ---------------- 报告网格 ---------------- */
|
||||
interface ReportItem {
|
||||
videoId: string
|
||||
videoName: string
|
||||
videoUrl: string
|
||||
min: string
|
||||
max: string
|
||||
avg: string
|
||||
trend: any[]
|
||||
rawJson: any
|
||||
_activeKey?: string[]
|
||||
}
|
||||
const reportGrid = ref<ReportItem[]>([])
|
||||
|
||||
async function loadReportGrid() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNo: 1,
|
||||
pageSize: 1000,
|
||||
projectId: reportFilterForm.projectId,
|
||||
turbineId: reportFilterForm.turbineId || undefined
|
||||
}
|
||||
const { data } = await getVideoPage(params)
|
||||
const rows = data.filter(
|
||||
(r: any) => r.type === 'clearance' && r.preTreatment
|
||||
)
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
|
||||
reportGrid.value = await Promise.all(
|
||||
rows.map(async (r: any) => {
|
||||
const jsonUrl = `${base}${r.preImagePath}/results.json`
|
||||
const json = await (await fetch(jsonUrl)).json()
|
||||
|
||||
const all = Object.values(json.clearance_history || {})
|
||||
.flat()
|
||||
.filter((v: any) => typeof v === 'number')
|
||||
const min = Math.min(...all)
|
||||
const max = Math.max(...all)
|
||||
const avg = all.reduce((a: number, b: number) => a + b, 0) / all.length
|
||||
|
||||
const { tip0, tip1, tip2 } = json.clearance_history || {}
|
||||
const len = Math.max(tip0?.length ?? 0, tip1?.length ?? 0, tip2?.length ?? 0)
|
||||
const trend = Array.from({ length: len }, (_, i) => ({
|
||||
frame: i + 1,
|
||||
tip0: tip0?.[i]?.toFixed(3) ?? '-',
|
||||
tip1: tip1?.[i]?.toFixed(3) ?? '-',
|
||||
tip2: tip2?.[i]?.toFixed(3) ?? '-'
|
||||
}))
|
||||
|
||||
return {
|
||||
videoId: r.videoId,
|
||||
videoName: r.videoName,
|
||||
videoUrl: new URL(r.videoPath.replace(/^\/+/, ''), base).href,
|
||||
min: min.toFixed(2),
|
||||
max: max.toFixed(2),
|
||||
avg: avg.toFixed(2),
|
||||
trend,
|
||||
rawJson: json,
|
||||
_activeKey: []
|
||||
}
|
||||
})
|
||||
)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDetail(item: ReportItem) {
|
||||
item._activeKey = item._activeKey?.length ? [] : ['detail']
|
||||
}
|
||||
|
||||
/* ---------------- 原列表逻辑 ---------------- */
|
||||
onMounted(async () => {
|
||||
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
|
||||
projectOptions.value = data.map((p: any) => ({
|
||||
label: p.projectName,
|
||||
value: p.projectId
|
||||
}))
|
||||
handleQuery()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => filterForm.projectId,
|
||||
async (val) => {
|
||||
filterForm.turbineId = ''
|
||||
turbineOptions.value = []
|
||||
if (!val) return
|
||||
const { data } = await getTurbineList({ projectId: val })
|
||||
turbineOptions.value = data.map((t: any) => ({
|
||||
label: t.turbineName,
|
||||
value: t.turbineId
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
/* ---------------- 列表查询 ---------------- */
|
||||
function handleQuery() {
|
||||
pagination.current = 1
|
||||
loadTable()
|
||||
}
|
||||
async function loadTable() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
projectId: filterForm.projectId,
|
||||
turbineId: filterForm.turbineId || undefined
|
||||
}
|
||||
const { data } = await getVideoPage(params)
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 上传 ---------------- */
|
||||
const showUploadModal = ref(false)
|
||||
const uploading = ref(false)
|
||||
|
||||
function openUploadModal() {
|
||||
uploadForm.projectId = ''
|
||||
uploadForm.turbineId = ''
|
||||
uploadForm.type = ''
|
||||
uploadForm.fileList = []
|
||||
showUploadModal.value = true
|
||||
}
|
||||
|
||||
async function onProjectChangeUpload(projectId: string) {
|
||||
uploadForm.turbineId = ''
|
||||
turbineOptionsUpload.value = []
|
||||
if (!projectId) return
|
||||
const { data } = await getTurbineList({ projectId })
|
||||
turbineOptionsUpload.value = data.map((t: any) => ({
|
||||
label: t.turbineName,
|
||||
value: t.turbineId
|
||||
}))
|
||||
}
|
||||
|
||||
async function handleUpload() {
|
||||
if (!uploadForm.projectId || !uploadForm.type || !uploadForm.fileList.length) {
|
||||
Message.warning('请完整填写')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const files = uploadForm.fileList.map((f: any) => f.file)
|
||||
if (uploadMode.value === 'single') {
|
||||
await uploadSingleVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files[0]
|
||||
)
|
||||
} else {
|
||||
await uploadBatchVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files
|
||||
)
|
||||
}
|
||||
Message.success('上传成功')
|
||||
showUploadModal.value = false
|
||||
loadTable()
|
||||
// 如果当前在报告页,顺带刷新
|
||||
if (topTab.value === 'report') await loadReportGrid()
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 下载 / 删除 ---------------- */
|
||||
async function handleDownload(row: any) {
|
||||
const url = await downloadVideo(row.videoId)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
async function handleDelete(row: any) {
|
||||
await deleteVideo(row.videoId)
|
||||
Message.success('删除成功')
|
||||
loadTable()
|
||||
if (topTab.value === 'report') await loadReportGrid()
|
||||
}
|
||||
|
||||
/* ---------------- 预览 ---------------- */
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const activePreviewTab = ref<'video' | 'result'>('video')
|
||||
|
||||
function handlePreview(row: any) {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
|
||||
previewVisible.value = true
|
||||
activePreviewTab.value = 'video'
|
||||
}
|
||||
|
||||
/* ---------------- 趋势表格列 ---------------- */
|
||||
const trendColumns = [
|
||||
{ title: '帧', dataIndex: 'frame', width: 70 },
|
||||
{ title: 'tip0 (m)', dataIndex: 'tip0' },
|
||||
{ title: 'tip1 (m)', dataIndex: 'tip1' },
|
||||
{ title: 'tip2 (m)', dataIndex: 'tip2' }
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.raw-data-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.filter-section {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-sections {
|
||||
.project-section {
|
||||
margin-bottom: 32px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px #f0f1f2;
|
||||
padding: 20px;
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.project-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.units-grid {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.unit-card {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: 360px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.unit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.unit-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.videos-list {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.video-item {
|
||||
width: 100px;
|
||||
|
||||
.video-thumbnail {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-top: 4px;
|
||||
|
||||
.video-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 11px;
|
||||
color: #86909c;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.video-status {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-progress {
|
||||
margin-top: 8px;
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-meta-info {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
|
||||
p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.video-name {
|
||||
margin-top: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,497 @@
|
|||
<template>
|
||||
<GiPageLayout>
|
||||
<div class="raw-data-container">
|
||||
<!-- 顶部按钮 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="openUploadModal">
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传视频
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-form :model="filterForm" layout="inline">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions"
|
||||
allow-clear :disabled="!filterForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
|
||||
:scroll="{ y: 'calc(100vh - 380px)' }">
|
||||
<template #type="{ record }">
|
||||
<a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.preTreatment ? 'green' : 'red'">
|
||||
{{ record.preTreatment ? '已处理' : '未处理' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="handlePreview(record)">预览</a-button>
|
||||
<a-button size="mini" @click="handleDownload(record)">下载</a-button>
|
||||
<a-popconfirm content="确认删除?" @ok="handleDelete(record)">
|
||||
<a-button size="mini" status="danger">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 上传弹窗 -->
|
||||
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading"
|
||||
@ok="handleUpload" @cancel="showUploadModal = false">
|
||||
<a-form :model="uploadForm" layout="vertical">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="onProjectChangeUpload" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload"
|
||||
allow-clear :disabled="!uploadForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" required>
|
||||
<a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="视频文件" required>
|
||||
<a-upload v-model:file-list="uploadForm.fileList" :multiple="uploadMode === 'batch'"
|
||||
:limit="uploadMode === 'batch' ? 10 : 1" accept="video/*" :auto-upload="false"
|
||||
list-type="picture-card" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model="uploadMode" type="button">
|
||||
<a-radio value="single">单文件</a-radio>
|
||||
<a-radio value="batch">批量</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</GiPageLayout>
|
||||
<!-- 视频预览弹窗 -->
|
||||
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false"
|
||||
@cancel="previewVisible = false">
|
||||
<a-tabs v-model:active-key="activePreviewTab" @change="activePreviewTab = $event as any">
|
||||
<!-- 原始视频 -->
|
||||
<a-tab-pane key="video" title="原始视频">
|
||||
<video v-if="previewUrl" :src="previewUrl" controls
|
||||
style="width: 100%; max-height: 60vh; border-radius: 4px"></video>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 处理结果 -->
|
||||
<a-tab-pane key="result" title="处理结果">
|
||||
<a-spin :loading="loadingResult">
|
||||
<a-space direction="vertical" size="medium" style="width: 100%">
|
||||
<!-- 图片 -->
|
||||
<img v-if="resultImgUrl" :src="resultImgUrl" style="max-width: 100%; border-radius: 4px"
|
||||
alt="last frame" />
|
||||
|
||||
<!-- JSON 预览 -->
|
||||
<a-card title="results.json" size="small">
|
||||
<pre>{{ JSON.stringify(resultJson, null, 2) }}</pre>
|
||||
</a-card>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import { IconUpload } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
getProjectList,
|
||||
getTurbineList
|
||||
} from '@/apis/industrial-image'
|
||||
import {
|
||||
getVideoPage,
|
||||
uploadBatchVideo,
|
||||
uploadSingleVideo,
|
||||
deleteVideo,
|
||||
downloadVideo
|
||||
} from '@/apis/video-monitor'
|
||||
|
||||
/* ---------------- 下拉 & 表单 ---------------- */
|
||||
const projectOptions = ref<{ label: string; value: string }[]>([])
|
||||
const turbineOptions = ref<{ label: string; value: string }[]>([]) // 筛选用
|
||||
const turbineOptionsUpload = ref<{ label: string; value: string }[]>([]) // 上传弹窗用
|
||||
const typeOptions = [
|
||||
{ label: '净空', value: 'clearance' },
|
||||
{ label: '形变', value: 'deformation' }
|
||||
]
|
||||
|
||||
const filterForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: ''
|
||||
})
|
||||
|
||||
const uploadForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: '',
|
||||
type: '',
|
||||
fileList: [] as any[]
|
||||
})
|
||||
|
||||
const uploadMode = ref<'single' | 'batch'>('single')
|
||||
|
||||
/* ---------------- 列表 ---------------- */
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
|
||||
// { title: '项目', dataIndex: 'projectName' },
|
||||
// { title: '机组', dataIndex: 'turbineName' },
|
||||
{ title: '类型', slotName: 'type' },
|
||||
{ title: '上传时间', dataIndex: 'uploadTime' },
|
||||
{ title: '状态', slotName: 'status' },
|
||||
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
|
||||
]
|
||||
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
|
||||
|
||||
/* ---------------- 控制弹窗 ---------------- */
|
||||
const showUploadModal = ref(false)
|
||||
const uploading = ref(false)
|
||||
|
||||
/* ---------------- 初始化 ---------------- */
|
||||
onMounted(async () => {
|
||||
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
|
||||
projectOptions.value = data.map((p: any) => ({ label: p.projectName, value: p.projectId }))
|
||||
handleQuery()
|
||||
})
|
||||
const activePreviewTab = ref<'video' | 'result'>('video') // 当前标签页
|
||||
const resultImgUrl = ref('')
|
||||
const resultJson = ref<Record<string, any>>({})
|
||||
const loadingResult = ref(false)
|
||||
|
||||
async function loadResultFiles(row: any) {
|
||||
if (!row.preTreatment) return
|
||||
loadingResult.value = true
|
||||
try {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
// 图片
|
||||
resultImgUrl.value = `${base}${row.preImagePath}/last_frame.jpg`
|
||||
|
||||
// JSON
|
||||
const jsonUrl = `${base}${row.preImagePath}/results.json`
|
||||
const res = await fetch(jsonUrl)
|
||||
resultJson.value = await res.json()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
resultJson.value = {}
|
||||
} finally {
|
||||
loadingResult.value = false
|
||||
}
|
||||
console.log('result', resultImgUrl.value)
|
||||
}
|
||||
/* 项目 -> 机组(筛选) */
|
||||
watch(
|
||||
() => filterForm.projectId,
|
||||
async (val) => {
|
||||
filterForm.turbineId = ''
|
||||
turbineOptions.value = []
|
||||
if (!val) return
|
||||
const { data } = await getTurbineList({ projectId: val })
|
||||
turbineOptions.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
)
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
|
||||
function handlePreview(row: any) {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
|
||||
previewVisible.value = true
|
||||
activePreviewTab.value = 'video' // 默认先显示视频
|
||||
if (row.preTreatment) {
|
||||
loadResultFiles(row) // 预加载结果
|
||||
}
|
||||
}
|
||||
/* 项目 -> 机组(上传弹窗) */
|
||||
async function onProjectChangeUpload(projectId: string) {
|
||||
uploadForm.turbineId = ''
|
||||
turbineOptionsUpload.value = []
|
||||
if (!projectId) return
|
||||
const { data } = await getTurbineList({ projectId })
|
||||
turbineOptionsUpload.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
|
||||
/* ---------------- 查询 ---------------- */
|
||||
function handleQuery() {
|
||||
pagination.current = 1
|
||||
loadTable()
|
||||
}
|
||||
async function loadTable() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
projectId: filterForm.projectId,
|
||||
turbineId: filterForm.turbineId || undefined
|
||||
}
|
||||
const { data } = await getVideoPage(params)
|
||||
console.log(data)
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 上传 ---------------- */
|
||||
function openUploadModal() {
|
||||
uploadForm.projectId = ''
|
||||
uploadForm.turbineId = ''
|
||||
uploadForm.type = ''
|
||||
uploadForm.fileList = []
|
||||
showUploadModal.value = true
|
||||
}
|
||||
async function handleUpload() {
|
||||
if (!uploadForm.projectId || !uploadForm.type || !uploadForm.fileList.length) {
|
||||
Message.warning('请完整填写')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const files = uploadForm.fileList.map((f: any) => f.file)
|
||||
if (uploadMode.value === 'single') {
|
||||
await uploadSingleVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files[0]
|
||||
)
|
||||
} else {
|
||||
await uploadBatchVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files
|
||||
)
|
||||
}
|
||||
Message.success('上传成功')
|
||||
showUploadModal.value = false
|
||||
loadTable()
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 下载 / 删除 ---------------- */
|
||||
async function handleDownload(row: any) {
|
||||
const url = await downloadVideo(row.videoId)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
async function handleDelete(row: any) {
|
||||
await deleteVideo(row.videoId)
|
||||
Message.success('删除成功')
|
||||
loadTable()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.raw-data-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.filter-section {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-sections {
|
||||
.project-section {
|
||||
margin-bottom: 32px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px #f0f1f2;
|
||||
padding: 20px;
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.project-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.units-grid {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.unit-card {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: 360px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.unit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.unit-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.videos-list {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.video-item {
|
||||
width: 100px;
|
||||
|
||||
.video-thumbnail {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-top: 4px;
|
||||
|
||||
.video-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 11px;
|
||||
color: #86909c;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.video-status {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-progress {
|
||||
margin-top: 8px;
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-meta-info {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
|
||||
p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,497 @@
|
|||
<template>
|
||||
<GiPageLayout>
|
||||
<div class="raw-data-container">
|
||||
<!-- 顶部按钮 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="openUploadModal">
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传视频
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-form :model="filterForm" layout="inline">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions"
|
||||
allow-clear :disabled="!filterForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
|
||||
:scroll="{ y: 'calc(100vh - 380px)' }">
|
||||
<template #type="{ record }">
|
||||
<a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.preTreatment ? 'green' : 'red'">
|
||||
{{ record.preTreatment ? '已处理' : '未处理' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="handlePreview(record)">预览</a-button>
|
||||
<a-button size="mini" @click="handleDownload(record)">下载</a-button>
|
||||
<a-popconfirm content="确认删除?" @ok="handleDelete(record)">
|
||||
<a-button size="mini" status="danger">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 上传弹窗 -->
|
||||
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading"
|
||||
@ok="handleUpload" @cancel="showUploadModal = false">
|
||||
<a-form :model="uploadForm" layout="vertical">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="onProjectChangeUpload" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload"
|
||||
allow-clear :disabled="!uploadForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" required>
|
||||
<a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="视频文件" required>
|
||||
<a-upload v-model:file-list="uploadForm.fileList" :multiple="uploadMode === 'batch'"
|
||||
:limit="uploadMode === 'batch' ? 10 : 1" accept="video/*" :auto-upload="false"
|
||||
list-type="picture-card" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model="uploadMode" type="button">
|
||||
<a-radio value="single">单文件</a-radio>
|
||||
<a-radio value="batch">批量</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</GiPageLayout>
|
||||
<!-- 视频预览弹窗 -->
|
||||
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false"
|
||||
@cancel="previewVisible = false">
|
||||
<a-tabs v-model:active-key="activePreviewTab" @change="activePreviewTab = $event as any">
|
||||
<!-- 原始视频 -->
|
||||
<a-tab-pane key="video" title="原始视频">
|
||||
<video v-if="previewUrl" :src="previewUrl" controls
|
||||
style="width: 100%; max-height: 60vh; border-radius: 4px"></video>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 处理结果 -->
|
||||
<a-tab-pane key="result" title="处理结果">
|
||||
<a-spin :loading="loadingResult">
|
||||
<a-space direction="vertical" size="medium" style="width: 100%">
|
||||
<!-- 图片 -->
|
||||
<img v-if="resultImgUrl" :src="resultImgUrl" style="max-width: 100%; border-radius: 4px"
|
||||
alt="last frame" />
|
||||
|
||||
<!-- JSON 预览 -->
|
||||
<a-card title="results.json" size="small">
|
||||
<pre>{{ JSON.stringify(resultJson, null, 2) }}</pre>
|
||||
</a-card>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import { IconUpload } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
getProjectList,
|
||||
getTurbineList
|
||||
} from '@/apis/industrial-image'
|
||||
import {
|
||||
getVideoPage,
|
||||
uploadBatchVideo,
|
||||
uploadSingleVideo,
|
||||
deleteVideo,
|
||||
downloadVideo
|
||||
} from '@/apis/video-monitor'
|
||||
|
||||
/* ---------------- 下拉 & 表单 ---------------- */
|
||||
const projectOptions = ref<{ label: string; value: string }[]>([])
|
||||
const turbineOptions = ref<{ label: string; value: string }[]>([]) // 筛选用
|
||||
const turbineOptionsUpload = ref<{ label: string; value: string }[]>([]) // 上传弹窗用
|
||||
const typeOptions = [
|
||||
{ label: '净空', value: 'clearance' },
|
||||
{ label: '形变', value: 'deformation' }
|
||||
]
|
||||
|
||||
const filterForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: ''
|
||||
})
|
||||
|
||||
const uploadForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: '',
|
||||
type: '',
|
||||
fileList: [] as any[]
|
||||
})
|
||||
|
||||
const uploadMode = ref<'single' | 'batch'>('single')
|
||||
|
||||
/* ---------------- 列表 ---------------- */
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
|
||||
// { title: '项目', dataIndex: 'projectName' },
|
||||
// { title: '机组', dataIndex: 'turbineName' },
|
||||
{ title: '类型', slotName: 'type' },
|
||||
{ title: '上传时间', dataIndex: 'uploadTime' },
|
||||
{ title: '状态', slotName: 'status' },
|
||||
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
|
||||
]
|
||||
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
|
||||
|
||||
/* ---------------- 控制弹窗 ---------------- */
|
||||
const showUploadModal = ref(false)
|
||||
const uploading = ref(false)
|
||||
|
||||
/* ---------------- 初始化 ---------------- */
|
||||
onMounted(async () => {
|
||||
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
|
||||
projectOptions.value = data.map((p: any) => ({ label: p.projectName, value: p.projectId }))
|
||||
handleQuery()
|
||||
})
|
||||
const activePreviewTab = ref<'video' | 'result'>('video') // 当前标签页
|
||||
const resultImgUrl = ref('')
|
||||
const resultJson = ref<Record<string, any>>({})
|
||||
const loadingResult = ref(false)
|
||||
|
||||
async function loadResultFiles(row: any) {
|
||||
if (!row.preTreatment) return
|
||||
loadingResult.value = true
|
||||
try {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
// 图片
|
||||
resultImgUrl.value = `${base}${row.preImagePath}/last_frame.jpg`
|
||||
|
||||
// JSON
|
||||
const jsonUrl = `${base}${row.preImagePath}/results.json`
|
||||
const res = await fetch(jsonUrl)
|
||||
resultJson.value = await res.json()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
resultJson.value = {}
|
||||
} finally {
|
||||
loadingResult.value = false
|
||||
}
|
||||
console.log('result', resultImgUrl.value)
|
||||
}
|
||||
/* 项目 -> 机组(筛选) */
|
||||
watch(
|
||||
() => filterForm.projectId,
|
||||
async (val) => {
|
||||
filterForm.turbineId = ''
|
||||
turbineOptions.value = []
|
||||
if (!val) return
|
||||
const { data } = await getTurbineList({ projectId: val })
|
||||
turbineOptions.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
)
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
|
||||
function handlePreview(row: any) {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
|
||||
previewVisible.value = true
|
||||
activePreviewTab.value = 'video' // 默认先显示视频
|
||||
if (row.preTreatment) {
|
||||
loadResultFiles(row) // 预加载结果
|
||||
}
|
||||
}
|
||||
/* 项目 -> 机组(上传弹窗) */
|
||||
async function onProjectChangeUpload(projectId: string) {
|
||||
uploadForm.turbineId = ''
|
||||
turbineOptionsUpload.value = []
|
||||
if (!projectId) return
|
||||
const { data } = await getTurbineList({ projectId })
|
||||
turbineOptionsUpload.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
|
||||
/* ---------------- 查询 ---------------- */
|
||||
function handleQuery() {
|
||||
pagination.current = 1
|
||||
loadTable()
|
||||
}
|
||||
async function loadTable() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
projectId: filterForm.projectId,
|
||||
turbineId: filterForm.turbineId || undefined
|
||||
}
|
||||
const { data } = await getVideoPage(params)
|
||||
console.log(data)
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 上传 ---------------- */
|
||||
function openUploadModal() {
|
||||
uploadForm.projectId = ''
|
||||
uploadForm.turbineId = ''
|
||||
uploadForm.type = ''
|
||||
uploadForm.fileList = []
|
||||
showUploadModal.value = true
|
||||
}
|
||||
async function handleUpload() {
|
||||
if (!uploadForm.projectId || !uploadForm.type || !uploadForm.fileList.length) {
|
||||
Message.warning('请完整填写')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const files = uploadForm.fileList.map((f: any) => f.file)
|
||||
if (uploadMode.value === 'single') {
|
||||
await uploadSingleVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files[0]
|
||||
)
|
||||
} else {
|
||||
await uploadBatchVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files
|
||||
)
|
||||
}
|
||||
Message.success('上传成功')
|
||||
showUploadModal.value = false
|
||||
loadTable()
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 下载 / 删除 ---------------- */
|
||||
async function handleDownload(row: any) {
|
||||
const url = await downloadVideo(row.videoId)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
async function handleDelete(row: any) {
|
||||
await deleteVideo(row.videoId)
|
||||
Message.success('删除成功')
|
||||
loadTable()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.raw-data-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.filter-section {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-sections {
|
||||
.project-section {
|
||||
margin-bottom: 32px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px #f0f1f2;
|
||||
padding: 20px;
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.project-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.units-grid {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.unit-card {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: 360px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.unit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.unit-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.videos-list {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.video-item {
|
||||
width: 100px;
|
||||
|
||||
.video-thumbnail {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-top: 4px;
|
||||
|
||||
.video-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 11px;
|
||||
color: #86909c;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.video-status {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-progress {
|
||||
margin-top: 8px;
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-meta-info {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
|
||||
p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue