zhangwencui
2 天以前 67aebf4dcaa249d99893a88685ea273cc46606f4
流程优化
已添加1个文件
已修改5个文件
190 ■■■■■ 文件已修改
sql/20261106_project_management_hardening.sql 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/components/ProjectExecutionPanel.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/components/formDia.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/index.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/projectCategory/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/20261106_project_management_hardening.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
-- é¡¹ç›®ç®¡ç†æƒé™ã€åˆ†ç±»ä¸»é”®å’Œæµç¨‹å®‰å…¨å‡çº§ã€‚
-- æ‰§è¡Œå‰è¯·å¤‡ä»½æ•°æ®åº“;本脚本只创建权限项,不自动扩大普通角色权限。
SET NAMES utf8mb4;
DROP PROCEDURE IF EXISTS `add_column_if_missing`;
DELIMITER $$
CREATE PROCEDURE `add_column_if_missing`(IN p_table varchar(64), IN p_column varchar(64), IN p_definition varchar(1000))
BEGIN
  IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=p_table AND COLUMN_NAME=p_column) THEN
    SET @ddl=CONCAT('ALTER TABLE `',p_table,'` ADD COLUMN `',p_column,'` ',p_definition);
    PREPARE stmt FROM @ddl;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
  END IF;
END$$
DELIMITER ;
CALL add_column_if_missing('project_management_info','project_category_id',"bigint DEFAULT NULL COMMENT '项目分类ID' AFTER project_classification");
DROP PROCEDURE IF EXISTS `add_column_if_missing`;
-- åç§°å”¯ä¸€çš„历史项目可安全回填;重名分类留给用户重新选择。
UPDATE project_management_info p
JOIN (SELECT category_name,MIN(id) id FROM project_management_category WHERE is_deleted=0 GROUP BY category_name HAVING COUNT(*)=1) c
  ON c.category_name=p.project_classification
SET p.project_category_id=c.id
WHERE p.project_category_id IS NULL AND p.is_delete=0;
SET @project_list_menu=2285;
UPDATE sys_menu SET perms='project:info:list',visible='0',status='0',update_time=NOW() WHERE menu_id=@project_list_menu;
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '查看项目',@project_list_menu,11,'#',NULL,'',1,0,'F','0','0','project:info:query','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:info:query');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '新增项目',@project_list_menu,12,'#',NULL,'',1,0,'F','0','0','project:info:add','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:info:add');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '修改项目',@project_list_menu,13,'#',NULL,'',1,0,'F','0','0','project:info:edit','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:info:edit');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '删除项目',@project_list_menu,14,'#',NULL,'',1,0,'F','0','0','project:info:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:info:remove');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '开始项目',@project_list_menu,15,'#',NULL,'',1,0,'F','0','0','project:start','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:start');
-- Ensure the category menu exists even when this hardening script is applied independently.
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '项目分类',2276,2,'projectCategory','projectManagement/projectCategory/index','ProjectCategory',1,0,'C','0','0','project:category:list','tree','admin',NOW(),'多层树形项目分类维护'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=2276 AND path='projectCategory');
-- Workflow and execution button permissions.
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '模块维护',@project_list_menu,21,'#',NULL,'',1,0,'F','0','0','project:module:edit','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:module:edit');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '模块删除',@project_list_menu,22,'#',NULL,'',1,0,'F','0','0','project:module:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:module:remove');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '新增或修改跟进',@project_list_menu,23,'#',NULL,'',1,0,'F','0','0','project:follow:add','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:follow:add');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '删除跟进',@project_list_menu,24,'#',NULL,'',1,0,'F','0','0','project:follow:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:follow:remove');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '新增或修改汇报',@project_list_menu,25,'#',NULL,'',1,0,'F','0','0','project:report:add','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:report:add');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '删除汇报',@project_list_menu,26,'#',NULL,'',1,0,'F','0','0','project:report:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:report:remove');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '新增或修改资料',@project_list_menu,27,'#',NULL,'',1,0,'F','0','0','project:material:add','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:material:add');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '删除资料',@project_list_menu,28,'#',NULL,'',1,0,'F','0','0','project:material:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:material:remove');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '维护项目阶段',@project_list_menu,29,'#',NULL,'',1,0,'F','0','0','project:phase:edit','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:phase:edit');
SET @project_category_menu=(SELECT menu_id FROM sys_menu WHERE parent_id=2276 AND path='projectCategory' ORDER BY menu_id LIMIT 1);
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '维护项目分类',@project_category_menu,1,'#',NULL,'',1,0,'F','0','0','project:category:edit','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:category:edit');
INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark)
SELECT '删除项目分类',@project_category_menu,2,'#',NULL,'',1,0,'F','0','0','project:category:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:category:remove');
-- è¶…级管理员角色补齐按钮;其他角色由“角色管理”按岗位勾选。
INSERT IGNORE INTO sys_role_menu(role_id,menu_id)
SELECT 1,menu_id FROM sys_menu
WHERE status='0' AND perms IN (
  'project:info:list','project:info:query','project:info:add','project:info:edit','project:info:remove','project:start',
  'project:module:edit','project:module:remove','project:follow:add','project:follow:remove',
  'project:report:add','project:report:remove','project:material:add','project:material:remove',
  'project:phase:edit','project:category:list','project:category:edit','project:category:remove'
);
src/router/index.js
@@ -165,7 +165,7 @@
        path: ":id",
        component: () => import("@/views/projectManagement/Management/projectDetail.vue"),
        name: "ProjectManagementDetail",
        meta: { title: "项目详情", activeMenu: "/projectManagement/Management" },
        meta: { title: "项目详情", activeMenu: "/projectManagement/projectList" },
      },
    ],
  },
src/views/projectManagement/Management/components/ProjectExecutionPanel.vue
@@ -51,7 +51,7 @@
            <span v-if="data.followWindow?.dueDate">本次应跟进日 {{ data.followWindow.dueDate }},可填写 {{ data.followWindow.windowStart }} è‡³ {{ data.followWindow.windowEnd }}</span>
            <span v-else>首次跟进可直接填写</span>
          </div>
          <el-button v-if="!readonly" v-hasPermi="['project:follow:add']" type="primary" @click="openFollow()">填写跟进</el-button>
          <el-button v-if="!readonly && canManageFollow" v-hasPermi="['project:follow:add']" type="primary" @click="openFollow()">填写跟进</el-button>
        </div>
        <el-table :data="data.followUps" border>
          <el-table-column prop="followDate" label="跟进日期" width="130" />
@@ -63,7 +63,7 @@
              <span v-else class="empty-file">无附件</span>
            </template>
          </el-table-column>
          <el-table-column v-if="!readonly" label="操作" width="140" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:follow:add']" link type="primary" @click="openFollow(row)">编辑</el-button><el-button v-hasPermi="['project:follow:remove']" link type="danger" @click="removeRecord('follow', row)">删除</el-button></template></el-table-column>
          <el-table-column v-if="!readonly && canManageFollow" label="操作" width="140" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:follow:add']" link type="primary" @click="openFollow(row)">编辑</el-button><el-button v-hasPermi="['project:follow:remove']" link type="danger" @click="removeRecord('follow', row)">删除</el-button></template></el-table-column>
        </el-table>
      </el-tab-pane>
@@ -124,7 +124,7 @@
              </el-button>
            </template>
          </el-table-column>
          <el-table-column v-if="!readonly" label="操作" width="90" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:material:remove']" link type="danger" @click="removeRecord('material', row)">删除</el-button></template></el-table-column>
          <el-table-column v-if="!readonly" label="操作" width="140" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:material:add']" link type="primary" @click="openMaterial(row)">编辑</el-button><el-button v-hasPermi="['project:material:remove']" link type="danger" @click="removeRecord('material', row)">删除</el-button></template></el-table-column>
        </el-table>
      </el-tab-pane>
@@ -160,7 +160,7 @@
        </div>
        <div class="module-grid">
          <el-form-item label="模块名称" required><el-input v-model="module.moduleName" :disabled="module.locked" /></el-form-item>
          <el-form-item label="预计开始" required><el-date-picker v-model="module.plannedStartDate" :disabled="module.locked" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
          <el-form-item label="预计开始" required><el-date-picker v-model="module.plannedStartDate" :disabled="module.locked" :disabled-date="moduleDateDisabled" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
          <el-form-item label="工作量占比" required><el-input-number v-model="module.projectWeight" :disabled="module.locked" :min="0.01" :max="draftWeightMax(moduleIndex)" :precision="2" /><span class="unit">%</span><div class="percent-hint">其他模块 {{ otherDraftWeight(moduleIndex) }}%,本模块最多 {{ draftWeightMax(moduleIndex) }}%</div></el-form-item>
          <el-form-item label="模块概要" required class="summary-item"><el-input v-model="module.summary" :disabled="module.locked" type="textarea" :rows="2" maxlength="1000" show-word-limit /></el-form-item>
          <el-form-item label="模块执行人" required class="members-item">
@@ -181,7 +181,7 @@
    <el-dialog v-model="followVisible" :title="followForm.id ? '编辑项目跟进' : '填写项目跟进'" width="650px" destroy-on-close>
      <el-form ref="followFormRef" :model="followForm" :rules="followRules" label-width="90px">
        <el-form-item label="跟进日期" prop="followDate"><el-date-picker v-model="followForm.followDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" /></el-form-item>
        <el-form-item label="跟进日期" prop="followDate"><el-date-picker v-model="followForm.followDate" :disabled="!!followForm.id" :disabled-date="date => dayjs(date).isAfter(dayjs(), 'day')" type="date" value-format="YYYY-MM-DD" style="width: 100%" /><div v-if="followForm.id" class="percent-hint">历史跟进日期不可修改,避免破坏后续跟进周期。</div></el-form-item>
        <el-form-item label="跟进详情" prop="detail"><el-input v-model="followForm.detail" type="textarea" :rows="5" maxlength="2000" show-word-limit /></el-form-item>
        <el-form-item label="附件"><AttachmentUploadFile v-model:file-list="followFiles" :limit="10" /></el-form-item>
      </el-form>
@@ -205,14 +205,14 @@
          </div>
        </el-form-item>
        <el-form-item label="本次进度" prop="incrementProgress"><el-input-number v-model="reportForm.incrementProgress" :min="0.01" :max="reportIncrementMax" :precision="2" /><span class="unit">%</span><span class="current-progress-hint">{{ reportProgressHint }}</span></el-form-item>
        <el-form-item label="汇报时间" prop="reportDate"><el-date-picker v-model="reportForm.reportDate" :disabled="!!reportForm.id" type="date" value-format="YYYY-MM-DD" style="width: 100%" /><div v-if="reportForm.id" class="percent-hint">编辑已有汇报时不允许调整时间,以保证历史进度顺序正确。</div></el-form-item>
        <el-form-item label="汇报时间" prop="reportDate"><el-date-picker v-model="reportForm.reportDate" :disabled="!!reportForm.id" :disabled-date="date => dayjs(date).isAfter(dayjs(), 'day')" type="date" value-format="YYYY-MM-DD" style="width: 100%" /><div v-if="reportForm.id" class="percent-hint">编辑已有汇报时不允许调整时间,以保证历史进度顺序正确。</div></el-form-item>
        <el-form-item label="备注"><el-input v-model="reportForm.remark" type="textarea" :rows="3" maxlength="2000" show-word-limit /></el-form-item>
        <el-form-item label="文件"><AttachmentUploadFile v-model:file-list="reportFiles" :limit="10" /></el-form-item>
      </el-form>
      <template #footer><el-button @click="reportVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="submitReport">{{ reportForm.id ? '保存修改' : '提交汇报' }}</el-button></template>
    </el-dialog>
    <el-dialog v-model="materialVisible" title="上传项目资料" width="650px" destroy-on-close>
    <el-dialog v-model="materialVisible" :title="materialForm.id ? '编辑项目资料' : '上传项目资料'" width="650px" destroy-on-close>
      <el-form ref="materialFormRef" :model="materialForm" :rules="materialRules" label-width="90px">
        <el-form-item label="资料名称" prop="materialName"><el-input v-model="materialForm.materialName" /></el-form-item>
        <el-form-item label="资料类型"><el-select v-model="materialForm.materialType" allow-create filterable clearable style="width: 100%"><el-option v-for="type in ['立项资料','开发资料','申报资料','结果资料','验收资料','其他']" :key="type" :label="type" :value="type" /></el-select></el-form-item>
@@ -261,6 +261,7 @@
const reportShareRemaining = computed(() => Math.max(0, 100 - Number(reportShareTotal.value)).toFixed(2))
const followCycleLabel = computed(() => ({ MONTH: '每月', QUARTER: '每季度', YEAR: '每年' }[props.projectInfo?.followCycle] || props.projectInfo?.followCycle || '每月'))
const isStarted = computed(() => !!props.projectInfo?.currentPhase && props.projectInfo.currentPhase !== '未开始')
const canManageFollow = computed(() => String(props.projectInfo?.projectLeaderId ?? '') === String(userStore.id ?? ''))
const memberReportModules = computed(() => data.modules.filter(module =>
  (module.members || []).some(member => String(member.userId) === String(userStore.id))
))
@@ -334,7 +335,7 @@
const reportRules = { moduleId: [{ required: true, message: '请选择项目模块', trigger: 'change' }], reportDate: [{ required: true, message: '请选择汇报时间', trigger: 'change' }] }
const materialVisible = ref(false)
const materialFormRef = ref()
const materialForm = reactive({ materialName: '', materialType: '', remark: '' })
const materialForm = reactive({ id: null, materialName: '', materialType: '', remark: '' })
const materialFiles = ref([])
const materialRules = { materialName: [{ required: true, message: '请输入资料名称', trigger: 'blur' }] }
async function loadUsers() {
@@ -468,7 +469,13 @@
}
function canEditReport(row) {
  return memberReportModules.value.some(module => String(module.id) === String(row.moduleId))
  return (row.reporterIdList || []).some(userId => String(userId) === String(userStore.id))
}
function moduleDateDisabled(date) {
  const start = props.projectInfo?.planStartTime
  const end = props.projectInfo?.planEndTime
  return (start && dayjs(date).isBefore(dayjs(start), 'day')) || (end && dayjs(date).isAfter(dayjs(end), 'day'))
}
function reporterShareLimit(index) {
@@ -494,7 +501,13 @@
  finally { saving.value = false }
}
function openMaterial() { Object.assign(materialForm, { materialName: '', materialType: '', remark: '' }); materialFiles.value = []; materialVisible.value = true }
function openMaterial(row = null) {
  Object.assign(materialForm, row
    ? { id: row.id, materialName: row.materialName || '', materialType: row.materialType || '', remark: row.remark || '' }
    : { id: null, materialName: '', materialType: '', remark: '' })
  materialFiles.value = row ? [...(row.attachmentList || [])] : []
  materialVisible.value = true
}
async function submitMaterial() {
  await materialFormRef.value?.validate()
  if (!materialFiles.value.length) return ElMessage.warning('请上传至少一个文件')
src/views/projectManagement/Management/components/formDia.vue
@@ -8,7 +8,7 @@
        <el-col :span="12"><el-form-item label="客户名称" prop="clientName"><el-select v-model="form.clientName" filterable clearable style="width:100%" @change="syncCustomer"><el-option v-for="item in customers" :key="item.id" :label="item.customerName" :value="item.customerName" /></el-select></el-form-item></el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12"><el-form-item label="项目分类" prop="projectClassification"><el-tree-select v-model="form.projectClassification" :data="categoryTree" :props="{ label:'categoryName',value:'categoryName',children:'children' }" check-strictly clearable :filterable="false" placeholder="请选择项目分类" style="width:100%" /></el-form-item></el-col>
        <el-col :span="12"><el-form-item label="项目分类" prop="projectCategoryId"><el-tree-select v-model="form.projectCategoryId" :data="categoryTree" :props="{ label:'categoryName',value:'id',children:'children' }" check-strictly clearable filterable placeholder="请选择项目分类" style="width:100%" @change="syncCategory" /></el-form-item></el-col>
        <el-col :span="12"><el-form-item label="项目来源"><el-input v-model="form.source" disabled /></el-form-item></el-col>
      </el-row>
      <el-row :gutter="20">
@@ -33,6 +33,7 @@
<script setup>
import { computed, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue'
import { addProject, getProject, updateProject } from '@/api/projectManagement/project'
import { listCustomer } from '@/api/basicData/customer'
@@ -49,10 +50,10 @@
const users = ref([])
const categoryTree = ref([])
const files = ref([])
const form = reactive({ id:null,no:'',title:'',clientId:null,clientName:'',source:'手动创建',sourceBusinessId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'未开始',remark:'' })
const form = reactive({ id:null,no:'',title:'',clientId:null,clientName:'',source:'手动创建',sourceBusinessId:null,projectCategoryId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'未开始',remark:'' })
const rules = {
  title:[{ required:true,message:'请输入项目名称',trigger:'blur' }], clientName:[{ required:true,message:'请选择客户',trigger:'change' }],
  projectClassification:[{ required:true,message:'请选择项目分类',trigger:'change' }], planStartTime:[{ required:true,message:'请选择预计开始时间',trigger:'change' }],
  projectCategoryId:[{ required:true,message:'请选择项目分类',trigger:'change' }], planStartTime:[{ required:true,message:'请选择预计开始时间',trigger:'change' }],
  planEndTime:[{ required:true,message:'请选择预计结束时间',trigger:'change' }], projectLeaderId:[{ required:true,message:'请选择项目负责人',trigger:'change' }],
  projectManagerIds:[{ required:true,type:'array',min:1,message:'请选择至少一名项目经理',trigger:'change' }], followCycle:[{ required:true,message:'请选择跟进周期',trigger:'change' }],
  followDetail:[{ required:true,message:'请输入跟进要求',trigger:'blur' }]
@@ -67,9 +68,11 @@
  const userData=userRes?.data||userRes?.rows||userRes||[]; users.value=(Array.isArray(userData)?userData:[]).map(u=>({ label:u.nickName||u.userName||u.name,value:u.userId||u.id }))
  categoryTree.value=buildTree(categoryRes?.data||[])
}
function reset() { Object.assign(form,{ id:null,no:'',title:'',clientId:null,clientName:'',source:'手动创建',sourceBusinessId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'未开始',remark:'' }); files.value=[] }
function reset() { Object.assign(form,{ id:null,no:'',title:'',clientId:null,clientName:'',source:'手动创建',sourceBusinessId:null,projectCategoryId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'未开始',remark:'' }); files.value=[] }
function syncCustomer(name) { const item=customers.value.find(x=>x.customerName===name); form.clientId=item?.id||item?.customerId||null }
function syncLeader(id) { form.projectLeaderName=users.value.find(u=>String(u.value)===String(id))?.label||'' }
function findCategory(nodes,id) { for(const node of nodes||[]){ if(String(node.id)===String(id)) return node; const found=findCategory(node.children,id); if(found) return found } }
function syncCategory(id) { form.projectClassification=findCategory(categoryTree.value,id)?.categoryName||'' }
function normalizeFiles(list) { return (list||[]).map(f=>({ id:f.id??f.fileId??f.rawData?.id,name:f.name??f.fileName??f.rawData?.originalFilename,url:f.url??f.fileUrl??f.rawData?.url })).filter(f=>f.id||f.url) }
async function openDialog(options={}) {
@@ -83,7 +86,7 @@
  await formRef.value?.validate()
  if(form.planEndTime < form.planStartTime) return ElMessage.warning('预计结束时间不能早于预计开始时间')
  form.projectManagerNames=form.projectManagerIds.map(id=>users.value.find(u=>String(u.value)===String(id))?.label||'').filter(Boolean)
  const info={ ...form, currentPhase:startProject.value?'未开始':form.currentPhase, establishTime:form.establishTime||new Date().toISOString().slice(0,10), managerId:form.projectLeaderId,managerName:form.projectLeaderName,attachmentIds:normalizeFiles(files.value).map(f=>f.id).filter(Boolean),projectManagementPlanId:null }
  const info={ ...form, currentPhase:startProject.value?'未开始':form.currentPhase, establishTime:form.establishTime||dayjs().format('YYYY-MM-DD'), managerId:form.projectLeaderId,managerName:form.projectLeaderName,attachmentIds:normalizeFiles(files.value).map(f=>f.id).filter(Boolean),projectManagementPlanId:null }
  saving.value=true
  try {
    await (form.id?updateProject:addProject)({ info,shippingAddress:null,contractInfo:null,salesLedgerProductList:null })
src/views/projectManagement/Management/index.vue
@@ -9,7 +9,7 @@
      </el-form>
    </el-card>
    <el-card shadow="never">
      <div class="actions"><el-button type="primary" @click="add">手动创建项目</el-button></div>
      <div class="actions"><el-button v-hasPermi="['project:info:add']" type="primary" @click="add">手动创建项目</el-button></div>
      <el-table :data="rows" v-loading="loading" border>
        <el-table-column prop="no" label="项目编号" width="170" />
        <el-table-column prop="title" label="项目名称" min-width="180" />
@@ -25,24 +25,24 @@
        <el-table-column prop="actualEndTime" label="实际结束" width="120"><template #default="{row}">{{ row.actualEndTime || '-' }}</template></el-table-column>
        <el-table-column label="操作" width="520" fixed="right" align="center">
          <template #default="{row}">
            <el-button v-if="row.currentPhase==='未开始'" link type="success" @click="start(row)">开始项目</el-button>
            <el-button v-if="row.currentPhase==='未开始'" link type="primary" @click="edit(row)">编辑</el-button>
            <el-button v-if="row.currentPhase==='未开始'" link type="primary" @click="maintain(row, 'modules')">模块管理</el-button>
            <el-dropdown v-if="row.currentPhase!=='未开始'" class="operation-dropdown" @command="tab => maintain(row, tab)">
            <el-button v-if="row.currentPhase==='未开始'" v-hasPermi="['project:start']" link type="success" @click="start(row)">开始项目</el-button>
            <el-button v-if="row.currentPhase==='未开始'" v-hasPermi="['project:info:edit']" link type="primary" @click="edit(row)">编辑</el-button>
            <el-button v-if="row.currentPhase==='未开始'" v-hasPermi="['project:module:edit']" link type="primary" @click="maintain(row, 'modules')">模块管理</el-button>
            <el-dropdown v-if="row.currentPhase!=='未开始'" v-hasPermi="['project:module:edit','project:follow:add','project:report:add','project:material:add']" class="operation-dropdown" @command="tab => maintain(row, tab)">
              <el-button link type="primary">项目维护<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item command="modules">模块管理</el-dropdown-item>
                  <el-dropdown-item command="follow">项目跟进</el-dropdown-item>
                  <el-dropdown-item command="reports">项目汇报</el-dropdown-item>
                  <el-dropdown-item command="materials">项目资料</el-dropdown-item>
                  <el-dropdown-item v-hasPermi="['project:module:edit']" command="modules">模块管理</el-dropdown-item>
                  <el-dropdown-item v-hasPermi="['project:follow:add']" command="follow">项目跟进</el-dropdown-item>
                  <el-dropdown-item v-hasPermi="['project:report:add']" command="reports">项目汇报</el-dropdown-item>
                  <el-dropdown-item v-hasPermi="['project:material:add']" command="materials">项目资料</el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
            <el-button link type="primary" @click="detail(row)">详情</el-button>
            <el-button v-if="canAdvanceStage(row)" link type="success" @click="advanceStage(row)">{{ nextStageAction(row) }}</el-button>
            <el-button link type="primary" @click="openLifecycle(row)">项目生命周期</el-button>
            <el-button v-if="row.currentPhase==='未开始'" link type="danger" @click="remove(row)">删除</el-button>
            <el-button v-hasPermi="['project:info:query']" link type="primary" @click="detail(row)">详情</el-button>
            <el-button v-if="canAdvanceStage(row)" v-hasPermi="['project:phase:edit']" link type="success" @click="advanceStage(row)">{{ nextStageAction(row) }}</el-button>
            <el-button v-hasPermi="['project:info:query']" link type="primary" @click="openLifecycle(row)">项目生命周期</el-button>
            <el-button v-if="row.currentPhase==='未开始'" v-hasPermi="['project:info:remove']" link type="danger" @click="remove(row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -109,8 +109,9 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import dayjs from 'dayjs'
import { listProject, delProject } from '@/api/projectManagement/project'
import { getProjectLifecycle, saveProjectPhase, transitionProjectStage } from '@/api/projectManagement/workflow'
import { getProjectLifecycle, saveProjectPhase, startProjectWorkflow, transitionProjectStage } from '@/api/projectManagement/workflow'
import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue'
import FormDia from './components/formDia.vue'
import ProjectExecutionPanel from './components/ProjectExecutionPanel.vue'
@@ -121,9 +122,15 @@
const stageVisible=ref(false); const stageSaving=ref(false); const stageProject=ref({}); const stageFiles=ref([])
const stageForm=reactive({applicationDate:'',applicationType:'客户交付',applicationName:'',expectedFeedbackDate:'',description:'',resultDate:'',resultStatus:'PASS',resultDescription:'',acceptancePlanDate:'',acceptor:'',acceptanceStandard:'',acceptanceDate:'',acceptanceResult:'PASS',acceptanceDescription:''})
const query=reactive({ keyword:'',customerName:'',source:'',pageNum:1,pageSize:10 })
async function load(){ loading.value=true; try{ const res=await listProject({ noOrName:query.keyword,clientName:query.customerName,current:query.pageNum,size:query.pageSize }); const data=res?.data?.records||res?.rows||[]; rows.value=(data||[]).filter(r=>!query.source||r.source===query.source); total.value=res?.data?.total||res?.total||0 }finally{loading.value=false} }
async function load(){ loading.value=true; try{ const res=await listProject({ noOrName:query.keyword,clientName:query.customerName,source:query.source,current:query.pageNum,size:query.pageSize }); const data=res?.data?.records||res?.rows||[]; rows.value=data||[]; total.value=res?.data?.total||res?.total||0 }finally{loading.value=false} }
function search(){query.pageNum=1;load()} function reset(){Object.assign(query,{keyword:'',customerName:'',source:'',pageNum:1,pageSize:10});load()}
function add(){formRef.value?.openDialog({operationType:'add'})} function edit(row){formRef.value?.openDialog({operationType:'edit',row})} function start(row){formRef.value?.openDialog({operationType:'edit',row,startProject:true})}
function add(){formRef.value?.openDialog({operationType:'add'})} function edit(row){formRef.value?.openDialog({operationType:'edit',row})}
async function start(row){
  await ElMessageBox.confirm(`确认开始项目“${row.title}”吗?开始后不能退回未开始状态。`,'开始项目',{type:'warning'})
  await startProjectWorkflow(row.id)
  ElMessage.success('项目已开始')
  await load()
}
const maintenanceNames={modules:'模块管理',follow:'项目跟进',reports:'项目汇报',materials:'项目资料'}
const nextStageMap={开发:'申报',申报:'申报结果',申报结果:'验收',验收:'已完成'}
const maintenanceReadonly=computed(()=>['申报','申报结果','验收','已完成'].includes(selectedProject.value.currentPhase))
@@ -146,7 +153,7 @@
const stageDialogTitle=computed(()=>nextStageAction(stageProject.value))
const stageSubmitText=computed(()=>stageProject.value.currentPhase==='验收'&&stageForm.acceptanceResult==='PASS'?'验收通过并完成项目':'保存')
function advanceStage(row){
  const today=new Date().toISOString().slice(0,10)
  const today=dayjs().format('YYYY-MM-DD')
  stageProject.value={...row}
  Object.assign(stageForm,{applicationDate:today,applicationType:'客户交付',applicationName:row.title||'',expectedFeedbackDate:'',description:'',resultDate:today,resultStatus:'PASS',resultDescription:'',acceptancePlanDate:today,acceptor:row.clientName||'',acceptanceStandard:'',acceptanceDate:today,acceptanceResult:'PASS',acceptanceDescription:''})
  stageFiles.value=[]
src/views/projectManagement/projectCategory/index.vue
@@ -77,8 +77,8 @@
const form = reactive({ id: null, parentId: null, categoryName: '', sort: 0, remark: '' })
const rules = { categoryName: [{ required: true, message: '请输入分类名称', trigger: 'blur' }] }
function buildTree(list, excludedId = null) {
  const source = list.filter(item => String(item.id) !== String(excludedId ?? ''))
function buildTree(list, excludedIds = new Set()) {
  const source = list.filter(item => !excludedIds.has(String(item.id)))
  const map = new Map(source.map(item => [String(item.id), { ...item, children: [] }]))
  const roots = []
  map.forEach(item => {
@@ -92,8 +92,21 @@
  return sortRows(roots)
}
function categoryBranchIds(id) {
  const excluded = new Set(id == null ? [] : [String(id)])
  let changed = true
  while (changed) {
    changed = false
    rows.value.forEach(item => {
      if (excluded.has(String(item.parentId)) && !excluded.has(String(item.id))) {
        excluded.add(String(item.id)); changed = true
      }
    })
  }
  return excluded
}
const treeRows = computed(() => buildTree(rows.value))
const parentOptions = computed(() => buildTree(rows.value, form.id))
const parentOptions = computed(() => buildTree(rows.value, categoryBranchIds(form.id)))
async function loadData() {
  loading.value = true