This commit is contained in:
crushing1111 2025-08-13 16:58:30 +08:00
commit b504f7f755
12 changed files with 2237 additions and 1650 deletions

View File

@ -100,9 +100,12 @@ export const equipmentProcurementApi = {
}, },
/** /**
* *
*/ */
receiveGoods: (equipmentId: string, data: ReceiptRequest) => { receiveGoods: (equipmentId: string, data: ReceiptRequest) => {
console.log('📦 收货API被调用设备ID:', equipmentId)
console.log('📦 收货数据:', data)
return http.post<ApiRes<null>>(`/equipment/procurement/receipt/${equipmentId}`, data) return http.post<ApiRes<null>>(`/equipment/procurement/receipt/${equipmentId}`, data)
}, },

View File

@ -410,33 +410,63 @@ export interface EquipmentApprovalResp {
} }
/** /**
* *
*/ */
export interface ReceiptRequest { export interface ReceiptRequest {
/** 收货时间 */ // 收货特有信息
receiptTime: string receiptTime: string
/** 收货人 */
receiptPerson: string receiptPerson: string
/** 收货数量 */
receiptQuantity: number receiptQuantity: number
/** 收货备注 */
receiptRemark?: string receiptRemark?: string
/** 外观检查结果 */
appearanceCheck: string appearanceCheck: string
/** 功能测试结果 */
functionTest: string functionTest: string
/** 包装完整性 */
packageIntegrity: string packageIntegrity: string
/** 配件完整性 */
accessoryIntegrity: string accessoryIntegrity: string
/** 检查结果 */
checkResult: 'PASS' | 'FAIL' | 'CONDITIONAL' checkResult: 'PASS' | 'FAIL' | 'CONDITIONAL'
/** 检查备注 */
checkRemark?: string checkRemark?: string
/** 入库位置 */
storageLocation: string storageLocation: string
/** 库管员 */
storageManager: string storageManager: string
// 设备基本信息(从采购数据继承)
equipmentName?: string
equipmentModel?: string
equipmentType?: string
equipmentSn?: string
brand?: string
specification?: string
assetCode?: string
// 采购信息(从采购数据继承)
purchaseOrder?: string
supplierName?: string
purchasePrice?: number
purchaseTime?: string
quantity?: number
unitPrice?: number
totalPrice?: number
// 入库信息
inStockTime?: string
physicalLocation?: string
locationStatus?: string
responsiblePerson?: string
inventoryBarcode?: string
// 状态信息
equipmentStatus?: string
useStatus?: string
healthStatus?: string
receiptStatus?: string
// 其他管理信息
depreciationMethod?: string
depreciationYears?: number
salvageValue?: number
currentNetValue?: number
// 系统字段
createTime?: string
updateTime?: string
} }
/** /**

View File

@ -18,6 +18,8 @@ export interface ProjectResp {
projectCategory?: string // 项目类型/服务 projectCategory?: string // 项目类型/服务
projectManagerId?: string // 项目经理ID projectManagerId?: string // 项目经理ID
projectManagerName?: string // 项目经理姓名 projectManagerName?: string // 项目经理姓名
projectOrigin?: string // 项目来源
projectStaff?: string[] // 施工人员 projectStaff?: string[] // 施工人员
startDate?: string // 开始日期 startDate?: string // 开始日期
endDate?: string // 结束日期 endDate?: string // 结束日期
@ -28,10 +30,10 @@ export interface ProjectResp {
coverUrl?: string // 封面URL coverUrl?: string // 封面URL
createDt?: Date createDt?: Date
updateDt?: Date updateDt?: Date
// 为了保持向后兼容,添加一些别名字段 // 为了保持向后兼容,添加一些别名字段
id?: string // projectId的别名 id?: string // projectId的别名
fieldName?: string // farmName的别名 fieldName?: string // farmName的别名
fieldLocation?: string // farmAddress的别名 fieldLocation?: string // farmAddress的别名
commissionUnit?: string // client的别名 commissionUnit?: string // client的别名
commissionContact?: string // clientContact的别名 commissionContact?: string // clientContact的别名
@ -85,7 +87,7 @@ export interface TaskQuery {
status?: string status?: string
} }
export interface TaskPageQuery extends TaskQuery, PageQuery {} export interface TaskPageQuery extends TaskQuery, PageQuery {}
// ==================== 人员调度相关类型 ==================== // ==================== 人员调度相关类型 ====================
@ -339,4 +341,4 @@ export interface PageRes<T> {
total: number total: number
page: number page: number
pageSize: number pageSize: number
} }

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue') import('vue')
} }

View File

@ -7,7 +7,70 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default']
ApprovalMessageItem: typeof import('./../components/NotificationCenter/ApprovalMessageItem.vue')['default']
Avatar: typeof import('./../components/Avatar/index.vue')['default']
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
CellCopy: typeof import('./../components/CellCopy/index.vue')['default']
Chart: typeof import('./../components/Chart/index.vue')['default']
CircularProgress: typeof import('./../components/CircularProgress/index.vue')['default']
ColumnSetting: typeof import('./../components/GiTable/src/components/ColumnSetting.vue')['default']
CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default']
CronModal: typeof import('./../components/GenCron/CronModal/index.vue')['default']
DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default']
DayForm: typeof import('./../components/GenCron/CronForm/component/day-form.vue')['default']
FilePreview: typeof import('./../components/FilePreview/index.vue')['default']
GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default']
GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default']
GiCellStatus: typeof import('./../components/GiCell/GiCellStatus.vue')['default']
GiCellTag: typeof import('./../components/GiCell/GiCellTag.vue')['default']
GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default']
GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default']
GiDot: typeof import('./../components/GiDot/index.tsx')['default']
GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default']
GiFooter: typeof import('./../components/GiFooter/index.vue')['default']
GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default']
GiIconBox: typeof import('./../components/GiIconBox/index.vue')['default']
GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default']
GiIframe: typeof import('./../components/GiIframe/index.vue')['default']
GiOption: typeof import('./../components/GiOption/index.vue')['default']
GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default']
GiPageLayout: typeof import('./../components/GiPageLayout/index.vue')['default']
GiSpace: typeof import('./../components/GiSpace/index.vue')['default']
GiSplitButton: typeof import('./../components/GiSplitButton/index.vue')['default']
GiSplitPane: typeof import('./../components/GiSplitPane/index.vue')['default']
GiSplitPaneFlexibleBox: typeof import('./../components/GiSplitPane/components/GiSplitPaneFlexibleBox.vue')['default']
GiSvgIcon: typeof import('./../components/GiSvgIcon/index.vue')['default']
GiTable: typeof import('./../components/GiTable/src/GiTable.vue')['default']
GiTag: typeof import('./../components/GiTag/index.tsx')['default']
GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default']
HourForm: typeof import('./../components/GenCron/CronForm/component/hour-form.vue')['default']
Icon403: typeof import('./../components/icons/Icon403.vue')['default']
Icon404: typeof import('./../components/icons/Icon404.vue')['default']
Icon500: typeof import('./../components/icons/Icon500.vue')['default']
IconBorders: typeof import('./../components/icons/IconBorders.vue')['default']
IconTableSize: typeof import('./../components/icons/IconTableSize.vue')['default']
IconTreeAdd: typeof import('./../components/icons/IconTreeAdd.vue')['default']
IconTreeReduce: typeof import('./../components/icons/IconTreeReduce.vue')['default']
ImageImport: typeof import('./../components/ImageImport/index.vue')['default']
ImageImportWizard: typeof import('./../components/ImageImportWizard/index.vue')['default']
IndustrialImageList: typeof import('./../components/IndustrialImageList/index.vue')['default']
JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default']
MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default']
MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default']
NotificationCenter: typeof import('./../components/NotificationCenter/index.vue')['default']
ParentView: typeof import('./../components/ParentView/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
TextCopy: typeof import('./../components/TextCopy/index.vue')['default']
TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default']
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
Verify: typeof import('./../components/Verify/index.vue')['default']
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']
VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default']
WeekForm: typeof import('./../components/GenCron/CronForm/component/week-form.vue')['default']
YearForm: typeof import('./../components/GenCron/CronForm/component/year-form.vue')['default']
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
<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>

View File

@ -0,0 +1,669 @@
<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>

View File

@ -0,0 +1,116 @@
<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>

View File

@ -0,0 +1,557 @@
<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>

View File

@ -2,19 +2,21 @@
项目管理页面 项目管理页面
已完成接口对接: 已完成接口对接:
1. 项目列表查询 (listProject) - 支持分页和条件查询 1. 项目列表查询 (listProject) - 支持分页和条件查询
2. 项目新增 (addProject) 2. 项目新增 (addProject)
3. 项目修改 (updateProject) 3. 项目修改 (updateProject)
4. 项目删除 (deleteProject) 4. 项目删除 (deleteProject)
5. 项目导出 (exportProject) 5. 项目导出 (exportProject)
6. 项目导入 (importProject) 6. 项目导入 (importProject)
所有API调用都已添加错误处理和类型安全检查 所有API调用都已添加错误处理和类型安全检查
--> -->
<template> <template>
<GiPageLayout> <GiPageLayout>
<GiTable row-key="id" :data="dataList" :columns="tableColumns" :loading="loading" <GiTable
row-key="id" :data="dataList" :columns="tableColumns" :loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }" :pagination="pagination" :disabled-tools="['size']" :scroll="{ x: '100%', y: '100%', minWidth: 1500 }" :pagination="pagination" :disabled-tools="['size']"
@page-change="onPageChange" @page-size-change="onPageSizeChange" @refresh="search"> @page-change="onPageChange" @page-size-change="onPageSizeChange" @refresh="search"
>
<template #top> <template #top>
<GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset"> <GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset">
</GiForm> </GiForm>
@ -71,10 +73,14 @@
</GiTable> </GiTable>
<!-- 新增/编辑项目弹窗 --> <!-- 新增/编辑项目弹窗 -->
<a-modal v-model:visible="addModalVisible" :title="modalTitle" @cancel="resetForm" <a-modal
:ok-button-props="{ loading: submitLoading }" @ok="handleSubmit" width="800px" modal-class="project-form-modal"> v-model:visible="addModalVisible" :title="modalTitle" :ok-button-props="{ loading: submitLoading }"
<a-form ref="formRef" :model="form" :rules="formRules" layout="vertical" width="800px" modal-class="project-form-modal" @cancel="resetForm" @ok="handleSubmit"
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"> >
<a-form
ref="formRef" :model="form" :rules="formRules" layout="vertical"
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"
>
<!-- 基本信息 --> <!-- 基本信息 -->
<a-divider orientation="left">基本信息</a-divider> <a-divider orientation="left">基本信息</a-divider>
<a-row :gutter="16"> <a-row :gutter="16">
@ -88,10 +94,12 @@
<a-input v-model="form.farmAddress" placeholder="请输入地址" /> <a-input v-model="form.farmAddress" placeholder="请输入地址" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col><a-button size="mini" @click="() => { Message.info(`待开发`) }"> <a-col>
<a-button size="mini" @click="() => { Message.info(`待开发`) }">
<template #icon><icon-location /></template> <template #icon><icon-location /></template>
地图选点 地图选点
</a-button></a-col> </a-button>
</a-col>
</a-row> </a-row>
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
@ -108,7 +116,7 @@
<a-col :span="12"> <a-col :span="12">
<a-form-item field="inspectionUnit" label="业主"> <a-form-item field="inspectionUnit" label="业主">
<a-input v-model="form.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (form.farmName = val)" /> <a-input v-model="form.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (form.farmName = val)" />
<!--风场名称同步业主 --> <!-- 风场名称同步业主 -->
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
@ -145,10 +153,133 @@
</a-col> </a-col>
</a-row> </a-row>
<a-row :gutter="16"> <a-row :gutter="16">
<a-form-item field="projectContent" label="项目内容"> <a-col :span="12">
<a-textarea v-model="form.coverUrl" placeholder="请输入项目内容" :rows="4" /> <a-form-item field="projectOrigin" label="项目来源" :rules="[{ required: true, message: '请输入项目来源' }]">
</a-form-item> <a-input v-model="form.projectOrigin" placeholder="请输入项目来源" />
</a-form-item>
</a-col>
</a-row> </a-row>
<a-divider orientation="left">任务设置</a-divider>
<div class="mb-2">
<a-button type="dashed" size="small" @click="addTask">
<template #icon><icon-plus /></template>
新增任务
</a-button>
</div>
<div v-if="form.tasks.length === 0" class="text-gray-500 mb-2">暂无任务请点击新增任务</div>
<a-space direction="vertical" fill>
<a-card v-for="(task, tIndex) in form.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="addSubtask(tIndex)">新增子任务</a-button>
<a-button size="mini" status="danger" @click="removeTask(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="removeSubtask(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-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item field="status" label="项目状态"> <a-form-item field="status" label="项目状态">
@ -198,7 +329,6 @@
</a-col> </a-col>
</a-row> </a-row>
<a-divider orientation="middle">地图</a-divider> <a-divider orientation="middle">地图</a-divider>
</a-form> </a-form>
</a-modal> </a-modal>
@ -221,53 +351,53 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Message, Modal } from '@arco-design/web-vue' import { Message, Modal } from '@arco-design/web-vue'
import { addProject, deleteProject, listProject, updateProject, exportProject, importProject } from '@/apis/project' import type { TableColumnData } from '@arco-design/web-vue'
import TurbineGrid from './TurbineGrid.vue'
import { addProject, deleteProject, exportProject, importProject, listProject, updateProject } from '@/apis/project'
import { isMobile } from '@/utils' import { isMobile } from '@/utils'
import has from '@/utils/has'
import http from '@/utils/http' import http from '@/utils/http'
import type { ColumnItem } from '@/components/GiForm' import type { ColumnItem } from '@/components/GiForm'
import type { TableColumnData } from '@arco-design/web-vue' import type { ProjectPageQuery } from '@/apis/project/type'
import type { ProjectResp, ProjectPageQuery } from '@/apis/project/type' import type * as T from '@/apis/project/type'
import * as T from '@/apis/project/type'
import TurbineGrid from './TurbineGrid.vue'
defineOptions({ name: 'ProjectManagement' }) defineOptions({ name: 'ProjectManagement' })
// (API) // (API)
const PROJECT_STATUS = { const PROJECT_STATUS = {
NOT_STARTED: 0, // / NOT_STARTED: 0, // /
IN_PROGRESS: 1, // IN_PROGRESS: 1, //
COMPLETED: 2, // COMPLETED: 2, //
} as const } as const
// //
const PROJECT_STATUS_MAP = { const PROJECT_STATUS_MAP = {
0: '待施工', 0: '待施工',
1: '施工中', 1: '施工中',
2: '已完成' 2: '已完成',
} as const } as const
// //
const PROJECT_STATUS_OPTIONS = [ const PROJECT_STATUS_OPTIONS = [
{ label: '待施工', value: 0 }, { label: '待施工', value: 0 },
{ label: '施工中', value: 1 }, { label: '施工中', value: 1 },
{ label: '已完成', value: 2 } { label: '已完成', value: 2 },
] ]
// //
const PROJECT_CATEGORY = { const PROJECT_CATEGORY = {
EXTERNAL_WORK: '外部工作', EXTERNAL_WORK: '外部工作',
INTERNAL_PROJECT: '内部项目', INTERNAL_PROJECT: '内部项目',
TECHNICAL_SERVICE: '技术服务' TECHNICAL_SERVICE: '技术服务',
} as const } as const
// //
const PROJECT_CATEGORY_OPTIONS = [ const PROJECT_CATEGORY_OPTIONS = [
{ label: PROJECT_CATEGORY.EXTERNAL_WORK, value: PROJECT_CATEGORY.EXTERNAL_WORK }, { label: PROJECT_CATEGORY.EXTERNAL_WORK, value: PROJECT_CATEGORY.EXTERNAL_WORK },
{ label: PROJECT_CATEGORY.INTERNAL_PROJECT, value: PROJECT_CATEGORY.INTERNAL_PROJECT }, { label: PROJECT_CATEGORY.INTERNAL_PROJECT, value: PROJECT_CATEGORY.INTERNAL_PROJECT },
{ label: PROJECT_CATEGORY.TECHNICAL_SERVICE, value: PROJECT_CATEGORY.TECHNICAL_SERVICE } { label: PROJECT_CATEGORY.TECHNICAL_SERVICE, value: PROJECT_CATEGORY.TECHNICAL_SERVICE },
] ]
const router = useRouter() const router = useRouter()
@ -280,12 +410,12 @@ const currentId = ref<string | null>(null)
const fileList = ref([]) const fileList = ref([])
const dataList = ref<T.ProjectResp[]>([]) const dataList = ref<T.ProjectResp[]>([])
const userLoading = ref(false) const userLoading = ref(false)
const userOptions = ref<{ label: string; value: string }[]>([]) const userOptions = ref<{ label: string, value: string }[]>([])
let searchForm = reactive<Partial<ProjectPageQuery>>({ const searchForm = reactive<Partial<ProjectPageQuery>>({
projectName: '', projectName: '',
status: undefined, status: undefined,
fieldName: '', // 使fieldNameAPI使 fieldName: '', // 使fieldNameAPI使
}) })
const queryFormColumns: ColumnItem[] = reactive([ const queryFormColumns: ColumnItem[] = reactive([
@ -320,28 +450,48 @@ const queryFormColumns: ColumnItem[] = reactive([
]) ])
const form = reactive({ const form = reactive({
projectId: '', // id projectId: '', // id
projectName: '', // projectName: '', //
projectManagerId: '', // id projectManagerId: '', // id
client: '', // client: '', //
clientContact: '', // clientContact: '', //
clientPhone: '', // clientPhone: '', //
inspectionUnit: '', // inspectionUnit: '', //
inspectionContact: '', // inspectionContact: '', //
inspectionPhone: '', // inspectionPhone: '', //
farmName: '', // farmName: '', //
farmAddress: '', // farmAddress: '', //
scale: '', // projectOrigin: '', //
turbineModel: '', // scale: '', //
status: '', // 01234 turbineModel: '', //
startDate: '', // status: '', // 01234
endDate: '', // startDate: '', //
coverUrl: '', // endDate: '', //
// coverUrl: '', // 使
constructionTeamLeaderId: '', // id constructionTeamLeaderId: '', // id
constructorIds: '', // id constructorIds: '', // id
qualityOfficerId: '', // id qualityOfficerId: '', // id
auditorId: '', // id auditorId: '', // id
turbineList: [] as { id: number; turbineNo: string; lat?: number; lng?: number; status: 0 | 1 | 2 }[], // //
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 }[], //
}) })
const pagination = reactive({ const pagination = reactive({
@ -350,7 +500,7 @@ const pagination = reactive({
total: 0, total: 0,
showTotal: true, showTotal: true,
showJumper: true, showJumper: true,
showPageSize: true showPageSize: true,
}) })
const openMapModal = (item: any) => { const openMapModal = (item: any) => {
Message.info(`地图选点功能待开发,当前机组编号:${item.turbineNo}`) Message.info(`地图选点功能待开发,当前机组编号:${item.turbineNo}`)
@ -384,70 +534,70 @@ const tableColumns = ref<TableColumnData[]>([
slotName: 'fieldInfo', slotName: 'fieldInfo',
minWidth: 180, minWidth: 180,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
slotName: 'status', slotName: 'status',
align: 'center', align: 'center',
width: 100 width: 100,
}, },
{ {
title: '委托单位', title: '委托单位',
dataIndex: 'commissionUnit', dataIndex: 'commissionUnit',
minWidth: 140, minWidth: 140,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '委托单位联系人/电话', title: '委托单位联系人/电话',
slotName: 'commissionInfo', slotName: 'commissionInfo',
minWidth: 160, minWidth: 160,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '业主', title: '业主',
dataIndex: 'inspectionUnit', dataIndex: 'inspectionUnit',
minWidth: 140, minWidth: 140,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '业主联系人/电话', title: '业主联系人/电话',
slotName: 'inspectionInfo', slotName: 'inspectionInfo',
minWidth: 160, minWidth: 160,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '项目规模', title: '项目规模',
dataIndex: 'projectScale', dataIndex: 'projectScale',
width: 100, width: 100,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '机组型号', title: '机组型号',
dataIndex: 'orgNumber', dataIndex: 'orgNumber',
width: 100, width: 100,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '项目经理/施工人员', title: '项目经理/施工人员',
slotName: 'projectManager', slotName: 'projectManager',
minWidth: 160, minWidth: 160,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '项目周期', title: '项目周期',
slotName: 'projectPeriod', slotName: 'projectPeriod',
minWidth: 180, minWidth: 180,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '操作', title: '操作',
@ -492,7 +642,7 @@ const fetchData = async () => {
const params: ProjectPageQuery = { const params: ProjectPageQuery = {
...searchForm, ...searchForm,
page: pagination.current, page: pagination.current,
size: pagination.pageSize size: pagination.pageSize,
} }
const res = await listProject(params) const res = await listProject(params)
@ -516,9 +666,9 @@ const fetchData = async () => {
projectManager: item.projectManagerName, projectManager: item.projectManagerName,
projectScale: item.scale, projectScale: item.scale,
// //
projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : [] projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : [],
}; }
return mappedItem; return mappedItem
}) })
// APItotal使 // APItotal使
@ -553,7 +703,7 @@ const reset = () => {
// //
Object.assign(searchForm, { Object.assign(searchForm, {
projectName: '', projectName: '',
fieldName: '', // 使fieldNameAPI使 fieldName: '', // 使fieldNameAPI使
status: undefined, status: undefined,
}) })
@ -576,27 +726,29 @@ const onPageSizeChange = (pageSize: number) => {
const resetForm = () => { const resetForm = () => {
// //
Object.assign(form, { Object.assign(form, {
projectId: '', // id projectId: '', // id
projectName: '', // projectName: '', //
projectManagerId: '', // id projectManagerId: '', // id
client: '', // client: '', //
clientContact: '', // clientContact: '', //
clientPhone: '', // clientPhone: '', //
inspectionUnit: '', // inspectionUnit: '', //
inspectionContact: '', // inspectionContact: '', //
inspectionPhone: '', // inspectionPhone: '', //
farmName: '', // farmName: '', //
farmAddress: '', // farmAddress: '', //
scale: '', // projectOrigin: '', //
turbineModel: '', // scale: '', //
status: 0, // 01234 turbineModel: '', //
startDate: '', // status: 0, // 01234
endDate: '', // startDate: '', //
coverUrl: '', // endDate: '', //
// coverUrl: '', //
constructionTeamLeaderId: '', // id constructionTeamLeaderId: '', // id
constructorIds: '', // id constructorIds: '', // id
qualityOfficerId: '', // id qualityOfficerId: '', // id
auditorId: '' // id auditorId: '', // id
tasks: [],
}) })
isEdit.value = false isEdit.value = false
@ -607,21 +759,35 @@ const openAddModal = () => {
resetForm() resetForm()
addModalVisible.value = true addModalVisible.value = true
} }
// /使
const addTask = () => {
;(form.tasks as any[]).push({ taskName: '', taskCode: '', mainUserId: undefined, planStartDate: '', planEndDate: '', scales: undefined, taskGroupId: undefined, children: [] })
}
const removeTask = (index: number) => {
;(form.tasks as any[]).splice(index, 1)
}
const addSubtask = (parentIndex: number) => {
const list = (form.tasks as any[])
if (!list[parentIndex].children) list[parentIndex].children = []
list[parentIndex].children!.push({ taskName: '', taskCode: '', mainUserId: undefined, planStartDate: '', planEndDate: '', scales: undefined, taskGroupId: undefined })
}
const removeSubtask = (parentIndex: number, index: number) => {
const list = (form.tasks as any[])
list[parentIndex].children!.splice(index, 1)
}
const openEditModal = (record: T.ProjectResp) => { const openEditModal = (record: T.ProjectResp) => {
isEdit.value = true isEdit.value = true
currentId.value = record.id || record.projectId || null currentId.value = record.id || record.projectId || null
// // tasksturbineList
Object.keys(form).forEach(key => { resetForm()
// @ts-ignore
form[key] = ''
})
// //
Object.keys(form).forEach(key => { Object.keys(form).forEach((key) => {
if (key in record && record[key as keyof T.ProjectResp] !== undefined) { if (key in record && record[key as keyof T.ProjectResp] !== undefined) {
// @ts-ignore - // @ts-expect-error -
form[key] = record[key as keyof T.ProjectResp] form[key] = record[key as keyof T.ProjectResp]
} }
}) })
@ -645,6 +811,7 @@ const openEditModal = (record: T.ProjectResp) => {
// //
const formRules = { const formRules = {
projectName: [{ required: true, message: '请输入项目名称' }], projectName: [{ required: true, message: '请输入项目名称' }],
projectOrigin: [{ required: true, message: '请输入项目来源' }],
} }
// //
@ -660,15 +827,32 @@ const handleSubmit = async () => {
await formRef.value.validate() await formRef.value.validate()
// //
const normalizeDate = (d: any) => (d ? (typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0]) : '')
//
const mapTasks = (tasks: any[]) =>
(tasks || []).map(t => ({
...t,
planStartDate: normalizeDate(t.planStartDate),
planEndDate: normalizeDate(t.planEndDate),
children: (t.children || []).map((c: any) => ({
...c,
planStartDate: normalizeDate(c.planStartDate),
planEndDate: normalizeDate(c.planEndDate),
}))
}))
const submitData = { const submitData = {
...form, ...form,
// projectId // projectId
projectId: isEdit.value && currentId.value ? currentId.value : form.projectId, projectId: isEdit.value && currentId.value ? currentId.value : form.projectId,
// - YYYY-MM-DD // - YYYY-MM-DD
startDate: form.startDate ? (typeof form.startDate === 'string' ? form.startDate : new Date(form.startDate).toISOString().split('T')[0]) : '', startDate: normalizeDate(form.startDate),
endDate: form.endDate ? (typeof form.endDate === 'string' ? form.endDate : new Date(form.endDate).toISOString().split('T')[0]) : '', endDate: normalizeDate(form.endDate),
// ID - // ID -
constructorIds: Array.isArray(form.constructorIds) ? form.constructorIds.join(',') : form.constructorIds constructorIds: Array.isArray(form.constructorIds) ? form.constructorIds.join(',') : form.constructorIds,
//
tasks: mapTasks(form.tasks as any[]),
} }
console.log('提交数据:', submitData) console.log('提交数据:', submitData)
@ -753,8 +937,8 @@ const viewDetail = (record: T.ProjectResp) => {
router.push({ router.push({
name: 'ProjectDetail', name: 'ProjectDetail',
params: { params: {
id: projectId.toString() id: projectId.toString(),
} },
}) })
} }
@ -811,7 +995,7 @@ const exportData = async () => {
const params = { const params = {
projectName: searchForm.projectName, projectName: searchForm.projectName,
status: searchForm.status, status: searchForm.status,
fieldName: searchForm.fieldName, // 使fieldNameAPI使 fieldName: searchForm.fieldName, // 使fieldNameAPI使
} }
await exportProject(params) await exportProject(params)
@ -829,9 +1013,9 @@ const fetchUserList = async () => {
// //
const res = await http.get('/user/list') const res = await http.get('/user/list')
if (res.data && Array.isArray(res.data)) { if (res.data && Array.isArray(res.data)) {
userOptions.value = res.data.map(item => ({ userOptions.value = res.data.map((item) => ({
label: item.userName || item.username || item.name || item.nickName || item.account || '未命名用户', label: item.userName || item.username || item.name || item.nickName || item.account || '未命名用户',
value: item.userId || item.id || '' value: item.userId || item.id || '',
})) }))
} else { } else {
userOptions.value = [] userOptions.value = []

View File

@ -48,6 +48,7 @@
v-model="formData.receiptTime" v-model="formData.receiptTime"
show-time show-time
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择收货时间" placeholder="请选择收货时间"
style="width: 100%" style="width: 100%"
/> />
@ -233,8 +234,9 @@ const emit = defineEmits<{
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const loading = ref(false) const loading = ref(false)
// // - 使
const formData = reactive<ReceiptRequest>({ const formData = reactive<ReceiptRequest>({
//
receiptTime: '', receiptTime: '',
receiptPerson: '', receiptPerson: '',
receiptQuantity: 1, receiptQuantity: 1,
@ -247,6 +249,47 @@ const formData = reactive<ReceiptRequest>({
checkRemark: '', checkRemark: '',
storageLocation: '', storageLocation: '',
storageManager: '', storageManager: '',
//
equipmentName: '',
equipmentModel: '',
equipmentType: '',
equipmentSn: '',
brand: '',
specification: '',
assetCode: '',
//
purchaseOrder: '',
supplierName: '',
purchasePrice: 0,
purchaseTime: '',
quantity: 1,
unitPrice: 0,
totalPrice: 0,
//
inStockTime: '',
physicalLocation: '',
locationStatus: '',
responsiblePerson: '',
inventoryBarcode: '',
//
equipmentStatus: '',
useStatus: '',
healthStatus: '',
receiptStatus: '',
//
depreciationMethod: '',
depreciationYears: 5,
salvageValue: 0,
currentNetValue: 0,
//
createTime: '',
updateTime: ''
}) })
// //
@ -287,28 +330,46 @@ const rules = {
], ],
} }
//
const initFormData = () => {
if (props.equipmentData) {
//
Object.keys(formData).forEach((key) => {
const formKey = key as keyof ReceiptRequest
const equipmentKey = key as keyof EquipmentResp
if (formKey in formData && equipmentKey in props.equipmentData!) {
const value = props.equipmentData![equipmentKey]
if (value !== undefined) {
(formData[formKey] as any) = value
}
}
})
//
formData.receiptQuantity = props.equipmentData.quantity || 1
formData.storageLocation = props.equipmentData.physicalLocation || ''
formData.storageManager = props.equipmentData.responsiblePerson || ''
//
formData.receiptTime = formatDateTime(new Date())
}
}
// //
watch(() => props.visible, (visible) => { watch(() => props.visible, (visible) => {
if (visible) { if (visible) {
// initFormData()
Object.assign(formData, {
receiptTime: '',
receiptPerson: '',
receiptQuantity: props.equipmentData?.quantity || 1,
receiptRemark: '',
appearanceCheck: '',
functionTest: '',
packageIntegrity: '',
accessoryIntegrity: '',
checkResult: 'PASS',
checkRemark: '',
storageLocation: '',
storageManager: '',
})
formRef.value?.clearValidate() formRef.value?.clearValidate()
} }
}) })
//
watch(() => props.equipmentData, () => {
if (props.visible && props.equipmentData) {
initFormData()
}
}, { deep: true })
// //
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
@ -319,17 +380,77 @@ const handleSubmit = async () => {
throw new Error('设备ID不能为空') throw new Error('设备ID不能为空')
} }
// console.log('📦 开始提交收货数据...')
const receiptTime = formData.receiptTime ? new Date(formData.receiptTime).toISOString() : new Date().toISOString() console.log('📦 设备数据:', props.equipmentData)
console.log('📦 表单数据:', formData)
const requestData: ReceiptRequest = {
...formData, //
receiptTime, const receiptData: ReceiptRequest = {
//
receiptTime: formData.receiptTime ? formatDateTime(formData.receiptTime) : formatDateTime(new Date()),
receiptPerson: formData.receiptPerson,
receiptQuantity: formData.receiptQuantity,
receiptRemark: formData.receiptRemark,
appearanceCheck: formData.appearanceCheck,
functionTest: formData.functionTest,
packageIntegrity: formData.packageIntegrity,
accessoryIntegrity: formData.accessoryIntegrity,
checkResult: formData.checkResult,
checkRemark: formData.checkRemark,
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())
} }
await equipmentProcurementApi.receiveGoods(props.equipmentData.equipmentId, requestData) console.log('📦 构建的收货数据:', receiptData)
// API
await equipmentProcurementApi.receiveGoods(
props.equipmentData.equipmentId,
receiptData
)
Message.success('收货成功') Message.success('收货成功,设备已自动入库')
emit('success') emit('success')
emit('update:visible', false) emit('update:visible', false)
} catch (error: any) { } catch (error: any) {
@ -340,6 +461,26 @@ const handleSubmit = async () => {
} }
} }
//
const generateInventoryBarcode = () => {
const timestamp = Date.now().toString(36)
const random = Math.random().toString(36).substr(2, 5)
return `INV-${timestamp}-${random}`.toUpperCase()
}
//
const formatDateTime = (date: string | Date) => {
const d = new Date(date);
let month = '' + (d.getMonth() + 1);
let day = '' + d.getDate();
const year = d.getFullYear();
if (month.length < 2)
month = '0' + month;
if (day.length < 2)
day = '0' + day;
return [year, month, day].join('-') + ' ' + [d.getHours(), d.getMinutes(), d.getSeconds()].join(':');
}
// //
const handleCancel = () => { const handleCancel = () => {
emit('update:visible', false) emit('update:visible', false)