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/paper/Form.vue |  676 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 676 insertions(+), 0 deletions(-)

diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/paper/Form.vue b/apps/jnpf-web-apps-main/src/views/x/tms/paper/Form.vue
new file mode 100644
index 0000000..1f98478
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/paper/Form.vue
@@ -0,0 +1,676 @@
+<script lang="ts" setup>
+import type { PaperEntity, PaperQuestionItem, PaperSectionItem } from './types';
+import type { QuestionEntity } from '#/views/x/tms/question/types';
+
+import { computed, nextTick, onMounted, reactive, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { useMessage } from '@jnpf/hooks';
+import { useModal } from '@jnpf/ui/modal';
+
+import { Modal } from 'ant-design-vue';
+import dayjs from 'dayjs';
+
+import { createPaper, getPaperInfo, updatePaper } from '#/api/x/tms/paper';
+import { TMS_DIC_FIELD_NAMES } from '#/views/x/tms/shared/dic';
+import { loadQuestionDics } from '#/views/x/tms/question/constants';
+
+import QuestionPicker from './components/QuestionPicker.vue';
+import {
+  QUESTION_TYPE_OPTIONS,
+  SORT_MODE_OPTIONS,
+  STATUS_OPTIONS,
+  labelOfType,
+  loadPaperDics,
+  nextTmpId,
+} from './constants';
+
+defineOptions({ name: 'TmsPaperForm' });
+
+const DT_FMT = 'YYYY-MM-DD HH:mm:ss';
+
+const route = useRoute();
+const router = useRouter();
+const { createMessage } = useMessage();
+
+const loading = ref(false);
+const submitting = ref(false);
+const formRef = ref();
+const activeSectionId = ref('');
+
+const isEdit = computed(() => !!route.params.id && route.params.id !== 'create');
+const pageTitle = computed(() => (isEdit.value ? '缂栬緫璇曞嵎' : '鏂板璇曞嵎'));
+
+const dataForm = reactive<PaperEntity>({
+  paperName: '',
+  bizStatus: 'open',
+  durationMin: 60,
+  examStart: undefined,
+  examEnd: undefined,
+  scorePublishTime: undefined,
+  passScore: undefined,
+  totalScore: 0,
+  sortMode: 'paper',
+  remark: '',
+  sections: [],
+});
+
+const statusTip = computed(
+  () => STATUS_OPTIONS.value.find((x) => (x.enCode || x.id) === dataForm.bizStatus)?.tip,
+);
+const statusTipColor = computed(
+  () => STATUS_OPTIONS.value.find((x) => (x.enCode || x.id) === dataForm.bizStatus)?.tipColor,
+);
+const editableStatusOptions = computed(() =>
+  STATUS_OPTIONS.value.filter((x) => (x.enCode || x.id) !== 'invalid'),
+);
+
+const computedTotal = computed(() => {
+  let sum = 0;
+  for (const sec of dataForm.sections || []) {
+    for (const q of sec.questions || []) {
+      sum += Number(q.score || 0);
+    }
+  }
+  return Math.round(sum * 10) / 10;
+});
+
+function toDayjs(v: any) {
+  if (v == null || v === '') return null;
+  if (typeof v === 'number') {
+    const d = dayjs(v);
+    return d.isValid() ? d : null;
+  }
+  const text = String(v).trim();
+  if (!text) return null;
+  if (/^\d+$/.test(text)) {
+    const n = Number(text);
+    const d = dayjs(text.length === 10 ? n * 1000 : n);
+    return d.isValid() ? d : null;
+  }
+  const d = dayjs(text);
+  return d.isValid() ? d : null;
+}
+
+function formatDateTime(v: any): string | undefined {
+  const d = toDayjs(v);
+  return d ? d.format(DT_FMT) : undefined;
+}
+
+const validateExamEnd = async (_rule: any, value: any) => {
+  const end = toDayjs(value);
+  const start = toDayjs(dataForm.examStart);
+  if (!end && !start) return;
+  if (start && !end) {
+    return Promise.reject('璇烽�夋嫨缁撴潫鏃堕棿');
+  }
+  if (!start && end) {
+    return Promise.reject('璇峰厛閫夋嫨寮�鑰冩椂闂�');
+  }
+  if (start && end && !end.isAfter(start)) {
+    return Promise.reject('缁撴潫鏃堕棿蹇呴』鏅氫簬寮�鑰冩椂闂�');
+  }
+  const duration = Number(dataForm.durationMin || 0);
+  if (start && end && duration > 0) {
+    const spanMin = end.diff(start, 'minute');
+    if (duration > spanMin) {
+      return Promise.reject(`鑰冭瘯鏃堕暱涓嶈兘瓒呰繃寮�鑰冭嚦缁撴潫鐨勯棿闅旓紙${spanMin} 鍒嗛挓锛塦);
+    }
+  }
+};
+
+const validateExamStart = async (_rule: any, value: any) => {
+  const start = toDayjs(value);
+  const end = toDayjs(dataForm.examEnd);
+  if (!start && end) {
+    return Promise.reject('璇烽�夋嫨寮�鑰冩椂闂�');
+  }
+  if (start && end && !end.isAfter(start)) {
+    return Promise.reject('寮�鑰冩椂闂村繀椤绘棭浜庣粨鏉熸椂闂�');
+  }
+};
+
+const validatePublishTime = async (_rule: any, value: any) => {
+  const publish = toDayjs(value);
+  if (!publish) return;
+  const end = toDayjs(dataForm.examEnd);
+  const start = toDayjs(dataForm.examStart);
+  if (end && publish.isBefore(end)) {
+    return Promise.reject('鎴愮哗鍏竷鏃堕棿涓嶈兘鏃╀簬缁撴潫鏃堕棿');
+  }
+  if (!end && start && publish.isBefore(start)) {
+    return Promise.reject('鎴愮哗鍏竷鏃堕棿涓嶈兘鏃╀簬寮�鑰冩椂闂�');
+  }
+};
+
+const validateDuration = async (_rule: any, value: any) => {
+  if (value == null || value === '') return;
+  const n = Number(value);
+  if (Number.isNaN(n) || n < 0) {
+    return Promise.reject('鑰冭瘯鏃堕暱涓嶈兘涓鸿礋鏁�');
+  }
+  const start = toDayjs(dataForm.examStart);
+  const end = toDayjs(dataForm.examEnd);
+  if (start && end && n > 0) {
+    const spanMin = end.diff(start, 'minute');
+    if (n > spanMin) {
+      return Promise.reject(`鑰冭瘯鏃堕暱涓嶈兘瓒呰繃寮�鑰冭嚦缁撴潫鐨勯棿闅旓紙${spanMin} 鍒嗛挓锛塦);
+    }
+  }
+};
+
+const validatePassScore = async (_rule: any, value: any) => {
+  if (value == null || value === '') return;
+  const pass = Number(value);
+  if (Number.isNaN(pass) || pass < 0) {
+    return Promise.reject('鍚堟牸鍒嗘暟涓嶈兘涓鸿礋鏁�');
+  }
+  const total = computedTotal.value;
+  if (pass > total) {
+    return Promise.reject(`鍚堟牸鍒嗘暟涓嶈兘澶т簬璇曞嵎鎬诲垎锛堝綋鍓嶆�诲垎 ${total}锛塦);
+  }
+};
+
+const rules = {
+  paperName: [{ required: true, message: '璇峰~鍐欒瘯鍗峰悕绉�', trigger: 'blur' }],
+  bizStatus: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }],
+  sortMode: [{ required: true, message: '璇烽�夋嫨璇曢鎺掑簭', trigger: 'change' }],
+  durationMin: [{ validator: validateDuration, trigger: 'change' }],
+  examStart: [{ validator: validateExamStart, trigger: 'change' }],
+  examEnd: [{ validator: validateExamEnd, trigger: 'change' }],
+  scorePublishTime: [{ validator: validatePublishTime, trigger: 'change' }],
+  passScore: [{ validator: validatePassScore, trigger: 'change' }],
+};
+
+function revalidateTimes() {
+  formRef.value?.validateFields?.(['examStart', 'examEnd', 'scorePublishTime', 'durationMin']).catch(() => undefined);
+}
+
+function revalidatePassScore() {
+  formRef.value?.validateFields?.(['passScore']).catch(() => undefined);
+}
+
+function toTimestamp(v: any): number | undefined {
+  const d = toDayjs(v);
+  return d ? d.valueOf() : undefined;
+}
+
+const [registerPicker, { openModal: openPicker }] = useModal();
+
+onMounted(async () => {
+  await Promise.all([loadPaperDics(), loadQuestionDics()]);
+  if (isEdit.value) {
+    await loadDetail(String(route.params.id));
+  }
+});
+
+async function loadDetail(id: string) {
+  loading.value = true;
+  try {
+    const info = await getPaperInfo(id);
+    Object.assign(dataForm, {
+      ...info,
+      sections: (info.sections || []).map((s) => ({
+        ...s,
+        id: s.id || nextTmpId('sec'),
+        questions: (s.questions || []).map((q) => ({ ...q })),
+      })),
+    });
+    if (dataForm.sections?.length) {
+      activeSectionId.value = dataForm.sections[0]!.id;
+    }
+  } catch (e: any) {
+    createMessage.error(e?.message || '鍔犺浇澶辫触');
+  } finally {
+    loading.value = false;
+  }
+}
+
+function goList() {
+  router.push('/tms/paper');
+}
+
+function handleCancel() {
+  Modal.confirm({
+    title: '纭鍙栨秷',
+    content: '纭畾鍙栨秷缂栬緫鍚楋紵鏈繚瀛樼殑鍐呭灏嗕涪澶便��',
+    okText: '纭畾',
+    cancelText: '缁х画缂栬緫',
+    onOk: () => goList(),
+  });
+}
+
+function addSection() {
+  const sec: PaperSectionItem = {
+    id: nextTmpId('sec'),
+    sectionName: '鍗曢�夐',
+    questionType: 'single',
+    sortNo: (dataForm.sections?.length || 0) + 1,
+    questions: [],
+  };
+  if (!dataForm.sections) dataForm.sections = [];
+  dataForm.sections.push(sec);
+  activeSectionId.value = sec.id;
+  nextTick(() => {
+    document.getElementById(`paper-sec-${sec.id}`)?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+  });
+}
+
+function removeSection(secId: string) {
+  dataForm.sections = (dataForm.sections || []).filter((s) => s.id !== secId);
+  if (activeSectionId.value === secId) {
+    activeSectionId.value = dataForm.sections[0]?.id || '';
+  }
+}
+
+function onSectionTypeChange(sec: PaperSectionItem) {
+  if (sec.questionType === 'single') sec.sectionName = '鍗曢�夐';
+  else if (sec.questionType === 'multi') sec.sectionName = '澶氶�夐';
+  else if (sec.questionType === 'judge') sec.sectionName = '鍒ゆ柇棰�';
+  else if (sec.questionType === 'blank') sec.sectionName = '濉┖棰�';
+  else if (sec.questionType === 'essay') sec.sectionName = '闂瓟棰�';
+}
+
+function openPick(sec: PaperSectionItem) {
+  if (!sec.questionType) {
+    createMessage.warning('璇峰厛閫夋嫨绔犺妭棰樺瀷');
+    return;
+  }
+  activeSectionId.value = sec.id;
+  const excludeIds = (dataForm.sections || [])
+    .flatMap((s) => s.questions || [])
+    .map((q) => q.questionId)
+    .filter(Boolean);
+  openPicker(true, { questionType: sec.questionType, excludeIds });
+}
+
+function onPickConfirm(rows: QuestionEntity[]) {
+  const sec = (dataForm.sections || []).find((s) => s.id === activeSectionId.value);
+  if (!sec) return;
+  const start = sec.questions.length;
+  for (let i = 0; i < rows.length; i++) {
+    const row = rows[i]!;
+    if (!row.id) continue;
+    if (sec.questionType && row.questionType && row.questionType !== sec.questionType) {
+      createMessage.warning(`璇曢 ${row.questionNo} 涓庣珷鑺傞鍨嬩笉涓�鑷达紝宸茶烦杩嘸);
+      continue;
+    }
+    sec.questions.push({
+      questionId: row.id,
+      questionNo: row.questionNo,
+      stem: row.stem,
+      questionType: row.questionType,
+      bankName: row.bankName,
+      score: 1,
+      sortNo: start + i + 1,
+    } as PaperQuestionItem);
+  }
+  syncTotal();
+}
+
+function removeQuestion(sec: PaperSectionItem, idx: number) {
+  sec.questions.splice(idx, 1);
+  sec.questions.forEach((q, i) => {
+    q.sortNo = i + 1;
+  });
+  syncTotal();
+}
+
+function moveQuestion(sec: PaperSectionItem, idx: number, delta: number) {
+  const target = idx + delta;
+  if (target < 0 || target >= sec.questions.length) return;
+  const list = sec.questions;
+  const tmp = list[idx]!;
+  list[idx] = list[target]!;
+  list[target] = tmp;
+  list.forEach((q, i) => {
+    q.sortNo = i + 1;
+  });
+}
+
+function syncTotal() {
+  dataForm.totalScore = computedTotal.value;
+  revalidatePassScore();
+}
+
+function stripHtml(html?: string) {
+  if (!html) return '';
+  const text = html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').trim();
+  return text.length > 80 ? `${text.slice(0, 80)}鈥 : text;
+}
+
+async function handleSubmit() {
+  try {
+    await formRef.value?.validate();
+  } catch {
+    return;
+  }
+  if (!(dataForm.sections || []).some((s) => (s.questions || []).length)) {
+    createMessage.warning('璇疯嚦灏戞坊鍔犱竴閬撹瘯棰�');
+    return;
+  }
+  for (const sec of dataForm.sections || []) {
+    for (const q of sec.questions || []) {
+      if (q.score == null || Number(q.score) <= 0) {
+        createMessage.warning(`璇曢 ${q.questionNo || ''} 鍒嗗�煎繀椤诲ぇ浜� 0`);
+        return;
+      }
+    }
+  }
+  if (dataForm.passScore != null && Number(dataForm.passScore) > computedTotal.value) {
+    createMessage.warning(`鍚堟牸鍒嗘暟涓嶈兘澶т簬璇曞嵎鎬诲垎锛堝綋鍓嶆�诲垎 ${computedTotal.value}锛塦);
+    return;
+  }
+  syncTotal();
+  submitting.value = true;
+  try {
+    const payload: PaperEntity = {
+      ...dataForm,
+      examStart: formatDateTime(dataForm.examStart) ?? null,
+      examEnd: formatDateTime(dataForm.examEnd) ?? null,
+      scorePublishTime: formatDateTime(dataForm.scorePublishTime) ?? null,
+      sections: (dataForm.sections || []).map((s, si) => ({
+        ...s,
+        sortNo: si + 1,
+        questions: (s.questions || []).map((q, qi) => ({
+          questionId: q.questionId,
+          score: q.score ?? 0,
+          sortNo: qi + 1,
+        })),
+      })),
+    };
+    if (isEdit.value) {
+      await updatePaper(payload);
+      createMessage.success('鏇存柊鎴愬姛');
+    } else {
+      await createPaper(payload);
+      createMessage.success('鍒涘缓鎴愬姛');
+    }
+    goList();
+  } catch (e: any) {
+    createMessage.error(e?.message || '淇濆瓨澶辫触');
+  } finally {
+    submitting.value = false;
+  }
+}
+</script>
+
+<template>
+  <div class="jnpf-content-wrapper tms-paper-form-page" v-loading="loading">
+    <div class="jnpf-content-wrapper-center">
+      <div class="jnpf-content-wrapper-content tms-paper-form-wrap">
+        <div class="page-toolbar">
+          <div class="page-title">{{ pageTitle }}</div>
+        </div>
+
+        <div class="tms-paper-form-body">
+          <a-card title="鍩烘湰淇℃伅" :bordered="false" class="form-card">
+            <a-form ref="formRef" :model="dataForm" :rules="rules" :label-col="{ style: { width: '120px' } }">
+              <a-row :gutter="16">
+                <a-col v-if="dataForm.id" :span="12">
+                  <a-form-item label="璇曞嵎缂栧彿">
+                    <a-input :value="dataForm.paperNo || '-'" disabled />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item label="璇曞嵎鍚嶇О" name="paperName">
+                    <a-input v-model:value="dataForm.paperName" placeholder="璇疯緭鍏ヨ瘯鍗峰悕绉�" :maxlength="200" />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item label="璇曞嵎鐘舵��" name="bizStatus">
+                    <a-select
+                      v-model:value="dataForm.bizStatus"
+                      :options="editableStatusOptions"
+                      :field-names="TMS_DIC_FIELD_NAMES"
+                      style="width: 160px"
+                    />
+                    <span v-if="statusTip" class="status-tip" :style="{ color: statusTipColor }">{{ statusTip }}</span>
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item label="鑰冭瘯鏃堕暱" name="durationMin">
+                    <a-input-number
+                      v-model:value="dataForm.durationMin"
+                      :min="0"
+                      :precision="0"
+                      style="width: 160px"
+                      @change="revalidateTimes"
+                    />
+                    <span class="field-hint">鍒嗛挓锛堜笉搴旇秴杩囪缃殑鏃堕棿闂撮殧锛�</span>
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                <a-form-item label="璇曢鎺掑簭" name="sortMode">
+                  <a-radio-group v-model:value="dataForm.sortMode">
+                    <a-radio v-for="opt in SORT_MODE_OPTIONS" :key="opt.enCode || opt.id" :value="opt.enCode || opt.id">
+                      {{ opt.fullName }}
+                    </a-radio>
+                  </a-radio-group>
+                </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item label="寮�鑰冩椂闂�" name="examStart">
+                    <jnpf-date-picker
+                      v-model:value="dataForm.examStart"
+                      format="YYYY-MM-DD HH:mm:ss"
+                      placeholder="璇烽�夋嫨寮�鑰冩椂闂�"
+                      style="width: 100%"
+                      :end-time="toTimestamp(dataForm.examEnd)"
+                      @change="revalidateTimes"
+                    />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item label="缁撴潫鏃堕棿" name="examEnd">
+                    <jnpf-date-picker
+                      v-model:value="dataForm.examEnd"
+                      format="YYYY-MM-DD HH:mm:ss"
+                      placeholder="璇烽�夋嫨缁撴潫鏃堕棿"
+                      style="width: 100%"
+                      :start-time="toTimestamp(dataForm.examStart)"
+                      @change="revalidateTimes"
+                    />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item label="鎴愮哗鍏竷鏃堕棿" name="scorePublishTime">
+                    <jnpf-date-picker
+                      v-model:value="dataForm.scorePublishTime"
+                      format="YYYY-MM-DD HH:mm:ss"
+                      placeholder="绔嬪嵆鍏竷璇风暀绌�"
+                      style="width: 100%"
+                      :start-time="toTimestamp(dataForm.examEnd || dataForm.examStart)"
+                      @change="revalidateTimes"
+                    />
+                    <div class="field-hint block">璁剧疆鎴愮哗鍏竷鏃堕棿锛岀珛鍗冲叕甯冭鐣欑┖</div>
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item label="璇曞嵎鎬诲垎">
+                    <a-input-number :value="computedTotal" disabled style="width: 160px" />
+                    <span class="field-hint">鐢辩粍鍗峰垎鍊艰嚜鍔ㄦ眹鎬�</span>
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item label="鍚堟牸鍒嗘暟" name="passScore">
+                    <a-input-number
+                      v-model:value="dataForm.passScore"
+                      :min="0"
+                      :precision="1"
+                      style="width: 160px"
+                      @change="revalidatePassScore"
+                    />
+                    <span class="field-hint">涓嶈兘澶т簬璇曞嵎鎬诲垎</span>
+                  </a-form-item>
+                </a-col>
+                <a-col :span="24">
+                  <a-form-item label="澶囨敞">
+                    <a-textarea v-model:value="dataForm.remark" :rows="3" placeholder="璇疯緭鍏ュ娉�" />
+                  </a-form-item>
+                </a-col>
+              </a-row>
+            </a-form>
+          </a-card>
+
+          <a-card title="缁勫嵎" :bordered="false" class="form-card">
+            <template #extra>
+              <a-button type="primary" @click="addSection">娣诲姞绔犺妭</a-button>
+            </template>
+
+            <a-empty v-if="!(dataForm.sections || []).length" description="璇锋坊鍔犵珷鑺傚苟浠庤瘯棰樼鐞嗛�夐缁勫嵎" />
+
+            <div
+              v-for="sec in dataForm.sections"
+              :id="`paper-sec-${sec.id}`"
+              :key="sec.id"
+              class="section-block"
+            >
+              <div class="section-head">
+                <a-input v-model:value="sec.sectionName" placeholder="绔犺妭鍚嶇О" style="width: 200px" />
+                <a-select
+                  v-model:value="sec.questionType"
+                  placeholder="棰樺瀷"
+                  style="width: 140px; margin-left: 8px"
+                  :options="QUESTION_TYPE_OPTIONS"
+                  :field-names="TMS_DIC_FIELD_NAMES"
+                  @change="onSectionTypeChange(sec)"
+                />
+                <a-space style="margin-left: auto">
+                  <a-button type="link" @click="openPick(sec)">閫夐</a-button>
+                  <a-button type="link" danger @click="removeSection(sec.id)">鍒犻櫎绔犺妭</a-button>
+                </a-space>
+              </div>
+
+              <a-table
+                size="small"
+                row-key="questionId"
+                :pagination="false"
+                :data-source="sec.questions"
+                :columns="[
+                  { title: '搴忓彿', width: 60, key: 'idx' },
+                  { title: '缂栧彿', dataIndex: 'questionNo', width: 90 },
+                  { title: '棰樺瀷', width: 80, key: 'qtype' },
+                  { title: '棰樺共', dataIndex: 'stem', ellipsis: true, key: 'stem' },
+                  { title: '棰樺簱', dataIndex: 'bankName', width: 140, ellipsis: true },
+                  { title: '鍒嗗��', width: 110, key: 'score' },
+                  { title: '鎿嶄綔', width: 160, key: 'action' },
+                ]"
+              >
+                <template #bodyCell="{ column, record, index }">
+                  <template v-if="column.key === 'idx'">{{ index + 1 }}</template>
+                  <template v-else-if="column.key === 'qtype'">{{ labelOfType(record.questionType) }}</template>
+                  <template v-else-if="column.key === 'stem'">{{ stripHtml(record.stem) }}</template>
+                  <template v-else-if="column.key === 'score'">
+                    <a-input-number
+                      v-model:value="record.score"
+                      :min="0"
+                      :precision="1"
+                      size="small"
+                      style="width: 90px"
+                      @change="syncTotal"
+                    />
+                  </template>
+                  <template v-else-if="column.key === 'action'">
+                    <a-space>
+                      <a @click="moveQuestion(sec, index, -1)">涓婄Щ</a>
+                      <a @click="moveQuestion(sec, index, 1)">涓嬬Щ</a>
+                      <a class="danger" @click="removeQuestion(sec, index)">绉婚櫎</a>
+                    </a-space>
+                  </template>
+                </template>
+              </a-table>
+            </div>
+          </a-card>
+
+          <div class="form-footer">
+            <a-space>
+              <a-button @click="handleCancel">鍙栨秷</a-button>
+              <a-button type="primary" :loading="submitting" @click="handleSubmit">淇濆瓨</a-button>
+            </a-space>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <QuestionPicker @register="registerPicker" @confirm="onPickConfirm" />
+  </div>
+</template>
+
+<style scoped>
+.tms-paper-form-page {
+  height: 100%;
+  min-height: 0;
+}
+
+.tms-paper-form-wrap {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  min-height: 0;
+  overflow: hidden;
+  background: #fff;
+  padding: 16px;
+}
+
+.page-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-shrink: 0;
+  margin-bottom: 12px;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.page-title {
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.tms-paper-form-body {
+  flex: 1;
+  min-height: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.form-card {
+  margin-bottom: 12px;
+}
+
+.status-tip,
+.field-hint {
+  margin-left: 8px;
+  color: #999;
+  font-size: 12px;
+}
+
+.field-hint.block {
+  display: block;
+  margin: 4px 0 0;
+}
+
+.section-block {
+  margin-bottom: 16px;
+  padding: 12px;
+  background: #fafafa;
+  border: 1px solid #f0f0f0;
+  border-radius: 4px;
+}
+
+.section-head {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.form-footer {
+  padding: 16px 0 24px;
+  text-align: center;
+}
+
+.danger {
+  color: #ff4d4f;
+}
+</style>

--
Gitblit v1.8.0