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(/ /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