This commit is contained in:
Mr.j 2025-08-13 16:46:17 +08:00
commit 5f676536dd
5 changed files with 600 additions and 183 deletions

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 // 结束日期

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']
} }
} }

View File

@ -2,7 +2,7 @@
<a-layout class="knowledge-container"> <a-layout class="knowledge-container">
<!-- 侧边栏 --> <!-- 侧边栏 -->
<a-layout-sider <a-layout-sider
width="260" :width="sidebarWidth"
:collapsed-width="80" :collapsed-width="80"
theme="dark" theme="dark"
class="folder-sidebar" class="folder-sidebar"
@ -101,7 +101,16 @@
</div> </div>
</div> </div>
<!-- 拖拽分隔线 -->
<div
v-if="!sidebarCollapsed"
class="sidebar-resizer"
@mousedown="startResize"
@touchstart="startResize"
:title="`当前宽度: ${sidebarWidth}px (拖拽调整)`"
>
<div class="resizer-handle"></div>
</div>
</a-layout-sider> </a-layout-sider>
<a-layout> <a-layout>
@ -143,10 +152,12 @@
<a-layout-content class="file-content"> <a-layout-content class="file-content">
<a-card :bordered="false" class="file-card"> <a-card :bordered="false" class="file-card">
<a-descriptions :title="`文件列表 (${fileList.length})`" v-if="currentFolderId" /> <!-- 文件列表标题和搜索框在同一行 -->
<div v-if="currentFolderId" class="file-header-container">
<!-- 文件搜索功能 --> <div class="file-title">
<div v-if="currentFolderId" class="file-search-container"> <span class="file-list-title">文件列表 ({{ fileList.length }})</span>
</div>
<div class="file-search-container">
<a-input-search <a-input-search
v-model="fileSearchKeyword" v-model="fileSearchKeyword"
placeholder="搜索文件名..." placeholder="搜索文件名..."
@ -157,6 +168,7 @@
allow-clear allow-clear
/> />
</div> </div>
</div>
<a-divider size="small" v-if="currentFolderId" /> <a-divider size="small" v-if="currentFolderId" />
@ -336,7 +348,7 @@
</div> </div>
<!-- 文件分页 --> <!-- 文件分页 -->
<div v-if="currentFolderId && !loading && totalFiles > 0" class="file-pagination"> <div v-if="currentFolderId && !loading && totalFiles > 0" class="pagination-container">
<a-pagination <a-pagination
:total="totalFiles" :total="totalFiles"
:current="fileCurrentPage" :current="fileCurrentPage"
@ -345,6 +357,8 @@
:show-page-size="true" :show-page-size="true"
:page-size-options="[10, 20, 50, 100]" :page-size-options="[10, 20, 50, 100]"
:show-jumper="true" :show-jumper="true"
:hide-on-single-page="false"
size="default"
@change="handleFilePageChange" @change="handleFilePageChange"
@page-size-change="handleFilePageSizeChange" @page-size-change="handleFilePageSizeChange"
/> />
@ -2357,6 +2371,86 @@ const fileTypeText = (type) => {
// //
const sidebarCollapsed = ref(false); const sidebarCollapsed = ref(false);
//
const sidebarWidth = ref(260); //
const isResizing = ref(false);
const startX = ref(0);
const startWidth = ref(0);
// localStorage
const loadSavedWidth = () => {
try {
const savedWidth = localStorage.getItem('bussiness-sidebar-width');
if (savedWidth) {
const width = parseInt(savedWidth);
if (width >= 200 && width <= 500) {
sidebarWidth.value = width;
}
}
} catch (error) {
console.warn('加载侧边栏宽度失败:', error);
}
};
// localStorage
const saveWidth = (width) => {
try {
localStorage.setItem('bussiness-sidebar-width', width.toString());
} catch (error) {
console.warn('保存侧边栏宽度失败:', error);
}
};
//
const startResize = (event) => {
event.preventDefault();
isResizing.value = true;
startX.value = event.type === 'mousedown' ? event.clientX : event.touches[0].clientX;
startWidth.value = sidebarWidth.value;
//
if (event.type === 'mousedown') {
document.addEventListener('mousemove', handleResize);
document.addEventListener('mouseup', stopResize);
} else {
document.addEventListener('touchmove', handleResize);
document.addEventListener('touchend', stopResize);
}
//
document.body.classList.add('resizing');
};
//
const handleResize = (event) => {
if (!isResizing.value) return;
const currentX = event.type === 'mousemove' ? event.clientX : event.touches[0].clientX;
const deltaX = currentX - startX.value;
const newWidth = Math.max(200, Math.min(500, startWidth.value + deltaX));
sidebarWidth.value = newWidth;
};
//
const stopResize = () => {
isResizing.value = false;
//
document.removeEventListener('mousemove', handleResize);
document.removeEventListener('mouseup', stopResize);
document.removeEventListener('touchmove', handleResize);
document.removeEventListener('touchend', stopResize);
//
document.body.classList.remove('resizing');
//
saveWidth(sidebarWidth.value);
};
// //
const handleCreateFolder = () => { const handleCreateFolder = () => {
folderForm.id = ''; folderForm.id = '';
@ -2443,6 +2537,7 @@ watch(uploadDialogVisible, (visible) => {
// //
onMounted(() => { onMounted(() => {
loadSavedWidth(); //
initData(); initData();
}); });
</script> </script>
@ -2629,6 +2724,7 @@ onMounted(() => {
background: var(--color-bg-1); background: var(--color-bg-1);
min-height: 0; min-height: 0;
max-height: calc(100vh - 120px); max-height: calc(100vh - 120px);
position: relative;
} }
.file-card { .file-card {
@ -2640,6 +2736,7 @@ onMounted(() => {
position: relative; position: relative;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
padding-bottom: 80px; /* 为分页器留出空间 */
} }
/* 表格容器 */ /* 表格容器 */
@ -2654,7 +2751,7 @@ onMounted(() => {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 0; margin-bottom: 0;
min-height: 300px; min-height: 300px;
max-height: calc(100vh - 300px); max-height: calc(100vh - 380px); /* 调整高度为分页器留出空间 */
} }
/* 表头行样式 */ /* 表头行样式 */
@ -3100,14 +3197,65 @@ onMounted(() => {
} }
/* 分页样式 */ /* 分页样式 */
.pagination-container, .file-pagination { .pagination-container {
margin-top: 16px; position: absolute;
text-align: right; bottom: 0;
padding: 0 16px 16px; 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;
.arco-pagination {
margin: 0;
.arco-pagination-item {
border-radius: 6px;
margin: 0 4px;
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
} }
.file-pagination { &.arco-pagination-item-active {
border-top: 1px solid var(--color-border); background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
}
.arco-pagination-prev,
.arco-pagination-next {
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
}
.arco-pagination-size-changer {
margin-left: 16px;
}
.arco-pagination-jumper {
margin-left: 16px;
}
.arco-pagination-total {
color: var(--color-text-2);
font-size: 14px;
}
}
} }
@ -3530,9 +3678,29 @@ onMounted(() => {
100% { transform: translateX(100%); } 100% { transform: translateX(100%); }
} }
/* 文件头部容器样式 */
.file-header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 0;
}
.file-title {
display: flex;
align-items: center;
}
.file-list-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text-1);
margin: 0;
}
/* 文件搜索样式 */ /* 文件搜索样式 */
.file-search-container { .file-search-container {
margin: 16px 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
@ -3601,55 +3769,7 @@ onMounted(() => {
border-top-color: var(--color-primary); border-top-color: var(--color-primary);
} }
/* 文件分页样式 */
.file-pagination {
position: sticky;
bottom: 0;
left: 0;
right: 0;
margin-top: 16px;
padding: 16px 0;
display: flex;
justify-content: center;
border-top: 1px solid var(--color-border);
background: var(--color-bg-1);
flex-shrink: 0;
z-index: 10;
.arco-pagination {
.arco-pagination-total {
color: var(--color-text-2);
font-size: 14px;
}
.arco-pagination-item {
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
&.arco-pagination-item-active {
background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
}
.arco-pagination-prev,
.arco-pagination-next {
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
}
}
}
/* 树形文件夹结构 */ /* 树形文件夹结构 */
.folder-tree-container { .folder-tree-container {
@ -3756,4 +3876,52 @@ onMounted(() => {
} }
} }
} }
/* 拖拽分隔线样式 */
.sidebar-resizer {
position: absolute;
top: 0;
right: 0;
width: 6px;
height: 100%;
cursor: col-resize;
background: transparent;
transition: background-color 0.2s ease;
z-index: 10;
&:hover {
background: rgba(var(--color-primary-6), 0.1);
}
&:active {
background: rgba(var(--color-primary-6), 0.2);
}
}
.resizer-handle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 40px;
background: var(--color-primary);
border-radius: 1px;
opacity: 0.6;
transition: opacity 0.2s ease;
}
.sidebar-resizer:hover .resizer-handle {
opacity: 1;
}
/* 拖拽时的全局样式 */
body.resizing {
cursor: col-resize !important;
user-select: none !important;
}
body.resizing * {
cursor: col-resize !important;
}
</style> </style>

View File

@ -12,9 +12,11 @@
--> -->
<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">
@ -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-input v-model="form.projectOrigin" placeholder="请输入项目来源" />
</a-form-item> </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,18 +351,18 @@
</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)
@ -246,28 +376,28 @@ const PROJECT_STATUS = {
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,9 +410,9 @@ 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使
@ -331,17 +461,37 @@ const form = reactive({
inspectionPhone: '', // inspectionPhone: '', //
farmName: '', // farmName: '', //
farmAddress: '', // farmAddress: '', //
projectOrigin: '', //
scale: '', // scale: '', //
turbineModel: '', // turbineModel: '', //
status: '', // 01234 status: '', // 01234
startDate: '', // startDate: '', //
endDate: '', // endDate: '', //
coverUrl: '', // // 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使
@ -587,16 +737,18 @@ const resetForm = () => {
inspectionPhone: '', // inspectionPhone: '', //
farmName: '', // farmName: '', //
farmAddress: '', // farmAddress: '', //
projectOrigin: '', //
scale: '', // scale: '', //
turbineModel: '', // turbineModel: '', //
status: 0, // 01234 status: 0, // 01234
startDate: '', // startDate: '', //
endDate: '', // endDate: '', //
coverUrl: '', // // 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(),
} },
}) })
} }
@ -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 = []