Merge remote-tracking branch 'origin/master'

This commit is contained in:
ye1023
2026-05-06 09:13:25 +08:00
47 changed files with 12310 additions and 110 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5 6.5a5 5 0 1 0-10.002.002A5 5 0 0 0 15.5 6.5Zm-5 3a3.001 3.001 0 0 1 0-6 3.001 3.001 0 0 1 0 6ZM3 19c0-.346.045-.68.125-1 .445-1.725 2.01-3 3.875-3h5.98v-2H7a6 6 0 0 0-6 6v1c0 1.1.9 2 2 2h9.954v-2H3v-1Zm15.5 3a1 1 0 0 1-1-1v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 1 1 2 0v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 0 1-1 1Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 444 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.22 18.46a1 1 0 0 0 1.56 0l8.305-10.334a1 1 0 0 0-.78-1.626H3.696a1 1 0 0 0-.78 1.626L11.22 18.46Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 240 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.304 7.315a1 1 0 0 1 1.414 1.414L8.13 14.317a1.485 1.485 0 0 0 0 2.1l.01.011a1.5 1.5 0 0 0 2.117-.005l7.43-7.43a3.5 3.5 0 0 0 0-4.95l-.036-.037a3.5 3.5 0 0 0-4.95 0l-7.778 7.777a5.521 5.521 0 0 0 7.808 7.809l7.07-7.07a1 1 0 0 1 1.415 1.414l-7.07 7.07A7.521 7.521 0 0 1 3.509 10.37l7.778-7.778a5.5 5.5 0 0 1 7.778 0l.037.037a5.5 5.5 0 0 1 0 7.778l-7.43 7.43a3.5 3.5 0 0 1-4.939.012l-.006-.006-.012-.012a3.485 3.485 0 0 1 0-4.928l5.589-5.588Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 582 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 15a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1Z" fill="currentColor"/>
<path d="M2 4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm2 0v2h16V4H4Zm16 4H4v12h16V8Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 348 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 5a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 7a1 1 0 0 1 1-1h11a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm1 6a1 1 0 1 0 0 2h11a1 1 0 1 0 0-2H3Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

@@ -0,0 +1,8 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="6" r="1.5" fill="currentColor"/>
<circle cx="15" cy="6" r="1.5" fill="currentColor"/>
<circle cx="9" cy="12" r="1.5" fill="currentColor"/>
<circle cx="15" cy="12" r="1.5" fill="currentColor"/>
<circle cx="9" cy="18" r="1.5" fill="currentColor"/>
<circle cx="15" cy="18" r="1.5" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 435 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 6.5a3 3 0 1 0-6 0 3 3 0 0 0 6 0Zm2 0a5 5 0 1 1-10 0 5 5 0 0 1 10 0ZM4 19v2h16v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4Zm-2 0a6 6 0 0 1 6-6h8a6 6 0 0 1 6 6v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 325 B

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.25 6.5a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Zm0 7.25a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Zm1.75 5.5a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM14.753 6.5a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5ZM16.5 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0Zm-1.747 9a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 462 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4.5A2.5 2.5 0 0 1 5.5 2h6.879a2.5 2.5 0 0 1 1.767.732l5.122 5.122a2.5 2.5 0 0 1 .732 1.767V19.5a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 2 19.5v-15ZM5.5 3.5a1 1 0 0 0-1 1v15a1 1 0 0 0 1 1h13a1 1 0 0 0 1-1V9.621a1 1 0 0 0-.293-.707l-5.122-5.121A1 1 0 0 0 13.38 3.5H5.5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 407 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.755 11.658a1 1 0 0 1 1.416-1.415L12 13.07l2.828-2.829a1 1 0 0 1 1.416 1.416c-1.181 1.189-2.356 2.386-3.553 3.56a.987.987 0 0 1-1.383 0c-1.196-1.175-2.371-2.371-3.553-3.56Z" fill="currentColor"/>
<path d="M12 23C5.925 23 1 18.075 1 12S5.925 1 12 1s11 4.925 11 11-4.925 11-11 11Zm0-2a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 456 B

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1a1 1 0 0 1 1 1v1.143A9.009 9.009 0 0 1 20.857 11H22a1 1 0 1 1 0 2h-1.143A9.009 9.009 0 0 1 13 20.857V22a1 1 0 1 1-2 0v-1.143A9.009 9.009 0 0 1 3.143 13H2a1 1 0 1 1 0-2h1.143A9.009 9.009 0 0 1 11 3.143V2a1 1 0 0 1 1-1Zm0 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm0 3a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 429 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 2h14v14H5V5Zm2 3h10v2H7V8Zm0 4h7v2H7v-2Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 253 B

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.111-.536 1.607l3.619 3.099-1.106 4.635c-.194.813.691 1.456 1.405 1.02l4.09-2.489 4.09 2.489c.714.436 1.599-.207 1.405-1.02l-1.106-4.635 3.619-3.099c.635-.496.297-1.54-.536-1.607l-4.753-.381-1.83-4.401Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 424 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 506 B

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.25 6.5a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Zm0 7.25a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Zm1.75 5.5a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM14.753 6.5a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5ZM16.5 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0Zm-1.747 9a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 462 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 14a3.001 3.001 0 0 1-2.83-2H7v1a5 5 0 0 0 5 5h2.17a3.001 3.001 0 1 1 0 2H12a7 7 0 0 1-7-7V3a1 1 0 0 1 2 0v7h7.17A3.001 3.001 0 1 1 17 14Zm-1 5a1 1 0 1 0 2 0 1 1 0 0 0-2 0Zm1-7a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 347 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Zm1 2h14v14H5V5Zm2 3h10v2H7V8Zm0 4h7v2H7v-2Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 253 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 8a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2h-4a1 1 0 0 1-1-1V8Z" fill="currentColor"/>
<path d="M12 23c6.075 0 11-4.925 11-11S18.075 1 12 1 1 5.925 1 12s4.925 11 11 11Zm0-2a9 9 0 1 1 0-18 9 9 0 0 1 0 18Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 338 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.437 4.898 5.447 13h6.063L8.437 4.898Zm6.025 15.881L12.269 15h-7.56l-2.131 5.78a1 1 0 1 1-1.873-.703L7.02 2.982c.491-1.31 2.344-1.31 2.835 0l6.48 17.095a1 1 0 1 1-1.872.702ZM15.056 5a1 1 0 1 0 0 2H23a1 1 0 1 0 0-2h-7.944Zm1.055 7a1 1 0 0 1 1-1H23a1 1 0 1 1 0 2h-5.89a1 1 0 0 1-1-1Zm3.056 5a1 1 0 1 0 0 2H23a1 1 0 1 0 0-2h-3.833Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 469 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 6.5a3 3 0 1 0-6 0 3 3 0 0 0 6 0Zm2 0a5 5 0 1 1-10 0 5 5 0 0 1 10 0ZM4 19v2h16v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4Zm-2 0a6 6 0 0 1 6-6h8a6 6 0 0 1 6 6v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 325 B

+154
View File
@@ -0,0 +1,154 @@
import { defHttp } from "/@/utils/http/axios";
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/bg/xispeak/bgXiSpeak/rootList',
save='/bg/xispeak/bgXiSpeak/add',
edit='/bg/xispeak/bgXiSpeak/edit',
deleteBgXiSpeak = '/bg/xispeak/bgXiSpeak/delete',
importExcel = '/bg/xispeak/bgXiSpeak/importExcel',
exportXls = '/bg/xispeak/bgXiSpeak/exportXls',
loadTreeData = '/bg/xispeak/bgXiSpeak/loadTreeRoot',
getChildList = '/bg/xispeak/bgXiSpeak/childList',
getChildListBatch = '/bg/xispeak/bgXiSpeak/getChildListBatch',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
* @param params
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 删除
* @param params
* @param handleSuccess
*/
export const deleteBgXiSpeak = (params,handleSuccess) => {
return defHttp.delete({ url: Api.deleteBgXiSpeak, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
* @param handleSuccess
*/
export const batchDeleteBgXiSpeak = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBgXiSpeak, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
* @param isUpdate
*/
export const saveOrUpdateDict = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params },{ isTransformResponse:false });
}
/**
* 查询全部树形节点数据
* @param params
*/
export const loadTreeData = (params) => defHttp.get({ url: Api.loadTreeData,params });
/**
* 查询子节点数据
* @param params
*/
export const getChildList = (params) => defHttp.get({ url: Api.getChildList, params });
/**
* 批量查询子节点数据
* @param params
*/
export const getChildListBatch = (params) => defHttp.get({ url: Api.getChildListBatch, params },{ isTransformResponse:false });
// ====================== 子表反馈相关API ======================
/**
* 子表列表接口
* @param params
*/
export const bgXiSpeakFeedbackList = (params) => {
if (params['mainId']) {
return defHttp.get({ url: '/bg/xispeak/bgXiSpeak/listBgXiSpeakFeedbackByMainId', params });
}
return Promise.resolve({});
};
/**
* 子表删除单个
*/
export const bgXiSpeakFeedbackDelete = (params, handleSuccess) => {
return defHttp.delete({ url: '/bg/xispeak/bgXiSpeak/deleteBgXiSpeakFeedback', params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 子表批量删除
* @param params
*/
export const bgXiSpeakFeedbackDeleteBatch = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: '/bg/xispeak/bgXiSpeak/deleteBatchBgXiSpeakFeedback', data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 子表保存或者更新
* @param params
* @param isUpdate
*/
export const bgXiSpeakFeedbackSaveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? '/bg/xispeak/bgXiSpeak/editBgXiSpeakFeedback' : '/bg/xispeak/bgXiSpeak/addBgXiSpeakFeedback';
return defHttp.post({ url: url, params }, { isTransformResponse: false });
};
/**
* 子表导入
*/
export const bgXiSpeakFeedbackImportUrl = '/bg/xispeak/bgXiSpeak/importBgXiSpeakFeedback';
/**
* 子表导出
*/
export const bgXiSpeakFeedbackExportXlsUrl = '/bg/xispeak/bgXiSpeak/exportBgXiSpeakFeedback';
+190
View File
@@ -0,0 +1,190 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '习近平总书记重要讲话指示批示情况',
align: 'center',
dataIndex: 'speakStatus'
},
{
title: '时间',
align: 'center',
dataIndex: 'time',
customRender:({text}) =>{
text = !text ? "" : (text.length > 10 ? text.substr(0,10) : text);
return text;
},
},
{
title: '企业负责同志批示情况',
align: 'center',
dataIndex: 'elmComment'
},
{
title: '学习传达研究部署情况',
align: 'center',
dataIndex: 'status'
},
{
title: '责任人',
align: 'center',
dataIndex: 'responsiblePerson_dictText'
},
{
title: '落实方案',
align: 'center',
dataIndex: 'implPlan'
},
{
title: '牵头部门',
align: 'center',
dataIndex: 'leadDepartment_dictText'
},
{
title: '贯彻落实措施',
align: 'center',
dataIndex: 'implMeasures'
},
{
title: '会议决策数',
align: 'center',
dataIndex: 'numberOfResolutions'
},
{
title: '落实情况',
align: 'left',
dataIndex: 'implStatus'
},
{
title: '落实条数',
align: 'center',
dataIndex: 'implCount'
},
{
title: '已闭环数量',
align: 'center',
dataIndex: 'closedCount'
},
{
title: '报告反馈情况',
align: 'center',
dataIndex: 'reportFeedbackStatus'
},
{
title: '是否存在完成风险(0不存在,1存在)',
align: 'center',
dataIndex: 'isCompletionRisk'
},
{
title: '拖期风险应对措施',
align: 'center',
dataIndex: 'delayRiskMitigation'
},
{
title: '完成状态(0推进中,1已完成)',
align: 'center',
dataIndex: 'completionStatus'
},
{
title: '落实部门id',
align: 'center',
dataIndex: 'implDept_dictText'
},
{
title: '层深(最顶层节点层深为0)',
align: 'center',
dataIndex: 'treeDepth'
},
{
title: '同级数据排序号',
align: 'center',
dataIndex: 'sortOrder'
},
{
title: '流程引擎状态字段',
align: 'center',
dataIndex: 'bpmStatus'
},
{
title: '督办次数',
align: 'center',
dataIndex: 'supervisionCount'
},
];
//子表列表数据
export const bgXiSpeakFeedbackColumns: BasicColumn[] = [
{
title: '反馈部门id',
align: 'center',
dataIndex: 'responDeptid',
},
{
title: '反馈部门名称',
align: 'center',
dataIndex: 'responDeptname',
},
{
title: '反馈人id',
align: 'center',
dataIndex: 'responPersonid',
},
{
title: '反馈人姓名',
align: 'center',
dataIndex: 'responPersonname',
},
{
title: '反馈时间',
align: 'center',
dataIndex: 'responTime',
customRender: ({ text }) => {
text = !text ? '' : text.length > 10 ? text.substr(0, 10) : text;
return text;
},
},
{
title: '落实情况',
align: 'center',
dataIndex: 'implStatus',
},
{
title: '完成状态',
align: 'center',
dataIndex: 'completionStatus',
},
{
title: '外键',
align: 'center',
dataIndex: 'mainId',
},
];
// 高级查询数据
export const superQuerySchema = {
speakStatus: {title: '习近平总书记重要讲话指示批示情况',order: 1,view: 'text', type: 'string',},
time: {title: '时间',order: 2,view: 'date', type: 'string',},
elmComment: {title: '企业负责同志批示情况',order: 3,view: 'text', type: 'string',},
status: {title: '学习传达研究部署情况',order: 4,view: 'text', type: 'string',},
responsiblePerson: {title: '责任人',order: 5,view: 'sel_user', type: 'string',},
implPlan: {title: '落实方案',order: 6,view: 'text', type: 'string',},
leadDepartment: {title: '牵头部门',order: 7,view: 'sel_depart', type: 'string',},
implMeasures: {title: '贯彻落实措施',order: 8,view: 'text', type: 'string',},
numberOfResolutions: {title: '会议决策数',order: 9,view: 'number', type: 'number',},
implStatus: {title: '落实情况',order: 10,view: 'text', type: 'string',},
implCount: {title: '落实条数',order: 11,view: 'number', type: 'number',},
closedCount: {title: '已闭环数量',order: 12,view: 'number', type: 'number',},
reportFeedbackStatus: {title: '报告反馈情况',order: 13,view: 'text', type: 'string',},
isCompletionRisk: {title: '是否存在完成风险(0不存在,1存在)',order: 14,view: 'number', type: 'number',},
delayRiskMitigation: {title: '拖期风险应对措施',order: 15,view: 'text', type: 'string',},
completionStatus: {title: '完成状态(0推进中,1已完成)',order: 16,view: 'number', type: 'number',},
implDept: {title: '落实部门id',order: 17,view: 'sel_depart', type: 'string',},
treeDepth: {title: '层深(最顶层节点层深为0)',order: 18,view: 'number', type: 'number',},
sortOrder: {title: '同级数据排序号',order: 19,view: 'number', type: 'number',},
bpmStatus: {title: '流程引擎状态字段',order: 21,view: 'number', type: 'number',},
supervisionCount: {title: '督办次数',order: 22,view: 'number', type: 'number',},
};
+639
View File
@@ -0,0 +1,639 @@
<template>
<div class="erpNativeList">
<div class="content">
<!--引用表格-->
<BasicTable
@register="registerTable"
:rowSelection="rowSelection"
:expandedRowKeys="expandedRowKeys"
@expand="handleExpand"
@fetch-success="onFetchSuccess"
>
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'bg.xispeak:bg_xi_speak:add'" @click="handleAdd" preIcon="ant-design:plus-outlined">新增</a-button>
<a-button type="primary" v-auth="'bg.xispeak:bg_xi_speak:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
<j-upload-button type="primary" v-auth="'bg.xispeak:bg_xi_speak:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'bg.xispeak:bg_xi_speak:deleteBatch'"
>批量操作
<Icon icon="ant-design:down-outlined"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }"> </template>
</BasicTable>
<!--子表表格tab-->
<a-tabs defaultActiveKey="1">
<a-tab-pane tab="习总书记重要讲话反馈表" key="1">
<BgXiSpeakFeedbackList />
</a-tab-pane>
</a-tabs>
</div>
<!-- 表单区域 -->
<BgXiSpeakModal ref="registerModal" @success="handleSuccess"></BgXiSpeakModal>
<!-- 审批记录 -->
<BpmPictureModal @register="registerBpmModal" />
<FlowScheduleStartModal ref="flowScheduleModalRef" @confirm="handleFlowScheduleConfirm" />
<BusinessProcessListModal ref="businessProcessListModalRef" />
</div>
</template>
<script lang="ts" name="bg.xispeak-bgXiSpeak" setup>
import { ref, reactive, unref, computed, provide } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { startProcessSchedules } from '/@/api/common/api';
import { dateUtil } from '/@/utils/dateUtil';
import BgXiSpeakModal from './components/BgXiSpeakModal.vue';
import BgXiSpeakFeedbackList from './BgXiSpeakFeedbackList.vue';
import BusinessProcessListModal from '/src/components/semri/process/BusinessProcessListModal.vue';
import FlowScheduleStartModal from '/src/components/semri/process/FlowScheduleStartModal.vue';
import { columns as taskTaskColumns } from '/@/views/taskProcess/TaskTask.data';
import { columns, superQuerySchema } from './BgXiSpeak.data';
import { list, deleteBgXiSpeak, batchDeleteBgXiSpeak, getExportUrl, getImportUrl, getChildList, getChildListBatch } from './BgXiSpeak.api';
import JSelectDept from '/@/components/Form/src/jeecg/components/JSelectDept.vue';
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
const FLOW_CODE = 'dev_bg_xi_speak_001';
const FLOW_NAME = '习总书记重要讲话指示批示';
const BUSINESS_TABLE_NAME = 'bg_xi_speak';
const FORM_URL = 'bg/xispeak/components/BgXiSpeakForm';
const [registerBpmModal, { openModal: bpmPicModal }] = useModal();
const { createMessage } = useMessage();
const expandedRowKeys = ref([]);
const queryParam = ref<any>({});
const toggleSearchStatus = ref<boolean>(false);
const registerModal = ref();
const flowScheduleModalRef = ref();
const businessProcessListModalRef = ref();
const processColumns = taskTaskColumns;
// 是否显示DW领导列表选择
const showSdwLeaderSelect = computed(() => queryParam.value.needSdwApproval === '1');
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '习总书记重要讲话指示批示情况',
api: list,
columns,
canResize: false,
useSearchForm: false,
isTreeTable: true,
clickToRowSelect: true,
rowSelection: { type: 'radio' },
actionColumn: {
width: 120,
fixed: 'right',
},
beforeFetch: async (params) => {
params.hasQuery = 'true';
return Object.assign(params, queryParam.value);
},
pagination: {
current: 1,
pageSize: 5,
pageSizeOptions: ['5', '10', '20'],
},
},
exportConfig: {
name: '习总书记重要讲话指示批示情况',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
success: success,
},
});
const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
const mainId = computed(() => (unref(selectedRowKeys).length > 0 ? unref(selectedRowKeys)[0] : ''));
provide('mainId', mainId);
const labelCol = reactive({
xs: 24,
sm: 4,
xl: 6,
xxl: 4,
});
const wrapperCol = reactive({
xs: 24,
sm: 20,
});
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam.value[k] = params[k];
});
reload();
}
/**
* 新增事件
*/
function handleAdd() {
registerModal.value.disableSubmit = false;
registerModal.value.add();
}
/**
* 编辑事件
*/
async function handleEdit(record) {
registerModal.value.disableSubmit = false;
registerModal.value.edit(record);
}
/**
* 详情
*/
async function handleDetail(record) {
registerModal.value.disableSubmit = true;
registerModal.value.edit(record);
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteBgXiSpeak({ id: record.id }, success);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
const ids = selectedRowKeys.value.filter((item) => !item.includes('loadChild'));
await batchDeleteBgXiSpeak({ id: ids }, success);
}
/**
* 成功回调刷新页面
*/
function success() {
selectedRowKeys.value = [];
reload();
}
/**
* 成功回调
*/
async function handleSuccess({ isUpdate, values, expandedArr, changeParent }) {
if (isUpdate) {
if (changeParent) {
reload();
} else {
let data = await list({ id: values.id, pageSize: 1, pageNo: 1, pid: values['pid'] });
if (data && data.records && data.records.length > 0) {
updateTableDataRecord(values.id, data.records[0]);
} else {
updateTableDataRecord(values.id, values);
}
}
} else {
if (!values['id'] || !values['pid']) {
reload();
} else {
expandedRowKeys.value = [];
for (let key of unref(expandedArr)) {
await expandTreeNode(key);
}
}
}
}
/**
* 添加下级
*/
function handleAddSub(record) {
registerModal.value.disableSubmit = false;
registerModal.value.add(record);
}
/**
* 接口请求成功后回调
*/
function onFetchSuccess(result) {
getDataByResult(result.items) && loadDataByExpandedRows();
}
/**
* 根据已展开的行查询数据
*/
async function loadDataByExpandedRows() {
if (unref(expandedRowKeys).length > 0) {
const res = await getChildListBatch({ parentIds: unref(expandedRowKeys).join(',') });
if (res.success && res.result.records.length > 0) {
let records = res.result.records;
const listMap = new Map();
for (let item of records) {
let pid = item['pid'];
if (unref(expandedRowKeys).includes(pid)) {
let mapList = listMap.get(pid);
if (mapList == null) {
mapList = [];
}
mapList.push(item);
listMap.set(pid, mapList);
}
}
let childrenMap = listMap;
let fn = (list) => {
if (list) {
list.forEach((data) => {
if (unref(expandedRowKeys).includes(data.id)) {
data.children = getDataByResult(childrenMap.get(data.id));
fn(data.children);
}
});
}
};
fn(getDataSource());
}
}
}
/**
* 处理数据集
*/
function getDataByResult(result) {
if (result && result.length > 0) {
return result.map((item) => {
if (item['hasChild'] == '1') {
let loadChild = { id: item.id + '_loadChild', name: 'loading...', isLoading: true };
item.children = [loadChild];
}
return item;
});
}
}
/**
* 树节点展开合并
*/
async function handleExpand(expanded, record) {
if (expanded) {
expandedRowKeys.value.push(record.id);
if (record.children.length > 0 && !!record.children[0].isLoading) {
let result = await getChildList({ pid: record.id });
result = result.records ? result.records : result;
if (result && result.length > 0) {
record.children = getDataByResult(result);
} else {
record.children = null;
record.hasChild = '0';
}
}
} else {
let keyIndex = expandedRowKeys.value.indexOf(record.id);
if (keyIndex >= 0) {
expandedRowKeys.value.splice(keyIndex, 1);
}
}
}
/**
* 操作表格后处理树节点展开合并
*/
async function expandTreeNode(key) {
let record = findTableDataRecord(key);
expandedRowKeys.value.push(key);
let result = await getChildList({ pid: key });
if (result && result.length > 0) {
record.children = getDataByResult(result);
} else {
record.children = null;
record.hasChild = '0';
}
updateTableDataRecord(key, record);
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'bg.xispeak:bg_xi_speak:edit',
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
let dropDownAction = [
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '添加下级',
onClick: handleAddSub.bind(null, { pid: record.id }),
},
{
label: '删除',
popConfirm: {
title: '确定删除吗?',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'bg.xispeak:bg_xi_speak:delete',
},
{
label: '审批进度',
onClick: handlePreviewPic.bind(null, record),
ifShow: !!record.bpmStatus && record.bpmStatus !== '1',
},
{
label: '查询督办流程',
onClick: handleQueryProcess.bind(null, record),
},
];
if (record.bpmStatus == '1' || !record.bpmStatus) {
dropDownAction.push({
label: '发起流程',
onClick: handleProcess.bind(null, record),
});
}
return dropDownAction;
}
function handleProcess(record) {
flowScheduleModalRef.value?.open({
title: '发起流程',
payload: { record },
});
}
function handleQueryProcess(record) {
businessProcessListModalRef.value?.open({
title: '流程列表',
businessId: record.id,
columns: '',
});
}
async function handleFlowScheduleConfirm({ payload, options }) {
const record = payload?.record || {};
const formatDate = (value, format) => {
return value ? dateUtil(value).format(format) : undefined;
};
// 合并选中的DW领导列表到record中
const recordWithSdwLeader = {
...record,
sdwLeaderList: queryParam.value.sdwLeaderList || '',
needSdwApproval: queryParam.value.needSdwApproval || '',
};
const params = {
intervalType: options.intervalType,
startCount: options.startCount,
taskTask: {
triggerType: options.triggerType,
startTime: options.intervalStartTime,
flowCode: FLOW_CODE,
formUrl: FORM_URL,
flowName: FLOW_NAME,
jsonData: recordWithSdwLeader,
businessTablename: BUSINESS_TABLE_NAME,
businessId: record.id,
title: record.speakStatus,
content: record.implStatus,
urgencyLevel: record.completionStatus,
deptId: record.leadDepartment,
deptLeaderId: record.responsiblePerson,
requireFinishDate: formatDate(record.time, 'YYYY-MM-DD'),
deadline: formatDate(record.time, 'YYYY-MM-DD HH:mm:ss'),
},
};
await startProcessSchedules(params);
handleSuccess({ isUpdate: false, values: {}, expandedArr: [], changeParent: false });
}
async function handlePreviewPic(record) {
bpmPicModal(true, {
flowCode: FLOW_CODE,
dataId: record.id,
});
}
/**
* 查询
*/
function searchQuery() {
reload();
}
/**
* 重置
*/
function searchReset() {
queryParam.value = {};
selectedRowKeys.value = [];
reload();
}
/**
* form点击事件(以逗号分割)
*/
function handleFormJoinChange(key, value) {
if (typeof value != 'string') {
queryParam.value[key] = value.join(',');
}
}
</script>
<style lang="less" scoped>
.erpNativeList {
height: 100%;
padding: 16px;
background: #f5f7fa;
// 查询区域卡片
.search-card {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
:deep(.ant-form-item) {
margin-bottom: 0;
}
:deep(.ant-select),
:deep(.ant-picker) {
border-radius: 6px;
}
}
// 内容区域
.content {
background: #fff;
border-radius: 8px;
padding: 0;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
overflow: hidden;
}
// 表格标题区域
:deep(.ant-table-title) {
padding: 16px 20px !important;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
.ant-btn {
height: 32px;
padding: 0 16px;
border-radius: 6px;
font-size: 13px;
margin-right: 8px;
border: none;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
&:active {
transform: translateY(0);
}
}
.ant-btn-primary {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
&:hover {
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
}
}
}
// 表格样式优化
:deep(.ant-table-thead > tr > th) {
background: #fafafa;
font-weight: 600;
color: #333;
border-bottom: 1px solid #f0f0f0;
padding: 12px 16px;
}
:deep(.ant-table-tbody > tr > td) {
padding: 14px 16px;
border-bottom: 1px solid #f5f5f5;
transition: background 0.2s ease;
}
:deep(.ant-table-tbody > tr:hover > td) {
background: #f5f7fa;
}
// 操作按钮样式
:deep(.ant-btn-link) {
padding: 4px 8px;
height: auto;
border-radius: 4px;
&:hover {
background: rgba(24, 144, 255, 0.1);
}
}
// 标签页样式
:deep(.ant-tabs) {
margin: 16px;
.ant-tabs-nav {
margin-bottom: 12px;
}
.ant-tabs-tab {
padding: 8px 0;
font-size: 14px;
&:hover {
color: #1890ff;
}
}
.ant-tabs-tab-active .ant-tabs-tab-btn {
color: #1890ff;
font-weight: 500;
}
}
// 下拉菜单样式
:deep(.ant-dropdown-menu-item) {
border-radius: 4px;
margin: 2px 4px;
&:hover {
background: #e6f7ff;
}
}
// 分页样式
:deep(.ant-pagination) {
padding: 16px 20px;
margin: 0 !important;
border-top: 1px solid #f0f0f0;
background: #fafafa;
}
// 搜索和重置按钮
:deep(.ant-form-item-with-search) {
display: flex;
justify-content: flex-end;
gap: 8px;
}
}
// 响应式适配
@media (max-width: 768px) {
.erpNativeList {
padding: 12px;
.search-card {
padding: 16px;
}
}
}
</style>
@@ -0,0 +1,365 @@
<template>
<a-spin :spinning="confirmLoading">
<JFormContainer :disabled="disabled">
<template #detail>
<a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol" name="BgXiSpeakForm">
<a-row :gutter="20">
<a-col :span="24">
<a-form-item label="习近平总书记重要讲话指示批示情况" v-bind="validateInfos.speakStatus">
<a-input v-model:value="formData.speakStatus" placeholder="请输入重要指示批示情况" allow-clear></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="时间" v-bind="validateInfos.time">
<a-date-picker placeholder="请选择时间" v-model:value="formData.time" value-format="YYYY-MM-DD" style="width: 100%" allow-clear />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="责任人" v-bind="validateInfos.responsiblePerson">
<j-select-user v-model:value="formData.responsiblePerson" allow-clear />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="是否需要督办领导" v-bind="validateInfos.needSdwApproval">
<a-radio-group v-model:value="formData.needSdwApproval" class="radio-group">
<a-radio :value="1"></a-radio>
<a-radio :value="0"></a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="24" v-if="formData.needSdwApproval === 1">
<a-form-item label="督导领导列表" v-bind="validateInfos.sdwLeaderList">
<j-select-user v-model:value="formData.sdwLeaderList" :multiple="true" allow-clear placeholder="请选择督导领导" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="企业负责同志批示情况" v-bind="validateInfos.elmComment">
<a-input v-model:value="formData.elmComment" placeholder="请输入" allow-clear></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="学习传达研究部署情况" v-bind="validateInfos.status">
<a-input v-model:value="formData.status" placeholder="请输入" allow-clear></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="牵头部门" v-bind="validateInfos.leadDepartment">
<j-select-dept v-model:value="formData.leadDepartment" :multiple="true" checkStrictly allow-clear />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="落实部门" v-bind="validateInfos.implDept">
<j-select-dept v-model:value="formData.implDept" :multiple="true" checkStrictly allow-clear />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="落实方案" v-bind="validateInfos.implPlan">
<a-input v-model:value="formData.implPlan" placeholder="请输入" allow-clear></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="贯彻落实措施" v-bind="validateInfos.implMeasures">
<a-input v-model:value="formData.implMeasures" placeholder="请输入" allow-clear></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="会议决策数" v-bind="validateInfos.numberOfResolutions">
<a-input-number v-model:value="formData.numberOfResolutions" placeholder="请输入" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="落实条数" v-bind="validateInfos.implCount">
<a-input-number v-model:value="formData.implCount" placeholder="请输入" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="已闭环数量" v-bind="validateInfos.closedCount">
<a-input-number v-model:value="formData.closedCount" placeholder="请输入" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="落实情况" v-bind="validateInfos.implStatus">
<a-input v-model:value="formData.implStatus" placeholder="请输入" allow-clear></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="报告反馈情况" v-bind="validateInfos.reportFeedbackStatus">
<a-input v-model:value="formData.reportFeedbackStatus" placeholder="请输入" allow-clear></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="是否存在完成风险">
<a-radio-group v-model:value="formData.isCompletionRisk" class="radio-group">
<a-radio :value="0">不存在</a-radio>
<a-radio :value="1">存在</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="完成状态">
<a-radio-group v-model:value="formData.completionStatus" class="radio-group">
<a-radio :value="0">推进中</a-radio>
<a-radio :value="1">已完成</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="拖期风险应对措施" v-bind="validateInfos.delayRiskMitigation">
<a-input v-model:value="formData.delayRiskMitigation" placeholder="请输入" allow-clear></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="督办次数" v-bind="validateInfos.supervisionCount">
<a-input-number v-model:value="formData.supervisionCount" placeholder="请输入" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="父级节点" v-bind="validateInfos.pid">
<j-tree-select
v-model:value="formData.pid"
dict="bg_xi_speak,impl_status,id"
pidField="pid"
pidValue="0"
hasChildField="has_child"
:disabled="disabled"
/>
</a-form-item>
</a-col>
<a-col v-if="showFlowSubmitButton" :span="24" class="submit-area">
<a-button preIcon="ant-design:check-outlined" style="width: 140px; height: 40px" type="primary" @click="submitForm"> </a-button>
</a-col>
</a-row>
</a-form>
</template>
</JFormContainer>
</a-spin>
</template>
<script lang="ts" setup>
import { ref, reactive, defineExpose, nextTick, unref, defineProps, computed, onMounted } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
import JSelectDept from '/@/components/Form/src/jeecg/components/JSelectDept.vue';
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
import JTreeSelect from '/@/components/Form/src/jeecg/components/JTreeSelect.vue';
import { getValueType } from '/@/utils';
import { loadTreeData, saveOrUpdateDict } from '../BgXiSpeak.api';
import { Form } from 'ant-design-vue';
import JFormContainer from '/@/components/Form/src/container/JFormContainer.vue';
import { usePermission } from '/@/hooks/web/usePermission';
const { initBpmFormData } = usePermission();
const useForm = Form.useForm;
const formRef = ref();
const expandedRowKeys = ref([]);
const treeData = ref([]);
const emit = defineEmits(['register', 'ok']);
let model: Nullable<Recordable> = null;
// 1. 初始化值修改:needSdwApproval 默认为 0
const formData = reactive<Record<string, any>>({
id: '',
pid: '',
speakStatus: '',
time: '',
elmComment: '',
status: '',
responsiblePerson: '',
needSdwApproval: 0,
sdwLeaderList: '',
implPlan: '',
leadDepartment: '',
implMeasures: '',
numberOfResolutions: undefined,
implStatus: '',
implCount: undefined,
closedCount: undefined,
reportFeedbackStatus: '',
isCompletionRisk: undefined,
delayRiskMitigation: '',
completionStatus: undefined,
implDept: '',
treeDepth: undefined,
sortOrder: undefined,
delFlag: undefined,
bpmStatus: undefined,
supervisionCount: undefined,
});
const { createMessage } = useMessage();
const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 6 } });
const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 18 } });
const confirmLoading = ref<boolean>(false);
// 2. 表单验证:明确 needSdwApproval 的验证
const validatorRules = reactive({
needSdwApproval: [{ required: true, message: '请选择是否需要督办领导' }],
speakStatus: [{ required: true, message: '请输入指示批示情况' }],
});
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });
const props = defineProps({
formDisabled: { type: Boolean, default: false },
formData: { type: Object, default: () => ({}) },
formBpm: { type: Boolean, default: true },
});
const disabled = computed(() => {
if (props.formBpm === true) {
return props.formData.disabled !== false;
}
return props.formDisabled;
});
onMounted(() => {
initBpmFormData(props.formData);
initFormData();
});
const queryByIdUrl = '/bg/xispeak/bgXiSpeak/queryById';
async function initFormData() {
if (props.formBpm === true && props.formData.dataId) {
let params = { id: props.formData.dataId };
const data = await defHttp.get({ url: queryByIdUrl, params });
edit({ ...data });
}
}
const showFlowSubmitButton = computed(() => {
if (props.formBpm === true) {
return props.formData.disabled === false;
}
return false;
});
function add(obj = {}) {
edit(obj);
}
function edit(record) {
nextTick(async () => {
resetFields();
expandedRowKeys.value = [];
treeData.value = await loadTreeData({ async: false, pcode: '' });
const tmpData = {};
Object.keys(formData).forEach((key) => {
// 3. 赋值增强:处理后端返回 null 的情况
if (record.hasOwnProperty(key)) {
tmpData[key] = record[key] === null ? (key === 'needSdwApproval' ? 0 : '') : record[key];
}
});
Object.assign(formData, tmpData);
model = { ...tmpData };
});
}
function getExpandKeysByPid(pid, arr) {
if (pid && arr && arr.length > 0) {
for (let i = 0; i < arr.length; i++) {
if (arr[i].key == pid && unref(expandedRowKeys).indexOf(pid) < 0) {
expandedRowKeys.value.push(arr[i].key);
getExpandKeysByPid(arr[i]['parentId'], unref(treeData));
} else if (arr[i].children) {
getExpandKeysByPid(pid, arr[i].children);
}
}
}
}
async function submitForm() {
try {
await validate();
} catch ({ errorFields }) {
if (errorFields && errorFields[0]) {
formRef.value.scrollToField(errorFields[0].name, { behavior: 'smooth', block: 'center' });
}
return Promise.reject(errorFields);
}
// 4. 提交兜底:确保数值有效
if (formData.needSdwApproval === undefined || formData.needSdwApproval === null) {
formData.needSdwApproval = 0;
}
confirmLoading.value = true;
const isUpdate = !!formData.id;
// 拷贝一份数据进行转换,避免污染原始响应式对象
const params = { ...formData };
for (let key in params) {
if (params[key] instanceof Array) {
let valueType = getValueType(formRef.value.getProps, key);
if (valueType === 'string') {
params[key] = params[key].join(',');
}
}
}
await saveOrUpdateDict(params, isUpdate)
.then(async (res) => {
if (res.success) {
await getExpandKeysByPid(params['pid'], unref(treeData));
delete params['children'];
emit('ok', {
isUpdate: isUpdate,
values: { ...params },
expandedArr: unref(expandedRowKeys).reverse(),
changeParent: model != null && model['pid'] != params['pid'],
});
createMessage.success(res.message);
} else {
createMessage.warning(res.message);
}
})
.finally(() => {
confirmLoading.value = false;
});
}
defineExpose({ add, edit, submitForm });
</script>
<style lang="less" scoped>
/* 保持原有样式不变 */
.antd-modal-form {
padding: 20px;
.submit-area {
text-align: center;
padding-top: 16px;
margin-top: 8px;
border-top: 1px dashed #e8e8e8;
}
:deep(.ant-form-item-label > label) {
font-weight: 500;
color: #333;
}
:deep(.ant-radio-group.radio-group .ant-radio-wrapper) {
margin-right: 16px;
font-size: 14px;
}
:deep(.ant-input),
:deep(.ant-input-number),
:deep(.ant-select),
:deep(.ant-picker) {
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
border-color: #40a9ff;
}
}
:deep(.ant-btn-primary) {
height: 40px;
padding: 0 32px;
border-radius: 6px;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border: none;
font-size: 15px;
&:hover {
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
}
}
}
</style>
@@ -0,0 +1,89 @@
<template>
<j-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
<BgXiSpeakForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false"></BgXiSpeakForm>
<template #footer>
<a-button @click="handleCancel">取消</a-button>
<a-button :class="{ 'jee-hidden': disableSubmit }" type="primary" @click="handleOk">确认</a-button>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import { ref, nextTick, defineExpose } from 'vue';
import BgXiSpeakForm from './BgXiSpeakForm.vue'
import JModal from '/@/components/Modal/src/JModal/JModal.vue';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
const title = ref<string>('');
const width = ref<number>(800);
const visible = ref<boolean>(false);
const disableSubmit = ref<boolean>(false);
const registerForm = ref();
const emit = defineEmits(['register', 'success']);
/**
* 新增
*/
function add(obj={}) {
title.value = '新增';
visible.value = true;
nextTick(() => {
registerForm.value.add(obj);
});
}
/**
* 编辑
* @param record
*/
function edit(record) {
title.value = disableSubmit.value ? '详情' : '编辑';
visible.value = true;
nextTick(() => {
registerForm.value.edit(record);
});
}
/**
* 确定按钮点击事件
*/
function handleOk() {
registerForm.value.submitForm();
}
/**
* form保存回调事件
*/
function submitCallback({ isUpdate, values, expandedArr, changeParent }) {
handleCancel();
emit('success', {
isUpdate: isUpdate,
values: values,
expandedArr: expandedArr,
// 是否更改了父级节点
changeParent: changeParent,
});
}
/**
* 取消按钮回调事件
*/
function handleCancel() {
visible.value = false;
}
defineExpose({
add,
edit,
disableSubmit,
});
</script>
<style lang="less">
/**隐藏样式-modal确定按钮 */
.jee-hidden {
display: none !important;
}
</style>
<style lang="less" scoped></style>
@@ -2,8 +2,8 @@
<BasicTable @register="registerTable"> <BasicTable @register="registerTable">
<!--插槽:table标题--> <!--插槽:table标题-->
<template #tableTitle> <template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate" v-if="isMiniDesgin=='default'">新建流程</a-button> <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate" v-if="isMiniDesgin == 'default'">新建流程</a-button>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreateMini" v-if="isMiniDesgin=='mini'">新建简流程</a-button> <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreateMini" v-if="isMiniDesgin == 'mini'">新建简流程</a-button>
</template> </template>
<!--操作栏--> <!--操作栏-->
<template #action="{ record }"> <template #action="{ record }">
@@ -43,7 +43,7 @@
</template> </template>
<script> <script>
import {ref, unref, reactive, nextTick} from 'vue'; import { ref, unref, reactive, nextTick } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table'; import { BasicTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage'; import { useListPage } from '/@/hooks/system/useListPage';
import { list, publish, deleteOne, copy, uploadProcess } from './process.design.api'; import { list, publish, deleteOne, copy, uploadProcess } from './process.design.api';
@@ -52,14 +52,14 @@
import { filterDictText } from '/@/utils/dict/JDictSelectUtil'; import { filterDictText } from '/@/utils/dict/JDictSelectUtil';
import { getToken } from '/@/utils/auth'; import { getToken } from '/@/utils/auth';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router';
import { useDrawer } from '/@/components/Drawer'; import { useDrawer } from '/@/components/Drawer';
import { useModal } from '/@/components/Modal'; import { useModal } from '/@/components/Modal';
import ProcessDesignModal from './modal/ProcessDesignModal.vue'; import ProcessDesignModal from './modal/ProcessDesignModal.vue';
import ProcessConfigModal from './modal/ProcessConfigModal.vue'; import ProcessConfigModal from './modal/ProcessConfigModal.vue';
import ProcessIconModal from './modal/ProcessIconModal.vue'; import ProcessIconModal from './modal/ProcessIconModal.vue';
import ProcessDeploymentModal from './modal/ProcessDeploymentModal.vue'; import ProcessDeploymentModal from './modal/ProcessDeploymentModal.vue';
import ProcessOtherConfigDrawer from './modal/ProcessOtherConfigDrawer.vue' import ProcessOtherConfigDrawer from './modal/ProcessOtherConfigDrawer.vue';
export default { export default {
name: 'ProcessDesignList', name: 'ProcessDesignList',
@@ -70,7 +70,7 @@
ProcessConfigModal, ProcessConfigModal,
ProcessIconModal, ProcessIconModal,
ProcessDeploymentModal, ProcessDeploymentModal,
ProcessOtherConfigDrawer ProcessOtherConfigDrawer,
}, },
setup() { setup() {
const { createMessage: $message } = useMessage(); const { createMessage: $message } = useMessage();
@@ -78,33 +78,33 @@
//update-begin---author:scott ---date:2022-10-21 for:默认流程和简版流程,拆分成两个菜单,方便维护----------- //update-begin---author:scott ---date:2022-10-21 for:默认流程和简版流程,拆分成两个菜单,方便维护-----------
//自定义查询条件默认值 //自定义查询条件默认值
let searchFormSchemaNew = searchFormSchema; let searchFormSchemaNew = searchFormSchema;
const isMiniDesgin = ref("default"); const isMiniDesgin = ref('default');
//获取路由地址 //获取路由地址
let router = useRouter() let router = useRouter();
console.log('router.currentRoute.value.path', router.currentRoute.value.path) console.log('router.currentRoute.value.path', router.currentRoute.value.path);
if(router.currentRoute.value.path.endsWith("/mini")){ if (router.currentRoute.value.path.endsWith('/mini')) {
isMiniDesgin.value = 'mini' isMiniDesgin.value = 'mini';
searchFormSchemaNew = searchFormSchema.map(item => { searchFormSchemaNew = searchFormSchema.map((item) => {
if(item.field == 'processDesginType'){ if (item.field == 'processDesginType') {
item.defaultValue = 'mini'; item.defaultValue = 'mini';
} }
}) });
}else if(router.currentRoute.value.path.endsWith("/list/desflow")){ } else if (router.currentRoute.value.path.endsWith('/list/desflow')) {
isMiniDesgin.value = 'mini' isMiniDesgin.value = 'mini';
searchFormSchemaNew = searchFormSchema.map(item => { searchFormSchemaNew = searchFormSchema.map((item) => {
if(item.field == 'processDesginType'){ if (item.field == 'processDesginType') {
item.defaultValue = 'mini'; item.defaultValue = 'mini';
} }
}) });
}else{ } else {
isMiniDesgin.value = 'default' isMiniDesgin.value = 'default';
searchFormSchemaNew = searchFormSchema.map(item => { searchFormSchemaNew = searchFormSchema.map((item) => {
if(item.field == 'processDesginType'){ if (item.field == 'processDesginType') {
item.defaultValue = 'default'; item.defaultValue = 'default';
} }
}) });
} }
//update-end---author:scott ---date::2022-10-21 for:默认流程和简版流程,拆分成两个菜单,方便维护-------------- //update-end---author:scott ---date::2022-10-21 for:默认流程和简版流程,拆分成两个菜单,方便维护--------------
@@ -124,7 +124,7 @@
schemas: searchFormSchema, schemas: searchFormSchema,
autoAdvancedCol: 4, autoAdvancedCol: 4,
baseColProps: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8, xxl: 5 }, baseColProps: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8, xxl: 5 },
actionColOptions: { ext_act_process_formactionColOptions: {
xs: 24, xs: 24,
sm: 12, sm: 12,
md: 8, md: 8,
@@ -135,12 +135,11 @@
}, },
}, },
}); });
const [registerTable, { getForm , reload }] = tableContext; const [registerTable, { getForm, reload }] = tableContext;
nextTick(() => { nextTick(() => {
console.log("getForm() data", getForm().getFieldsValue()); console.log('getForm() data', getForm().getFieldsValue());
}); });
const [registerDesignModal, { openModal: openDesignModal }] = useModal(); const [registerDesignModal, { openModal: openDesignModal }] = useModal();
const [registerMiniDesFlowModal, { openModal: openDesignModalMini }] = useModal(); const [registerMiniDesFlowModal, { openModal: openDesignModalMini }] = useModal();
@@ -171,7 +170,6 @@
}); });
} }
function handleUpdate(record) { function handleUpdate(record) {
openDesignModal(true, { openDesignModal(true, {
isUpdate: true, isUpdate: true,
@@ -211,7 +209,7 @@
label: '简流设计', label: '简流设计',
onClick: handleUpdateTest.bind(null, record), onClick: handleUpdateTest.bind(null, record),
ifShow: () => { ifShow: () => {
return isMiniDesgin.value =='mini'; return isMiniDesgin.value == 'mini';
}, },
}, },
{ {
@@ -264,8 +262,8 @@
{ {
label: '上传流程', label: '上传流程',
slot: 'updateProcess', slot: 'updateProcess',
}, }
/* { /* {
label: '表单图标', label: '表单图标',
onClick: handleIcon.bind(null, record), onClick: handleIcon.bind(null, record),
},*/ },*/
@@ -282,8 +280,8 @@
return arr; return arr;
} }
function handleOtherConfig(record){ function handleOtherConfig(record) {
openDrawer(true, record) openDrawer(true, record);
} }
async function handleDelete(record) { async function handleDelete(record) {
@@ -353,7 +351,7 @@
registerDeploymentModal, registerDeploymentModal,
registerOtherDrawer, registerOtherDrawer,
isMiniDesgin isMiniDesgin,
}; };
}, },
}; };
@@ -362,7 +360,7 @@
<style lang="less"> <style lang="less">
/*QQYUN-3131【流程列表】删除效果,为什么跟复制流程效果不一样*/ /*QQYUN-3131【流程列表】删除效果,为什么跟复制流程效果不一样*/
.ant-popconfirm { .ant-popconfirm {
.ant-popover-buttons{ .ant-popover-buttons {
min-width: 120px; min-width: 120px;
text-align: center; text-align: center;
} }
+76 -70
View File
@@ -4,37 +4,42 @@
<div class="jeecg-basic-table-form-container"> <div class="jeecg-basic-table-form-container">
<a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol"> <a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-row :gutter="24"> <a-row :gutter="24">
<a-col :lg="6"> <a-col :lg="6">
<a-form-item name="title"> <a-form-item name="title">
<template #label><span title="任务标题">任务标题</span></template> <template #label><span title="任务标题">任务标题</span></template>
<JInput v-model:value="queryParam.title"/> <JInput v-model:value="queryParam.title" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :lg="6"> <a-col :lg="6">
<a-form-item name="content"> <a-form-item name="content">
<template #label><span title="任务内容">任务内容</span></template> <template #label><span title="任务内容">任务内容</span></template>
<JInput v-model:value="queryParam.content"/> <JInput v-model:value="queryParam.content" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<template v-if="toggleSearchStatus"> <template v-if="toggleSearchStatus">
<a-col :lg="6"> <a-col :lg="6">
<a-form-item name="urgencyLevel"> <a-form-item name="urgencyLevel">
<template #label><span title="紧急程度(一般/紧急/特急)">紧急程度</span></template> <template #label><span title="紧急程度(一般/紧急/特急)">紧急程度</span></template>
<j-select-multiple placeholder="请选择紧急程度(一般/紧急/特急)" v-model:value="queryParam.urgencyLevel" dictCode="urgency_level" allow-clear /> <j-select-multiple
placeholder="请选择紧急程度(一般/紧急/特急)"
v-model:value="queryParam.urgencyLevel"
dictCode="urgency_level"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :lg="6">
<a-form-item name="deptLeaderName">
<template #label><span title="部门负责人姓名(冗余)">部门负责</span></template>
<JInput v-model:value="queryParam.deptLeaderName" />
</a-form-item>
</a-col>
<a-col :lg="6">
<a-form-item name="deptHandlerName">
<template #label><span title="部门经办人姓名(冗余)">部门经办</span></template>
<JInput v-model:value="queryParam.deptHandlerName" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :lg="6">
<a-form-item name="deptLeaderName">
<template #label><span title="部门负责人姓名(冗余)">部门负责</span></template>
<JInput v-model:value="queryParam.deptLeaderName"/>
</a-form-item>
</a-col>
<a-col :lg="6">
<a-form-item name="deptHandlerName">
<template #label><span title="部门经办人姓名(冗余)">部门经办</span></template>
<JInput v-model:value="queryParam.deptHandlerName"/>
</a-form-item>
</a-col>
</template> </template>
<a-col :xl="6" :lg="7" :md="8" :sm="24"> <a-col :xl="6" :lg="7" :md="8" :sm="24">
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons"> <span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
@@ -55,9 +60,11 @@
<BasicTable @register="registerTable" :rowSelection="rowSelection"> <BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题--> <!--插槽:table标题-->
<template #tableTitle> <template #tableTitle>
<a-button type="primary" v-auth="'tasktask:task_task:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button> <a-button type="primary" v-auth="'tasktask:task_task:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'tasktask:task_task:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button> <a-button type="primary" v-auth="'tasktask:task_task:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'tasktask:task_task:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button> <j-upload-button type="primary" v-auth="'tasktask:task_task:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls"
>导入</j-upload-button
>
<a-dropdown v-if="selectedRowKeys.length > 0"> <a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay> <template #overlay>
<a-menu> <a-menu>
@@ -67,7 +74,8 @@
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
</template> </template>
<a-button v-auth="'tasktask:task_task:deleteBatch'">批量操作 <a-button v-auth="'tasktask:task_task:deleteBatch'"
>批量操作
<Icon icon="mdi:chevron-down"></Icon> <Icon icon="mdi:chevron-down"></Icon>
</a-button> </a-button>
</a-dropdown> </a-dropdown>
@@ -76,10 +84,9 @@
</template> </template>
<!--操作栏--> <!--操作栏-->
<template #action="{ record }"> <template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/> <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
<template v-slot:bodyCell="{ column, record, index, text }">
</template> </template>
<template v-slot:bodyCell="{ column, record, index, text }"> </template>
</BasicTable> </BasicTable>
<!-- 表单区域 --> <!-- 表单区域 -->
<TaskTaskModal ref="registerModal" @success="handleSuccess"></TaskTaskModal> <TaskTaskModal ref="registerModal" @success="handleSuccess"></TaskTaskModal>
@@ -95,15 +102,15 @@
import { columns, superQuerySchema } from './TaskTask.data'; import { columns, superQuerySchema } from './TaskTask.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './TaskTask.api'; import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './TaskTask.api';
import { downloadFile } from '/@/utils/common/renderUtils'; import { downloadFile } from '/@/utils/common/renderUtils';
import TaskTaskModal from './components/TaskTaskModal.vue' import TaskTaskModal from './components/TaskTaskModal.vue';
import { useUserStore } from '/@/store/modules/user'; import { useUserStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import {useModal} from '/@/components/Modal'; import { useModal } from '/@/components/Modal';
import JDictSelectTag from '/@/components/Form/src/jeecg/components/JDictSelectTag.vue'; import JDictSelectTag from '/@/components/Form/src/jeecg/components/JDictSelectTag.vue';
import JSelectMultiple from '/@/components/Form/src/jeecg/components/JSelectMultiple.vue'; import JSelectMultiple from '/@/components/Form/src/jeecg/components/JSelectMultiple.vue';
import JSelectDept from '/@/components/Form/src/jeecg/components/JSelectDept.vue'; import JSelectDept from '/@/components/Form/src/jeecg/components/JSelectDept.vue';
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue'; import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
import JInput from "/@/components/Form/src/jeecg/components/JInput.vue"; import JInput from '/@/components/Form/src/jeecg/components/JInput.vue';
import { startProcess } from '/@/api/common/api'; import { startProcess } from '/@/api/common/api';
const [registerBpmModal, { openModal: bpmPicModal }] = useModal(); const [registerBpmModal, { openModal: bpmPicModal }] = useModal();
@@ -119,7 +126,7 @@
title: '事项任务计划表(发起流程)', title: '事项任务计划表(发起流程)',
api: list, api: list,
columns, columns,
canResize:false, canResize: false,
useSearchForm: false, useSearchForm: false,
actionColumn: { actionColumn: {
width: 120, width: 120,
@@ -130,21 +137,22 @@
}, },
}, },
exportConfig: { exportConfig: {
name: "事项任务计划表(发起流程)", name: '事项任务计划表(发起流程)',
url: getExportUrl, url: getExportUrl,
params: queryParam, params: queryParam,
}, },
importConfig: { importConfig: {
url: getImportUrl, url: getImportUrl,
success: handleSuccess success: handleSuccess,
}, },
}); });
const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext; const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] =
tableContext;
const labelCol = reactive({ const labelCol = reactive({
xs:24, xs: 24,
sm:4, sm: 4,
xl:6, xl: 6,
xxl:4 xxl: 4,
}); });
const wrapperCol = reactive({ const wrapperCol = reactive({
xs: 24, xs: 24,
@@ -217,7 +225,7 @@
{ {
label: '编辑', label: '编辑',
onClick: handleEdit.bind(null, record), onClick: handleEdit.bind(null, record),
auth: 'tasktask:task_task:edit' auth: 'tasktask:task_task:edit',
}, },
]; ];
} }
@@ -230,30 +238,31 @@
{ {
label: '详情', label: '详情',
onClick: handleDetail.bind(null, record), onClick: handleDetail.bind(null, record),
}, { },
{
label: '删除', label: '删除',
popConfirm: { popConfirm: {
title: '是否确认删除', title: '是否确认删除',
confirm: handleDelete.bind(null, record), confirm: handleDelete.bind(null, record),
placement: 'topLeft', placement: 'topLeft',
}, },
auth: 'tasktask:task_task:delete' auth: 'tasktask:task_task:delete',
},
{
label: '审批进度',
onClick: handlePreviewPic.bind(null, record),
ifShow: !!record.bpmStatus && record.bpmStatus !== '1',
}, },
{
label: '审批进度',
onClick: handlePreviewPic.bind(null, record),
ifShow: !!record.bpmStatus && record.bpmStatus !== '1',
}
]; ];
if(record.bpmStatus == '1'){ if (record.bpmStatus == '1') {
dropDownAction.push({ dropDownAction.push({
label: '发起流程', label: '发起流程',
popConfirm: { popConfirm: {
title: '确认提交流程吗?', title: '确认提交流程吗?',
confirm: handleProcess.bind(null, record), confirm: handleProcess.bind(null, record),
placement: 'topLeft', placement: 'topLeft',
} },
}) });
} }
return dropDownAction; return dropDownAction;
} }
@@ -275,7 +284,6 @@
reload(); reload();
} }
/** /**
* form点击事件(以逗号分割) * form点击事件(以逗号分割)
* @param key * @param key
@@ -295,23 +303,20 @@
flowCode: 'dev_task_task_001', flowCode: 'dev_task_task_001',
id: record.id, id: record.id,
formUrl: 'taskProcess/components/TaskTaskForm', formUrl: 'taskProcess/components/TaskTaskForm',
formUrlMobile: '' formUrlMobile: '',
} };
await startProcess(params); await startProcess(params);
handleSuccess(); handleSuccess();
} }
/** /**
* 审批进度 * 审批进度
*/ */
async function handlePreviewPic(record) { async function handlePreviewPic(record) {
bpmPicModal(true, { bpmPicModal(true, {
flowCode: 'dev_task_task_001', flowCode: record.flowCode,
dataId: record.id, dataId: record.id,
}); });
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@@ -322,19 +327,20 @@
margin-bottom: 24px; margin-bottom: 24px;
white-space: nowrap; white-space: nowrap;
} }
.query-group-cust{ .query-group-cust {
min-width: 100px !important; min-width: 100px !important;
} }
.query-group-split-cust{ .query-group-split-cust {
width: 30px; width: 30px;
display: inline-block; display: inline-block;
text-align: center text-align: center;
} }
.ant-form-item:not(.ant-form-item-with-help){ .ant-form-item:not(.ant-form-item-with-help) {
margin-bottom: 16px; margin-bottom: 16px;
height: 32px; height: 32px;
} }
:deep(.ant-picker),:deep(.ant-input-number){ :deep(.ant-picker),
:deep(.ant-input-number) {
width: 100%; width: 100%;
} }
} }
+155
View File
@@ -0,0 +1,155 @@
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
enum Api {
list = '/tasklist/taskList/list',
save = '/tasklist/taskList/add',
edit = '/tasklist/taskList/edit',
deleteOne = '/tasklist/taskList/delete',
deleteBatch = '/tasklist/taskList/deleteBatch',
importExcel = '/tasklist/taskList/importExcel',
exportXls = '/tasklist/taskList/exportXls',
taskListDetialList = '/tasklist/taskList/queryTaskListDetialByMainId',
treeList = '/tasklist/taskList/treeList',
myFavorites = '/tasklist/taskList/myFavorites',
addTaskList = '/tasklist/taskList/addTaskList',
addTaskListGroup = '/tasklist/taskList/addTaskListGroup',
moveTaskList = '/tasklist/taskList/moveTaskList',
moveGroup = '/tasklist/taskList/moveGroup',
deleteTaskList = '/tasklist/taskList/deleteTaskList',
removeFavorite = '/tasklist/taskList/removeFavorite',
removeFavoriteGroup = '/tasklist/taskList/removeFavoriteGroup',
addToFavorites = '/tasklist/taskList/addToFavorites',
renameGroup = '/tasklist/taskList/renameGroup',
renameTaskList = '/tasklist/taskList/renameTaskList',
myOwnLists = '/tasklist/taskList/myOwnLists',
allLists = '/tasklist/taskList/allLists',
myCollabLists = '/tasklist/taskList/myCollabLists',
addTask = '/tasklist/taskListDetial/add',
editTask = '/tasklist/taskListDetial/edit',
deleteTask = '/tasklist/taskListDetial/delete',
toggleTaskStatus = '/tasklist/taskListDetial/toggleStatus',
getCollaborators = '/tasklist/taskList/getCollaborators',
addCollaborator = '/tasklist/taskList/addCollaborator',
removeCollaborator = '/tasklist/taskList/removeCollaborator',
updateCollaboratorPermission = '/tasklist/taskList/updateCollaboratorPermission',
getMyPermission = '/tasklist/taskList/getMyPermission',
moveTask = '/tasklist/taskListDetial/moveTask',
moveTaskGroup = '/tasklist/taskListDetial/moveTaskGroup',
followTask = '/tasklist/taskListDetial/follow',
unfollowTask = '/tasklist/taskListDetial/unfollow',
loadSubTasks = '/tasklist/taskListDetial/loadSubTasks',
listByMainId = '/tasklist/taskListDetial/listByMainId',
listAllByMainId = '/tasklist/taskListDetial/listAllByMainId',
myResponsibleTasks = '/tasklist/taskList/myResponsibleTasks',
myFollowedTasks = '/tasklist/taskList/myFollowedTasks',
getCurrentUserSecurityLevel = '/tasklist/taskList/getCurrentUserSecurityLevel',
}
export const getExportUrl = Api.exportXls;
export const getImportUrl = Api.importExcel;
export const taskListDetialList = Api.taskListDetialList;
export const list = (params) => defHttp.get({ url: Api.list, params });
export const treeList = (params) => defHttp.get({ url: Api.treeList, params });
export const deleteOne = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
export const saveOrUpdate = (params, isUpdate) => {
const url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
export const addGroup = (params) => defHttp.post({ url: Api.addTaskListGroup, params });
export const addTaskList = (params) => defHttp.post({ url: Api.addTaskList, params });
export const addTaskListGroup = (params) => defHttp.post({ url: Api.addTaskListGroup, params });
export const moveTaskList = (params) => defHttp.post({ url: Api.moveTaskList, params });
export const moveGroup = (params) => defHttp.post({ url: Api.moveGroup, params });
export const getMyFavorites = () => defHttp.get({ url: Api.myFavorites });
export const deleteTaskListApi = (params) => defHttp.post({ url: Api.deleteTaskList, params });
export const removeFavorite = (params) => defHttp.post({ url: Api.removeFavorite, params });
export const addToFavorites = (params) => defHttp.post({ url: Api.addToFavorites, params });
export const removeFavoriteGroup = (params) => defHttp.post({ url: Api.removeFavoriteGroup, params });
export const renameGroup = (params) => defHttp.post({ url: Api.renameGroup, params });
export const renameTaskList = (params) => defHttp.post({ url: Api.renameTaskList, params });
export const getMyOwnLists = () => defHttp.get({ url: Api.myOwnLists });
export const getAllLists = () => defHttp.get({ url: Api.allLists });
export const getMyCollabLists = () => defHttp.get({ url: Api.myCollabLists });
export const addTask = (params) => defHttp.post({ url: Api.addTask, params });
export const editTask = (params) => defHttp.post({ url: Api.editTask, params });
export const deleteTask = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteTask, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
export const toggleTaskStatus = (params) => defHttp.post({ url: Api.toggleTaskStatus, params });
export const getCollaborators = (params) => defHttp.get({ url: Api.getCollaborators, params });
export const addCollaborator = (params) => defHttp.post({ url: Api.addCollaborator, params });
export const removeCollaborator = (params) => defHttp.post({ url: Api.removeCollaborator, params });
export const updateCollaboratorPermission = (params) => defHttp.post({ url: Api.updateCollaboratorPermission, params });
export const getMyPermission = (params) => defHttp.get({ url: Api.getMyPermission, params });
export const moveTask = (params) => defHttp.post({ url: Api.moveTask, params });
export const moveTaskGroup = (params) => defHttp.post({ url: Api.moveTaskGroup, params });
export const followTask = (params) => defHttp.post({ url: Api.followTask, params });
export const unfollowTask = (params) => defHttp.post({ url: Api.unfollowTask, params });
export const loadSubTasks = (params) => defHttp.get({ url: Api.loadSubTasks, params });
export const listByMainId = (params) => defHttp.get({ url: Api.listByMainId, params });
export const listAllByMainId = (params) => defHttp.get({ url: Api.listAllByMainId, params });
export const myResponsibleTasks = () => defHttp.get({ url: Api.myResponsibleTasks });
export const myFollowedTasks = () => defHttp.get({ url: Api.myFollowedTasks });
export const getCurrentUserSecurityLevel = () => defHttp.get({ url: Api.getCurrentUserSecurityLevel });
+606
View File
@@ -0,0 +1,606 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { JVxeTypes, JVxeColumn } from '/@/components/jeecg/JVxeTable/types';
import type { TaskListGroup } from './types';
import { getAvailableSecretLevels } from './types';
export const columns: BasicColumn[] = [
{
title: '任务清单名称',
align: 'center',
dataIndex: 'tasklistName',
},
{
title: '父级节点',
align: 'center',
dataIndex: 'pid',
},
{
title: '是否有子节点',
align: 'center',
dataIndex: 'hasChild',
},
{
title: '排序号',
align: 'center',
dataIndex: 'sortOrder',
},
{
title: '类型',
align: 'center',
dataIndex: 'type',
},
];
export const searchFormSchema: FormSchema[] = [];
export const formSchema: FormSchema[] = [
{
label: '任务清单名称',
field: 'tasklistName',
component: 'Input',
},
{
label: '父级节点',
field: 'pid',
component: 'Input',
},
{
label: '是否有子节点',
field: 'hasChild',
component: 'Input',
},
{
label: '排序号',
field: 'sortOrder',
component: 'InputNumber',
},
{
label: '类型',
field: 'type',
component: 'Input',
},
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
];
export const taskListDetialColumns: JVxeColumn[] = [
{
title: '父节点ID',
key: 'pid',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '是否有子节点',
key: 'hasChild',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '排序号',
key: 'sortOrder',
type: JVxeTypes.inputNumber,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '任务名称',
key: 'taskName',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '任务描述',
key: 'taskDesc',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '优先级',
key: 'priority',
type: JVxeTypes.select,
options: [],
dictCode: 'task_priority',
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '完成状态',
key: 'taskStatus',
type: JVxeTypes.inputNumber,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '负责人ID',
key: 'assigneeId',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '关注人ID',
key: 'followersId',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '分配人ID',
key: 'assignId',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '开始时间',
key: 'startTime',
type: JVxeTypes.datetime,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '结束时间',
key: 'endTime',
type: JVxeTypes.datetime,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '完成时间',
key: 'completeTime',
type: JVxeTypes.datetime,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '类型',
key: 'type',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '子任务数',
key: 'subTaskCount',
type: JVxeTypes.inputNumber,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '子任务完成数',
key: 'completedSubTaskCount',
type: JVxeTypes.inputNumber,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
];
export const superQuerySchema = {
tasklistName: { title: '任务清单名称', order: 0, view: 'text', type: 'string' },
pid: { title: '父级节点', order: 1, view: 'text', type: 'string' },
hasChild: { title: '是否有子节点', order: 2, view: 'text', type: 'string' },
sortOrder: { title: '排序号', order: 3, view: 'number', type: 'number' },
type: { title: '类型', order: 4, view: 'text', type: 'string' },
taskListDetial: {
title: '工作任务清单详情',
view: 'table',
fields: {
mainId: { title: '外键', order: 0, view: 'text', type: 'string' },
pid: { title: '父节点ID', order: 1, view: 'text', type: 'string' },
hasChild: { title: '是否有子节点', order: 2, view: 'text', type: 'string' },
sortOrder: { title: '排序号', order: 3, view: 'number', type: 'number' },
taskName: { title: '任务名称', order: 4, view: 'text', type: 'string' },
taskDesc: { title: '任务描述', order: 5, view: 'text', type: 'string' },
priority: { title: '优先级', order: 6, view: 'list', type: 'string', dictCode: 'task_priority' },
taskStatus: { title: '完成状态', order: 7, view: 'number', type: 'number' },
assigneeId: { title: '负责人ID', order: 8, view: 'text', type: 'string' },
followersId: { title: '关注人ID', order: 9, view: 'text', type: 'string' },
assignId: { title: '分配人ID', order: 10, view: 'text', type: 'string' },
startTime: { title: '开始时间', order: 11, view: 'datetime', type: 'string' },
endTime: { title: '结束时间', order: 12, view: 'datetime', type: 'string' },
completeTime: { title: '完成时间', order: 13, view: 'datetime', type: 'string' },
type: { title: '类型', order: 14, view: 'text', type: 'string' },
subTaskCount: { title: '子任务数', order: 15, view: 'number', type: 'number' },
completedSubTaskCount: { title: '子任务完成数', order: 16, view: 'number', type: 'number' },
},
},
};
export function getBpmFormSchema(_formData): FormSchema[] {
return formSchema;
}
export const createTaskFormSchema: FormSchema[] = [
{
label: '任务标题',
field: 'taskName',
component: 'Input',
required: true,
},
{
label: '任务描述',
field: 'taskDesc',
component: 'InputTextArea',
componentProps: { rows: 3 },
},
{
label: '优先级',
field: 'priority',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'task_priority',
placeholder: '请选择优先级',
},
},
{
label: '负责人',
field: 'assigneeId',
component: 'JSelectUser',
componentProps: {
labelKey: 'realname',
rowKey: 'id',
isRadioSelection: true,
},
},
{
label: '分配人',
field: 'assignId',
component: 'JSelectUser',
componentProps: {
labelKey: 'realname',
rowKey: 'id',
isRadioSelection: true,
},
},
{
label: '开始日期',
field: 'startTime',
component: 'DatePicker',
componentProps: {
style: { width: '100%' },
},
},
{
label: '截止日期',
field: 'endTime',
component: 'DatePicker',
componentProps: {
style: { width: '100%' },
},
},
{
label: '',
field: 'groupId',
component: 'Input',
show: false,
},
];
export function getCreateListFormSchema(userSecurityLevel: number): FormSchema[] {
if (!userSecurityLevel) userSecurityLevel = 3;
return [
{
label: '清单名称',
field: 'tasklistName',
component: 'Input',
required: true,
componentProps: {
placeholder: '请输入清单名称',
},
},
{
label: '密级',
field: 'secretLevel',
component: 'Select',
required: true,
defaultValue: 1,
componentProps: {
getPopupContainer: () => document.body,
options: getAvailableSecretLevels(userSecurityLevel),
placeholder: '请选择密级',
},
},
];
}
export const mockTaskListGroups: TaskListGroup[] = [
{
id: 'tlg-1',
name: '任务清单',
pid: '0',
hasChild: '1',
type: 0,
sortOrder: 1,
delFlag: 0,
taskLists: [
{
id: 'tl-1',
name: '测试',
pid: 'tlg-1',
hasChild: '0',
type: 1,
sortOrder: 1,
delFlag: 0,
groups: [
{
id: 'g-1',
name: '默认分组',
taskListId: 'tl-1',
collapsed: false,
tasks: [
{
id: 't-1',
taskName: '导入数据、说明书编写、bug调试',
taskDesc: '',
completed: true,
priority: 'high',
assigneeId: 'u1',
assigneeName: '惠灵佳',
assignId: 'u2',
assignName: '管理员',
followersId: '',
followersName: '',
createBy: '惠灵佳',
startTime: '2025-12-15',
endTime: '2025-12-19',
completeTime: '2025-12-18',
createTime: '2025-12-15',
updateTime: '2025-12-18',
groupId: 'g-1',
groupName: '默认分组',
childCount: 2,
completedChildCount: 1,
pid: '',
hasChild: '1',
},
{
id: 't-1-1',
taskName: '数据导入脚本',
taskDesc: '编写Python导入脚本',
completed: true,
priority: 'high',
assigneeId: 'u1',
assigneeName: '惠灵佳',
assignId: 'u2',
assignName: '管理员',
followersId: '',
followersName: '',
createBy: '惠灵佳',
startTime: '2025-12-15',
endTime: '2025-12-16',
completeTime: '2025-12-16',
createTime: '2025-12-15',
updateTime: '2025-12-16',
groupId: 'g-1',
groupName: '默认分组',
childCount: 0,
completedChildCount: 0,
pid: 't-1',
hasChild: '0',
},
{
id: 't-1-2',
taskName: '说明书编写',
taskDesc: '',
completed: false,
priority: 'medium',
assigneeId: 'u1',
assigneeName: '惠灵佳',
assignId: 'u2',
assignName: '管理员',
followersId: '',
followersName: '',
createBy: '惠灵佳',
startTime: '2025-12-17',
endTime: '2025-12-19',
completeTime: '',
createTime: '2025-12-15',
updateTime: '2025-12-17',
groupId: 'g-1',
groupName: '默认分组',
childCount: 0,
completedChildCount: 0,
pid: 't-1',
hasChild: '0',
},
{
id: 't-2',
taskName: '完成新能体开发与调试',
taskDesc: '',
completed: true,
priority: 'medium',
assigneeId: 'u1',
assigneeName: '惠灵佳,张三,李四,王五,赵六,钱七',
assignId: 'u2',
assignName: '管理员',
followersId: '',
followersName: '',
createBy: '惠灵佳',
startTime: '2025-12-11',
endTime: '2025-12-12',
completeTime: '2025-12-12',
createTime: '2025-12-09',
updateTime: '2025-12-12',
groupId: 'g-1',
groupName: '默认分组',
childCount: 0,
completedChildCount: 0,
pid: '',
hasChild: '0',
},
{
id: 't-3',
taskName: '完成其他数据的导入与知识库构建',
taskDesc: '',
completed: false,
priority: 'high',
assigneeId: 'u1',
assigneeName: '惠灵佳,张三,李四,王五,赵六,钱七,孙八,周九,吴十,郑十一',
assignId: 'u2',
assignName: '管理员',
followersId: '',
followersName: '',
createBy: '惠灵佳',
startTime: '2025-12-10',
endTime: '2025-12-20',
completeTime: '',
createTime: '2025-12-09',
updateTime: '2025-12-10',
groupId: 'g-1',
groupName: '默认分组',
childCount: 0,
completedChildCount: 0,
pid: '',
hasChild: '0',
},
{
id: 't-4',
taskName: '简历筛选脚本编写',
taskDesc: '',
completed: false,
priority: 'low',
assigneeId: 'u3',
assigneeName: '张三,惠灵佳',
assignId: 'u2',
assignName: '管理员',
followersId: 'u1',
followersName: '惠灵佳',
createBy: '张三',
startTime: '2025-11-27',
endTime: '2025-12-27',
completeTime: '',
createTime: '2025-11-27',
updateTime: '2025-11-27',
groupId: 'g-1',
groupName: '默认分组',
childCount: 0,
completedChildCount: 0,
pid: '',
hasChild: '0',
},
{
id: 't-5',
taskName: '使用HiAgent开发AI应用',
taskDesc: '保罗办需求',
completed: false,
priority: 'high',
assigneeId: 'u1',
assigneeName: '惠灵佳,张三,李四,王五',
assignId: 'u2',
assignName: '管理员',
followersId: '',
followersName: '',
createBy: '惠灵佳',
startTime: '2025-10-20',
endTime: '2025-12-31',
completeTime: '',
createTime: '2025-10-03',
updateTime: '2025-10-20',
groupId: 'g-1',
groupName: '默认分组',
childCount: 0,
completedChildCount: 0,
pid: '',
hasChild: '0',
},
],
},
{
id: 'g-2',
name: '高优先级',
taskListId: 'tl-1',
collapsed: false,
tasks: [
{
id: 't-6',
taskName: '考研关系梳理配置',
taskDesc: '',
completed: false,
priority: 'high',
assigneeId: 'u1',
assigneeName: '惠灵佳,李四,王五,赵六,钱七,孙八',
assignId: 'u2',
assignName: '管理员',
followersId: '',
followersName: '',
createBy: '惠灵佳',
startTime: '2025-12-11',
endTime: '2025-12-24',
completeTime: '',
createTime: '2025-11-27',
updateTime: '2025-12-11',
groupId: 'g-2',
groupName: '高优先级',
childCount: 0,
completedChildCount: 0,
pid: '',
hasChild: '0',
},
],
},
],
},
{
id: 'tl-2',
name: '2025年所有工作登记',
pid: 'tlg-1',
hasChild: '0',
type: 1,
sortOrder: 2,
delFlag: 0,
groups: [
{
id: 'g-tl2-1',
name: '默认分组',
taskListId: 'tl-2',
collapsed: false,
tasks: [],
},
],
},
{
id: 'tl-3',
name: 'AI应用开发与实施',
pid: 'tlg-1',
hasChild: '0',
type: 1,
sortOrder: 3,
delFlag: 0,
groups: [
{
id: 'g-tl3-1',
name: '默认分组',
taskListId: 'tl-3',
collapsed: false,
tasks: [],
},
],
},
],
},
];
@@ -0,0 +1,155 @@
<template>
<div class="avatar-display" :class="{ 'avatar-display-completed': completed }">
<template v-if="names.length === 0">
<span class="avatar-empty">-</span>
</template>
<template v-else-if="names.length === 1">
<div class="avatar-single-wrap">
<div class="avatar-circle" :style="{ background: getColor(names[0]), width: size + 'px', height: size + 'px' }">
<span class="avatar-text" :style="{ fontSize: fontSize + 'px' }">{{ names[0].charAt(0) }}</span>
</div>
<span v-if="showName" class="avatar-name">{{ names[0] }}</span>
</div>
</template>
<template v-else-if="names.length <= maxShow">
<div
v-for="(name, idx) in names"
:key="idx"
class="avatar-circle"
:style="{ background: getColor(name), width: size + 'px', height: size + 'px', marginLeft: idx > 0 ? overlap + 'px' : '0', zIndex: idx + 1 }"
>
<span class="avatar-text" :style="{ fontSize: fontSize + 'px' }">{{ name.charAt(0) }}</span>
</div>
</template>
<template v-else>
<div
v-for="(name, idx) in names.slice(0, maxShow - 1)"
:key="idx"
class="avatar-circle"
:style="{ background: getColor(name), width: size + 'px', height: size + 'px', marginLeft: idx > 0 ? overlap + 'px' : '0', zIndex: idx + 1 }"
>
<span class="avatar-text" :style="{ fontSize: fontSize + 'px' }">{{ name.charAt(0) }}</span>
</div>
<div
class="avatar-overflow"
:style="{ width: size + 'px', height: size + 'px', marginLeft: overlap + 'px', fontSize: overflowFontSize + 'px', zIndex: names.length }"
>
+{{ names.length - (maxShow - 1) }}
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { getAvatarColor } from '../types';
const props = withDefaults(
defineProps<{
names: string;
size?: number;
maxShow?: number;
overlap?: number;
showName?: boolean;
completed?: boolean;
}>(),
{
size: 24,
maxShow: 5,
overlap: -8,
showName: true,
completed: false,
},
);
const fontSize = computed(() => Math.max(10, Math.round(props.size * 0.54)));
const overflowFontSize = computed(() => Math.max(8, Math.round(props.size * 0.42)));
const parsedNames = computed(() => {
if (!props.names) return [];
return props.names
.split(/[,,、]/)
.map((s) => s.trim())
.filter(Boolean);
});
const names = computed(() => parsedNames.value);
function getColor(name: string): string {
return getAvatarColor(name);
}
</script>
<style lang="less" scoped>
.avatar-display {
display: flex;
align-items: center;
gap: 0;
overflow: hidden;
}
.avatar-display-completed {
opacity: 0.5;
}
.avatar-single-wrap {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 8px 2px 2px;
background: #f5f6f7;
border-radius: 14px;
max-width: 100%;
overflow: hidden;
}
.avatar-circle {
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
box-shadow: 0 0 0 2px #fff;
}
.avatar-text {
color: #fff;
font-weight: 500;
line-height: 1;
font-size: 14px !important;
}
.avatar-name {
color: #646a73;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
}
.avatar-count {
font-size: 14px;
color: #646a73;
margin-left: 4px;
white-space: nowrap;
}
.avatar-overflow {
border-radius: 50%;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
color: #8c8c8c;
font-weight: 500;
position: relative;
box-shadow: 0 0 0 2px #fff;
flex-shrink: 0;
}
.avatar-empty {
color: #c0c4cc;
font-size: 14px;
}
</style>
@@ -0,0 +1,506 @@
<template>
<a-modal
v-model:open="visible"
title="协作人管理"
:footer="null"
width="520px"
:bodyStyle="{ padding: '0' }"
:centered="true"
:destroyOnClose="true"
@cancel="onClose"
>
<div class="collab-modal">
<div class="collab-body">
<div class="member-list">
<div v-for="item in allMembers" :key="item.permissionId || item.userId" class="member-item" @mouseenter="hoverId = item.permissionId || item.userId" @mouseleave="hoverId = ''">
<div class="member-avatar" :style="{ background: getColor(item.username) }">
{{ (item.username || '?').charAt(0) }}
</div>
<div class="member-main">
<div class="member-name-row">
<span class="member-name">{{ item.username }}</span>
<span v-if="item.permission === '1'" class="role-badge is-owner">所有者</span>
</div>
</div>
<div class="member-right">
<template v-if="item.permission === '1'">
<span class="role-label">所有者</span>
</template>
<template v-else-if="isOwner && item.permissionId">
<a-dropdown :trigger="['click']" :overlayStyle="{ minWidth: '120px' }">
<span class="role-label clickable" @click.stop>
{{ item.permission === '2' ? '可编辑' : '可阅读' }}
<Icon icon="ant-design:down-outlined" style="font-size: 10px; margin-left: 2px" />
</span>
<template #overlay>
<a-menu @click="({ key }) => onPermissionChange(item.permissionId, key)">
<a-menu-item key="2" :class="{ 'active-key': item.permission === '2' }">
<span>可编辑</span>
<Icon v-if="item.permission === '2'" icon="ant-design:check-outlined" class="check-icon" />
</a-menu-item>
<a-menu-item key="3" :class="{ 'active-key': item.permission === '3' }">
<span>可阅读</span>
<Icon v-if="item.permission === '3'" icon="ant-design:check-outlined" class="check-icon" />
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<span v-show="hoverId === item.permissionId" class="remove-icon" @click="onRemove(item.permissionId)">
<Icon icon="ant-design:close-outlined" />
</span>
</template>
<template v-else>
<span class="role-label">{{ item.permission === '2' ? '可编辑' : '可阅读' }}</span>
</template>
</div>
</div>
<div v-if="allMembers.length === 0" class="empty-state">
<Icon icon="ant-design:team-outlined" style="font-size: 36px; color: #d9d9d9; margin-bottom: 8px" />
<span>暂无协作人</span>
</div>
</div>
<div v-if="showAddPanel && isOwner" class="add-panel">
<div class="add-panel-header">
<span>添加协作人</span>
<span class="add-panel-close" @click="showAddPanel = false">
<Icon icon="ant-design:close-outlined" />
</span>
</div>
<div class="add-panel-body">
<SzUserSelect
v-model:value="addForm.userIds"
:labelKey="'realname'"
:rowKey="'id'"
placeholder="搜索并选择用户(支持多选)"
/>
<div class="add-panel-row">
<span class="add-panel-label">权限</span>
<a-radio-group v-model:value="addForm.permission" size="small">
<a-radio-button value="2">可编辑</a-radio-button>
<a-radio-button value="3">可阅读</a-radio-button>
</a-radio-group>
</div>
</div>
<div class="add-panel-footer">
<a-button size="small" @click="showAddPanel = false">取消</a-button>
<a-button size="small" type="primary" :disabled="!addForm.userIds" @click="onConfirmAdd">
确认添加
</a-button>
</div>
</div>
</div>
<div v-if="isOwner && !showAddPanel" class="collab-footer">
<a-button type="text" size="small" @click="showAddPanel = true">
<Icon icon="ant-design:user-add-outlined" style="margin-right: 4px" />
添加协作人
</a-button>
<a-button type="primary" size="small" @click="onDone">完成</a-button>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue';
import { message } from 'ant-design-vue';
import Icon from '/@/components/Icon/index';
import SzUserSelect from '/@/components/semri/userComponent/SzUserSelect.vue';
import { getCollaborators, addCollaborator, removeCollaborator, updateCollaboratorPermission } from '../TaskList.api';
import { getAvatarColor } from '../types';
import { checkUserSecLevel } from '../utils/taskUtils';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
interface CollaboratorItem {
permissionId: string;
userId: string;
username: string;
permission: string;
}
interface PendingAddItem {
userId: string;
username: string;
permission: string;
}
const props = defineProps<{
taskListId: string;
isOwner: boolean;
secretLevel?: number;
secretText?: string;
}>();
const emit = defineEmits<{
changed: [];
}>();
const visible = ref(false);
const collaborators = ref<CollaboratorItem[]>([]);
const hoverId = ref('');
const showAddPanel = ref(false);
const addForm = reactive({
userIds: '',
permission: '2',
});
const pendingAddList = ref<PendingAddItem[]>([]);
const allMembers = computed(() => {
const pendingAsCollab: CollaboratorItem[] = pendingAddList.value.map((p) => ({
permissionId: '',
userId: p.userId,
username: p.username,
permission: p.permission,
}));
return [...collaborators.value, ...pendingAsCollab];
});
function getColor(name: string): string {
return getAvatarColor(name || '');
}
async function open() {
visible.value = true;
showAddPanel.value = false;
pendingAddList.value = [];
addForm.userIds = '';
addForm.permission = '2';
await loadCollaborators();
}
function onClose() {
visible.value = false;
showAddPanel.value = false;
addForm.userIds = '';
addForm.permission = '2';
pendingAddList.value = [];
}
async function loadCollaborators() {
try {
const data: any[] = await getCollaborators({ taskListId: props.taskListId });
collaborators.value = data;
} catch (e) {
console.error('[CollaboratorModal] load failed', e);
}
}
async function onConfirmAdd() {
if (!addForm.userIds) return;
const ids = addForm.userIds.split(',').filter(Boolean);
if (ids.length === 0) return;
const secretLevel = props.secretLevel || 1;
const { ok, invalidNames } = await checkUserSecLevel(addForm.userIds, secretLevel);
if (!ok) {
message.warning(`以下人员的密级不满足当前清单(${props.secretText || '非密'})要求:${invalidNames.join('、')}`);
return;
}
const nameMap: Record<string, string> = {};
try {
const { getUserList } = await import('/@/api/common/api');
const res = await getUserList({ id: ids.join(','), pageNo: 1, pageSize: ids.length * 2 });
if (res.records && res.records.length > 0) {
res.records.forEach((u: any) => {
nameMap[u.id] = u.realname || u.username;
});
}
} catch (e) {
console.warn('获取用户名失败', e);
}
const existingIds = new Set(collaborators.value.map((c) => c.userId));
const pendingIds = new Set(pendingAddList.value.map((p) => p.userId));
let addedCount = 0;
for (const uid of ids) {
if (!existingIds.has(uid) && !pendingIds.has(uid)) {
pendingAddList.value.push({
userId: uid,
username: nameMap[uid] || uid,
permission: addForm.permission,
});
addedCount++;
}
}
if (addedCount > 0) {
createMessage.success(`已添加 ${addedCount} 位协作人`);
} else {
createMessage.warning('所选用户均已是协作人');
}
addForm.userIds = '';
addForm.permission = '2';
showAddPanel.value = false;
}
async function onDone() {
if (pendingAddList.value.length > 0) {
let successCount = 0;
let failCount = 0;
for (const item of pendingAddList.value) {
try {
await addCollaborator({
taskListId: props.taskListId,
userId: item.userId,
permission: item.permission,
});
successCount++;
} catch {
failCount++;
}
}
if (successCount > 0) {
createMessage.success(`成功添加 ${successCount} 位协作人${failCount > 0 ? `${failCount} 位失败` : ''}`);
} else {
createMessage.warning('添加协作人失败');
}
pendingAddList.value = [];
emit('changed');
}
visible.value = false;
showAddPanel.value = false;
addForm.userIds = '';
addForm.permission = '2';
}
async function onRemove(permissionId: string) {
try {
await removeCollaborator({ permissionId });
await loadCollaborators();
emit('changed');
} catch (e: any) {
createMessage.error(e?.message || '移除失败');
}
}
async function onPermissionChange(permissionId: string, newPermission: string) {
try {
await updateCollaboratorPermission({ permissionId, permission: newPermission });
await loadCollaborators();
emit('changed');
} catch (e: any) {
createMessage.error(e?.message || '修改失败');
}
}
defineExpose({ open });
</script>
<style lang="less" scoped>
.collab-modal {
display: flex;
flex-direction: column;
}
.collab-body {
position: relative;
min-height: 200px;
}
.member-list {
padding: 8px 0;
max-height: 400px;
overflow-y: auto;
}
.member-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 20px;
transition: background 0.12s;
&:hover {
background: #f5f6f7;
}
}
.member-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #fff;
font-size: 13px;
font-weight: 500;
}
.member-main {
flex: 1;
min-width: 0;
}
.member-name-row {
display: flex;
align-items: center;
gap: 6px;
}
.member-name {
font-size: 14px;
color: #1f2329;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.role-badge {
display: inline-flex;
align-items: center;
padding: 0 6px;
font-size: 11px;
line-height: 18px;
border-radius: 3px;
flex-shrink: 0;
&.is-owner {
background: #e8f3ff;
color: #3370ff;
}
}
.member-right {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
min-width: 70px;
justify-content: flex-end;
}
.role-label {
font-size: 13px;
color: #8f959e;
&.clickable {
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
transition: background 0.12s;
&:hover {
background: #e8e8e8;
}
}
}
.remove-icon {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
cursor: pointer;
color: #8f959e;
font-size: 12px;
transition: all 0.12s;
&:hover {
background: #f0f0f0;
color: #f5222d;
}
}
.check-icon {
margin-left: auto;
color: #3370ff;
font-size: 12px;
}
.active-key {
color: #3370ff;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #bfbfbf;
font-size: 13px;
}
.add-panel {
position: absolute;
inset: 0;
background: #fff;
display: flex;
flex-direction: column;
z-index: 10;
}
.add-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
font-size: 14px;
font-weight: 500;
color: #1f2329;
}
.add-panel-close {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
color: #8f959e;
&:hover {
background: #f0f0f0;
}
}
.add-panel-body {
padding: 0 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
.add-panel-row {
display: flex;
align-items: center;
gap: 12px;
}
.add-panel-label {
font-size: 13px;
color: #595959;
white-space: nowrap;
}
.add-panel-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 16px 20px;
margin-top: auto;
}
.collab-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
border-top: 1px solid #f0f0f0;
}
</style>
@@ -0,0 +1,49 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroy-on-close title="创建清单" :width="500" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { getCreateListFormSchema } from '../TaskList.data';
import { addTaskList } from '../TaskList.api';
import { getSecretText } from '../types';
const emit = defineEmits(['register', 'success']);
let pendingGroupId = '';
let pendingUserSecurityLevel = 3;
const [registerForm, { resetFields, validate, updateSchema }] = useForm({
labelWidth: 100,
schemas: getCreateListFormSchema(3),
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
pendingGroupId = data?.groupId || '';
pendingUserSecurityLevel = data?.userSecurityLevel || 3;
updateSchema(getCreateListFormSchema(pendingUserSecurityLevel));
});
async function handleSubmit() {
try {
setModalProps({ confirmLoading: true });
const values = await validate();
const result = await addTaskList({
tasklistName: values.tasklistName,
pid: pendingGroupId || undefined,
secretLevel: values.secretLevel,
secretText: getSecretText(values.secretLevel),
});
closeModal();
emit('success', result);
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,115 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroy-on-close title="创建任务" :width="600" @ok="handleSubmit">
<BasicForm @register="registerForm" />
<div class="attachment-row">
<span class="attachment-label">附件</span>
<span class="attachment-trigger" @click="openFileUploadModal">添加附件</span>
<TaskAttachmentUpload
ref="fileUploadRef"
:bussiness-id="''"
:bussiness-secret-level="currentSecretLevel"
:disabled="false"
style="display: none"
/>
</div>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import TaskAttachmentUpload from './TaskAttachmentUpload.vue';
import { createTaskFormSchema } from '../TaskList.data';
import { addTask } from '../TaskList.api';
import { checkUserSecLevel } from '../utils/taskUtils';
const emit = defineEmits(['register', 'success']);
const groupId = ref('');
const mainId = ref('');
const currentSecretLevel = ref(1);
const currentSecretText = ref('非密');
const fileUploadRef = ref<InstanceType<typeof TaskAttachmentUpload> | null>(null);
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 100,
schemas: createTaskFormSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
groupId.value = data?.groupId || '';
mainId.value = data?.mainId || '';
currentSecretLevel.value = data?.secretLevel || 1;
currentSecretText.value = data?.secretText || '非密';
if (groupId.value) {
await setFieldsValue({ groupId: groupId.value });
}
});
async function handleSubmit() {
try {
setModalProps({ confirmLoading: true });
const values = await validate();
if (values.assigneeId) {
const { ok, invalidNames } = await checkUserSecLevel(values.assigneeId, currentSecretLevel.value);
if (!ok) {
message.warning(`以下人员的密级不满足当前清单(${currentSecretText.value})要求:${invalidNames.join('、')}`);
return;
}
}
const result = await addTask({
mainId: mainId.value,
taskName: values.taskName,
taskDesc: values.taskDesc || '',
priority: values.priority || '',
type: '1',
pid: groupId.value || '',
assigneeId: values.assigneeId || '',
startTime: values.startTime || null,
endTime: values.endTime || null,
});
if (fileUploadRef.value?.hasPendingFiles()) {
await fileUploadRef.value.bindBussinessId(result);
}
closeModal();
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
function openFileUploadModal() {
fileUploadRef.value?.handleOpenUploadModal();
}
</script>
<style lang="less" scoped>
.attachment-row {
display: flex;
align-items: center;
padding: 0 0 16px;
gap: 8px;
}
.attachment-label {
width: 100px;
flex-shrink: 0;
text-align: right;
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
}
.attachment-trigger {
color: #8c8c8c;
cursor: pointer;
font-size: 14px;
&:hover {
color: #3370ff;
}
}
</style>
@@ -0,0 +1,462 @@
<!-- 文件上传下载删除组件任务附件专用副本 -->
<template>
<div class="s-upload-file-container">
<div class="upload-btn-wrapper" v-if="!disabled">
<a-button type="primary" @click="handleOpenUploadModal">
<Icon icon="ant-design:upload-outlined" />
选择文件上传
</a-button>
</div>
<div class="file-list-wrapper">
<div v-if="displayFileList.length > 0" class="file-list-header">
<span class="file-count"> {{ displayFileList.length }} 个文件</span>
<a-button
type="link"
size="small"
:loading="downloadingAll"
@click="handleDownloadAll"
>
<Icon icon="ant-design:download-outlined" />
下载全部
</a-button>
</div>
<a-spin :spinning="loading">
<a-empty v-if="!loading && displayFileList.length === 0" description="暂无文件" />
<div v-else class="file-list">
<div
v-for="file in displayFileList"
:key="file.fileId || file.id"
class="file-item"
>
<div class="file-info">
<Icon icon="ant-design:file-outlined" class="file-icon" />
<span class="file-name">{{ file.fileName }}</span>
<a-tag :color="getSecretLevelColor(file.secretLevel)" class="secret-tag">
{{ getSecretLevelLabel(file.secretLevel) }}
</a-tag>
<a-tag v-if="file.pending" color="orange" class="secret-tag">
待保存
</a-tag>
<span v-if="file.fileSize" class="file-size">{{ formatFileSize(file.fileSize) }}</span>
</div>
<div class="file-actions">
<a-button
type="link"
size="small"
@click="handleDownload(file)"
>
<Icon icon="ant-design:download-outlined" />
下载
</a-button>
<a-button
v-if="!disabled"
type="link"
size="small"
danger
@click="handleDelete(file)"
>
<Icon icon="ant-design:delete-outlined" />
删除
</a-button>
</div>
</div>
</div>
</a-spin>
</div>
<TaskAttachmentUploadModal
v-model:visible="uploadModalVisible"
:secret-level-options="secretLevelOptions"
:business-id="bussinessId"
:multiple="multiple"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { Icon } from '/src/components/Icon';
import { defHttp } from '/src/utils/http/axios';
import { useMessage } from '/src/hooks/web/useMessage';
import TaskAttachmentUploadModal from './TaskAttachmentUploadModal.vue';
const { createMessage, createConfirm } = useMessage();
const props = defineProps({
bussinessId: {
type: String,
default: ''
},
bussinessSecretLevel: {
type: Number,
default: 1,
validator: (value: number) => [1, 2, 3, 4].includes(value)
},
disabled: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['upload-success', 'delete-success', 'upload-error']);
const serverFileList = ref<any[]>([]);
const pendingFileList = ref<any[]>([]);
const loading = ref<boolean>(false);
const uploadModalVisible = ref<boolean>(false);
const binding = ref<boolean>(false);
const downloadingAll = ref<boolean>(false);
const displayFileList = computed(() => {
return [
...serverFileList.value.map(f => ({ ...f, pending: false })),
...pendingFileList.value.map(f => ({ ...f, pending: true }))
];
});
const SECRET_LEVEL_CONFIG = {
1: { label: '非密', color: 'success' },
2: { label: '内部', color: 'processing' },
3: { label: '秘密', color: 'warning' },
4: { label: '机密', color: 'error' }
};
function getSecretLevelInfo(level: number) {
return SECRET_LEVEL_CONFIG[level] || SECRET_LEVEL_CONFIG[1];
}
function getSecretLevelLabel(level: number) {
return getSecretLevelInfo(level).label;
}
function getSecretLevelColor(level: number) {
return getSecretLevelInfo(level).color;
}
function formatFileSize(bytes: number): string {
if (!bytes || bytes === 0) return '';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
const secretLevelOptions = computed(() => {
const level = props.bussinessSecretLevel;
const options = [];
options.push({ label: '非密', value: 1 });
if (level >= 2) options.push({ label: '内部', value: 2 });
if (level >= 3) options.push({ label: '秘密', value: 3 });
if (level >= 4) options.push({ label: '机密', value: 4 });
return options;
});
function handleOpenUploadModal() {
uploadModalVisible.value = true;
}
async function handleUploadSuccess(result: any) {
if (result.results && Array.isArray(result.results)) {
result.results.forEach((fileInfo: any) => {
if (!fileInfo.bound) {
pendingFileList.value.push({
fileId: fileInfo.fileId,
fileName: fileInfo.fileName,
filePath: fileInfo.filePath,
fileSize: fileInfo.fileSize,
secretLevel: fileInfo.secretLevel,
fileUploadType: fileInfo.fileUploadType
});
}
});
}
if (props.bussinessId) {
await loadFileList();
}
emit('upload-success', result);
}
function handleUploadError(error: any) {
emit('upload-error', error);
}
async function handleDownload(file: any) {
try {
const fileUrl = file.filePath;
const fileName = file.fileName || fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL;
const data = await defHttp.get(
{ url: `${domainUrl}/sys/file/download`, params: { fileUrl }, responseType: 'blob' },
{ isTransformResponse: false }
);
if (!data || data.size === 0) {
createMessage.warning('文件下载失败');
return;
}
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(new Blob([data]), fileName);
} else {
const blobUrl = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement('a');
link.style.display = 'none';
link.href = blobUrl;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
}
} catch (error) {
console.error('下载失败:', error);
createMessage.error('文件下载失败');
}
}
async function handleDownloadAll() {
const files = displayFileList.value;
if (files.length === 0) return;
downloadingAll.value = true;
let successCount = 0;
let failCount = 0;
for (const file of files) {
try {
await handleDownload(file);
successCount++;
} catch (error) {
failCount++;
console.error(`下载文件"${file.fileName}"失败:`, error);
}
}
downloadingAll.value = false;
if (successCount > 0) {
createMessage.success(`成功下载 ${successCount} 个文件${failCount > 0 ? `${failCount} 个失败` : ''}`);
} else {
createMessage.error('所有文件下载失败');
}
}
function handleDelete(file: any) {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: `确定要删除文件"${file.fileName}"吗?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
loading.value = true;
await defHttp.get({
url: '/sys/file/delete',
params: { id: file.fileId || file.id }
});
createMessage.success('文件删除成功');
if (file.pending) {
pendingFileList.value = pendingFileList.value.filter(f => f.fileId !== file.fileId);
} else {
serverFileList.value = serverFileList.value.filter(f => (f.id || f.fileId) !== (file.id || file.fileId));
}
emit('delete-success', file.fileId || file.id);
} catch (error) {
console.error('删除失败:', error);
createMessage.error('文件删除失败');
} finally {
loading.value = false;
}
}
});
}
async function loadFileList() {
if (!props.bussinessId) {
return;
}
try {
loading.value = true;
const result = await defHttp.get({
url: '/sys/file/getFileInfoByBussinessId',
params: { bussinessId: props.bussinessId }
});
if (Array.isArray(result)) {
serverFileList.value = result;
} else {
serverFileList.value = [];
}
} catch (error) {
console.error('加载文件列表失败:', error);
serverFileList.value = [];
createMessage.error('加载文件列表失败');
} finally {
loading.value = false;
}
}
async function bindBussinessId(newBussinessId: string) {
if (!newBussinessId) {
createMessage.warning('bussinessId 不能为空');
return false;
}
if (pendingFileList.value.length === 0) {
return true;
}
const fileIds = pendingFileList.value.map(f => f.fileId).join(',');
try {
binding.value = true;
await defHttp.get({
url: '/sys/file/updateFilesInfo',
params: {
bussinessId: newBussinessId,
fileIds: fileIds
}
});
createMessage.success(`成功关联 ${pendingFileList.value.length} 个文件`);
pendingFileList.value = [];
await loadFileList();
return true;
} catch (error) {
console.error('关联文件失败:', error);
createMessage.error('关联文件失败,请重试');
return false;
} finally {
binding.value = false;
}
}
function getPendingFileIds(): string[] {
return pendingFileList.value.map(f => f.fileId);
}
function hasPendingFiles(): boolean {
return pendingFileList.value.length > 0;
}
defineExpose({
bindBussinessId,
getPendingFileIds,
hasPendingFiles,
loadFileList,
handleOpenUploadModal
});
watch(() => props.bussinessId, (newVal) => {
if (newVal && newVal.length > 0) {
loadFileList();
} else {
serverFileList.value = [];
}
}, { immediate: true });
</script>
<style lang="less" scoped>
.s-upload-file-container {
width: 100%;
max-width: 100%;
.upload-btn-wrapper {
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.file-list-wrapper {
width: 100%;
min-height: 100px;
padding: 12px;
background: #fafafa;
border-radius: 4px;
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
.file-count {
font-size: 13px;
color: #8c8c8c;
}
}
}
.file-list {
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
margin-bottom: 8px;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:last-child {
margin-bottom: 0;
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow: hidden;
.file-icon {
font-size: 16px;
color: #1890ff;
flex-shrink: 0;
}
.file-name {
font-size: 14px;
color: #262626;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.secret-tag {
margin-left: 4px;
font-weight: 500;
flex-shrink: 0;
}
.file-size {
font-size: 12px;
color: #8c8c8c;
flex-shrink: 0;
}
}
.file-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
}
}
}
</style>
@@ -0,0 +1,253 @@
<!-- 文件上传弹窗组件任务附件专用副本 -->
<template>
<a-modal
:visible="visible"
title="选择文件密级并上传"
:width="600"
:confirm-loading="uploading"
@ok="handleUpload"
@cancel="handleCancel"
:destroyOnClose="true"
>
<a-form layout="vertical" style="padding: 0 20px">
<a-form-item label="文件密级" required>
<a-radio-group v-model:value="selectedSecretLevel">
<a-radio
v-for="option in secretLevelOptions"
:key="option.value"
:value="option.value"
>
<a-tag :color="getSecretLevelColor(option.value)">
{{ option.label }}
</a-tag>
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="选择文件" required>
<a-upload
:before-upload="beforeUpload"
:file-list="tempFileList"
@remove="handleRemoveFile"
:multiple="multiple"
:max-count="multiple ? undefined : 1"
>
<a-button>
<Icon icon="ant-design:file-add-outlined" />
{{ multiple ? '选择文件(可多选)' : '选择文件' }}
</a-button>
</a-upload>
</a-form-item>
<a-alert
v-if="selectedFiles.length > 0"
:message="`已选择 ${selectedFiles.length} 个文件`"
type="info"
show-icon
>
<template #description>
<div class="selected-files-summary">
<div v-for="(f, idx) in selectedFiles" :key="f.uid" class="summary-item">
{{ idx + 1}}. {{ f.name }}{{ formatFileSize(f.size) }}
</div>
</div>
</template>
</a-alert>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Icon } from '/src/components/Icon';
import { useMessage } from '/src/hooks/web/useMessage';
import { uploadMyFile } from '/src/api/common/api';
const { createMessage } = useMessage();
const props = defineProps({
visible: {
type: Boolean,
required: true
},
secretLevelOptions: {
type: Array,
default: () => []
},
businessId: {
type: String,
default: ''
},
multiple: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['update:visible', 'upload-success', 'upload-error']);
const selectedSecretLevel = ref<number>(1);
const selectedFiles = ref<any[]>([]);
const tempFileList = ref<any[]>([]);
const uploading = ref<boolean>(false);
function getSecretLevelColor(level: number) {
const colors = {
1: 'success',
2: 'processing',
3: 'warning',
4: 'error'
};
return colors[level] || 'default';
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
function beforeUpload(file) {
const isValidSize = file.size / 1024 / 1024 < 500;
if (!isValidSize) {
createMessage.error(`文件"${file.name}"大小超过500MB,已跳过`);
return false;
}
if (!props.multiple) {
selectedFiles.value = [file];
tempFileList.value = [{
uid: file.uid,
name: file.name,
status: 'done',
size: file.size
}];
} else {
selectedFiles.value = [...selectedFiles.value, file];
tempFileList.value = [...tempFileList.value, {
uid: file.uid,
name: file.name,
status: 'done',
size: file.size
}];
}
return false;
}
function handleRemoveFile(file) {
selectedFiles.value = selectedFiles.value.filter(f => f.uid !== file.uid);
tempFileList.value = tempFileList.value.filter(f => f.uid !== file.uid);
}
async function handleUpload() {
if (selectedFiles.value.length === 0) {
createMessage.warning('请先选择文件');
return;
}
if (!selectedSecretLevel.value) {
createMessage.warning('请选择文件密级');
return;
}
uploading.value = true;
try {
const uploadPromises = selectedFiles.value.map(file => {
const formData = new FormData();
formData.append('file', file);
formData.append('secretLevel', selectedSecretLevel.value.toString());
if (props.businessId) {
formData.append('business_id', props.businessId);
}
return uploadMyFile('/sys/file/upload', formData).then(res => ({
rawFile: file,
response: res
}));
});
const results = await Promise.allSettled(uploadPromises);
let successCount = 0;
let failCount = 0;
const successResults: any[] = [];
results.forEach((r) => {
if (r.status === 'fulfilled') {
const result = r.value?.response?.data;
if (result && result.success) {
successCount++;
successResults.push({
fileId: result.result.fileId,
fileName: r.value.rawFile.name,
filePath: result.result.savePath,
fileSize: r.value.rawFile.size,
secretLevel: selectedSecretLevel.value,
fileUploadType: result.result.fileUploadType,
bound: !!props.businessId
});
} else {
failCount++;
}
} else {
failCount++;
}
});
if (successCount > 0) {
createMessage.success(`成功上传 ${successCount} 个文件${failCount > 0 ? `${failCount} 个失败` : ''}`);
emit('upload-success', { success: true, results: successResults });
handleCancel();
} else {
createMessage.error('所有文件上传失败');
emit('upload-error', new Error('所有文件上传失败'));
}
} catch (error) {
console.error('上传失败:', error);
createMessage.error('文件上传失败');
emit('upload-error', error);
} finally {
uploading.value = false;
}
}
function handleCancel() {
emit('update:visible', false);
setTimeout(() => {
selectedSecretLevel.value = 1;
selectedFiles.value = [];
tempFileList.value = [];
}, 300);
}
watch(() => props.visible, (newVal) => {
if (newVal && props.secretLevelOptions.length > 0) {
selectedSecretLevel.value = props.secretLevelOptions[0].value;
}
});
</script>
<style lang="less" scoped>
.secret-tip {
margin-top: 8px;
color: #faad14;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.selected-files-summary {
max-height: 150px;
overflow-y: auto;
.summary-item {
padding: 2px 0;
font-size: 12px;
color: #595959;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,123 @@
<template>
<a-date-picker
v-if="mode === 'start' || mode === 'range'"
v-model:value="startDayjs"
placeholder="选择开始时间"
format="YYYY-MM-DD"
:allow-clear="true"
:open="startOpen"
@change="onStartChange"
@open-change="onStartOpenChange"
/>
<a-date-picker
v-if="mode === 'end' || mode === 'range'"
v-model:value="endDayjs"
placeholder="选择结束时间"
format="YYYY-MM-DD"
:allow-clear="true"
:open="endOpen"
@change="onEndChange"
@open-change="onEndOpenChange"
/>
</template>
<script lang="ts" setup>
import { ref, watch, nextTick } from 'vue';
import dayjs from 'dayjs';
type DateRangeValue = [string | null, string | null];
type PickerMode = 'start' | 'end' | 'range';
const props = withDefaults(
defineProps<{
startValue?: string | null;
endValue?: string | null;
visible?: boolean;
triggerRef?: HTMLElement | null;
mode?: PickerMode;
}>(),
{
startValue: null,
endValue: null,
visible: false,
triggerRef: null,
mode: 'range',
}
);
const emit = defineEmits<{
change: [value: DateRangeValue];
'update:visible': [value: boolean];
}>();
const startDayjs = ref<any>(null);
const endDayjs = ref<any>(null);
const startOpen = ref(false);
const endOpen = ref(false);
watch(
() => props.startValue,
(val) => {
startDayjs.value = val ? dayjs(val) : null;
},
{ immediate: true }
);
watch(
() => props.endValue,
(val) => {
endDayjs.value = val ? dayjs(val) : null;
},
{ immediate: true }
);
watch(
() => props.visible,
async (visible) => {
if (visible) {
await nextTick();
if (props.mode === 'start' || props.mode === 'range') {
startOpen.value = true;
}
if (props.mode === 'end') {
endOpen.value = true;
}
} else {
startOpen.value = false;
endOpen.value = false;
}
}
);
function onStartOpenChange(open: boolean) {
startOpen.value = open;
if (!open) {
emit('update:visible', false);
}
}
function onEndOpenChange(open: boolean) {
endOpen.value = open;
if (!open) {
emit('update:visible', false);
}
}
function onStartChange(date: any) {
const value = date ? date.format('YYYY-MM-DD') : null;
emit('change', [value, props.endValue]);
if (props.mode === 'start') {
startOpen.value = false;
emit('update:visible', false);
}
}
function onEndChange(date: any) {
const value = date ? date.format('YYYY-MM-DD') : null;
emit('change', [props.startValue, value]);
if (props.mode === 'end') {
endOpen.value = false;
emit('update:visible', false);
}
}
</script>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,123 @@
<template>
<div>
<BasicForm @register="registerForm" ref="formRef" />
<!-- 子表单区域 -->
<a-tabs v-model:activeKey="activeKey" animated @change="handleChangeTabs">
<a-tab-pane tab="工作任务清单详情" key="taskListDetial" :forceRender="true">
<JVxeTable
keep-source
resizable
ref="taskListDetial"
v-if="taskListDetialTable.show"
:loading="taskListDetialTable.loading"
:columns="taskListDetialTable.columns"
:dataSource="taskListDetialTable.dataSource"
:height="340"
:rowNumber="true"
:rowSelection="true"
:disabled="formDisabled"
:toolbar="true"
/>
</a-tab-pane>
</a-tabs>
<div style="width: 100%; text-align: center" v-if="!formDisabled">
<a-button @click="handleSubmit" pre-icon="ant-design:check" type="primary"> </a-button>
</div>
</div>
</template>
<script lang="ts">
import { BasicForm, useForm } from '/@/components/Form/index';
import { computed, defineComponent, reactive, ref } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods';
import { getBpmFormSchema, taskListDetialColumns } from '../TaskList.data';
import { saveOrUpdate, taskListDetialList } from '../TaskList.api';
export default defineComponent({
name: 'TaskListForm',
components: {
BasicForm,
},
props: {
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
},
setup(props) {
const [registerForm, { setFieldsValue, setProps }] = useForm({
labelWidth: 150,
schemas: getBpmFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
const formDisabled = computed(() => {
if (props.formData.disabled === false) {
return false;
}
return true;
});
const refKeys = ref(['taskListDetial']);
const activeKey = ref('taskListDetial');
const taskListDetial = ref();
const tableRefs = { taskListDetial };
const taskListDetialTable = reactive({
loading: false,
dataSource: [],
columns: taskListDetialColumns,
show: false,
});
const [handleChangeTabs, handleSubmit, requestSubTableData, formRef] = useJvxeMethod(
requestAddOrEdit,
classifyIntoFormData,
tableRefs,
activeKey,
refKeys,
validateSubForm
);
function classifyIntoFormData(allValues) {
let main = Object.assign({}, allValues.formValue);
return {
...main, // 展开
taskListDetialList: allValues.tablesValue[0].tableData,
};
}
//表单提交事件
async function requestAddOrEdit(values) {
await saveOrUpdate(values, true);
}
const queryByIdUrl = '/tasklist/taskList/queryById';
async function initFormData() {
let params = { id: props.formData.dataId };
const data = await defHttp.get({ url: queryByIdUrl, params });
//设置表单的值
await setFieldsValue({ ...data });
requestSubTableData(taskListDetialList, { id: data.id }, taskListDetialTable, () => {
taskListDetialTable.show = true;
});
//默认是禁用
await setProps({ disabled: formDisabled.value });
}
initFormData();
return {
registerForm,
formDisabled,
formRef,
handleSubmit,
activeKey,
handleChangeTabs,
taskListDetial,
taskListDetialTable,
};
},
});
</script>
@@ -0,0 +1,119 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" ref="formRef" name="TaskListForm" />
<!-- 子表单区域 -->
<a-tabs v-model:activeKey="activeKey" animated @change="handleChangeTabs">
<a-tab-pane tab="工作任务清单详情" key="taskListDetial" :forceRender="true">
<JVxeTable
keep-source
resizable
ref="taskListDetial"
:loading="taskListDetialTable.loading"
:columns="taskListDetialTable.columns"
:dataSource="taskListDetialTable.dataSource"
:height="340"
:rowNumber="true"
:rowSelection="true"
:disabled="formDisabled"
:toolbar="true"
/>
</a-tab-pane>
</a-tabs>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { JVxeTable } from '/@/components/jeecg/JVxeTable';
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods.ts';
import { formSchema, taskListDetialColumns } from '../TaskList.data';
import { saveOrUpdate, taskListDetialList } from '../TaskList.api';
// Emits声明
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const formDisabled = ref(false);
const refKeys = ref(['taskListDetial']);
const activeKey = ref('taskListDetial');
const taskListDetial = ref();
const tableRefs = { taskListDetial };
const taskListDetialTable = reactive({
loading: false,
dataSource: [],
columns: taskListDetialColumns,
});
//表单配置
const [registerForm, { setProps, resetFields, setFieldsValue }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await reset();
setModalProps({ confirmLoading: false, showCancelBtn: data?.showFooter, showOkBtn: data?.showFooter });
isUpdate.value = !!data?.isUpdate;
formDisabled.value = !data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
requestSubTableData(taskListDetialList, { id: data?.record?.id }, taskListDetialTable);
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter });
});
//方法配置
const [handleChangeTabs, handleSubmit, requestSubTableData, formRef] = useJvxeMethod(
requestAddOrEdit,
classifyIntoFormData,
tableRefs,
activeKey,
refKeys
);
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(formDisabled) ? '编辑' : '详情'));
async function reset() {
await resetFields();
activeKey.value = 'taskListDetial';
taskListDetialTable.dataSource = [];
}
function classifyIntoFormData(allValues) {
let main = Object.assign({}, allValues.formValue);
return {
...main, // 展开
taskListDetialList: allValues.tablesValue[0].tableData,
};
}
//表单提交事件
async function requestAddOrEdit(values) {
try {
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>
@@ -0,0 +1,820 @@
<template>
<div class="task-sidebar">
<div class="sidebar-header">
<span class="sidebar-title">
<Icon icon="ant-design:menu-outlined" class="mr-1" />
任务
</span>
</div>
<div class="sidebar-nav">
<div
v-for="item in NAV_ITEMS"
:key="item.id"
class="sidebar-item"
:class="{ active: currentView === item.id }"
@click="emit('view-select', item.id)"
>
<Icon :icon="item.icon" class="sidebar-item-icon" />
<span>{{ item.label }}</span>
</div>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-section">
<div class="sidebar-quick-header" @click="quickAccessExpanded = !quickAccessExpanded">
<Icon icon="tasklist-arrow-down|svg" class="quick-access-arrow" :class="{ expanded: quickAccessExpanded }" />
<span class="sidebar-quick-title">快速访问</span>
</div>
<template v-if="quickAccessExpanded">
<div
v-for="item in QUICK_ACCESS_ITEMS"
:key="item.id"
class="sidebar-item sidebar-quick-access-sub"
:class="{ active: currentView === item.id }"
@click="onQuickAccessClick(item)"
>
<Icon :icon="item.icon" class="sidebar-item-icon" />
<span>{{ item.label }}</span>
</div>
</template>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-section sidebar-lists-section">
<div class="sidebar-favorites-header">
<Icon icon="tasklist-task-list|svg" class="sidebar-favorites-icon" />
<span class="sidebar-favorites-name">收藏清单</span>
<a-dropdown :trigger="['click']">
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add" @click.stop />
<template #overlay>
<a-menu @click="onAddMenuClick">
<a-menu-item key="new-list">
<Icon icon="ant-design:file-add-outlined" class="mr-1" />
新建清单
</a-menu-item>
<a-menu-item key="new-group">
<Icon icon="ant-design:folder-add-outlined" class="mr-1" />
新建分组
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div class="sidebar-favorites-items">
<draggable :list="localUngroupedTaskLists" group="tasklists" item-key="id" handle=".list-drag-handle" :animation="200" @end="onDragEnd">
<template #item="{ element: taskList }">
<div
v-if="renamingId !== taskList.id"
class="sidebar-item"
:class="{ active: currentListId === taskList.id }"
:data-list-id="taskList.id"
@click="emit('list-select', taskList.id)"
@mouseenter="hoveredListId = taskList.id"
@mouseleave="hoveredListId = ''"
>
<span class="list-drag-handle">
<Icon icon="tasklist-list-drag-handle|svg" />
</span>
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" />
<span class="sidebar-item-name">{{ taskList.name }}</span>
<a-dropdown :trigger="['click']">
<Icon v-show="hoveredListId === taskList.id" icon="ant-design:ellipsis-outlined" class="sidebar-item-more" @click.stop />
<template #overlay>
<a-menu @click="onListMenuClick($event, taskList.id, taskList.name)">
<a-menu-item v-if="canEditList(taskList.id)" key="rename">
<Icon icon="ant-design:edit-outlined" class="mr-1" />
重命名
</a-menu-item>
<a-menu-item key="remove">
<Icon icon="ant-design:minus-circle-outlined" class="mr-1" />
移除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div v-else class="sidebar-item sidebar-item-renaming">
<span class="list-drag-handle-placeholder"></span>
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
<a-input ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
</div>
</template>
</draggable>
</div>
<draggable :list="localUserGroups" group="tasklistgroups" item-key="id" :animation="200" drag-class="group-drag-ghost" @start="onGroupDragStart" @end="onGroupDragEnd">
<template #item="{ element: group }">
<div :data-group-id="group.id" :class="{ 'group-being-dragged': draggingGroupId === group.id }">
<div
v-if="group.delFlag !== 1 && renamingId !== group.id"
class="sidebar-group-header"
@mousedown="onGroupMouseDown($event, group.id)"
@mousemove="onGroupMouseMove($event)"
@mouseup="onGroupMouseUp($event, group.id)"
>
<Icon icon="tasklist-arrow-down|svg" class="group-collapse-arrow" :class="{ collapsed: collapsedGroups[group.id] || draggingGroupId === group.id }" />
<span class="sidebar-group-name">{{ group.name }}</span>
<a-dropdown :trigger="['click']" @click.stop>
<Icon icon="ant-design:ellipsis-outlined" class="sidebar-group-more" @click.stop />
<template #overlay>
<a-menu @click="onGroupMenuClick($event, group.id, group.name)">
<a-menu-item key="rename">
<Icon icon="ant-design:edit-outlined" class="mr-1" />
重命名
</a-menu-item>
<a-menu-item key="delete" danger>
<Icon icon="ant-design:delete-outlined" class="mr-1" />
删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<Icon icon="ant-design:plus-outlined" class="sidebar-group-add" @click.stop="startNewList(group.id)" />
</div>
<div v-if="group.delFlag !== 1 && renamingId === group.id" class="sidebar-group-header sidebar-group-renaming" @click.stop>
<Icon icon="tasklist-arrow-down|svg" class="group-collapse-arrow" :class="{ collapsed: collapsedGroups[group.id] || draggingGroupId === group.id }" />
<a-input ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
</div>
<template v-if="group.delFlag !== 1 && !collapsedGroups[group.id]">
<div class="sidebar-group-items" :data-group-id="group.id">
<draggable
:list="getLocalGroupTaskLists(group)"
group="tasklists"
item-key="id"
handle=".list-drag-handle"
:animation="200"
@end="onDragEnd"
>
<template #item="{ element: taskList }">
<div
v-if="renamingId !== taskList.id"
class="sidebar-item"
:class="{ active: currentListId === taskList.id }"
:data-list-id="taskList.id"
@click="emit('list-select', taskList.id)"
@mouseenter="hoveredListId = taskList.id"
@mouseleave="hoveredListId = ''"
>
<span class="list-drag-handle">
<Icon icon="tasklist-list-drag-handle|svg" />
</span>
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" />
<span class="sidebar-item-name">{{ taskList.name }}</span>
<a-dropdown :trigger="['click']">
<Icon v-show="hoveredListId === taskList.id" icon="ant-design:ellipsis-outlined" class="sidebar-item-more" @click.stop />
<template #overlay>
<a-menu @click="onListMenuClick($event, taskList.id, taskList.name)">
<a-menu-item v-if="canEditList(taskList.id)" key="rename">
<Icon icon="ant-design:edit-outlined" class="mr-1" />
重命名
</a-menu-item>
<a-menu-item key="remove">
<Icon icon="ant-design:minus-circle-outlined" class="mr-1" />
移除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div v-else class="sidebar-item sidebar-item-renaming">
<span class="list-drag-handle-placeholder"></span>
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
<a-input ref="renameInputRef" v-model:value="renameValue" size="small" style="flex: 1" @press-enter="confirmInlineRename" @blur="confirmInlineRename" />
</div>
</template>
</draggable>
<div
v-if="getLocalGroupTaskLists(group).length === 0"
class="sidebar-empty-hint"
>
可拖拽清单加入该分组
</div>
</div>
</template>
</div>
</template>
</draggable>
<div v-if="showNewGroupInput" class="sidebar-new-group-row">
<Icon icon="ant-design:plus-outlined" class="sidebar-new-icon" />
<a-input
v-model:value="newGroupName"
size="small"
placeholder="输入分组名称..."
autofocus
style="flex: 1"
@press-enter="submitNewGroup"
@blur="showNewGroupInput = false"
/>
</div>
<div v-else class="sidebar-new-group-btn" @click="showNewGroupInput = true">
<Icon icon="ant-design:plus-outlined" class="sidebar-new-icon" />
<span>新建分组</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, nextTick } from 'vue';
import Icon from '/@/components/Icon/index';
import draggable from 'vuedraggable';
import { useMessage } from '/@/hooks/web/useMessage';
import { NAV_ITEMS, QUICK_ACCESS_ITEMS, QUICK_ACCESS_TAB_MAP } from '../types';
import type { TaskListGroup, ViewType } from '../types';
const { createConfirm } = useMessage();
const props = defineProps<{
taskListGroups: TaskListGroup[];
currentView: ViewType;
currentListId: string;
favoriteIdMap: Map<string, string>;
permissionMap: Map<string, string>;
}>();
const emit = defineEmits<{
'view-select': [view: ViewType];
'list-select': [listId: string];
'create-list-modal': [groupId: string];
'create-group': [name: string];
'rename-list': [listId: string, newName: string];
'delete-list': [listId: string];
'remove-favorite': [listId: string];
'rename-group': [groupId: string, newName: string];
'delete-group': [groupId: string];
'quick-access-tab-click': [tabKey: string];
'move-list': [favoriteId: string, targetGroupId: string, sortOrder: number];
'move-group': [groupId: string, targetSortOrder: number];
}>();
function onQuickAccessClick(item: { id: string }) {
const tabKey = QUICK_ACCESS_TAB_MAP[item.id];
if (tabKey) {
emit('quick-access-tab-click', tabKey);
}
}
const quickAccessExpanded = ref(true);
const ungroupedTaskLists = computed(() => {
const group = props.taskListGroups.find((g) => g.id === '__ungrouped__');
if (group) {
return [...group.taskLists].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
}
return [];
});
const userGroups = computed(() => {
return props.taskListGroups.filter((g) => g.id !== '__ungrouped__' && g.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
});
// 本地可变副本,供 draggable :list 模式直接修改,实现乐观更新
const localUngroupedTaskLists = ref<any[]>([]);
const localUserGroups = ref<any[]>([]);
const localGroupTaskListsMap = ref<Record<string, any[]>>({});
watch(ungroupedTaskLists, (val) => {
localUngroupedTaskLists.value = [...val];
}, { immediate: true });
watch(userGroups, (val) => {
localUserGroups.value = val.map((g) => ({
...g,
taskLists: [...(g.taskLists || [])],
}));
// 同步分组内清单到 map
const newMap: Record<string, any[]> = {};
for (const g of val) {
newMap[g.id] = [...(g.taskLists || [])].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
}
localGroupTaskListsMap.value = newMap;
}, { immediate: true });
function getGroupTaskLists(group: any) {
return [...group.taskLists].filter((l) => l.delFlag !== 1).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
}
function getLocalGroupTaskLists(group: any) {
if (localGroupTaskListsMap.value[group.id]) {
return localGroupTaskListsMap.value[group.id];
}
// 兜底:如果没有本地缓存,从原始数据计算
const list = getGroupTaskLists(group);
localGroupTaskListsMap.value[group.id] = list;
return list;
}
const hoveredListId = ref('');
const collapsedGroups = ref<Record<string, boolean>>({});
const showNewGroupInput = ref(false);
const newGroupName = ref('');
const renamingId = ref('');
const renamingType = ref<'list' | 'group'>('list');
const renameValue = ref('');
const renameInputRef = ref<HTMLInputElement | null>(null);
watch(renamingId, async (val) => {
if (val) {
await nextTick();
renameInputRef.value?.focus();
}
});
const draggingGroupId = ref('');
const dragStartX = ref(0);
const dragStartY = ref(0);
const isDragging = ref(false);
const DRAG_THRESHOLD = 5;
function toggleGroup(groupId: string) {
collapsedGroups.value = {
...collapsedGroups.value,
[groupId]: !collapsedGroups.value[groupId],
};
}
function onDragEnd(evt: any) {
const itemEl = evt.item;
const listId = itemEl?.dataset?.listId;
if (!listId) return;
const favoriteId = props.favoriteIdMap.get(listId);
if (!favoriteId) return;
const toContainer = evt.to;
const targetGroupId = toContainer?.closest('[data-group-id]')?.dataset?.groupId || '';
const newIndex = evt.newIndex ?? 0;
emit('move-list', favoriteId, targetGroupId, newIndex + 1);
}
function onGroupMouseDown(evt: MouseEvent, groupId: string) {
dragStartX.value = evt.clientX;
dragStartY.value = evt.clientY;
isDragging.value = false;
}
function onGroupMouseMove(evt: MouseEvent) {
const dx = Math.abs(evt.clientX - dragStartX.value);
const dy = Math.abs(evt.clientY - dragStartY.value);
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
isDragging.value = true;
}
}
function onGroupMouseUp(evt: MouseEvent, groupId: string) {
if (!isDragging.value) {
toggleGroup(groupId);
}
isDragging.value = false;
dragStartX.value = 0;
dragStartY.value = 0;
}
function onGroupDragStart(evt: any) {
const itemEl = evt.item;
const groupId = itemEl?.dataset?.groupId;
if (groupId) {
draggingGroupId.value = groupId;
}
}
function onGroupDragEnd(evt: any) {
draggingGroupId.value = '';
isDragging.value = false;
const itemEl = evt.item;
const groupId = itemEl?.dataset?.groupId || itemEl?.closest('[data-group-id]')?.dataset?.groupId;
if (!groupId) return;
// 拖拽结束后保持分组折叠状态,防止展开跳动
collapsedGroups.value[groupId] = true;
const newIndex = evt.newIndex ?? 0;
emit('move-group', groupId, newIndex + 1);
}
function canEditList(listId: string): boolean {
const perm = props.permissionMap.get(listId);
return perm === '1' || perm === '2';
}
function onAddMenuClick({ key }: { key: string }) {
if (key === 'new-list') {
startNewList('');
} else if (key === 'new-group') {
showNewGroupInput.value = true;
newGroupName.value = '';
}
}
function startNewList(groupId: string) {
emit('create-list-modal', groupId);
}
function onListMenuClick({ key }: { key: string }, listId: string, currentName: string) {
if (key === 'rename') {
renamingId.value = listId;
renamingType.value = 'list';
renameValue.value = currentName;
} else if (key === 'remove') {
createConfirm({
iconType: 'warning',
title: '确认移除',
content: '确定从收藏清单中移除吗?',
okText: '确认',
cancelText: '取消',
onOk: () => {
emit('remove-favorite', listId);
},
});
}
}
function onGroupMenuClick({ key }: { key: string }, groupId: string, currentName: string) {
if (key === 'rename') {
renamingId.value = groupId;
renamingType.value = 'group';
renameValue.value = currentName;
} else if (key === 'delete') {
emit('delete-group', groupId);
}
}
function confirmInlineRename() {
const name = renameValue.value.trim();
if (name && renamingId.value) {
if (renamingType.value === 'list') {
emit('rename-list', renamingId.value, name);
} else {
emit('rename-group', renamingId.value, name);
}
}
renamingId.value = '';
renameValue.value = '';
}
function submitNewGroup() {
if (newGroupName.value.trim()) {
emit('create-group', newGroupName.value.trim());
newGroupName.value = '';
showNewGroupInput.value = false;
}
}
</script>
<style lang="less" scoped>
.task-sidebar {
width: 240px;
min-width: 240px;
height: 100%;
background: #fff;
border-right: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
overflow: hidden;
user-select: none;
}
.sidebar-header {
padding: 14px 16px 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-title {
font-size: 16px;
font-weight: 600;
color: #1f1f1f;
display: flex;
align-items: center;
}
.sidebar-nav {
padding: 4px 8px;
}
.sidebar-divider {
height: 1px;
background: #f0f0f0;
margin: 4px 12px;
}
.sidebar-section {
padding: 4px 8px;
}
.sidebar-quick-header {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
cursor: pointer;
color: #8c8c8c;
font-size: 14px;
user-select: none;
&:hover {
color: #595959;
}
}
.sidebar-quick-title {
font-size: 14px;
font-weight: 600;
color: #595959;
}
.quick-access-arrow {
font-size: 12px;
color: #8c8c8c;
transition: transform 0.2s ease;
transform: rotate(-90deg);
&.expanded {
transform: rotate(0deg);
}
}
.sidebar-item.sidebar-quick-access-sub {
padding-left: 30px;
font-size: 14px;
}
.sidebar-section-header {
padding: 6px 8px 2px;
}
.sidebar-section-title {
font-size: 14px;
font-weight: 600;
color: #8c8c8c;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.sidebar-lists-section {
flex: 1;
overflow-y: auto;
padding-bottom: 0;
}
.sidebar-favorites-header {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px 2px;
position: relative;
}
.sidebar-favorites-icon {
color: #8f959e;
flex-shrink: 0;
font-size: 14px;
}
.sidebar-favorites-name {
font-size: 14px;
font-weight: 600;
color: #595959;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-favorites-items {
.sidebar-item {
padding-left: 8px;
}
}
.sidebar-group-items {
.sidebar-item {
padding-left: 8px;
}
}
.sidebar-group-header {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px 2px;
cursor: pointer;
position: relative;
&:hover .sidebar-group-name {
color: #333;
}
.sidebar-group-more,
.sidebar-group-add {
opacity: 0;
transition: opacity 0.15s;
}
&:hover .sidebar-group-more,
&:hover .sidebar-group-add {
opacity: 1;
}
}
.group-collapse-arrow {
font-size: 12px;
color: #8f959e;
transition: transform 0.2s ease;
flex-shrink: 0;
width: 16px;
text-align: center;
&.collapsed {
transform: rotate(-90deg);
}
}
.sidebar-group-name {
font-size: 14px;
font-weight: 600;
color: #595959;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-group-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: 10px;
color: #999;
}
.sidebar-group-add {
margin-left: auto;
font-size: 14px;
color: #999;
cursor: pointer;
padding: 2px 4px;
border-radius: 4px;
&:hover {
color: #1677ff;
background: #e6f4ff;
}
}
.sidebar-group-more {
font-size: 16px;
color: #999;
padding: 2px;
border-radius: 4px;
flex-shrink: 0;
&:hover {
color: #1677ff;
background: #e6f4ff;
}
}
.sidebar-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
cursor: pointer;
border-radius: 6px;
margin: 1px 0;
font-size: 14px;
color: #595959;
transition: all 0.15s ease;
position: relative;
&:hover {
background: #f5f5f5;
color: #333;
}
&.active {
background: #e6f4ff;
color: #1677ff;
font-weight: 500;
}
}
.sidebar-item-icon {
font-size: 14px;
flex-shrink: 0;
}
.sidebar-item-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.sidebar-item-more {
font-size: 14px;
color: #999;
padding: 2px;
border-radius: 4px;
flex-shrink: 0;
&:hover {
color: #1677ff;
background: #e6f4ff;
}
}
.list-drag-handle {
cursor: grab;
font-size: 12px;
color: transparent;
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
flex-shrink: 0;
transition: color 0.15s;
&:active {
cursor: grabbing;
}
}
.sidebar-item:hover .list-drag-handle {
color: #c0c4cc;
&:hover {
color: #8c8c8c;
}
}
.list-drag-handle-placeholder {
width: 14px;
flex-shrink: 0;
}
.sidebar-empty-hint {
padding: 8px 10px 4px 38px;
font-size: 12px;
color: #bfbfbf;
font-style: italic;
}
.sidebar-new-group-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
margin: 4px 0;
cursor: pointer;
border-radius: 6px;
font-size: 14px;
color: #8c8c8c;
&:hover {
background: #f5f5f5;
color: #333;
}
}
.sidebar-new-group-row {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
margin: 4px 0;
}
.sidebar-new-item-row {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px 4px 8px;
margin: 2px 0;
}
.group-being-dragged > .sidebar-group-items {
display: none !important;
}
.group-drag-ghost {
opacity: 0.6;
}
.sidebar-item-renaming {
background: #f5f6f7;
}
.sidebar-group-renaming {
background: #f5f6f7;
}
.sidebar-new-icon {
font-size: 12px;
color: #8c8c8c;
}
</style>
@@ -0,0 +1,383 @@
<template>
<a-modal
v-model:open="visible"
:title="title"
:footer="null"
width="520px"
:bodyStyle="{ padding: '0' }"
:centered="true"
:destroyOnClose="true"
@cancel="onClose"
>
<div class="user-modal">
<div class="user-body">
<div class="member-list">
<div
v-for="item in memberList"
:key="item.userId"
class="member-item"
@mouseenter="hoverId = item.userId"
@mouseleave="hoverId = ''"
>
<div class="member-avatar" :style="{ background: getColor(item.username) }">
{{ (item.username || '?').charAt(0) }}
</div>
<div class="member-main">
<div class="member-name-row">
<span class="member-name">{{ item.username }}</span>
</div>
</div>
<div class="member-right">
<span v-show="hoverId === item.userId" class="remove-icon" @click="onRemove(item.userId)">
<Icon icon="ant-design:close-outlined" />
</span>
</div>
</div>
<div v-if="memberList.length === 0" class="empty-state">
<Icon icon="ant-design:team-outlined" style="font-size: 36px; color: #d9d9d9; margin-bottom: 8px" />
<span>暂未选择</span>
</div>
</div>
<div v-if="showAddPanel" class="add-panel">
<div class="add-panel-header">
<span>{{ addPanelTitle }}</span>
<span class="add-panel-close" @click="showAddPanel = false">
<Icon icon="ant-design:close-outlined" />
</span>
</div>
<div class="add-panel-body">
<SzUserSelect
v-model:value="addForm.userIds"
:labelKey="'realname'"
:rowKey="'id'"
placeholder="搜索并选择用户(支持多选)"
/>
</div>
<div class="add-panel-footer">
<a-button size="small" @click="showAddPanel = false">取消</a-button>
<a-button size="small" type="primary" :disabled="!addForm.userIds" @click="onConfirmAdd">
确认添加
</a-button>
</div>
</div>
</div>
<div v-if="!showAddPanel" class="user-footer">
<a-button type="text" size="small" @click="showAddPanel = true">
<Icon icon="ant-design:user-add-outlined" style="margin-right: 4px" />
{{ addPanelTitle }}
</a-button>
<a-button type="primary" size="small" @click="onDone">完成</a-button>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue';
import { message } from 'ant-design-vue';
import Icon from '/@/components/Icon/index';
import SzUserSelect from '/@/components/semri/userComponent/SzUserSelect.vue';
import { getAvatarColor } from '../types';
import { getUserList } from '/@/api/common/api';
import { checkUserSecLevel } from '../utils/taskUtils';
interface MemberItem {
userId: string;
username: string;
}
const props = withDefaults(
defineProps<{
title?: string;
userIds: string;
userNames: string;
multiple?: boolean;
secretLevel?: number;
}>(),
{
title: '选择人员',
multiple: true,
secretLevel: 1,
}
);
const emit = defineEmits<{
confirm: [userIds: string, userNames: string];
}>();
const visible = ref(false);
const hoverId = ref('');
const showAddPanel = ref(false);
const addForm = reactive({
userIds: '',
});
const memberList = ref<MemberItem[]>([]);
const addPanelTitle = computed(() => {
return props.title === '选择负责人' ? '添加负责人' : props.title === '选择参与人' ? '添加参与人' : props.title === '选择关注人' ? '添加关注人' : '添加人员';
});
function getColor(name: string): string {
return getAvatarColor(name || '');
}
function parseMembers(userIds: string, userNames: string): MemberItem[] {
if (!userIds) return [];
const ids = userIds.split(',').filter(Boolean);
const names = userNames ? userNames.split(',').filter(Boolean) : [];
return ids.map((id, i) => ({
userId: id.trim(),
username: (names[i] || id).trim(),
}));
}
async function open() {
memberList.value = parseMembers(props.userIds, props.userNames);
const needFetch = memberList.value.filter((m) => m.username === m.userId);
if (needFetch.length > 0) {
try {
const ids = needFetch.map((m) => m.userId);
const res = await getUserList({ id: ids.join(','), pageNo: 1, pageSize: ids.length * 2 });
if (res.records && res.records.length > 0) {
const nameMap: Record<string, string> = {};
res.records.forEach((u: any) => {
nameMap[u.id] = u.realname || u.username;
});
memberList.value.forEach((m) => {
if (nameMap[m.userId]) {
m.username = nameMap[m.userId];
}
});
}
} catch (e) {
console.warn('获取用户名失败', e);
}
}
showAddPanel.value = false;
addForm.userIds = '';
visible.value = true;
}
function onClose() {
visible.value = false;
showAddPanel.value = false;
addForm.userIds = '';
}
function onRemove(userId: string) {
memberList.value = memberList.value.filter((m) => m.userId !== userId);
}
async function onConfirmAdd() {
if (!addForm.userIds) return;
const secretLevel = props.secretLevel || 1;
const { ok, invalidNames } = await checkUserSecLevel(addForm.userIds, secretLevel);
if (!ok) {
const map: Record<number, string> = { 1: '非密', 2: '内部', 3: '秘密', 4: '机密' };
message.warning(`以下人员的密级不满足当前清单(${map[secretLevel] || '非密'})要求:${invalidNames.join('、')}`);
return;
}
const newIds = addForm.userIds.split(',').filter(Boolean);
const existingIds = new Set(memberList.value.map((m) => m.userId));
if (!props.multiple) {
memberList.value = [];
}
const nameMap: Record<string, string> = {};
try {
const res = await getUserList({ id: newIds.join(','), pageNo: 1, pageSize: newIds.length * 2 });
if (res.records && res.records.length > 0) {
res.records.forEach((u: any) => {
nameMap[u.id] = u.realname || u.username;
});
}
} catch (e) {
console.warn('获取用户名失败,使用ID作为名称', e);
}
let addedCount = 0;
for (const nid of newIds) {
if (!existingIds.has(nid) || !props.multiple) {
memberList.value.push({
userId: nid,
username: nameMap[nid] || nid,
});
addedCount++;
}
}
if (addedCount > 0) {
message.success(`已添加 ${addedCount} 位人员`);
}
addForm.userIds = '';
showAddPanel.value = false;
}
function onDone() {
const ids = memberList.value.map((m) => m.userId).join(',');
const names = memberList.value.map((m) => m.username).join(',');
emit('confirm', ids, names);
visible.value = false;
}
defineExpose({ open });
</script>
<style lang="less" scoped>
.user-modal {
display: flex;
flex-direction: column;
}
.user-body {
position: relative;
min-height: 200px;
}
.member-list {
padding: 8px 0;
max-height: 400px;
overflow-y: auto;
}
.member-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 20px;
transition: background 0.12s;
&:hover {
background: #f5f6f7;
}
}
.member-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #fff;
font-size: 13px;
font-weight: 500;
}
.member-main {
flex: 1;
min-width: 0;
}
.member-name-row {
display: flex;
align-items: center;
gap: 6px;
}
.member-name {
font-size: 14px;
color: #1f2329;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-right {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
min-width: 30px;
justify-content: flex-end;
}
.remove-icon {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
cursor: pointer;
color: #8f959e;
font-size: 12px;
transition: all 0.12s;
&:hover {
background: #f0f0f0;
color: #f5222d;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #bfbfbf;
font-size: 13px;
}
.add-panel {
position: absolute;
inset: 0;
background: #fff;
display: flex;
flex-direction: column;
z-index: 10;
}
.add-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
font-size: 14px;
font-weight: 500;
color: #1f2329;
}
.add-panel-close {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
color: #8f959e;
&:hover {
background: #f0f0f0;
}
}
.add-panel-body {
padding: 0 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
.add-panel-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 16px 20px;
margin-top: auto;
}
.user-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
border-top: 1px solid #f0f0f0;
}
</style>
File diff suppressed because it is too large Load Diff
+275
View File
@@ -0,0 +1,275 @@
export type ViewType = 'my-tasks' | 'followed' | 'all-tasks' | 'created' | 'assigned' | '';
export interface NavItem {
id: ViewType;
label: string;
icon: string;
}
export const NAV_ITEMS: NavItem[] = [
{ id: 'my-tasks', label: '我负责的', icon: 'ant-design:user-outlined' },
{ id: 'followed', label: '我关注的', icon: 'ant-design:star-outlined' },
];
export const QUICK_ACCESS_ITEMS: NavItem[] = [
{ id: 'all-tasks', label: '全部清单', icon: 'ant-design:unordered-list-outlined' },
{ id: 'created', label: '我所有的', icon: 'ant-design:edit-outlined' },
{ id: 'assigned', label: '我协作的', icon: 'ant-design:send-outlined' },
];
export const QUICK_ACCESS_TAB_MAP: Record<string, string> = {
'all-tasks': 'all',
created: 'own',
assigned: 'collab',
};
export const MAX_TASK_DEPTH = 5;
const AVATAR_COLORS = ['#3370ff', '#14c9c9', '#f77234', '#722ed1', '#eb2f96', '#f53f3f', '#0fc6c2', '#3491fa', '#9fdb1d', '#f7ba1e'];
export function getAvatarColor(name: string): string {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
}
export interface TaskItem {
id: string;
taskName: string;
taskDesc: string;
completed: boolean;
priority: string;
assigneeId: string;
assigneeName: string;
assignId: string;
assignName: string;
participantId: string;
participantName: string;
followersId: string;
followersName: string;
createBy: string;
startTime: string;
endTime: string;
completeTime: string;
createTime: string;
updateTime: string;
type: string;
subTaskCount: number;
completedSubTaskCount: number;
remark: string;
groupId: string;
groupName: string;
childCount: number;
completedChildCount: number;
pid: string;
hasChild: string;
sortOrder: number;
isDefault?: number;
listName: string;
mainId: string;
myPermission?: string;
}
export interface TaskGroup {
id: string;
name: string;
taskListId: string;
collapsed: boolean;
tasks: TaskItem[];
isDefault?: number;
sortOrder?: number;
}
export interface TaskList {
id: string;
name: string;
pid: string;
hasChild: string;
type: number;
sortOrder: number;
delFlag: number;
icon?: string;
groups: TaskGroup[];
ownerName?: string;
collaboratorNames?: string;
createTimeStr?: string;
myPermission?: string;
secretLevel?: number;
secretText?: string;
}
export interface TaskListGroup {
id: string;
name: string;
pid: string;
hasChild: string;
type: number;
sortOrder: number;
delFlag: number;
taskLists: TaskList[];
}
export type StatusFilter = 'all' | 'completed' | 'uncompleted';
export type GroupDimension = 'custom' | 'assignee' | 'startTime' | 'endTime' | 'createBy' | 'priority' | 'none';
export type SortField = 'sortOrder' | 'startTime' | 'endTime' | 'createTime' | 'completeTime' | 'updateTime' | 'priority';
export type SortDirection = 'asc' | 'desc';
export type FieldKey =
| 'listName'
| 'taskDesc'
| 'assigneeName'
| 'participantName'
| 'startTime'
| 'endTime'
| 'subTaskProgress'
| 'createBy'
| 'assignName'
| 'followersName'
| 'createTime'
| 'completeTime'
| 'updateTime'
| 'priority'
| 'remark';
export interface FieldConfig {
key: FieldKey;
label: string;
defaultVisible: boolean;
}
export const FIELD_CONFIG: FieldConfig[] = [
{ key: 'taskDesc', label: '任务描述', defaultVisible: false },
{ key: 'assigneeName', label: '负责人', defaultVisible: true },
{ key: 'participantName', label: '参与人', defaultVisible: true },
{ key: 'startTime', label: '开始时间', defaultVisible: true },
{ key: 'endTime', label: '截止时间', defaultVisible: true },
{ key: 'subTaskProgress', label: '子任务进度', defaultVisible: true },
{ key: 'createBy', label: '创建人', defaultVisible: true },
{ key: 'assignName', label: '分配人', defaultVisible: false },
{ key: 'followersName', label: '关注人', defaultVisible: false },
{ key: 'createTime', label: '创建时间', defaultVisible: false },
{ key: 'completeTime', label: '完成时间', defaultVisible: true },
{ key: 'updateTime', label: '更新时间', defaultVisible: false },
{ key: 'priority', label: '优先级', defaultVisible: true },
{ key: 'remark', label: '其他事项说明', defaultVisible: false },
{ key: 'listName', label: '来源', defaultVisible: true },
];
export const DEFAULT_VISIBLE_FIELDS: FieldKey[] = FIELD_CONFIG.filter((f) => f.defaultVisible).map((f) => f.key);
export interface GroupOption {
value: GroupDimension;
label: string;
}
export const GROUP_OPTIONS: GroupOption[] = [
{ value: 'custom', label: '自定义分组' },
{ value: 'assignee', label: '按负责人' },
{ value: 'startTime', label: '按开始时间' },
{ value: 'endTime', label: '按截止时间' },
{ value: 'createBy', label: '按创建人' },
{ value: 'priority', label: '按优先级' },
{ value: 'none', label: '无分组' },
];
export interface SortOption {
value: SortField;
label: string;
}
export const SORT_OPTIONS: SortOption[] = [
{ value: 'sortOrder', label: '拖拽自定义' },
{ value: 'createTime', label: '创建时间' },
{ value: 'startTime', label: '开始时间' },
{ value: 'endTime', label: '截止时间' },
{ value: 'completeTime', label: '完成时间' },
{ value: 'updateTime', label: '更新时间' },
{ value: 'priority', label: '优先级' },
];
export type FilterFieldType =
| 'assigneeName'
| 'createBy'
| 'assignName'
| 'followersName'
| 'priority'
| 'startTime'
| 'endTime'
| 'createTime'
| 'completeTime'
| 'updateTime';
export type FilterOperator = 'contains' | 'equals' | 'before' | 'after' | 'between' | 'isEmpty' | 'isNotEmpty';
export interface FilterFieldConfig {
value: FilterFieldType;
label: string;
type: 'text' | 'select' | 'date';
operators: FilterOperator[];
}
export const FILTER_FIELD_CONFIGS: FilterFieldConfig[] = [
{ value: 'assigneeName', label: '负责人', type: 'text', operators: ['contains', 'isEmpty', 'isNotEmpty'] },
{ value: 'createBy', label: '创建人', type: 'text', operators: ['contains', 'isEmpty', 'isNotEmpty'] },
{ value: 'assignName', label: '分配人', type: 'text', operators: ['contains', 'isEmpty', 'isNotEmpty'] },
{ value: 'followersName', label: '关注人', type: 'text', operators: ['contains', 'isEmpty', 'isNotEmpty'] },
{ value: 'priority', label: '优先级', type: 'select', operators: ['equals', 'isEmpty', 'isNotEmpty'] },
{ value: 'startTime', label: '开始时间', type: 'date', operators: ['before', 'after', 'between', 'isEmpty', 'isNotEmpty'] },
{ value: 'endTime', label: '截止时间', type: 'date', operators: ['before', 'after', 'between', 'isEmpty', 'isNotEmpty'] },
{ value: 'createTime', label: '创建时间', type: 'date', operators: ['before', 'after', 'between', 'isEmpty', 'isNotEmpty'] },
{ value: 'completeTime', label: '完成时间', type: 'date', operators: ['before', 'after', 'between', 'isEmpty', 'isNotEmpty'] },
{ value: 'updateTime', label: '更新时间', type: 'date', operators: ['before', 'after', 'between', 'isEmpty', 'isNotEmpty'] },
];
export const FILTER_OPERATOR_LABELS: Record<FilterOperator, string> = {
contains: '包含',
equals: '等于',
before: '早于',
after: '晚于',
between: '介于',
isEmpty: '为空',
isNotEmpty: '不为空',
};
export interface FilterCondition {
id: string;
field: FilterFieldType;
operator: FilterOperator;
value: any;
valueEnd?: any;
}
export interface FlatTaskItem {
task: TaskItem;
level: number;
}
export const SECRET_LEVEL_MAP = [
{ value: 1, label: '非密' },
{ value: 2, label: '内部' },
{ value: 3, label: '秘密' },
{ value: 4, label: '机密' },
] as const;
export const SECRET_LEVEL_COLOR: Record<number, string> = {
1: '#8c8c8c',
2: '#1677ff',
3: '#fa8c16',
4: '#f5222d',
};
export function getAvailableSecretLevels(userSecurityLevel: number) {
if (!userSecurityLevel) userSecurityLevel = 3;
return SECRET_LEVEL_MAP.filter((item) => userSecurityLevel > item.value);
}
export function getSecretText(level?: number | null): string {
if (level == null) return '非密';
const found = SECRET_LEVEL_MAP.find((item) => item.value === level);
return found ? found.label : '非密';
}
+60
View File
@@ -0,0 +1,60 @@
export const PRIORITY_OPTIONS = [
{ label: '高', value: 'high' },
{ label: '中', value: 'medium' },
{ label: '低', value: 'low' },
];
const PRIORITY_COLOR_MAP: Record<string, string> = {
high: '#ffccc7',
medium: '#ffe58f',
low: '#91d5ff',
};
export function getPriorityBg(value: string): string {
return PRIORITY_COLOR_MAP[value] || '#f5f5f5';
}
export function getPriorityLabel(value: string): string {
const opt = PRIORITY_OPTIONS.find((o) => o.value === value);
return opt?.label || value;
}
export function isOverdue(task: { endTime?: string; completed?: boolean }): boolean {
if (!task.endTime || task.completed) return false;
const end = new Date(task.endTime);
const now = new Date();
now.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
return now > end;
}
export async function checkUserSecLevel(
userIds: string,
secretLevel: number
): Promise<{ ok: boolean; invalidNames: string[] }> {
if (!userIds || !userIds.trim()) return { ok: true, invalidNames: [] };
const { getUserList } = await import('/@/api/common/api');
const ids = userIds.split(',').map((id) => id.trim()).filter(Boolean);
if (ids.length === 0) return { ok: true, invalidNames: [] };
const level = secretLevel || 1;
try {
const res = await getUserList({ id: ids.join(','), pageNo: 1, pageSize: ids.length });
const records: any[] = res?.records || [];
const nameMap: Record<string, string> = {};
const levelMap: Record<string, number> = {};
for (const r of records) {
nameMap[r.id] = r.realname || r.id;
levelMap[r.id] = r.userSecurityLevel ?? 3;
}
const invalidNames: string[] = [];
for (const id of ids) {
const userSecLevel = levelMap[id] ?? 3;
if (userSecLevel <= level) {
invalidNames.push(nameMap[id] || id);
}
}
return { ok: invalidNames.length === 0, invalidNames };
} catch {
return { ok: true, invalidNames: [] };
}
}
@@ -1,3 +1,3 @@
export const ROLE_SCOPE_CONFIG = { export const ROLE_SCOPE_CONFIG = {
DEFAULT_PERMESSION_IDS: '2035916880060223489', DEFAULT_PERMESSION_IDS: '2035916880060223489,2026041610467690430',
}; };