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/components/QuestionPicker.vue |  229 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 229 insertions(+), 0 deletions(-)

diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/paper/components/QuestionPicker.vue b/apps/jnpf-web-apps-main/src/views/x/tms/paper/components/QuestionPicker.vue
new file mode 100644
index 0000000..8a3f316
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/paper/components/QuestionPicker.vue
@@ -0,0 +1,229 @@
+<script lang="ts" setup>
+import type { QuestionBankOption, QuestionEntity } from '#/views/x/tms/question/types';
+
+import { computed, reactive, ref } from 'vue';
+
+import { useMessage } from '@jnpf/hooks';
+import { BasicModal, useModalInner } from '@jnpf/ui/modal';
+
+import { getQuestionBanks, getQuestionList } from '#/api/x/tms/question';
+import { labelOfType } from '#/views/x/tms/question/constants';
+
+defineOptions({ name: 'TmsPaperQuestionPicker' });
+
+const emit = defineEmits<{
+  confirm: [QuestionEntity[]];
+}>();
+
+const { createMessage } = useMessage();
+
+const banks = ref<QuestionBankOption[]>([]);
+const loading = ref(false);
+const list = ref<QuestionEntity[]>([]);
+const selectedKeys = ref<string[]>([]);
+const selectedRows = ref<QuestionEntity[]>([]);
+const excludeIds = ref<Set<string>>(new Set());
+/** 绔犺妭宸插畾棰樺瀷锛氶�夐鏃堕攣瀹氾紝鍙渶鍐嶉�夐搴� */
+const lockedType = ref<string | undefined>();
+
+const query = reactive({
+  bankId: undefined as string | undefined,
+  keyword: '',
+  currentPage: 1,
+  pageSize: 10,
+  total: 0,
+});
+
+const columns = [
+  { title: '缂栧彿', dataIndex: 'questionNo', width: 90 },
+  { title: '棰樺簱', dataIndex: 'bankName', width: 140, ellipsis: true },
+  { title: '棰樺瀷', dataIndex: 'questionType', width: 80, key: 'questionType' },
+  { title: '棰樺共', dataIndex: 'stem', ellipsis: true, key: 'stem' },
+];
+
+const typeLabel = computed(() => (lockedType.value ? labelOfType(lockedType.value) : ''));
+
+const [registerModal, { closeModal }] = useModalInner(async (data: any) => {
+  excludeIds.value = new Set(Array.isArray(data?.excludeIds) ? data.excludeIds : []);
+  lockedType.value = data?.questionType || undefined;
+  query.bankId = undefined;
+  query.keyword = '';
+  query.currentPage = 1;
+  query.total = 0;
+  list.value = [];
+  selectedKeys.value = [];
+  selectedRows.value = [];
+  if (!banks.value.length) {
+    banks.value = (await getQuestionBanks()) || [];
+  }
+});
+
+async function loadList() {
+  if (!query.bankId) {
+    list.value = [];
+    query.total = 0;
+    return;
+  }
+  loading.value = true;
+  try {
+    const page = await getQuestionList({
+      bankId: query.bankId,
+      questionType: lockedType.value || '',
+      bizStatus: 'open',
+      keyword: query.keyword || undefined,
+      currentPage: query.currentPage,
+      pageSize: query.pageSize,
+    });
+    list.value = Array.isArray(page?.list) ? page.list : [];
+    query.total = Number(page?.pagination?.total || 0);
+  } catch (e: any) {
+    createMessage.error(e?.message || '鍔犺浇璇曢澶辫触');
+    list.value = [];
+  } finally {
+    loading.value = false;
+  }
+}
+
+function onSelectChange(keys: (string | number)[], rows: QuestionEntity[]) {
+  selectedKeys.value = keys.map(String);
+  selectedRows.value = rows;
+}
+
+function rowDisabled(record: QuestionEntity) {
+  return !!(record.id && excludeIds.value.has(record.id));
+}
+
+function stripHtml(html?: string) {
+  if (!html) return '';
+  const text = html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').trim();
+  return text.length > 60 ? `${text.slice(0, 60)}鈥 : text;
+}
+
+function onBankChange() {
+  query.currentPage = 1;
+  selectedKeys.value = [];
+  selectedRows.value = [];
+  loadList();
+}
+
+function handleSearch() {
+  if (!query.bankId) {
+    createMessage.warning('璇峰厛閫夋嫨棰樺簱');
+    return;
+  }
+  query.currentPage = 1;
+  loadList();
+}
+
+function handlePageChange(page: number, pageSize: number) {
+  query.currentPage = page;
+  query.pageSize = pageSize;
+  loadList();
+}
+
+function handleOk() {
+  if (!query.bankId) {
+    createMessage.warning('璇峰厛閫夋嫨棰樺簱');
+    return;
+  }
+  const rows = selectedRows.value.filter((r) => r.id && !excludeIds.value.has(r.id));
+  if (!rows.length) {
+    createMessage.warning('璇烽�夋嫨璇曢');
+    return;
+  }
+  emit('confirm', rows);
+  closeModal();
+}
+</script>
+
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    title="閫夋嫨璇曢"
+    width="900px"
+    destroy-on-close
+    @register="registerModal"
+    @ok="handleOk"
+  >
+    <div v-if="lockedType" class="picker-tip">
+      褰撳墠绔犺妭棰樺瀷锛�<b>{{ typeLabel }}</b>锛岃閫夋嫨棰樺簱鍚庡嬀閫夎瘯棰�
+    </div>
+
+    <a-form layout="inline" class="picker-query" @submit.prevent>
+      <a-form-item label="棰樺簱" required>
+        <a-select
+          v-model:value="query.bankId"
+          allow-clear
+          show-search
+          placeholder="璇烽�夋嫨棰樺簱"
+          style="width: 220px"
+          :options="banks"
+          :field-names="{ label: 'fullName', value: 'id' }"
+          option-filter-prop="fullName"
+          @change="onBankChange"
+        />
+      </a-form-item>
+      <a-form-item label="鍏抽敭璇�">
+        <a-input
+          v-model:value="query.keyword"
+          allow-clear
+          placeholder="缂栧彿/棰樺共"
+          style="width: 180px"
+          :disabled="!query.bankId"
+          @press-enter="handleSearch"
+        />
+      </a-form-item>
+      <a-form-item>
+        <a-button type="primary" :disabled="!query.bankId" @click="handleSearch">鏌ヨ</a-button>
+      </a-form-item>
+    </a-form>
+
+    <a-empty v-if="!query.bankId" description="璇峰厛閫夋嫨棰樺簱" />
+
+    <a-table
+      v-else
+      size="small"
+      row-key="id"
+      :loading="loading"
+      :data-source="list"
+      :columns="columns"
+      :pagination="{
+        current: query.currentPage,
+        pageSize: query.pageSize,
+        total: query.total,
+        showSizeChanger: true,
+        onChange: handlePageChange,
+      }"
+      :row-selection="{
+        selectedRowKeys: selectedKeys,
+        onChange: onSelectChange,
+        getCheckboxProps: (record: QuestionEntity) => ({ disabled: rowDisabled(record) }),
+      }"
+    >
+      <template #bodyCell="{ column, record }">
+        <template v-if="column.key === 'questionType'">
+          {{ labelOfType(record.questionType) }}
+        </template>
+        <template v-else-if="column.key === 'stem'">
+          {{ stripHtml(record.stem) }}
+          <span v-if="rowDisabled(record)" class="picked">锛堝凡鍏ュ嵎锛�</span>
+        </template>
+      </template>
+    </a-table>
+  </BasicModal>
+</template>
+
+<style scoped>
+.picker-tip {
+  margin-bottom: 12px;
+  color: #666;
+  font-size: 13px;
+}
+.picker-query {
+  margin-bottom: 12px;
+}
+.picked {
+  color: #999;
+  margin-left: 4px;
+}
+</style>

--
Gitblit v1.8.0