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 // 结束日期
@ -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']
} }
} }

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,19 +152,22 @@
<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>
<a-input-search </div>
v-model="fileSearchKeyword" <div class="file-search-container">
placeholder="搜索文件名..." <a-input-search
class="file-search-input" v-model="fileSearchKeyword"
@search="handleFileSearch" placeholder="搜索文件名..."
@input="handleFileSearchInput" class="file-search-input"
@clear="handleFileSearchClear" @search="handleFileSearch"
allow-clear @input="handleFileSearchInput"
/> @clear="handleFileSearchClear"
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);
.file-pagination { padding: 16px 24px;
border-top: 1px solid var(--color-border); 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);
}
&.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);
}
}
.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

@ -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 = []