From 8534025c45b4736975730678b9ccf23570a75f95 Mon Sep 17 00:00:00 2001
From: liuyu <yu.liu@cqgbkj.com>
Date: 星期二, 22 九月 2026 09:25:48 +0800
Subject: [PATCH] feat(tms): 新增试卷、培训任务、个人任务页面
---
apps/jnpf-web-apps-main/src/views/x/tms/task/Form.vue | 959 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 959 insertions(+), 0 deletions(-)
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/task/Form.vue b/apps/jnpf-web-apps-main/src/views/x/tms/task/Form.vue
new file mode 100644
index 0000000..863885a
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/task/Form.vue
@@ -0,0 +1,959 @@
+<script lang="ts" setup>
+/**
+ * 鍩硅浠诲姟琛ㄥ崟锛堝榻愯鏄庝功 6.2.18锛�
+ * - 绌虹櫧鏂板 = 涓存椂鍩硅锛氬唴瀹瑰瓧娈靛彲濉�
+ * - 璁″垝甯﹀嚭浠诲姟锛堝勾璁″垝/宀楄鍒掔瓑锛夛細缂栧彿/鏉ユ簮鍙锛屽叾浣欏唴瀹逛笌鎵ц淇℃伅鍧囧彲缁存姢鍚庡彂甯�
+ */
+import type { TaskEntity, TaskFile } from './types';
+
+import { computed, h, onMounted, reactive, ref, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { useMessage } from '@jnpf/hooks';
+import { BasicForm, useForm } from '@jnpf/ui/form';
+
+import { Alert as AAlert, Steps as ASteps } from 'ant-design-vue';
+import dayjs from 'dayjs';
+
+import { getCoursePaperOptions } from '#/api/x/tms/course';
+import { getPlaceOptions } from '#/api/x/tms/place';
+import { createTask, getTaskInfo, publishTask, updateTask } from '#/api/x/tms/task';
+import { JnpfPopupSelect } from '#/components/Jnpf/popupSelect';
+import { TMS_DIC_FIELD_NAMES } from '#/views/x/tms/shared/dic';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
+
+import { EVAL_MODE_OPTIONS, loadTaskDics, SOURCE_TYPE_OPTIONS, TASK_CATEGORY_OPTIONS, TASK_KIND_OPTIONS, TRAIN_MODE_OPTIONS } from './constants';
+
+defineOptions({ name: 'TmsTaskForm' });
+
+const DT_FMT = 'YYYY-MM-DD HH:mm:ss';
+
+/** 鏃ユ湡缁勪欢鏍¢獙绫诲瀷鏄� number锛屽瓧绗︿覆浼氳褰撴垚绌哄�� */
+function toTimeNumber(value?: null | number | string) {
+ if (value == null || value === '') return undefined;
+ if (typeof value === 'number' && !Number.isNaN(value)) return value;
+ const text = String(value).trim();
+ if (/^\d{10,13}$/.test(text)) {
+ const n = Number(text);
+ return text.length === 10 ? n * 1000 : n;
+ }
+ const date = dayjs(text);
+ return date.isValid() ? date.valueOf() : undefined;
+}
+
+function formatTaskTime(value?: null | number | string) {
+ if (value == null || value === '') return undefined;
+ const date = dayjs(value);
+ return date.isValid() ? date.format(DT_FMT) : undefined;
+}
+/** 鍙敱绌虹櫧鏂板鐨勬潵婧愶紙璇存槑涔︼細涓存椂锛涙枃浠剁瓑鐢辩郴缁熷甫鍑猴級 */
+const BLANK_CREATE_SOURCES = new Set(['temp']);
+
+const route = useRoute();
+const router = useRouter();
+const { createMessage } = useMessage();
+
+const loading = ref(false);
+const submitting = ref(false);
+const paperOptions = ref<{ fullName: string; id: string }[]>([]);
+const placeOptions = ref<{ fullName: string; id: string; placeNo?: string }[]>([]);
+/** 闈炶崏绋跨瓑鍦烘櫙閿佸唴瀹癸紱璁″垝甯﹀嚭榛樿鍙敼 */
+const contentLocked = ref(false);
+const taskFiles = ref<TaskFile[]>([]);
+const uploadFiles = ref<any[]>([]);
+
+/** 鏉ヨ嚜璁″垝/绯荤粺甯﹀嚭锛氫富棰樼瓑鍐呭鍙 */
+const fromPlan = ref(false);
+/** 1=鍩硅鍐呭 2=鎵ц瀹夋帓 */
+const currentStep = ref(1);
+const state = reactive({ id: '', publishStatus: 'draft' as string });
+
+const isEdit = computed(() => !!route.params.id && route.params.id !== 'create');
+const pageTitle = computed(() => {
+ if (!isEdit.value) return '鏂板涓存椂鍩硅浠诲姟';
+ return fromPlan.value ? '缁存姢鍩硅浠诲姟锛堣鍒掑甫鍑猴級' : '缂栬緫涓存椂鍩硅浠诲姟';
+});
+const tipText = computed(() => {
+ if (currentStep.value === 1) {
+ if (fromPlan.value) {
+ return '璁″垝宸插甫鍑轰富棰樸�佹柟寮忋�佸璞$瓑锛涘彲缁х画琛ュ叏鎴栦慨鏀瑰悗杩涘叆涓嬩竴姝ャ�傜紪鍙蜂笌鏉ユ簮涓嶅彲鏀广��';
+ }
+ return '鍏堝~鍐欏煿璁唴瀹癸紝涓嬩竴姝ュ啀瀹夋帓鏃堕棿銆佸湴鐐广�佽瘯鍗风瓑鎵ц淇℃伅銆�';
+ }
+ if (fromPlan.value) {
+ return '璇疯ˉ鍏ㄦ墽琛屽畨鎺掑悗淇濆瓨鎴栧彂甯冦�傚彲杩斿洖涓婁竴姝ヤ慨鏀瑰煿璁唴瀹广��';
+ }
+ return '璇峰~鍐欐墽琛屽畨鎺掞紱鍙繑鍥炰笂涓�姝ヤ慨鏀瑰煿璁唴瀹广��';
+});
+const stepItems = computed(() => [{ title: fromPlan.value ? '璁″垝鍐呭锛堝彲缁存姢锛�' : '鍩硅鍐呭' }, { title: fromPlan.value ? '鎵ц瀹夋帓锛堝彂甯冿級' : '鎵ц瀹夋帓' }]);
+
+const [registerContentForm, contentFormApi] = useForm({
+ labelWidth: 120,
+ baseColProps: { span: 12 },
+ schemas: [
+ {
+ field: 'taskNo',
+ label: '鍩硅缂栧彿',
+ component: 'Input',
+ componentProps: { disabled: true, placeholder: '鑽夌鍙蜂繚瀛樼敓鎴愶紝鍙戝竷鍚庢寮忕紪鍙�' },
+ },
+ {
+ field: 'publishStatusLabel',
+ label: '鍙戝竷鐘舵��',
+ component: 'Input',
+ componentProps: { disabled: true },
+ ifShow: () => isEdit.value,
+ },
+ {
+ field: 'sourceType',
+ label: '鏉ユ簮绫诲瀷',
+ component: 'Select',
+ defaultValue: 'temp',
+ componentProps: {
+ disabled: true,
+ options: [],
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ },
+ },
+ {
+ field: 'category',
+ label: '鍩硅鍒嗙被',
+ component: 'Select',
+ defaultValue: 'temp',
+ componentProps: {
+ disabled: true,
+ options: [],
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ },
+ },
+ {
+ field: 'sourcePlanNo',
+ label: '鏉ユ簮璁″垝鍙�',
+ component: 'Input',
+ componentProps: { disabled: true, placeholder: '璁″垝甯﹀嚭' },
+ ifShow: () => fromPlan.value,
+ },
+ {
+ field: 'subject',
+ label: '鍩硅涓婚',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ュ煿璁富棰�', maxlength: 200 },
+ rules: [{ required: true, message: '蹇呭~', trigger: 'blur' }],
+ },
+ {
+ field: 'trainMode',
+ label: '鍩硅鏂瑰紡',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇烽�夋嫨',
+ options: [],
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ onChange: (val: string) => syncModeRules(val),
+ },
+ rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
+ },
+ {
+ field: 'evalMode',
+ label: '鑰冩牳鏂瑰紡',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇烽�夋嫨',
+ options: [],
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ onChange: (val: string) => syncEvalRules(val),
+ },
+ rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
+ },
+ {
+ field: 'teacherId',
+ label: '鍩硅甯�',
+ component: 'UserSelect',
+ componentProps: { placeholder: '璇烽�夋嫨鍩硅甯�' },
+ },
+ {
+ field: 'traineeIds',
+ label: '鍩硅瀵硅薄',
+ component: 'UserSelect',
+ componentProps: { placeholder: '璇烽�夋嫨鍩硅瀵硅薄', multiple: true },
+ rules: [{ required: true, type: 'array', min: 1, message: '蹇呭~', trigger: 'change' }],
+ },
+ {
+ field: 'materialId',
+ label: '鍩硅鏁欐潗',
+ component: 'Input',
+ render: ({ model }) =>
+ h(JnpfPopupSelect, {
+ columnOptions: [
+ { label: '鏁欐潗缂栧彿', value: 'materialno' },
+ { label: '鏁欐潗鍚嶇О', value: 'fullname' },
+ {
+ label: '鏂囦欢绫诲瀷',
+ value: 'filetype',
+ jnpfKey: 'select',
+ props: { label: 'fullName', value: 'enCode' },
+ __config__: {
+ jnpfKey: 'select',
+ dataType: 'dictionary',
+ dictionaryType: 'tmsdt008',
+ },
+ },
+ ],
+ disabled: contentLocked.value,
+ hasPage: true,
+ pageSize: 20,
+ interfaceId: '871690100000000001',
+ placeholder: '璇烽�夋嫨鍩硅鏁欐潗',
+ popupTitle: '閫夋嫨鏁版嵁',
+ popupType: 'popover',
+ popupWidth: '800px',
+ propsValue: 'id',
+ relationField: 'fullname',
+ value: model.materialId || undefined,
+ 'onUpdate:value': (id?: string) => {
+ contentFormApi.setFieldsValue({ materialId: id || undefined });
+ contentFormApi.clearValidate(['materialId']);
+ if (!id) return;
+ },
+ onChange: (_id: string, row: any) => {
+ const data = row || {};
+ contentFormApi.setFieldsValue({
+ materialNo: data.materialno || undefined,
+ });
+ ensureMainFile(data);
+ },
+ }),
+ },
+ {
+ field: 'taskKind',
+ label: '浠诲姟绫诲瀷',
+ component: 'Select',
+ defaultValue: 'new',
+ componentProps: {
+ options: [],
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ },
+ },
+ {
+ field: 'prevTaskNo',
+ label: '涓婃浠诲姟缂栧彿',
+ component: 'Input',
+ componentProps: { placeholder: '鍐嶇淮鎶ゆ椂鐢辩郴缁熷甫鍑�', disabled: true },
+ ifShow: ({ values }) => values.taskKind === 'continue',
+ },
+ {
+ field: 'keyPoints',
+ label: '鍩硅瑕佺偣',
+ component: 'Textarea',
+ colProps: { span: 24 },
+ componentProps: { rows: 2, placeholder: '閫夊~', maxlength: 2000 },
+ },
+ ],
+});
+
+const [registerExecForm, execFormApi] = useForm({
+ labelWidth: 120,
+ baseColProps: { span: 12 },
+ schemas: [
+ {
+ field: 'startTime',
+ label: '寮�濮嬫椂闂�',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: DT_FMT,
+ valueFormat: DT_FMT,
+ style: { width: '100%' },
+ },
+ rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
+ },
+ {
+ field: 'endTime',
+ label: '缁撴潫鏃堕棿',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: DT_FMT,
+ valueFormat: DT_FMT,
+ style: { width: '100%' },
+ },
+ rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
+ },
+ {
+ field: 'closeTime',
+ label: '鍏抽棴鏃堕棿',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: DT_FMT,
+ valueFormat: DT_FMT,
+ style: { width: '100%' },
+ },
+ rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
+ },
+ {
+ field: 'placeId',
+ label: '鍩硅鍦扮偣',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ showSearch: true,
+ placeholder: '闆嗕腑/鎿嶄綔鎺堣蹇呴��',
+ options: [],
+ optionFilterProp: 'fullName',
+ },
+ },
+ {
+ field: 'paperId',
+ label: '鑰冩牳璇曞嵎',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ showSearch: true,
+ placeholder: '鍦ㄧ嚎鑰冭瘯鏃跺繀閫�',
+ options: [],
+ optionFilterProp: 'fullName',
+ },
+ },
+ {
+ field: 'evalMode',
+ label: '鑰冩牳鏂瑰紡',
+ component: 'Input',
+ ifShow: () => false,
+ },
+ {
+ field: 'examStart',
+ label: '鑰冭瘯寮�濮�',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: DT_FMT,
+ valueFormat: DT_FMT,
+ style: { width: '100%' },
+ },
+ ifShow: ({ values }) => values.evalMode === 'exam',
+ },
+ {
+ field: 'examEnd',
+ label: '鑰冭瘯缁撴潫',
+ component: 'DatePicker',
+ componentProps: {
+ showTime: true,
+ format: DT_FMT,
+ valueFormat: DT_FMT,
+ style: { width: '100%' },
+ },
+ ifShow: ({ values }) => values.evalMode === 'exam',
+ },
+ {
+ field: 'retakeLimit',
+ label: '琛ヨ�冩鏁�',
+ component: 'InputNumber',
+ defaultValue: 1,
+ componentProps: { min: 0, max: 99, precision: 0, style: { width: '100%' } },
+ helpMessage: '涓嶅悎鏍煎彲琛ヨ�冩鏁帮紝榛樿 1',
+ },
+ {
+ field: 'passScore',
+ label: '鍚堟牸鍒�',
+ component: 'InputNumber',
+ componentProps: { min: 0, max: 999, precision: 1, style: { width: '100%' } },
+ },
+ {
+ field: 'overdueRemind',
+ label: '閫炬湡鎻愰啋',
+ component: 'Select',
+ defaultValue: '0',
+ componentProps: {
+ options: [
+ { id: '1', enCode: '1', fullName: '鏄�' },
+ { id: '0', enCode: '0', fullName: '鍚�' },
+ ],
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ },
+ },
+ {
+ field: 'inviteFlag',
+ label: '鍙個璇锋梺鍚�',
+ component: 'Select',
+ defaultValue: '0',
+ componentProps: {
+ options: [
+ { id: '1', enCode: '1', fullName: '鏄�' },
+ { id: '0', enCode: '0', fullName: '鍚�' },
+ ],
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ },
+ },
+ {
+ field: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ colProps: { span: 24 },
+ componentProps: { rows: 2, placeholder: '閫夊~', maxlength: 500 },
+ },
+ ],
+});
+
+onMounted(async () => {
+ loading.value = true;
+ try {
+ await loadTaskDics();
+ await Promise.all([loadPapers(), loadPlaces()]);
+ refreshOptions();
+ if (isEdit.value) {
+ await loadDetail(String(route.params.id));
+ } else {
+ fromPlan.value = false;
+ contentFormApi.resetFields();
+ execFormApi.resetFields();
+ contentFormApi.setFieldsValue({
+ sourceType: 'temp',
+ category: 'temp',
+ taskKind: 'new',
+ });
+ execFormApi.setFieldsValue({
+ retakeLimit: 1,
+ overdueRemind: '0',
+ inviteFlag: '0',
+ startTime: dayjs().valueOf(),
+ endTime: dayjs().add(2, 'hour').valueOf(),
+ closeTime: dayjs().add(1, 'day').valueOf(),
+ });
+ applyContentEditable(true);
+ currentStep.value = 1;
+ }
+ } finally {
+ loading.value = false;
+ }
+});
+
+watch(
+ () => route.params.id,
+ async (id) => {
+ if (!id || id === 'create') return;
+ loading.value = true;
+ try {
+ await loadDetail(String(id));
+ } finally {
+ loading.value = false;
+ }
+ },
+);
+
+function isPlanSourced(info: TaskEntity) {
+ if (info.sourceId || info.sourceItemId || info.sourcePlanNo) return true;
+ const st = info.sourceType || '';
+ return !!st && !BLANK_CREATE_SOURCES.has(st);
+}
+
+function applyContentEditable(editable: boolean) {
+ contentLocked.value = !editable;
+ const disabled = !editable;
+ contentFormApi.updateSchema([
+ { field: 'subject', componentProps: { disabled, placeholder: disabled ? '' : '璇疯緭鍏ュ煿璁富棰�', maxlength: 200 } },
+ {
+ field: 'trainMode',
+ componentProps: {
+ disabled,
+ allowClear: !disabled,
+ options: TRAIN_MODE_OPTIONS.value,
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ onChange: (val: string) => syncModeRules(val),
+ },
+ },
+ {
+ field: 'evalMode',
+ componentProps: {
+ disabled,
+ allowClear: !disabled,
+ options: EVAL_MODE_OPTIONS.value,
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ onChange: (val: string) => {
+ syncEvalRules(val);
+ },
+ },
+ },
+ { field: 'teacherId', componentProps: { disabled, placeholder: '璇烽�夋嫨鍩硅甯�' } },
+ { field: 'traineeIds', componentProps: { disabled, placeholder: '璇烽�夋嫨鍩硅瀵硅薄', multiple: true } },
+ {
+ field: 'taskKind',
+ componentProps: {
+ disabled,
+ options: TASK_KIND_OPTIONS.value,
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ },
+ },
+ { field: 'keyPoints', componentProps: { disabled, rows: 2, placeholder: disabled ? '' : '閫夊~', maxlength: 2000 } },
+ ]);
+}
+
+function refreshOptions() {
+ contentFormApi.updateSchema([
+ {
+ field: 'sourceType',
+ componentProps: {
+ disabled: true,
+ options: SOURCE_TYPE_OPTIONS.value,
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ },
+ },
+ {
+ field: 'category',
+ componentProps: {
+ disabled: true,
+ options: TASK_CATEGORY_OPTIONS.value,
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ },
+ },
+ {
+ field: 'trainMode',
+ componentProps: {
+ allowClear: true,
+ options: TRAIN_MODE_OPTIONS.value,
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ onChange: (val: string) => syncModeRules(val),
+ },
+ },
+ {
+ field: 'evalMode',
+ componentProps: {
+ allowClear: true,
+ options: EVAL_MODE_OPTIONS.value,
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ onChange: (val: string) => {
+ syncEvalRules(val);
+ },
+ },
+ },
+ {
+ field: 'taskKind',
+ componentProps: {
+ options: TASK_KIND_OPTIONS.value,
+ fieldNames: TMS_DIC_FIELD_NAMES,
+ },
+ },
+ ]);
+ execFormApi.updateSchema([
+ {
+ field: 'placeId',
+ componentProps: {
+ allowClear: true,
+ showSearch: true,
+ options: placeOptions.value,
+ optionFilterProp: 'fullName',
+ },
+ },
+ {
+ field: 'paperId',
+ componentProps: {
+ allowClear: true,
+ showSearch: true,
+ options: paperOptions.value,
+ optionFilterProp: 'fullName',
+ },
+ },
+ ]);
+}
+
+function syncModeRules(mode?: string) {
+ const needPlace = mode === 'onsite' || mode === 'practice';
+ const needMaterial = mode === 'online';
+ execFormApi.updateSchema([
+ {
+ field: 'placeId',
+ rules: needPlace ? [{ required: true, message: '鐜板満绫婚』閫夊湴鐐�', trigger: 'change' }] : [],
+ },
+ ]);
+ contentFormApi.updateSchema([
+ {
+ field: 'materialId',
+ rules: needMaterial ? [{ required: true, message: '鍦ㄧ嚎瀛︿範椤婚�夋暀鏉�', trigger: 'change' }] : [],
+ },
+ ]);
+ execFormApi.clearValidate(['placeId']);
+ contentFormApi.clearValidate(['materialId']);
+}
+
+function ensureMainFile(row: any) {
+ const materialId = row?.id;
+ const fileName = row?.fullname;
+ if (!materialId || !fileName) return;
+ if (taskFiles.value.some((item) => item.materialId === materialId)) return;
+ taskFiles.value = [
+ {
+ materialId,
+ fileNo: row.materialno,
+ fileName,
+ canDownload: '0',
+ },
+ ...taskFiles.value,
+ ];
+}
+
+function removeTaskFile(index: number) {
+ const removed = taskFiles.value[index];
+ taskFiles.value = taskFiles.value.filter((_, i) => i !== index);
+ if (!removed?.fileJson) return;
+ try {
+ const file = JSON.parse(removed.fileJson);
+ uploadFiles.value = uploadFiles.value.filter((item) => item.fileId !== file.fileId);
+ } catch {
+ // 闈炰笂浼犻檮浠�
+ }
+}
+
+function onUploadChange(list: any[]) {
+ const files = Array.isArray(list) ? list : [];
+ uploadFiles.value = files;
+ for (const file of files) {
+ if (!file?.name) continue;
+ const existed = taskFiles.value.some((row) => {
+ if (!row.fileJson) return false;
+ try {
+ return JSON.parse(row.fileJson).fileId === file.fileId;
+ } catch {
+ return false;
+ }
+ });
+ if (existed) continue;
+ taskFiles.value = [
+ ...taskFiles.value,
+ {
+ fileNo: file.fileId,
+ fileName: file.name,
+ fileJson: JSON.stringify(file),
+ canDownload: '1',
+ },
+ ];
+ }
+}
+
+function syncEvalRules(evalMode?: string) {
+ execFormApi.updateSchema([
+ {
+ field: 'paperId',
+ rules: evalMode === 'exam' ? [{ required: true, message: '鍦ㄧ嚎鑰冭瘯椤婚�夎瘯鍗�', trigger: 'change' }] : [],
+ },
+ ]);
+ execFormApi.setFieldsValue({ evalMode });
+ execFormApi.clearValidate(['paperId']);
+}
+
+async function loadPapers() {
+ try {
+ const list = await getCoursePaperOptions();
+ paperOptions.value = (Array.isArray(list) ? list : []).map((x: any) => ({
+ id: x.id,
+ fullName: x.fullName || x.paperName || x.id,
+ }));
+ } catch {
+ paperOptions.value = [];
+ }
+}
+
+async function loadPlaces() {
+ try {
+ const list = await getPlaceOptions();
+ placeOptions.value = (Array.isArray(list) ? list : []).map((x: any) => ({
+ id: x.id,
+ fullName: x.fullName || x.placeName || x.id,
+ placeNo: x.placeNo,
+ }));
+ } catch {
+ placeOptions.value = [];
+ }
+}
+
+async function loadDetail(id: string) {
+ const info = await getTaskInfo(id);
+ if (info.publishStatus && info.publishStatus !== 'draft') {
+ createMessage.warning('浠呮湭鍙戝竷浠诲姟鍙紪杈戯紝宸茶烦杞鎯�');
+ router.replace(`/tms/task/detail/${id}`);
+ return;
+ }
+ state.id = info.id || id;
+ state.publishStatus = info.publishStatus || 'draft';
+ fromPlan.value = isPlanSourced(info);
+ // 璁″垝甯﹀嚭涔熶粠鍐呭姝ュ紑濮嬶紝渚夸簬琛ュ叏鍩硅甯�/鏁欐潗绛�
+ currentStep.value = 1;
+
+ const traineeIds = Array.isArray(info.traineeIds)
+ ? info.traineeIds
+ : String(info.trainees || '')
+ .split(/[,;锛岋紱\s]+/)
+ .filter(Boolean);
+
+ const statusMap: Record<string, string> = {
+ draft: '鏈彂甯�',
+ published: '宸插彂甯�',
+ cancelled: '宸插彇娑�',
+ done: '宸插畬鎴�',
+ };
+
+ contentFormApi.setFieldsValue({
+ ...info,
+ traineeIds,
+ materialId: String(info.materialId || '').split(',')[0] || undefined,
+ publishStatusLabel: statusMap[info.publishStatus || 'draft'] || info.publishStatus,
+ });
+ taskFiles.value = Array.isArray(info.files) ? info.files : [];
+ uploadFiles.value = taskFiles.value
+ .map((row) => {
+ if (!row.fileJson) return null;
+ try {
+ return JSON.parse(row.fileJson);
+ } catch {
+ return null;
+ }
+ })
+ .filter(Boolean);
+ execFormApi.setFieldsValue({
+ ...info,
+ evalMode: info.evalMode,
+ startTime: toTimeNumber(info.startTime),
+ endTime: toTimeNumber(info.endTime),
+ closeTime: toTimeNumber(info.closeTime),
+ examStart: toTimeNumber(info.examStart),
+ examEnd: toTimeNumber(info.examEnd),
+ });
+ refreshOptions();
+ // 璁″垝甯﹀嚭锛氫粎缂栧彿/鏉ユ簮绫诲彧璇伙紝涓氬姟鍐呭鍙淮鎶�
+ applyContentEditable(true);
+ syncModeRules(info.trainMode);
+ syncEvalRules(info.evalMode);
+}
+
+async function goNext() {
+ try {
+ await contentFormApi.validate();
+ const content = contentFormApi.getFieldsValue() as TaskEntity;
+ syncModeRules(content.trainMode);
+ syncEvalRules(content.evalMode);
+ currentStep.value = 2;
+ } catch {
+ // 鏍¢獙澶辫触鐢辫〃鍗曟彁绀�
+ }
+}
+
+function goPrev() {
+ currentStep.value = 1;
+}
+
+async function handleSubmit(andPublish = false) {
+ const [content, exec] = await Promise.all([contentFormApi.validate(), execFormApi.validate()]);
+ submitting.value = true;
+ try {
+ let traineeIds = (content as TaskEntity).traineeIds as string | string[] | undefined;
+ if (typeof traineeIds === 'string') {
+ traineeIds = traineeIds.split(/[,;锛岋紱\s]+/).filter(Boolean);
+ }
+ const teacherRaw = (content as TaskEntity).teacherId as any;
+ const materialRaw = (content as TaskEntity).materialId;
+ const materialId =
+ String(materialRaw || '')
+ .split(/[,;锛岋紱]+/)[0]
+ ?.trim() || undefined;
+ const contentData = content as TaskEntity;
+ const execData = exec as TaskEntity;
+ // 鎵ц琛ㄥ崟鍔犺浇鏃跺啓鍏ヤ簡鏁存潯浠诲姟锛屽悗鍐欏叆浼氱洊鎺夌涓�姝ユ敼杩囩殑鍩硅鏂瑰紡/鑰冩牳鏂瑰紡
+ const payload: Partial<TaskEntity> = {
+ ...execData,
+ ...contentData,
+ sourceType: fromPlan.value ? contentData.sourceType : 'temp',
+ category: fromPlan.value ? contentData.category : 'temp',
+ traineeIds: Array.isArray(traineeIds) ? traineeIds : [],
+ teacherId: Array.isArray(teacherRaw) ? teacherRaw[0] : teacherRaw,
+ materialId,
+ trainMode: contentData.trainMode,
+ evalMode: contentData.evalMode,
+ files: taskFiles.value.filter((row) => row.fileName),
+ startTime: formatTaskTime(execData.startTime),
+ endTime: formatTaskTime(execData.endTime),
+ closeTime: formatTaskTime(execData.closeTime),
+ examStart: formatTaskTime(execData.examStart),
+ examEnd: formatTaskTime(execData.examEnd),
+ placeId: execData.placeId,
+ placeName: execData.placeName,
+ paperId: execData.paperId,
+ retakeLimit: execData.retakeLimit,
+ passScore: execData.passScore,
+ overdueRemind: execData.overdueRemind,
+ inviteFlag: execData.inviteFlag,
+ remark: execData.remark,
+ };
+ delete (payload as any).publishStatusLabel;
+
+ let id = state.id;
+ if (isEdit.value && id) {
+ await updateTask({ ...payload, id });
+ createMessage.success('鏇存柊鎴愬姛');
+ } else {
+ id = await createTask(payload);
+ state.id = id;
+ createMessage.success('鍒涘缓鎴愬姛');
+ }
+ if (andPublish && id) {
+ await publishTask(id);
+ createMessage.success('鍙戝竷鎴愬姛');
+ }
+ router.push('/tms/task');
+ } catch (error: any) {
+ if (error?.errorFields) return;
+ createMessage.error(error?.message || '淇濆瓨澶辫触');
+ } finally {
+ submitting.value = false;
+ }
+}
+
+function handleCancel() {
+ router.push('/tms/task');
+}
+</script>
+
+<template>
+ <div class="jnpf-content-wrapper tms-task-form-page" v-loading="loading">
+ <div class="jnpf-content-wrapper-center">
+ <div class="jnpf-content-wrapper-content tms-task-form-wrap">
+ <div class="tms-task-form-scroll">
+ <div class="tms-task-page-title">{{ pageTitle }}</div>
+ <ASteps class="tms-task-steps" size="small" :current="currentStep - 1" :items="stepItems" />
+ <AAlert class="tms-task-tip" type="info" show-icon :message="tipText" />
+
+ <a-card v-show="currentStep === 1" :title="fromPlan ? '璁″垝甯﹀嚭锛堝彲缁存姢锛�' : '鍩硅鍐呭'" :bordered="false" class="tms-task-section">
+ <BasicForm @register="registerContentForm" />
+ <div class="tms-task-files">
+ <div class="tms-task-files-head">
+ <span>鏁欐潗/闄勪欢</span>
+ <JnpfUploadFile
+ v-if="!contentLocked"
+ v-model:value="uploadFiles"
+ accept="*"
+ button-text="涓婁紶闄勪欢"
+ :file-size="0"
+ :limit="0"
+ :show-all-download="false"
+ :show-upload-list="false"
+ tip-text="鏀寔鏂囨。鍜岃棰�"
+ show-tip
+ @change="onUploadChange" />
+ </div>
+ <a-table
+ size="small"
+ :pagination="false"
+ row-key="materialId"
+ :data-source="taskFiles"
+ :columns="[
+ { title: '缂栧彿', dataIndex: 'fileNo', width: 160 },
+ { title: '鏂囦欢鍚嶇О', dataIndex: 'fileName' },
+ { title: '鍙笅杞�', dataIndex: 'canDownload', width: 90 },
+ { title: '鎿嶄綔', dataIndex: 'action', width: 80 },
+ ]">
+ <template #bodyCell="{ column, record, index }">
+ <template v-if="column.dataIndex === 'fileName'">
+ <a-input v-model:value="record.fileName" :disabled="contentLocked" maxlength="300" />
+ </template>
+ <template v-else-if="column.dataIndex === 'canDownload'">
+ <a-switch
+ :checked="record.canDownload === '1'"
+ :disabled="contentLocked"
+ checked-children="鏄�"
+ un-checked-children="鍚�"
+ @change="(checked) => (record.canDownload = checked ? '1' : '0')" />
+ </template>
+ <template v-else-if="column.dataIndex === 'action'">
+ <a-button type="link" danger :disabled="contentLocked" @click="removeTaskFile(index)">鍒犻櫎</a-button>
+ </template>
+ </template>
+ </a-table>
+ </div>
+ </a-card>
+
+ <a-card v-show="currentStep === 2" :title="fromPlan ? '鎵ц瀹夋帓锛堣ˉ鍏ㄥ悗鍙戝竷锛�' : '鎵ц瀹夋帓'" :bordered="false" class="tms-task-section">
+ <BasicForm @register="registerExecForm" />
+ </a-card>
+ </div>
+
+ <div class="tms-task-form-footer">
+ <a-space>
+ <a-button @click="handleCancel">{{ TMS_BTN.cancel }}</a-button>
+ <a-button v-if="currentStep === 2" @click="goPrev">涓婁竴姝�</a-button>
+ <a-button v-if="currentStep === 1" type="primary" @click="goNext">涓嬩竴姝�</a-button>
+ <template v-if="currentStep === 2">
+ <a-button type="primary" ghost :loading="submitting" @click="handleSubmit(false)">
+ {{ TMS_BTN.saveDraft }}
+ </a-button>
+ <a-button type="primary" :loading="submitting" @click="handleSubmit(true)">
+ {{ TMS_BTN.savePublish }}
+ </a-button>
+ </template>
+ </a-space>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<style scoped>
+.tms-task-form-page {
+ height: 100%;
+ min-height: 0;
+}
+
+.tms-task-form-wrap {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ min-height: 0;
+ overflow: hidden;
+ background: #fff;
+}
+
+.tms-task-form-scroll {
+ flex: 1;
+ min-height: 0;
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding: 16px 16px 8px;
+}
+
+.tms-task-page-title {
+ margin-bottom: 12px;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.tms-task-steps {
+ max-width: 480px;
+ margin-bottom: 12px;
+}
+
+.tms-task-tip {
+ margin-bottom: 12px;
+}
+
+.tms-task-section {
+ margin-bottom: 12px;
+}
+
+.tms-task-files {
+ margin-top: 8px;
+}
+
+.tms-task-files-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ font-weight: 600;
+}
+
+.tms-task-form-footer {
+ flex-shrink: 0;
+ padding: 12px 16px;
+ border-top: 1px solid #f0f0f0;
+ text-align: right;
+ background: #fff;
+}
+</style>
--
Gitblit v1.8.0