czh-20260501-增加密级相关功能
This commit is contained in:
@@ -45,6 +45,7 @@ enum Api {
|
|||||||
listAllByMainId = '/tasklist/taskListDetial/listAllByMainId',
|
listAllByMainId = '/tasklist/taskListDetial/listAllByMainId',
|
||||||
myResponsibleTasks = '/tasklist/taskList/myResponsibleTasks',
|
myResponsibleTasks = '/tasklist/taskList/myResponsibleTasks',
|
||||||
myFollowedTasks = '/tasklist/taskList/myFollowedTasks',
|
myFollowedTasks = '/tasklist/taskList/myFollowedTasks',
|
||||||
|
getCurrentUserSecurityLevel = '/tasklist/taskList/getCurrentUserSecurityLevel',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getExportUrl = Api.exportXls;
|
export const getExportUrl = Api.exportXls;
|
||||||
@@ -150,3 +151,5 @@ export const listAllByMainId = (params) => defHttp.get({ url: Api.listAllByMainI
|
|||||||
export const myResponsibleTasks = () => defHttp.get({ url: Api.myResponsibleTasks });
|
export const myResponsibleTasks = () => defHttp.get({ url: Api.myResponsibleTasks });
|
||||||
|
|
||||||
export const myFollowedTasks = () => defHttp.get({ url: Api.myFollowedTasks });
|
export const myFollowedTasks = () => defHttp.get({ url: Api.myFollowedTasks });
|
||||||
|
|
||||||
|
export const getCurrentUserSecurityLevel = () => defHttp.get({ url: Api.getCurrentUserSecurityLevel });
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { BasicColumn } from '/@/components/Table';
|
|||||||
import { FormSchema } from '/@/components/Table';
|
import { FormSchema } from '/@/components/Table';
|
||||||
import { JVxeTypes, JVxeColumn } from '/@/components/jeecg/JVxeTable/types';
|
import { JVxeTypes, JVxeColumn } from '/@/components/jeecg/JVxeTable/types';
|
||||||
import type { TaskListGroup } from './types';
|
import type { TaskListGroup } from './types';
|
||||||
|
import { getAvailableSecretLevels } from './types';
|
||||||
|
|
||||||
export const columns: BasicColumn[] = [
|
export const columns: BasicColumn[] = [
|
||||||
{
|
{
|
||||||
@@ -300,14 +301,32 @@ export const createTaskFormSchema: FormSchema[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const createListFormSchema: FormSchema[] = [
|
export function getCreateListFormSchema(userSecurityLevel: number): FormSchema[] {
|
||||||
|
if (!userSecurityLevel) userSecurityLevel = 3;
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
label: '清单名称',
|
label: '清单名称',
|
||||||
field: 'tasklistName',
|
field: 'tasklistName',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
required: true,
|
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[] = [
|
export const mockTaskListGroups: TaskListGroup[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="collab-modal">
|
<div class="collab-modal">
|
||||||
<div class="collab-body">
|
<div class="collab-body">
|
||||||
<div class="member-list">
|
<div class="member-list">
|
||||||
<div v-for="item in collaborators" :key="item.permissionId" class="member-item" @mouseenter="hoverId = item.permissionId" @mouseleave="hoverId = ''">
|
<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) }">
|
<div class="member-avatar" :style="{ background: getColor(item.username) }">
|
||||||
{{ (item.username || '?').charAt(0) }}
|
{{ (item.username || '?').charAt(0) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<template v-if="item.permission === '1'">
|
<template v-if="item.permission === '1'">
|
||||||
<span class="role-label">所有者</span>
|
<span class="role-label">所有者</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="isOwner">
|
<template v-else-if="isOwner && item.permissionId">
|
||||||
<a-dropdown :trigger="['click']" :overlayStyle="{ minWidth: '120px' }">
|
<a-dropdown :trigger="['click']" :overlayStyle="{ minWidth: '120px' }">
|
||||||
<span class="role-label clickable" @click.stop>
|
<span class="role-label clickable" @click.stop>
|
||||||
{{ item.permission === '2' ? '可编辑' : '可阅读' }}
|
{{ item.permission === '2' ? '可编辑' : '可阅读' }}
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="collaborators.length === 0" class="empty-state">
|
<div v-if="allMembers.length === 0" class="empty-state">
|
||||||
<Icon icon="ant-design:team-outlined" style="font-size: 36px; color: #d9d9d9; margin-bottom: 8px" />
|
<Icon icon="ant-design:team-outlined" style="font-size: 36px; color: #d9d9d9; margin-bottom: 8px" />
|
||||||
<span>暂无协作人</span>
|
<span>暂无协作人</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,10 +68,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="add-panel-body">
|
<div class="add-panel-body">
|
||||||
<JSelectUser
|
<SzUserSelect
|
||||||
v-model:value="addForm.userIds"
|
v-model:value="addForm.userIds"
|
||||||
:label-key="'realname'"
|
:labelKey="'realname'"
|
||||||
:row-key="'id'"
|
:rowKey="'id'"
|
||||||
placeholder="搜索并选择用户(支持多选)"
|
placeholder="搜索并选择用户(支持多选)"
|
||||||
/>
|
/>
|
||||||
<div class="add-panel-row">
|
<div class="add-panel-row">
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="add-panel-footer">
|
<div class="add-panel-footer">
|
||||||
<a-button size="small" @click="showAddPanel = false">取消</a-button>
|
<a-button size="small" @click="showAddPanel = false">取消</a-button>
|
||||||
<a-button size="small" type="primary" :disabled="!addForm.userIds" @click="onBatchAdd">
|
<a-button size="small" type="primary" :disabled="!addForm.userIds" @click="onConfirmAdd">
|
||||||
确认添加
|
确认添加
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,18 +96,20 @@
|
|||||||
<Icon icon="ant-design:user-add-outlined" style="margin-right: 4px" />
|
<Icon icon="ant-design:user-add-outlined" style="margin-right: 4px" />
|
||||||
添加协作人
|
添加协作人
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="primary" size="small" @click="onClose">完成</a-button>
|
<a-button type="primary" size="small" @click="onDone">完成</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive } from 'vue';
|
import { ref, reactive, computed } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
import Icon from '/@/components/Icon/index';
|
import Icon from '/@/components/Icon/index';
|
||||||
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
|
import SzUserSelect from '/@/components/semri/userComponent/SzUserSelect.vue';
|
||||||
import { getCollaborators, addCollaborator, removeCollaborator, updateCollaboratorPermission } from '../TaskList.api';
|
import { getCollaborators, addCollaborator, removeCollaborator, updateCollaboratorPermission } from '../TaskList.api';
|
||||||
import { getAvatarColor } from '../types';
|
import { getAvatarColor } from '../types';
|
||||||
|
import { checkUserSecLevel } from '../utils/taskUtils';
|
||||||
import { useMessage } from '/@/hooks/web/useMessage';
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
|
||||||
const { createMessage } = useMessage();
|
const { createMessage } = useMessage();
|
||||||
@@ -119,9 +121,17 @@
|
|||||||
permission: string;
|
permission: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PendingAddItem {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
permission: string;
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
taskListId: string;
|
taskListId: string;
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
|
secretLevel?: number;
|
||||||
|
secretText?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -136,6 +146,17 @@
|
|||||||
userIds: '',
|
userIds: '',
|
||||||
permission: '2',
|
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 {
|
function getColor(name: string): string {
|
||||||
return getAvatarColor(name || '');
|
return getAvatarColor(name || '');
|
||||||
@@ -144,6 +165,9 @@
|
|||||||
async function open() {
|
async function open() {
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
showAddPanel.value = false;
|
showAddPanel.value = false;
|
||||||
|
pendingAddList.value = [];
|
||||||
|
addForm.userIds = '';
|
||||||
|
addForm.permission = '2';
|
||||||
await loadCollaborators();
|
await loadCollaborators();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +176,7 @@
|
|||||||
showAddPanel.value = false;
|
showAddPanel.value = false;
|
||||||
addForm.userIds = '';
|
addForm.userIds = '';
|
||||||
addForm.permission = '2';
|
addForm.permission = '2';
|
||||||
|
pendingAddList.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCollaborators() {
|
async function loadCollaborators() {
|
||||||
@@ -163,19 +188,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onBatchAdd() {
|
async function onConfirmAdd() {
|
||||||
if (!addForm.userIds) return;
|
if (!addForm.userIds) return;
|
||||||
const ids = addForm.userIds.split(',').filter(Boolean);
|
const ids = addForm.userIds.split(',').filter(Boolean);
|
||||||
if (ids.length === 0) return;
|
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 successCount = 0;
|
||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
for (const uid of ids) {
|
for (const item of pendingAddList.value) {
|
||||||
try {
|
try {
|
||||||
await addCollaborator({
|
await addCollaborator({
|
||||||
taskListId: props.taskListId,
|
taskListId: props.taskListId,
|
||||||
userId: uid,
|
userId: item.userId,
|
||||||
permission: addForm.permission,
|
permission: item.permission,
|
||||||
});
|
});
|
||||||
successCount++;
|
successCount++;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -184,16 +257,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (successCount > 0) {
|
if (successCount > 0) {
|
||||||
createMessage.success(`成功添加 ${successCount} 位协作人${failCount > 0 ? `,${failCount} 位已存在` : ''}`);
|
createMessage.success(`成功添加 ${successCount} 位协作人${failCount > 0 ? `,${failCount} 位失败` : ''}`);
|
||||||
} else {
|
} else {
|
||||||
createMessage.warning('所选用户均已是协作人');
|
createMessage.warning('添加协作人失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pendingAddList.value = [];
|
||||||
|
emit('changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
visible.value = false;
|
||||||
|
showAddPanel.value = false;
|
||||||
addForm.userIds = '';
|
addForm.userIds = '';
|
||||||
addForm.permission = '2';
|
addForm.permission = '2';
|
||||||
showAddPanel.value = false;
|
|
||||||
await loadCollaborators();
|
|
||||||
emit('changed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRemove(permissionId: string) {
|
async function onRemove(permissionId: string) {
|
||||||
|
|||||||
@@ -7,35 +7,41 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||||
import { createListFormSchema } from '../TaskList.data';
|
import { getCreateListFormSchema } from '../TaskList.data';
|
||||||
import { addTaskList } from '../TaskList.api';
|
import { addTaskList } from '../TaskList.api';
|
||||||
|
import { getSecretText } from '../types';
|
||||||
|
|
||||||
const emit = defineEmits(['register', 'success']);
|
const emit = defineEmits(['register', 'success']);
|
||||||
|
|
||||||
const [registerForm, { resetFields, validate }] = useForm({
|
let pendingGroupId = '';
|
||||||
|
let pendingUserSecurityLevel = 3;
|
||||||
|
|
||||||
|
const [registerForm, { resetFields, validate, updateSchema }] = useForm({
|
||||||
labelWidth: 100,
|
labelWidth: 100,
|
||||||
schemas: createListFormSchema,
|
schemas: getCreateListFormSchema(3),
|
||||||
showActionButtonGroup: false,
|
showActionButtonGroup: false,
|
||||||
baseColProps: { span: 24 },
|
baseColProps: { span: 24 },
|
||||||
});
|
});
|
||||||
|
|
||||||
let pendingGroupId = '';
|
|
||||||
|
|
||||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||||
await resetFields();
|
await resetFields();
|
||||||
pendingGroupId = data?.groupId || '';
|
pendingGroupId = data?.groupId || '';
|
||||||
|
pendingUserSecurityLevel = data?.userSecurityLevel || 3;
|
||||||
|
updateSchema(getCreateListFormSchema(pendingUserSecurityLevel));
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
try {
|
try {
|
||||||
setModalProps({ confirmLoading: true });
|
setModalProps({ confirmLoading: true });
|
||||||
const values = await validate();
|
const values = await validate();
|
||||||
const listId = await addTaskList({
|
const result = await addTaskList({
|
||||||
tasklistName: values.tasklistName || values.name,
|
tasklistName: values.tasklistName,
|
||||||
pid: pendingGroupId || undefined,
|
pid: pendingGroupId || undefined,
|
||||||
|
secretLevel: values.secretLevel,
|
||||||
|
secretText: getSecretText(values.secretLevel),
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
emit('success', listId);
|
emit('success', result);
|
||||||
} finally {
|
} finally {
|
||||||
setModalProps({ confirmLoading: false });
|
setModalProps({ confirmLoading: false });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicModal v-bind="$attrs" @register="registerModal" destroy-on-close title="创建任务" :width="600" @ok="handleSubmit">
|
<BasicModal v-bind="$attrs" @register="registerModal" destroy-on-close title="创建任务" :width="600" @ok="handleSubmit">
|
||||||
<BasicForm @register="registerForm" />
|
<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>
|
</BasicModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||||
|
import TaskAttachmentUpload from './TaskAttachmentUpload.vue';
|
||||||
import { createTaskFormSchema } from '../TaskList.data';
|
import { createTaskFormSchema } from '../TaskList.data';
|
||||||
import { addTask } from '../TaskList.api';
|
import { addTask } from '../TaskList.api';
|
||||||
|
import { checkUserSecLevel } from '../utils/taskUtils';
|
||||||
|
|
||||||
const emit = defineEmits(['register', 'success']);
|
const emit = defineEmits(['register', 'success']);
|
||||||
const groupId = ref('');
|
const groupId = ref('');
|
||||||
const mainId = 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({
|
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
|
||||||
labelWidth: 100,
|
labelWidth: 100,
|
||||||
@@ -26,6 +43,8 @@
|
|||||||
await resetFields();
|
await resetFields();
|
||||||
groupId.value = data?.groupId || '';
|
groupId.value = data?.groupId || '';
|
||||||
mainId.value = data?.mainId || '';
|
mainId.value = data?.mainId || '';
|
||||||
|
currentSecretLevel.value = data?.secretLevel || 1;
|
||||||
|
currentSecretText.value = data?.secretText || '非密';
|
||||||
if (groupId.value) {
|
if (groupId.value) {
|
||||||
await setFieldsValue({ groupId: groupId.value });
|
await setFieldsValue({ groupId: groupId.value });
|
||||||
}
|
}
|
||||||
@@ -35,7 +54,16 @@
|
|||||||
try {
|
try {
|
||||||
setModalProps({ confirmLoading: true });
|
setModalProps({ confirmLoading: true });
|
||||||
const values = await validate();
|
const values = await validate();
|
||||||
await addTask({
|
|
||||||
|
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,
|
mainId: mainId.value,
|
||||||
taskName: values.taskName,
|
taskName: values.taskName,
|
||||||
taskDesc: values.taskDesc || '',
|
taskDesc: values.taskDesc || '',
|
||||||
@@ -46,10 +74,42 @@
|
|||||||
startTime: values.startTime || null,
|
startTime: values.startTime || null,
|
||||||
endTime: values.endTime || null,
|
endTime: values.endTime || null,
|
||||||
});
|
});
|
||||||
|
if (fileUploadRef.value?.hasPendingFiles()) {
|
||||||
|
await fileUploadRef.value.bindBussinessId(result);
|
||||||
|
}
|
||||||
closeModal();
|
closeModal();
|
||||||
emit('success');
|
emit('success');
|
||||||
} finally {
|
} finally {
|
||||||
setModalProps({ confirmLoading: false });
|
setModalProps({ confirmLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function openFileUploadModal() {
|
||||||
|
fileUploadRef.value?.handleOpenUploadModal();
|
||||||
|
}
|
||||||
</script>
|
</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>
|
||||||
@@ -36,6 +36,9 @@
|
|||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentList" class="content-header-actions">
|
<div v-if="currentList" class="content-header-actions">
|
||||||
|
<span v-if="currentList.secretText" class="header-secret-badge" :style="{ color: SECRET_LEVEL_COLOR[currentList.secretLevel || 1] || '#8c8c8c', borderColor: SECRET_LEVEL_COLOR[currentList.secretLevel || 1] || '#8c8c8c' }">
|
||||||
|
{{ currentList.secretText }}
|
||||||
|
</span>
|
||||||
<div class="collaborator-bar" @click="emit('open-collaborator')">
|
<div class="collaborator-bar" @click="emit('open-collaborator')">
|
||||||
<AvatarDisplay
|
<AvatarDisplay
|
||||||
v-if="collaboratorDisplayNames"
|
v-if="collaboratorDisplayNames"
|
||||||
@@ -246,6 +249,7 @@
|
|||||||
<div class="list-group-col list-group-col-name">名称</div>
|
<div class="list-group-col list-group-col-name">名称</div>
|
||||||
<div class="list-group-col list-group-col-owner">所有者</div>
|
<div class="list-group-col list-group-col-owner">所有者</div>
|
||||||
<div class="list-group-col list-group-col-collaborators">协作者</div>
|
<div class="list-group-col list-group-col-collaborators">协作者</div>
|
||||||
|
<div class="list-group-col list-group-col-secret">密级</div>
|
||||||
<div class="list-group-col list-group-col-time">创建时间</div>
|
<div class="list-group-col list-group-col-time">创建时间</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="list in allTaskLists" :key="list.id" class="list-group-row" @click="emit('list-select', list.id)">
|
<div v-for="list in allTaskLists" :key="list.id" class="list-group-row" @click="emit('list-select', list.id)">
|
||||||
@@ -273,6 +277,16 @@
|
|||||||
</template>
|
</template>
|
||||||
<span v-else class="field-placeholder">-</span>
|
<span v-else class="field-placeholder">-</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="list-group-col list-group-col-secret">
|
||||||
|
<span
|
||||||
|
v-if="list.secretText"
|
||||||
|
class="secret-badge-inline"
|
||||||
|
:style="{ color: SECRET_LEVEL_COLOR[(list as any).secretLevel || 1] || '#8c8c8c', borderColor: SECRET_LEVEL_COLOR[(list as any).secretLevel || 1] || '#8c8c8c' }"
|
||||||
|
>
|
||||||
|
{{ list.secretText }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="field-placeholder">-</span>
|
||||||
|
</div>
|
||||||
<div class="list-group-col list-group-col-time">
|
<div class="list-group-col list-group-col-time">
|
||||||
<span class="field-value">{{ getFormattedCreateTime(list) }}</span>
|
<span class="field-value">{{ getFormattedCreateTime(list) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -790,6 +804,7 @@
|
|||||||
:user-ids="userSelectModalCurrentIds"
|
:user-ids="userSelectModalCurrentIds"
|
||||||
:user-names="userSelectModalCurrentNames"
|
:user-names="userSelectModalCurrentNames"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
|
:secret-level="currentList?.secretLevel || 1"
|
||||||
@confirm="onUserSelectConfirm"
|
@confirm="onUserSelectConfirm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -804,7 +819,7 @@
|
|||||||
import UserSelectModal from './UserSelectModal.vue';
|
import UserSelectModal from './UserSelectModal.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 { FIELD_CONFIG, GROUP_OPTIONS, SORT_OPTIONS, MAX_TASK_DEPTH, FILTER_FIELD_CONFIGS, FILTER_OPERATOR_LABELS } from '../types';
|
import { FIELD_CONFIG, GROUP_OPTIONS, SORT_OPTIONS, MAX_TASK_DEPTH, FILTER_FIELD_CONFIGS, FILTER_OPERATOR_LABELS, SECRET_LEVEL_COLOR } from '../types';
|
||||||
import { PRIORITY_OPTIONS, getPriorityBg, getPriorityLabel, isOverdue } from '../utils/taskUtils';
|
import { PRIORITY_OPTIONS, getPriorityBg, getPriorityLabel, isOverdue } from '../utils/taskUtils';
|
||||||
import type {
|
import type {
|
||||||
TaskList,
|
TaskList,
|
||||||
@@ -1851,6 +1866,15 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-secret-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid;
|
||||||
|
line-height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.collaborator-bar {
|
.collaborator-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -2150,6 +2174,9 @@
|
|||||||
.content-body {
|
.content-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-group-header {
|
.task-group-header {
|
||||||
@@ -2651,6 +2678,21 @@
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-group-col-secret {
|
||||||
|
width: 80px;
|
||||||
|
padding: 0 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-badge-inline {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid;
|
||||||
|
line-height: 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.list-group-col-time {
|
.list-group-col-time {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
|||||||
@@ -76,12 +76,19 @@
|
|||||||
<div class="detail-row-content">
|
<div class="detail-row-content">
|
||||||
<div class="detail-field-row">
|
<div class="detail-field-row">
|
||||||
<span class="detail-field-label">负责人</span>
|
<span class="detail-field-label">负责人</span>
|
||||||
<div v-if="taskData.assigneeName" class="assignee-info" :class="{ 'readonly-field': isReadOnly }" @click="!isReadOnly && openDrawerUserSelect('assignee')">
|
<div v-if="!isFullReadOnly" class="detail-field-clickable" @click="openDrawerUserSelect('assignee')">
|
||||||
|
<template v-if="taskData.assigneeName">
|
||||||
|
<a-tooltip :title="taskData.assigneeName" placement="top">
|
||||||
|
<AvatarDisplay :names="taskData.assigneeName" :size="28" :show-name="true" />
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<span v-else class="field-placeholder">未指定</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="taskData.assigneeName" class="assignee-info readonly-field">
|
||||||
<a-tooltip :title="taskData.assigneeName" placement="top">
|
<a-tooltip :title="taskData.assigneeName" placement="top">
|
||||||
<AvatarDisplay :names="taskData.assigneeName" :size="28" :show-name="true" />
|
<AvatarDisplay :names="taskData.assigneeName" :size="28" :show-name="true" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<a-button v-else-if="!isReadOnly" type="text" class="add-field-btn" @click="openDrawerUserSelect('assignee')">添加</a-button>
|
|
||||||
<span v-else class="detail-field-value">-</span>
|
<span v-else class="detail-field-value">-</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,12 +101,19 @@
|
|||||||
<div class="detail-row-content">
|
<div class="detail-row-content">
|
||||||
<div class="detail-field-row">
|
<div class="detail-field-row">
|
||||||
<span class="detail-field-label">参与人</span>
|
<span class="detail-field-label">参与人</span>
|
||||||
<div v-if="taskData.participantName" class="assignee-info" :class="{ 'readonly-field': isFullReadOnly }" @click="!isFullReadOnly && openDrawerUserSelect('participant')">
|
<div v-if="!isFullReadOnly" class="detail-field-clickable" @click="openDrawerUserSelect('participant')">
|
||||||
|
<template v-if="taskData.participantName">
|
||||||
|
<a-tooltip :title="taskData.participantName" placement="top">
|
||||||
|
<AvatarDisplay :names="taskData.participantName" :size="28" :show-name="true" />
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<span v-else class="field-placeholder">未指定</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="taskData.participantName" class="assignee-info readonly-field">
|
||||||
<a-tooltip :title="taskData.participantName" placement="top">
|
<a-tooltip :title="taskData.participantName" placement="top">
|
||||||
<AvatarDisplay :names="taskData.participantName" :size="28" :show-name="true" />
|
<AvatarDisplay :names="taskData.participantName" :size="28" :show-name="true" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<a-button v-else-if="!isFullReadOnly" type="text" class="add-field-btn" @click="openDrawerUserSelect('participant')">添加</a-button>
|
|
||||||
<span v-else class="detail-field-value">-</span>
|
<span v-else class="detail-field-value">-</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,7 +236,14 @@
|
|||||||
<Icon icon="tasklist-attachment|svg" />
|
<Icon icon="tasklist-attachment|svg" />
|
||||||
</span>
|
</span>
|
||||||
<div class="detail-row-content">
|
<div class="detail-row-content">
|
||||||
<span class="desc-trigger"> 添加附件 </span>
|
<span class="desc-trigger" :class="{ 'readonly-field': isFullReadOnly }" @click="!isFullReadOnly && openFileUploadModal()"> 添加附件 </span>
|
||||||
|
<TaskAttachmentUpload
|
||||||
|
ref="fileUploadRef"
|
||||||
|
:bussiness-id="taskData.id || ''"
|
||||||
|
:bussiness-secret-level="currentListSecretLevel"
|
||||||
|
:disabled="isFullReadOnly"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -323,32 +344,29 @@
|
|||||||
<span class="detail-row-icon">
|
<span class="detail-row-icon">
|
||||||
<Icon icon="tasklist-followers|svg" />
|
<Icon icon="tasklist-followers|svg" />
|
||||||
</span>
|
</span>
|
||||||
<a-tooltip v-if="followers.length > 0" :title="taskData.followersName" placement="top">
|
<div v-if="!isFullReadOnly" class="follower-clickable" @click="openDrawerUserSelect('followers')">
|
||||||
|
<template v-if="followers.length > 0">
|
||||||
|
<AvatarDisplay :names="taskData.followersName" :size="24" :show-name="false" />
|
||||||
|
</template>
|
||||||
|
<span v-else class="follower-empty">将通知 0 人</span>
|
||||||
|
</div>
|
||||||
|
<a-tooltip v-else-if="followers.length > 0" :title="taskData.followersName" placement="top">
|
||||||
<AvatarDisplay :names="taskData.followersName" :size="24" :show-name="false" />
|
<AvatarDisplay :names="taskData.followersName" :size="24" :show-name="false" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<span v-else class="follower-empty">将通知 0 人</span>
|
<span v-else class="follower-empty">将通知 0 人</span>
|
||||||
<a-button v-if="!isFullReadOnly" type="text" size="small" @click="openDrawerUserSelect('followers')">
|
|
||||||
<Icon icon="ant-design:plus-outlined" />
|
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<UserSelectModal
|
<UserSelectModal
|
||||||
ref="drawerUserSelectRef"
|
ref="userSelectModalRef"
|
||||||
:title="
|
:title="drawerUserSelectType === 'assignee' ? '选择负责人' : drawerUserSelectType === 'participant' ? '选择参与人' : '选择关注人'"
|
||||||
subTaskUserSelectId
|
|
||||||
? '选择子任务负责人'
|
|
||||||
: drawerUserSelectType === 'assignee'
|
|
||||||
? '选择负责人'
|
|
||||||
: drawerUserSelectType === 'participant'
|
|
||||||
? '选择参与人'
|
|
||||||
: '选择关注人'
|
|
||||||
"
|
|
||||||
:user-ids="drawerUserSelectCurrentIds"
|
:user-ids="drawerUserSelectCurrentIds"
|
||||||
:user-names="drawerUserSelectCurrentNames"
|
:user-names="drawerUserSelectCurrentNames"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
@confirm="onDrawerUserSelectConfirm"
|
:secret-level="currentListSecretLevel || 1"
|
||||||
|
@confirm="onUserSelectModalConfirm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</BasicDrawer>
|
</BasicDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -357,7 +375,8 @@
|
|||||||
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
||||||
import Icon from '/@/components/Icon/index';
|
import Icon from '/@/components/Icon/index';
|
||||||
import AvatarDisplay from './AvatarDisplay.vue';
|
import AvatarDisplay from './AvatarDisplay.vue';
|
||||||
import UserSelectModal from './UserSelectModal.vue';
|
import SzUserSelectBySecLevel from '/@/components/semri/userComponent/SzUserSelectBySecLevel.vue';
|
||||||
|
import TaskAttachmentUpload from './TaskAttachmentUpload.vue';
|
||||||
import TaskDateRangePicker from './TaskDateRangePicker.vue';
|
import TaskDateRangePicker from './TaskDateRangePicker.vue';
|
||||||
import draggable from 'vuedraggable';
|
import draggable from 'vuedraggable';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -412,16 +431,20 @@
|
|||||||
const editingSubTaskValue = ref<any>(null);
|
const editingSubTaskValue = ref<any>(null);
|
||||||
const editingSubTaskStartValue = ref<string | null>(null);
|
const editingSubTaskStartValue = ref<string | null>(null);
|
||||||
const editingSubTaskEndValue = ref<string | null>(null);
|
const editingSubTaskEndValue = ref<string | null>(null);
|
||||||
const subTaskDatePickerOpen = ref(false);
|
|
||||||
const subTaskUserSelectId = ref<string>('');
|
const subTaskUserSelectId = ref<string>('');
|
||||||
|
const subTaskDatePickerOpen = ref(false);
|
||||||
const drawerDatePickerVisible = ref(false);
|
const drawerDatePickerVisible = ref(false);
|
||||||
|
|
||||||
const drawerDateEditMode = ref<'start' | 'end' | null>(null);
|
const drawerDateEditMode = ref<'start' | 'end' | null>(null);
|
||||||
const drawerPickerEpoch = ref(0);
|
const drawerPickerEpoch = ref(0);
|
||||||
|
|
||||||
const drawerUserSelectRef = ref();
|
const currentListSecretLevel = ref<number>(1);
|
||||||
|
|
||||||
const drawerUserSelectType = ref<'assignee' | 'followers' | 'participant'>('assignee');
|
const drawerUserSelectType = ref<'assignee' | 'followers' | 'participant'>('assignee');
|
||||||
const drawerUserSelectCurrentIds = ref('');
|
const drawerUserSelectCurrentIds = ref<string>('');
|
||||||
const drawerUserSelectCurrentNames = ref('');
|
const drawerUserSelectCurrentNames = ref<string>('');
|
||||||
|
const userSelectModalRef = ref<InstanceType<typeof UserSelectModal> | null>(null);
|
||||||
|
const fileUploadRef = ref<InstanceType<typeof TaskAttachmentUpload> | null>(null);
|
||||||
|
|
||||||
const parentTask = computed(() => {
|
const parentTask = computed(() => {
|
||||||
if (!taskData.value || !taskData.value.pid) return null;
|
if (!taskData.value || !taskData.value.pid) return null;
|
||||||
@@ -508,6 +531,7 @@
|
|||||||
taskData.value = data?.task ? { ...data.task } : null;
|
taskData.value = data?.task ? { ...data.task } : null;
|
||||||
allTasks.value = data?.allTasks ? [...data.allTasks] : [];
|
allTasks.value = data?.allTasks ? [...data.allTasks] : [];
|
||||||
isReadOnly.value = data?.readOnly === true;
|
isReadOnly.value = data?.readOnly === true;
|
||||||
|
currentListSecretLevel.value = data?.secretLevel || 1;
|
||||||
showNewSubTaskInput.value = false;
|
showNewSubTaskInput.value = false;
|
||||||
newSubTaskName.value = '';
|
newSubTaskName.value = '';
|
||||||
editingTitle.value = false;
|
editingTitle.value = false;
|
||||||
@@ -681,24 +705,59 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openDrawerUserSelect(type: 'assignee' | 'followers' | 'participant') {
|
function openDrawerUserSelect(type: 'assignee' | 'followers' | 'participant') {
|
||||||
|
if (!taskData.value) return;
|
||||||
drawerUserSelectType.value = type;
|
drawerUserSelectType.value = type;
|
||||||
if (type === 'assignee') {
|
if (type === 'assignee') {
|
||||||
drawerUserSelectCurrentIds.value = taskData.value?.assigneeId || '';
|
drawerUserSelectCurrentIds.value = taskData.value.assigneeId || '';
|
||||||
drawerUserSelectCurrentNames.value = taskData.value?.assigneeName || '';
|
drawerUserSelectCurrentNames.value = taskData.value.assigneeName || '';
|
||||||
} else if (type === 'participant') {
|
} else if (type === 'participant') {
|
||||||
drawerUserSelectCurrentIds.value = taskData.value?.participantId || '';
|
drawerUserSelectCurrentIds.value = taskData.value.participantId || '';
|
||||||
drawerUserSelectCurrentNames.value = taskData.value?.participantName || '';
|
drawerUserSelectCurrentNames.value = taskData.value.participantName || '';
|
||||||
} else {
|
} else {
|
||||||
drawerUserSelectCurrentIds.value = taskData.value?.followersId || '';
|
drawerUserSelectCurrentIds.value = taskData.value.followersId || '';
|
||||||
drawerUserSelectCurrentNames.value = taskData.value?.followersName || '';
|
drawerUserSelectCurrentNames.value = taskData.value.followersName || '';
|
||||||
}
|
}
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
drawerUserSelectRef.value?.open();
|
userSelectModalRef.value?.open();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrawerUserSelectConfirm(userIds: string, userNames: string) {
|
function onUserSelectModalConfirm(userIds: string, userNames: string) {
|
||||||
if (subTaskUserSelectId.value) {
|
if (subTaskUserSelectId.value) {
|
||||||
|
onSubTaskUserSelectConfirm(userIds, userNames);
|
||||||
|
} else {
|
||||||
|
onDrawerUserSelectConfirm(userIds, userNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrawerUserSelectConfirm(userIds: string, userNames: string) {
|
||||||
|
if (!taskData.value) return;
|
||||||
|
const type = drawerUserSelectType.value;
|
||||||
|
if (type === 'assignee') {
|
||||||
|
taskData.value.assigneeId = userIds;
|
||||||
|
taskData.value.assigneeName = userNames;
|
||||||
|
} else if (type === 'participant') {
|
||||||
|
taskData.value.participantId = userIds;
|
||||||
|
taskData.value.participantName = userNames;
|
||||||
|
} else {
|
||||||
|
taskData.value.followersId = userIds;
|
||||||
|
taskData.value.followersName = userNames;
|
||||||
|
}
|
||||||
|
autoSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSubTaskUserSelect(child: TaskItem) {
|
||||||
|
subTaskUserSelectId.value = child.id;
|
||||||
|
drawerUserSelectCurrentIds.value = child.assigneeId || '';
|
||||||
|
drawerUserSelectCurrentNames.value = child.assigneeName || '';
|
||||||
|
drawerUserSelectType.value = 'assignee';
|
||||||
|
nextTick(() => {
|
||||||
|
userSelectModalRef.value?.open();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubTaskUserSelectConfirm(userIds: string, userNames: string) {
|
||||||
|
if (!subTaskUserSelectId.value) return;
|
||||||
emit('update-sub-task-fields', subTaskUserSelectId.value, {
|
emit('update-sub-task-fields', subTaskUserSelectId.value, {
|
||||||
assigneeId: userIds,
|
assigneeId: userIds,
|
||||||
assigneeName: userNames,
|
assigneeName: userNames,
|
||||||
@@ -709,32 +768,6 @@
|
|||||||
child.assigneeName = userNames;
|
child.assigneeName = userNames;
|
||||||
}
|
}
|
||||||
subTaskUserSelectId.value = '';
|
subTaskUserSelectId.value = '';
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!taskData.value) return;
|
|
||||||
const type = drawerUserSelectType.value;
|
|
||||||
if (type === 'assignee') {
|
|
||||||
taskData.value.assigneeId = userIds;
|
|
||||||
taskData.value.assigneeName = userNames;
|
|
||||||
autoSave();
|
|
||||||
} else if (type === 'participant') {
|
|
||||||
taskData.value.participantId = userIds;
|
|
||||||
taskData.value.participantName = userNames;
|
|
||||||
autoSave();
|
|
||||||
} else {
|
|
||||||
taskData.value.followersId = userIds;
|
|
||||||
taskData.value.followersName = userNames;
|
|
||||||
autoSave();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSubTaskUserSelect(child: TaskItem) {
|
|
||||||
subTaskUserSelectId.value = child.id;
|
|
||||||
drawerUserSelectCurrentIds.value = child.assigneeId || '';
|
|
||||||
drawerUserSelectCurrentNames.value = child.assigneeName || '';
|
|
||||||
nextTick(() => {
|
|
||||||
drawerUserSelectRef.value?.open();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSubTaskDateEditor(childId: string) {
|
function openSubTaskDateEditor(childId: string) {
|
||||||
@@ -780,6 +813,10 @@
|
|||||||
showNewSubTaskInput.value = false;
|
showNewSubTaskInput.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openFileUploadModal() {
|
||||||
|
fileUploadRef.value?.handleOpenUploadModal();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
@@ -103,18 +103,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<div v-if="showNewListInput && newListGroupId === ''" class="sidebar-new-item-row">
|
|
||||||
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
|
||||||
<a-input
|
|
||||||
v-model:value="newListName"
|
|
||||||
size="small"
|
|
||||||
placeholder="输入清单名称..."
|
|
||||||
autofocus
|
|
||||||
style="flex: 1"
|
|
||||||
@press-enter="submitNewList"
|
|
||||||
@blur="showNewListInput = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<draggable :list="localUserGroups" group="tasklistgroups" item-key="id" :animation="200" drag-class="group-drag-ghost" @start="onGroupDragStart" @end="onGroupDragEnd">
|
<draggable :list="localUserGroups" group="tasklistgroups" item-key="id" :animation="200" drag-class="group-drag-ghost" @start="onGroupDragStart" @end="onGroupDragEnd">
|
||||||
@@ -152,18 +140,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<template v-if="group.delFlag !== 1 && !collapsedGroups[group.id]">
|
<template v-if="group.delFlag !== 1 && !collapsedGroups[group.id]">
|
||||||
<div class="sidebar-group-items" :data-group-id="group.id">
|
<div class="sidebar-group-items" :data-group-id="group.id">
|
||||||
<div v-if="showNewListInput && newListGroupId === group.id" class="sidebar-new-item-row">
|
|
||||||
<Icon icon="ant-design:file-text-outlined" class="sidebar-item-icon" style="opacity: 0.4" />
|
|
||||||
<a-input
|
|
||||||
v-model:value="newListName"
|
|
||||||
size="small"
|
|
||||||
placeholder="输入清单名称..."
|
|
||||||
autofocus
|
|
||||||
style="flex: 1"
|
|
||||||
@press-enter="submitNewList"
|
|
||||||
@blur="showNewListInput = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<draggable
|
<draggable
|
||||||
:list="getLocalGroupTaskLists(group)"
|
:list="getLocalGroupTaskLists(group)"
|
||||||
group="tasklists"
|
group="tasklists"
|
||||||
@@ -211,7 +187,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<div
|
<div
|
||||||
v-if="getLocalGroupTaskLists(group).length === 0 && !(showNewListInput && newListGroupId === group.id)"
|
v-if="getLocalGroupTaskLists(group).length === 0"
|
||||||
class="sidebar-empty-hint"
|
class="sidebar-empty-hint"
|
||||||
>
|
>
|
||||||
可拖拽清单加入该分组
|
可拖拽清单加入该分组
|
||||||
@@ -263,7 +239,7 @@
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'view-select': [view: ViewType];
|
'view-select': [view: ViewType];
|
||||||
'list-select': [listId: string];
|
'list-select': [listId: string];
|
||||||
'create-list': [name: string, groupId: string];
|
'create-list-modal': [groupId: string];
|
||||||
'create-group': [name: string];
|
'create-group': [name: string];
|
||||||
'rename-list': [listId: string, newName: string];
|
'rename-list': [listId: string, newName: string];
|
||||||
'delete-list': [listId: string];
|
'delete-list': [listId: string];
|
||||||
@@ -336,9 +312,6 @@
|
|||||||
const collapsedGroups = ref<Record<string, boolean>>({});
|
const collapsedGroups = ref<Record<string, boolean>>({});
|
||||||
const showNewGroupInput = ref(false);
|
const showNewGroupInput = ref(false);
|
||||||
const newGroupName = ref('');
|
const newGroupName = ref('');
|
||||||
const showNewListInput = ref(false);
|
|
||||||
const newListGroupId = ref('');
|
|
||||||
const newListName = ref('');
|
|
||||||
const renamingId = ref('');
|
const renamingId = ref('');
|
||||||
const renamingType = ref<'list' | 'group'>('list');
|
const renamingType = ref<'list' | 'group'>('list');
|
||||||
const renameValue = ref('');
|
const renameValue = ref('');
|
||||||
@@ -439,20 +412,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startNewList(groupId: string) {
|
function startNewList(groupId: string) {
|
||||||
showNewListInput.value = true;
|
emit('create-list-modal', groupId);
|
||||||
newListGroupId.value = groupId;
|
|
||||||
newListName.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitNewList() {
|
|
||||||
const name = newListName.value.trim();
|
|
||||||
if (!name) {
|
|
||||||
showNewListInput.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit('create-list', name, newListGroupId.value);
|
|
||||||
showNewListInput.value = false;
|
|
||||||
newListName.value = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onListMenuClick({ key }: { key: string }, listId: string, currentName: string) {
|
function onListMenuClick({ key }: { key: string }, listId: string, currentName: string) {
|
||||||
|
|||||||
@@ -47,11 +47,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="add-panel-body">
|
<div class="add-panel-body">
|
||||||
<JSelectUser
|
<SzUserSelect
|
||||||
v-model:value="addForm.userIds"
|
v-model:value="addForm.userIds"
|
||||||
:label-key="'realname'"
|
:labelKey="'realname'"
|
||||||
:row-key="'id'"
|
:rowKey="'id'"
|
||||||
multiple="multiple"
|
|
||||||
placeholder="搜索并选择用户(支持多选)"
|
placeholder="搜索并选择用户(支持多选)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,10 +76,12 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, computed } from 'vue';
|
import { ref, reactive, computed } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
import Icon from '/@/components/Icon/index';
|
import Icon from '/@/components/Icon/index';
|
||||||
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
|
import SzUserSelect from '/@/components/semri/userComponent/SzUserSelect.vue';
|
||||||
import { getAvatarColor } from '../types';
|
import { getAvatarColor } from '../types';
|
||||||
import { getUserList } from '/@/api/common/api';
|
import { getUserList } from '/@/api/common/api';
|
||||||
|
import { checkUserSecLevel } from '../utils/taskUtils';
|
||||||
|
|
||||||
interface MemberItem {
|
interface MemberItem {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -93,10 +94,12 @@
|
|||||||
userIds: string;
|
userIds: string;
|
||||||
userNames: string;
|
userNames: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
secretLevel?: number;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
title: '选择人员',
|
title: '选择人员',
|
||||||
multiple: true,
|
multiple: true,
|
||||||
|
secretLevel: 1,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -170,6 +173,15 @@
|
|||||||
|
|
||||||
async function onConfirmAdd() {
|
async function onConfirmAdd() {
|
||||||
if (!addForm.userIds) return;
|
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 newIds = addForm.userIds.split(',').filter(Boolean);
|
||||||
const existingIds = new Set(memberList.value.map((m) => m.userId));
|
const existingIds = new Set(memberList.value.map((m) => m.userId));
|
||||||
if (!props.multiple) {
|
if (!props.multiple) {
|
||||||
@@ -186,14 +198,19 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('获取用户名失败,使用ID作为名称', e);
|
console.warn('获取用户名失败,使用ID作为名称', e);
|
||||||
}
|
}
|
||||||
|
let addedCount = 0;
|
||||||
for (const nid of newIds) {
|
for (const nid of newIds) {
|
||||||
if (!existingIds.has(nid) || !props.multiple) {
|
if (!existingIds.has(nid) || !props.multiple) {
|
||||||
memberList.value.push({
|
memberList.value.push({
|
||||||
userId: nid,
|
userId: nid,
|
||||||
username: nameMap[nid] || nid,
|
username: nameMap[nid] || nid,
|
||||||
});
|
});
|
||||||
|
addedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (addedCount > 0) {
|
||||||
|
message.success(`已添加 ${addedCount} 位人员`);
|
||||||
|
}
|
||||||
addForm.userIds = '';
|
addForm.userIds = '';
|
||||||
showAddPanel.value = false;
|
showAddPanel.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
:permission-map="permissionMap"
|
:permission-map="permissionMap"
|
||||||
@view-select="onViewSelect"
|
@view-select="onViewSelect"
|
||||||
@list-select="onListSelect"
|
@list-select="onListSelect"
|
||||||
@create-list="onCreateList"
|
@create-list-modal="onCreateListModal"
|
||||||
@create-group="onCreateGroup"
|
@create-group="onCreateGroup"
|
||||||
@rename-list="onRenameList"
|
@rename-list="onRenameList"
|
||||||
@delete-list="onDeleteList"
|
@delete-list="onDeleteList"
|
||||||
@@ -90,12 +90,13 @@
|
|||||||
@unfollow-task="onUnfollowTask"
|
@unfollow-task="onUnfollowTask"
|
||||||
@move-task="onMoveTask"
|
@move-task="onMoveTask"
|
||||||
/>
|
/>
|
||||||
<CollaboratorModal ref="collaboratorModalRef" :task-list-id="currentListId" :is-owner="isCurrentOwner" @changed="onCollaboratorsChanged" />
|
<CollaboratorModal ref="collaboratorModalRef" :task-list-id="currentListId" :is-owner="isCurrentOwner" :secret-level="currentList?.secretLevel || 1" :secret-text="currentList?.secretText || '非密'" @changed="onCollaboratorsChanged" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" name="tasklist-index-page" setup>
|
<script lang="ts" name="tasklist-index-page" setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
import { useModal } from '/@/components/Modal';
|
import { useModal } from '/@/components/Modal';
|
||||||
import { useDrawer } from '/@/components/Drawer';
|
import { useDrawer } from '/@/components/Drawer';
|
||||||
import { useUserStore } from '/@/store/modules/user';
|
import { useUserStore } from '/@/store/modules/user';
|
||||||
@@ -134,8 +135,10 @@
|
|||||||
myFollowedTasks,
|
myFollowedTasks,
|
||||||
moveTask,
|
moveTask,
|
||||||
moveTaskGroup,
|
moveTaskGroup,
|
||||||
|
getCurrentUserSecurityLevel,
|
||||||
} from './TaskList.api';
|
} from './TaskList.api';
|
||||||
import { DEFAULT_VISIBLE_FIELDS } from './types';
|
import { DEFAULT_VISIBLE_FIELDS } from './types';
|
||||||
|
import { isOverdue, checkUserSecLevel } from './utils/taskUtils';
|
||||||
import type {
|
import type {
|
||||||
TaskListGroup,
|
TaskListGroup,
|
||||||
StatusFilter,
|
StatusFilter,
|
||||||
@@ -162,7 +165,7 @@
|
|||||||
const currentDrawerTaskId = ref<string>('');
|
const currentDrawerTaskId = ref<string>('');
|
||||||
|
|
||||||
const [registerTaskModal, { openModal: openTaskModal }] = useModal();
|
const [registerTaskModal, { openModal: openTaskModal }] = useModal();
|
||||||
const [registerListModal, { openModal: _openListModal }] = useModal();
|
const [registerListModal, { openModal: openListModal }] = useModal();
|
||||||
const [registerDetailDrawer, { openDrawer: openDetailDrawer }] = useDrawer();
|
const [registerDetailDrawer, { openDrawer: openDetailDrawer }] = useDrawer();
|
||||||
|
|
||||||
const collaboratorModalRef = ref<InstanceType<typeof CollaboratorModal> | null>(null);
|
const collaboratorModalRef = ref<InstanceType<typeof CollaboratorModal> | null>(null);
|
||||||
@@ -170,6 +173,8 @@
|
|||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const currentUserSecurityLevel = ref<number>(3);
|
||||||
|
|
||||||
const flatTasks = ref<TaskItem[]>([]);
|
const flatTasks = ref<TaskItem[]>([]);
|
||||||
const flatViewTitle = ref('');
|
const flatViewTitle = ref('');
|
||||||
const flatViewEditable = ref(false);
|
const flatViewEditable = ref(false);
|
||||||
@@ -227,6 +232,8 @@
|
|||||||
sortOrder: fav.sortOrder || 0,
|
sortOrder: fav.sortOrder || 0,
|
||||||
delFlag: 0,
|
delFlag: 0,
|
||||||
groups: [],
|
groups: [],
|
||||||
|
secretLevel: fav.secretLevel,
|
||||||
|
secretText: fav.secretText,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fav.pid) {
|
if (fav.pid) {
|
||||||
@@ -261,7 +268,14 @@
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const level = await getCurrentUserSecurityLevel();
|
||||||
|
currentUserSecurityLevel.value = level || 3;
|
||||||
|
console.log('[TaskList] 当前用户密级:', currentUserSecurityLevel.value);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[TaskList] 获取用户密级失败', e);
|
||||||
|
}
|
||||||
loadFavorites();
|
loadFavorites();
|
||||||
onViewSelect('my-tasks');
|
onViewSelect('my-tasks');
|
||||||
});
|
});
|
||||||
@@ -565,6 +579,8 @@
|
|||||||
ownerName: item.ownerName || '',
|
ownerName: item.ownerName || '',
|
||||||
collaboratorNames: item.collaboratorNames || '',
|
collaboratorNames: item.collaboratorNames || '',
|
||||||
createTimeStr: item.createTimeStr || '',
|
createTimeStr: item.createTimeStr || '',
|
||||||
|
secretLevel: (item as any).secretLevel,
|
||||||
|
secretText: (item as any).secretText,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
@@ -587,32 +603,22 @@
|
|||||||
console.error('[TaskList] 创建任务失败', e);
|
console.error('[TaskList] 创建任务失败', e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
openTaskModal(true, { groupId, mainId: currentListId.value });
|
openTaskModal(true, { groupId, mainId: currentListId.value, secretLevel: currentList.value?.secretLevel || 1, secretText: currentList.value?.secretText || '非密' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onCreateList(name: string, groupId: string) {
|
function onCreateListModal(groupId: string) {
|
||||||
if (!name || !name.trim()) return;
|
openListModal(true, {
|
||||||
try {
|
groupId,
|
||||||
const listId = await addTaskList({
|
userSecurityLevel: currentUserSecurityLevel.value,
|
||||||
tasklistName: name.trim(),
|
|
||||||
pid: groupId || undefined,
|
|
||||||
sortOrder: 1,
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onListCreated(listId?: string) {
|
||||||
if (listId) {
|
if (listId) {
|
||||||
await addToFavorites({ taskListId: listId, pid: groupId || undefined });
|
await addToFavorites({ taskListId: listId });
|
||||||
}
|
}
|
||||||
await loadFavorites();
|
await loadFavorites();
|
||||||
} catch (e) {
|
|
||||||
console.error('[TaskList] 创建清单失败', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onListCreated(listId?: string) {
|
|
||||||
if (listId) {
|
|
||||||
addToFavorites({ taskListId: listId });
|
|
||||||
}
|
|
||||||
loadFavorites();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTaskCreated() {
|
async function onTaskCreated() {
|
||||||
@@ -775,7 +781,7 @@
|
|||||||
const task = allTasks.find((t) => t.id === taskId);
|
const task = allTasks.find((t) => t.id === taskId);
|
||||||
if (task) {
|
if (task) {
|
||||||
const readOnly = isFlatView.value ? !canEditFlatTask(task) : currentList.value?.myPermission === '3';
|
const readOnly = isFlatView.value ? !canEditFlatTask(task) : currentList.value?.myPermission === '3';
|
||||||
openDetailDrawer(true, { task, allTasks, readOnly });
|
openDetailDrawer(true, { task, allTasks, readOnly, secretLevel: currentList.value?.secretLevel || 1 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,11 +840,20 @@
|
|||||||
const task = allTasks.find((t) => t.id === currentDrawerTaskId.value);
|
const task = allTasks.find((t) => t.id === currentDrawerTaskId.value);
|
||||||
if (task) {
|
if (task) {
|
||||||
const readOnly = isFlatView.value ? !canEditFlatTask(task) : currentList.value?.myPermission === '3';
|
const readOnly = isFlatView.value ? !canEditFlatTask(task) : currentList.value?.myPermission === '3';
|
||||||
openDetailDrawer(true, { task, allTasks, readOnly });
|
openDetailDrawer(true, { task, allTasks, readOnly, secretLevel: currentList.value?.secretLevel || 1 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onUpdateTaskField(taskId: string, field: string, value: any) {
|
async function onUpdateTaskField(taskId: string, field: string, value: any) {
|
||||||
|
const userFields = ['assigneeId', 'participantId', 'followersId'];
|
||||||
|
if (userFields.includes(field) && value) {
|
||||||
|
const secretLevel = currentList.value?.secretLevel || 1;
|
||||||
|
const { ok, invalidNames } = await checkUserSecLevel(value, secretLevel);
|
||||||
|
if (!ok) {
|
||||||
|
message.warning(`以下人员的密级不满足当前清单(${currentList.value?.secretText || '非密'})要求:${invalidNames.join('、')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await editTask({ id: taskId, [field]: value });
|
await editTask({ id: taskId, [field]: value });
|
||||||
if (isFlatView.value) {
|
if (isFlatView.value) {
|
||||||
@@ -855,6 +870,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onUpdateTaskFields(taskId: string, fields: Record<string, any>) {
|
async function onUpdateTaskFields(taskId: string, fields: Record<string, any>) {
|
||||||
|
const userFields = ['assigneeId', 'participantId', 'followersId'];
|
||||||
|
for (const f of userFields) {
|
||||||
|
if (fields[f]) {
|
||||||
|
const secretLevel = currentList.value?.secretLevel || 1;
|
||||||
|
const { ok, invalidNames } = await checkUserSecLevel(fields[f], secretLevel);
|
||||||
|
if (!ok) {
|
||||||
|
message.warning(`以下人员的密级不满足当前清单(${currentList.value?.secretText || '非密'})要求:${invalidNames.join('、')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await editTask({ id: taskId, ...fields });
|
await editTask({ id: taskId, ...fields });
|
||||||
if (isFlatView.value) {
|
if (isFlatView.value) {
|
||||||
@@ -871,6 +897,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onSaveTask(updatedTask: TaskItem) {
|
async function onSaveTask(updatedTask: TaskItem) {
|
||||||
|
const secretLevel = currentList.value?.secretLevel || 1;
|
||||||
|
const secretText = currentList.value?.secretText || '非密';
|
||||||
|
const userIdFields = [
|
||||||
|
{ value: updatedTask.assigneeId, label: '负责人' },
|
||||||
|
{ value: updatedTask.participantId, label: '参与人' },
|
||||||
|
{ value: updatedTask.followersId, label: '关注人' },
|
||||||
|
];
|
||||||
|
for (const { value } of userIdFields) {
|
||||||
|
if (value) {
|
||||||
|
const { ok, invalidNames } = await checkUserSecLevel(value, secretLevel);
|
||||||
|
if (!ok) {
|
||||||
|
message.warning(`以下人员的密级不满足当前清单(${secretText})要求:${invalidNames.join('、')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await editTask({
|
await editTask({
|
||||||
id: updatedTask.id,
|
id: updatedTask.id,
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ export interface TaskList {
|
|||||||
collaboratorNames?: string;
|
collaboratorNames?: string;
|
||||||
createTimeStr?: string;
|
createTimeStr?: string;
|
||||||
myPermission?: string;
|
myPermission?: string;
|
||||||
|
secretLevel?: number;
|
||||||
|
secretText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskListGroup {
|
export interface TaskListGroup {
|
||||||
@@ -246,3 +248,28 @@ export interface FlatTaskItem {
|
|||||||
task: TaskItem;
|
task: TaskItem;
|
||||||
level: number;
|
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 : '非密';
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,3 +27,34 @@ export function isOverdue(task: { endTime?: string; completed?: boolean }): bool
|
|||||||
end.setHours(0, 0, 0, 0);
|
end.setHours(0, 0, 0, 0);
|
||||||
return now > end;
|
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: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user