czh-20260501-增加密级相关功能
This commit is contained in:
@@ -45,6 +45,7 @@ enum Api {
|
||||
listAllByMainId = '/tasklist/taskListDetial/listAllByMainId',
|
||||
myResponsibleTasks = '/tasklist/taskList/myResponsibleTasks',
|
||||
myFollowedTasks = '/tasklist/taskList/myFollowedTasks',
|
||||
getCurrentUserSecurityLevel = '/tasklist/taskList/getCurrentUserSecurityLevel',
|
||||
}
|
||||
|
||||
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 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 { JVxeTypes, JVxeColumn } from '/@/components/jeecg/JVxeTable/types';
|
||||
import type { TaskListGroup } from './types';
|
||||
import { getAvailableSecretLevels } from './types';
|
||||
|
||||
export const columns: BasicColumn[] = [
|
||||
{
|
||||
@@ -300,14 +301,32 @@ export const createTaskFormSchema: FormSchema[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const createListFormSchema: FormSchema[] = [
|
||||
{
|
||||
label: '清单名称',
|
||||
field: 'tasklistName',
|
||||
component: 'Input',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
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[] = [
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="collab-modal">
|
||||
<div class="collab-body">
|
||||
<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) }">
|
||||
{{ (item.username || '?').charAt(0) }}
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@
|
||||
<template v-if="item.permission === '1'">
|
||||
<span class="role-label">所有者</span>
|
||||
</template>
|
||||
<template v-else-if="isOwner">
|
||||
<template v-else-if="isOwner && item.permissionId">
|
||||
<a-dropdown :trigger="['click']" :overlayStyle="{ minWidth: '120px' }">
|
||||
<span class="role-label clickable" @click.stop>
|
||||
{{ item.permission === '2' ? '可编辑' : '可阅读' }}
|
||||
@@ -54,7 +54,7 @@
|
||||
</template>
|
||||
</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" />
|
||||
<span>暂无协作人</span>
|
||||
</div>
|
||||
@@ -68,10 +68,10 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="add-panel-body">
|
||||
<JSelectUser
|
||||
<SzUserSelect
|
||||
v-model:value="addForm.userIds"
|
||||
:label-key="'realname'"
|
||||
:row-key="'id'"
|
||||
:labelKey="'realname'"
|
||||
:rowKey="'id'"
|
||||
placeholder="搜索并选择用户(支持多选)"
|
||||
/>
|
||||
<div class="add-panel-row">
|
||||
@@ -84,7 +84,7 @@
|
||||
</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="onBatchAdd">
|
||||
<a-button size="small" type="primary" :disabled="!addForm.userIds" @click="onConfirmAdd">
|
||||
确认添加
|
||||
</a-button>
|
||||
</div>
|
||||
@@ -96,18 +96,20 @@
|
||||
<Icon icon="ant-design:user-add-outlined" style="margin-right: 4px" />
|
||||
添加协作人
|
||||
</a-button>
|
||||
<a-button type="primary" size="small" @click="onClose">完成</a-button>
|
||||
<a-button type="primary" size="small" @click="onDone">完成</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<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 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 { getAvatarColor } from '../types';
|
||||
import { checkUserSecLevel } from '../utils/taskUtils';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
@@ -119,9 +121,17 @@
|
||||
permission: string;
|
||||
}
|
||||
|
||||
interface PendingAddItem {
|
||||
userId: string;
|
||||
username: string;
|
||||
permission: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
taskListId: string;
|
||||
isOwner: boolean;
|
||||
secretLevel?: number;
|
||||
secretText?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -136,6 +146,17 @@
|
||||
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 || '');
|
||||
@@ -144,6 +165,9 @@
|
||||
async function open() {
|
||||
visible.value = true;
|
||||
showAddPanel.value = false;
|
||||
pendingAddList.value = [];
|
||||
addForm.userIds = '';
|
||||
addForm.permission = '2';
|
||||
await loadCollaborators();
|
||||
}
|
||||
|
||||
@@ -152,6 +176,7 @@
|
||||
showAddPanel.value = false;
|
||||
addForm.userIds = '';
|
||||
addForm.permission = '2';
|
||||
pendingAddList.value = [];
|
||||
}
|
||||
|
||||
async function loadCollaborators() {
|
||||
@@ -163,28 +188,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function onBatchAdd() {
|
||||
async function onConfirmAdd() {
|
||||
if (!addForm.userIds) return;
|
||||
const ids = addForm.userIds.split(',').filter(Boolean);
|
||||
if (ids.length === 0) return;
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
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) {
|
||||
try {
|
||||
await addCollaborator({
|
||||
taskListId: props.taskListId,
|
||||
if (!existingIds.has(uid) && !pendingIds.has(uid)) {
|
||||
pendingAddList.value.push({
|
||||
userId: uid,
|
||||
username: nameMap[uid] || uid,
|
||||
permission: addForm.permission,
|
||||
});
|
||||
successCount++;
|
||||
} catch {
|
||||
failCount++;
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
createMessage.success(`成功添加 ${successCount} 位协作人${failCount > 0 ? `,${failCount} 位已存在` : ''}`);
|
||||
if (addedCount > 0) {
|
||||
createMessage.success(`已添加 ${addedCount} 位协作人`);
|
||||
} else {
|
||||
createMessage.warning('所选用户均已是协作人');
|
||||
}
|
||||
@@ -192,8 +237,39 @@
|
||||
addForm.userIds = '';
|
||||
addForm.permission = '2';
|
||||
showAddPanel.value = false;
|
||||
await loadCollaborators();
|
||||
emit('changed');
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -7,35 +7,41 @@
|
||||
<script lang="ts" setup>
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { BasicForm, useForm } from '/@/components/Form/index';
|
||||
import { createListFormSchema } from '../TaskList.data';
|
||||
import { getCreateListFormSchema } from '../TaskList.data';
|
||||
import { addTaskList } from '../TaskList.api';
|
||||
import { getSecretText } from '../types';
|
||||
|
||||
const emit = defineEmits(['register', 'success']);
|
||||
|
||||
const [registerForm, { resetFields, validate }] = useForm({
|
||||
let pendingGroupId = '';
|
||||
let pendingUserSecurityLevel = 3;
|
||||
|
||||
const [registerForm, { resetFields, validate, updateSchema }] = useForm({
|
||||
labelWidth: 100,
|
||||
schemas: createListFormSchema,
|
||||
schemas: getCreateListFormSchema(3),
|
||||
showActionButtonGroup: false,
|
||||
baseColProps: { span: 24 },
|
||||
});
|
||||
|
||||
let pendingGroupId = '';
|
||||
|
||||
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 listId = await addTaskList({
|
||||
tasklistName: values.tasklistName || values.name,
|
||||
const result = await addTaskList({
|
||||
tasklistName: values.tasklistName,
|
||||
pid: pendingGroupId || undefined,
|
||||
secretLevel: values.secretLevel,
|
||||
secretText: getSecretText(values.secretLevel),
|
||||
});
|
||||
closeModal();
|
||||
emit('success', listId);
|
||||
emit('success', result);
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
|
||||
@@ -1,19 +1,36 @@
|
||||
<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,
|
||||
@@ -26,6 +43,8 @@
|
||||
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 });
|
||||
}
|
||||
@@ -35,7 +54,16 @@
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
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,
|
||||
taskName: values.taskName,
|
||||
taskDesc: values.taskDesc || '',
|
||||
@@ -46,10 +74,42 @@
|
||||
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>
|
||||
@@ -36,6 +36,9 @@
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<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')">
|
||||
<AvatarDisplay
|
||||
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-owner">所有者</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>
|
||||
<div v-for="list in allTaskLists" :key="list.id" class="list-group-row" @click="emit('list-select', list.id)">
|
||||
@@ -273,6 +277,16 @@
|
||||
</template>
|
||||
<span v-else class="field-placeholder">-</span>
|
||||
</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">
|
||||
<span class="field-value">{{ getFormattedCreateTime(list) }}</span>
|
||||
</div>
|
||||
@@ -790,6 +804,7 @@
|
||||
:user-ids="userSelectModalCurrentIds"
|
||||
:user-names="userSelectModalCurrentNames"
|
||||
:multiple="true"
|
||||
:secret-level="currentList?.secretLevel || 1"
|
||||
@confirm="onUserSelectConfirm"
|
||||
/>
|
||||
</div>
|
||||
@@ -804,7 +819,7 @@
|
||||
import UserSelectModal from './UserSelectModal.vue';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
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 type {
|
||||
TaskList,
|
||||
@@ -1851,6 +1866,15 @@
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2150,6 +2174,9 @@
|
||||
.content-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.task-group-header {
|
||||
@@ -2651,6 +2678,21 @@
|
||||
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 {
|
||||
width: 160px;
|
||||
padding: 0 12px;
|
||||
|
||||
@@ -76,12 +76,19 @@
|
||||
<div class="detail-row-content">
|
||||
<div class="detail-field-row">
|
||||
<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">
|
||||
<AvatarDisplay :names="taskData.assigneeName" :size="28" :show-name="true" />
|
||||
</a-tooltip>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,12 +101,19 @@
|
||||
<div class="detail-row-content">
|
||||
<div class="detail-field-row">
|
||||
<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">
|
||||
<AvatarDisplay :names="taskData.participantName" :size="28" :show-name="true" />
|
||||
</a-tooltip>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,7 +236,14 @@
|
||||
<Icon icon="tasklist-attachment|svg" />
|
||||
</span>
|
||||
<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>
|
||||
|
||||
@@ -323,32 +344,29 @@
|
||||
<span class="detail-row-icon">
|
||||
<Icon icon="tasklist-followers|svg" />
|
||||
</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" />
|
||||
</a-tooltip>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<UserSelectModal
|
||||
ref="drawerUserSelectRef"
|
||||
:title="
|
||||
subTaskUserSelectId
|
||||
? '选择子任务负责人'
|
||||
: drawerUserSelectType === 'assignee'
|
||||
? '选择负责人'
|
||||
: drawerUserSelectType === 'participant'
|
||||
? '选择参与人'
|
||||
: '选择关注人'
|
||||
"
|
||||
ref="userSelectModalRef"
|
||||
:title="drawerUserSelectType === 'assignee' ? '选择负责人' : drawerUserSelectType === 'participant' ? '选择参与人' : '选择关注人'"
|
||||
:user-ids="drawerUserSelectCurrentIds"
|
||||
:user-names="drawerUserSelectCurrentNames"
|
||||
:multiple="true"
|
||||
@confirm="onDrawerUserSelectConfirm"
|
||||
:secret-level="currentListSecretLevel || 1"
|
||||
@confirm="onUserSelectModalConfirm"
|
||||
/>
|
||||
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
|
||||
@@ -357,7 +375,8 @@
|
||||
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
||||
import Icon from '/@/components/Icon/index';
|
||||
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 draggable from 'vuedraggable';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -412,16 +431,20 @@
|
||||
const editingSubTaskValue = ref<any>(null);
|
||||
const editingSubTaskStartValue = ref<string | null>(null);
|
||||
const editingSubTaskEndValue = ref<string | null>(null);
|
||||
const subTaskDatePickerOpen = ref(false);
|
||||
const subTaskUserSelectId = ref<string>('');
|
||||
const subTaskDatePickerOpen = ref(false);
|
||||
const drawerDatePickerVisible = ref(false);
|
||||
|
||||
const drawerDateEditMode = ref<'start' | 'end' | null>(null);
|
||||
const drawerPickerEpoch = ref(0);
|
||||
|
||||
const drawerUserSelectRef = ref();
|
||||
const currentListSecretLevel = ref<number>(1);
|
||||
|
||||
const drawerUserSelectType = ref<'assignee' | 'followers' | 'participant'>('assignee');
|
||||
const drawerUserSelectCurrentIds = ref('');
|
||||
const drawerUserSelectCurrentNames = ref('');
|
||||
const drawerUserSelectCurrentIds = ref<string>('');
|
||||
const drawerUserSelectCurrentNames = ref<string>('');
|
||||
const userSelectModalRef = ref<InstanceType<typeof UserSelectModal> | null>(null);
|
||||
const fileUploadRef = ref<InstanceType<typeof TaskAttachmentUpload> | null>(null);
|
||||
|
||||
const parentTask = computed(() => {
|
||||
if (!taskData.value || !taskData.value.pid) return null;
|
||||
@@ -508,6 +531,7 @@
|
||||
taskData.value = data?.task ? { ...data.task } : null;
|
||||
allTasks.value = data?.allTasks ? [...data.allTasks] : [];
|
||||
isReadOnly.value = data?.readOnly === true;
|
||||
currentListSecretLevel.value = data?.secretLevel || 1;
|
||||
showNewSubTaskInput.value = false;
|
||||
newSubTaskName.value = '';
|
||||
editingTitle.value = false;
|
||||
@@ -681,62 +705,71 @@
|
||||
}
|
||||
|
||||
function openDrawerUserSelect(type: 'assignee' | 'followers' | 'participant') {
|
||||
if (!taskData.value) return;
|
||||
drawerUserSelectType.value = type;
|
||||
if (type === 'assignee') {
|
||||
drawerUserSelectCurrentIds.value = taskData.value?.assigneeId || '';
|
||||
drawerUserSelectCurrentNames.value = taskData.value?.assigneeName || '';
|
||||
drawerUserSelectCurrentIds.value = taskData.value.assigneeId || '';
|
||||
drawerUserSelectCurrentNames.value = taskData.value.assigneeName || '';
|
||||
} else if (type === 'participant') {
|
||||
drawerUserSelectCurrentIds.value = taskData.value?.participantId || '';
|
||||
drawerUserSelectCurrentNames.value = taskData.value?.participantName || '';
|
||||
drawerUserSelectCurrentIds.value = taskData.value.participantId || '';
|
||||
drawerUserSelectCurrentNames.value = taskData.value.participantName || '';
|
||||
} else {
|
||||
drawerUserSelectCurrentIds.value = taskData.value?.followersId || '';
|
||||
drawerUserSelectCurrentNames.value = taskData.value?.followersName || '';
|
||||
drawerUserSelectCurrentIds.value = taskData.value.followersId || '';
|
||||
drawerUserSelectCurrentNames.value = taskData.value.followersName || '';
|
||||
}
|
||||
nextTick(() => {
|
||||
drawerUserSelectRef.value?.open();
|
||||
userSelectModalRef.value?.open();
|
||||
});
|
||||
}
|
||||
|
||||
function onDrawerUserSelectConfirm(userIds: string, userNames: string) {
|
||||
function onUserSelectModalConfirm(userIds: string, userNames: string) {
|
||||
if (subTaskUserSelectId.value) {
|
||||
emit('update-sub-task-fields', subTaskUserSelectId.value, {
|
||||
assigneeId: userIds,
|
||||
assigneeName: userNames,
|
||||
});
|
||||
const child = allTasks.value.find((t) => t.id === subTaskUserSelectId.value);
|
||||
if (child) {
|
||||
child.assigneeId = userIds;
|
||||
child.assigneeName = userNames;
|
||||
}
|
||||
subTaskUserSelectId.value = '';
|
||||
return;
|
||||
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;
|
||||
autoSave();
|
||||
} else if (type === 'participant') {
|
||||
taskData.value.participantId = userIds;
|
||||
taskData.value.participantName = userNames;
|
||||
autoSave();
|
||||
} else {
|
||||
taskData.value.followersId = userIds;
|
||||
taskData.value.followersName = userNames;
|
||||
autoSave();
|
||||
}
|
||||
autoSave();
|
||||
}
|
||||
|
||||
function openSubTaskUserSelect(child: TaskItem) {
|
||||
subTaskUserSelectId.value = child.id;
|
||||
drawerUserSelectCurrentIds.value = child.assigneeId || '';
|
||||
drawerUserSelectCurrentNames.value = child.assigneeName || '';
|
||||
drawerUserSelectType.value = 'assignee';
|
||||
nextTick(() => {
|
||||
drawerUserSelectRef.value?.open();
|
||||
userSelectModalRef.value?.open();
|
||||
});
|
||||
}
|
||||
|
||||
function onSubTaskUserSelectConfirm(userIds: string, userNames: string) {
|
||||
if (!subTaskUserSelectId.value) return;
|
||||
emit('update-sub-task-fields', subTaskUserSelectId.value, {
|
||||
assigneeId: userIds,
|
||||
assigneeName: userNames,
|
||||
});
|
||||
const child = allTasks.value.find((t) => t.id === subTaskUserSelectId.value);
|
||||
if (child) {
|
||||
child.assigneeId = userIds;
|
||||
child.assigneeName = userNames;
|
||||
}
|
||||
subTaskUserSelectId.value = '';
|
||||
}
|
||||
|
||||
function openSubTaskDateEditor(childId: string) {
|
||||
editingSubTask.value = childId;
|
||||
editingSubTaskField.value = 'dateRange';
|
||||
@@ -780,6 +813,10 @@
|
||||
showNewSubTaskInput.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openFileUploadModal() {
|
||||
fileUploadRef.value?.handleOpenUploadModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -103,18 +103,6 @@
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<draggable :list="localUserGroups" group="tasklistgroups" item-key="id" :animation="200" drag-class="group-drag-ghost" @start="onGroupDragStart" @end="onGroupDragEnd">
|
||||
@@ -152,18 +140,6 @@
|
||||
</div>
|
||||
<template v-if="group.delFlag !== 1 && !collapsedGroups[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
|
||||
:list="getLocalGroupTaskLists(group)"
|
||||
group="tasklists"
|
||||
@@ -211,7 +187,7 @@
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
v-if="getLocalGroupTaskLists(group).length === 0 && !(showNewListInput && newListGroupId === group.id)"
|
||||
v-if="getLocalGroupTaskLists(group).length === 0"
|
||||
class="sidebar-empty-hint"
|
||||
>
|
||||
可拖拽清单加入该分组
|
||||
@@ -263,7 +239,7 @@
|
||||
const emit = defineEmits<{
|
||||
'view-select': [view: ViewType];
|
||||
'list-select': [listId: string];
|
||||
'create-list': [name: string, groupId: string];
|
||||
'create-list-modal': [groupId: string];
|
||||
'create-group': [name: string];
|
||||
'rename-list': [listId: string, newName: string];
|
||||
'delete-list': [listId: string];
|
||||
@@ -336,9 +312,6 @@
|
||||
const collapsedGroups = ref<Record<string, boolean>>({});
|
||||
const showNewGroupInput = ref(false);
|
||||
const newGroupName = ref('');
|
||||
const showNewListInput = ref(false);
|
||||
const newListGroupId = ref('');
|
||||
const newListName = ref('');
|
||||
const renamingId = ref('');
|
||||
const renamingType = ref<'list' | 'group'>('list');
|
||||
const renameValue = ref('');
|
||||
@@ -439,20 +412,7 @@
|
||||
}
|
||||
|
||||
function startNewList(groupId: string) {
|
||||
showNewListInput.value = true;
|
||||
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 = '';
|
||||
emit('create-list-modal', groupId);
|
||||
}
|
||||
|
||||
function onListMenuClick({ key }: { key: string }, listId: string, currentName: string) {
|
||||
|
||||
@@ -47,11 +47,10 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="add-panel-body">
|
||||
<JSelectUser
|
||||
<SzUserSelect
|
||||
v-model:value="addForm.userIds"
|
||||
:label-key="'realname'"
|
||||
:row-key="'id'"
|
||||
multiple="multiple"
|
||||
:labelKey="'realname'"
|
||||
:rowKey="'id'"
|
||||
placeholder="搜索并选择用户(支持多选)"
|
||||
/>
|
||||
</div>
|
||||
@@ -77,10 +76,12 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
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 { getUserList } from '/@/api/common/api';
|
||||
import { checkUserSecLevel } from '../utils/taskUtils';
|
||||
|
||||
interface MemberItem {
|
||||
userId: string;
|
||||
@@ -93,10 +94,12 @@
|
||||
userIds: string;
|
||||
userNames: string;
|
||||
multiple?: boolean;
|
||||
secretLevel?: number;
|
||||
}>(),
|
||||
{
|
||||
title: '选择人员',
|
||||
multiple: true,
|
||||
secretLevel: 1,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -170,6 +173,15 @@
|
||||
|
||||
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) {
|
||||
@@ -186,14 +198,19 @@
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
:permission-map="permissionMap"
|
||||
@view-select="onViewSelect"
|
||||
@list-select="onListSelect"
|
||||
@create-list="onCreateList"
|
||||
@create-list-modal="onCreateListModal"
|
||||
@create-group="onCreateGroup"
|
||||
@rename-list="onRenameList"
|
||||
@delete-list="onDeleteList"
|
||||
@@ -90,12 +90,13 @@
|
||||
@unfollow-task="onUnfollowTask"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="tasklist-index-page" setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useDrawer } from '/@/components/Drawer';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
@@ -134,8 +135,10 @@
|
||||
myFollowedTasks,
|
||||
moveTask,
|
||||
moveTaskGroup,
|
||||
getCurrentUserSecurityLevel,
|
||||
} from './TaskList.api';
|
||||
import { DEFAULT_VISIBLE_FIELDS } from './types';
|
||||
import { isOverdue, checkUserSecLevel } from './utils/taskUtils';
|
||||
import type {
|
||||
TaskListGroup,
|
||||
StatusFilter,
|
||||
@@ -162,7 +165,7 @@
|
||||
const currentDrawerTaskId = ref<string>('');
|
||||
|
||||
const [registerTaskModal, { openModal: openTaskModal }] = useModal();
|
||||
const [registerListModal, { openModal: _openListModal }] = useModal();
|
||||
const [registerListModal, { openModal: openListModal }] = useModal();
|
||||
const [registerDetailDrawer, { openDrawer: openDetailDrawer }] = useDrawer();
|
||||
|
||||
const collaboratorModalRef = ref<InstanceType<typeof CollaboratorModal> | null>(null);
|
||||
@@ -170,6 +173,8 @@
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const currentUserSecurityLevel = ref<number>(3);
|
||||
|
||||
const flatTasks = ref<TaskItem[]>([]);
|
||||
const flatViewTitle = ref('');
|
||||
const flatViewEditable = ref(false);
|
||||
@@ -227,6 +232,8 @@
|
||||
sortOrder: fav.sortOrder || 0,
|
||||
delFlag: 0,
|
||||
groups: [],
|
||||
secretLevel: fav.secretLevel,
|
||||
secretText: fav.secretText,
|
||||
};
|
||||
|
||||
if (fav.pid) {
|
||||
@@ -261,7 +268,14 @@
|
||||
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();
|
||||
onViewSelect('my-tasks');
|
||||
});
|
||||
@@ -565,6 +579,8 @@
|
||||
ownerName: item.ownerName || '',
|
||||
collaboratorNames: item.collaboratorNames || '',
|
||||
createTimeStr: item.createTimeStr || '',
|
||||
secretLevel: (item as any).secretLevel,
|
||||
secretText: (item as any).secretText,
|
||||
},
|
||||
],
|
||||
}));
|
||||
@@ -587,32 +603,22 @@
|
||||
console.error('[TaskList] 创建任务失败', e);
|
||||
}
|
||||
} 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) {
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
const listId = await addTaskList({
|
||||
tasklistName: name.trim(),
|
||||
pid: groupId || undefined,
|
||||
sortOrder: 1,
|
||||
});
|
||||
if (listId) {
|
||||
await addToFavorites({ taskListId: listId, pid: groupId || undefined });
|
||||
}
|
||||
await loadFavorites();
|
||||
} catch (e) {
|
||||
console.error('[TaskList] 创建清单失败', e);
|
||||
}
|
||||
function onCreateListModal(groupId: string) {
|
||||
openListModal(true, {
|
||||
groupId,
|
||||
userSecurityLevel: currentUserSecurityLevel.value,
|
||||
});
|
||||
}
|
||||
|
||||
function onListCreated(listId?: string) {
|
||||
async function onListCreated(listId?: string) {
|
||||
if (listId) {
|
||||
addToFavorites({ taskListId: listId });
|
||||
await addToFavorites({ taskListId: listId });
|
||||
}
|
||||
loadFavorites();
|
||||
await loadFavorites();
|
||||
}
|
||||
|
||||
async function onTaskCreated() {
|
||||
@@ -775,7 +781,7 @@
|
||||
const task = allTasks.find((t) => t.id === taskId);
|
||||
if (task) {
|
||||
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);
|
||||
if (task) {
|
||||
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) {
|
||||
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 {
|
||||
await editTask({ id: taskId, [field]: value });
|
||||
if (isFlatView.value) {
|
||||
@@ -855,6 +870,17 @@
|
||||
}
|
||||
|
||||
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 {
|
||||
await editTask({ id: taskId, ...fields });
|
||||
if (isFlatView.value) {
|
||||
@@ -871,6 +897,22 @@
|
||||
}
|
||||
|
||||
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 {
|
||||
await editTask({
|
||||
id: updatedTask.id,
|
||||
|
||||
@@ -96,6 +96,8 @@ export interface TaskList {
|
||||
collaboratorNames?: string;
|
||||
createTimeStr?: string;
|
||||
myPermission?: string;
|
||||
secretLevel?: number;
|
||||
secretText?: string;
|
||||
}
|
||||
|
||||
export interface TaskListGroup {
|
||||
@@ -246,3 +248,28 @@ 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 : '非密';
|
||||
}
|
||||
|
||||
@@ -27,3 +27,34 @@ export function isOverdue(task: { endTime?: string; completed?: boolean }): bool
|
||||
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: [] };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user