|
@@ -0,0 +1,299 @@
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import type {
|
|
|
|
|
+ ContinuingEducationCategory,
|
|
|
|
|
+ ContinuingEducationVO,
|
|
|
|
|
+} from '#/api/outcome';
|
|
|
|
|
+
|
|
|
|
|
+import { computed, h, ref, shallowRef, triggerRef, watch } from 'vue';
|
|
|
|
|
+
|
|
|
|
|
+import { Page } from '@vben/common-ui';
|
|
|
|
|
+import { Plus } from '@vben/icons';
|
|
|
|
|
+
|
|
|
|
|
+import { watchDebounced } from '@vueuse/core';
|
|
|
|
|
+import {
|
|
|
|
|
+ Button,
|
|
|
|
|
+ Empty,
|
|
|
|
|
+ Input,
|
|
|
|
|
+ message,
|
|
|
|
|
+ Modal,
|
|
|
|
|
+ Pagination,
|
|
|
|
|
+ Select,
|
|
|
|
|
+ Spin,
|
|
|
|
|
+} from 'ant-design-vue';
|
|
|
|
|
+import { storeToRefs } from 'pinia';
|
|
|
|
|
+
|
|
|
|
|
+import { useShell } from '#/adapter/shell';
|
|
|
|
|
+import { invokeMethod } from '#/adapter/vxe-table/proxy/invoke-method';
|
|
|
|
|
+import {
|
|
|
|
|
+ CONTINUING_EDUCATION_CATEGORY_OPTIONS,
|
|
|
|
|
+ deleteContinuingEducationMethod,
|
|
|
|
|
+ getContinuingEducationCategoryLabel,
|
|
|
|
|
+ getContinuingEducationStatusLabel,
|
|
|
|
|
+ listContinuingEducationMethod,
|
|
|
|
|
+ unpublishContinuingEducationMethod,
|
|
|
|
|
+} from '#/api/outcome';
|
|
|
|
|
+import { useWorkroomStore } from '#/stores';
|
|
|
|
|
+
|
|
|
|
|
+import ContinuingEducationCard from './components/ContinuingEducationCard.vue';
|
|
|
|
|
+import ContinuingEducationEdit from './modules/ContinuingEducationEdit.vue';
|
|
|
|
|
+
|
|
|
|
|
+const workroomStore = useWorkroomStore();
|
|
|
|
|
+const { workroomId } = storeToRefs(workroomStore);
|
|
|
|
|
+
|
|
|
|
|
+const keyword = ref('');
|
|
|
|
|
+const searchKeyword = ref('');
|
|
|
|
|
+const category = ref<'' | ContinuingEducationCategory>('');
|
|
|
|
|
+const pageNum = ref(1);
|
|
|
|
|
+const pageSize = ref(20);
|
|
|
|
|
+const loading = ref(false);
|
|
|
|
|
+const pageData = ref<{ items: ContinuingEducationVO[]; total: number }>({
|
|
|
|
|
+ total: 0,
|
|
|
|
|
+ items: [],
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const deletingIds = shallowRef(new Set<string>());
|
|
|
|
|
+const unpublishingIds = shallowRef(new Set<string>());
|
|
|
|
|
+
|
|
|
|
|
+const categoryOptions = [
|
|
|
|
|
+ { label: '全部类别', value: '' },
|
|
|
|
|
+ ...CONTINUING_EDUCATION_CATEGORY_OPTIONS,
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+async function loadList() {
|
|
|
|
|
+ if (!workroomId.value) {
|
|
|
|
|
+ pageData.value = { total: 0, items: [] };
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ pageData.value = await invokeMethod(
|
|
|
|
|
+ listContinuingEducationMethod(pageNum.value, pageSize.value, {
|
|
|
|
|
+ $filters: [],
|
|
|
|
|
+ $sorts: [],
|
|
|
|
|
+ keyword: searchKeyword.value || undefined,
|
|
|
|
|
+ workroomId: workroomId.value,
|
|
|
|
|
+ category: category.value || undefined,
|
|
|
|
|
+ }),
|
|
|
|
|
+ { force: true },
|
|
|
|
|
+ );
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const items = computed(() => pageData.value.items);
|
|
|
|
|
+const total = computed(() => pageData.value.total);
|
|
|
|
|
+
|
|
|
|
|
+const [Edit, editApi] = useShell('modal', {
|
|
|
|
|
+ connectedComponent: ContinuingEducationEdit,
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+function isDeleting(id?: string) {
|
|
|
|
|
+ return id ? deletingIds.value.has(id) : false;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function isUnpublishing(id?: string) {
|
|
|
|
|
+ return id ? unpublishingIds.value.has(id) : false;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function setDeleting(id: string, value: boolean) {
|
|
|
|
|
+ if (value) {
|
|
|
|
|
+ deletingIds.value.add(id);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ deletingIds.value.delete(id);
|
|
|
|
|
+ }
|
|
|
|
|
+ triggerRef(deletingIds);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function setUnpublishing(id: string, value: boolean) {
|
|
|
|
|
+ if (value) {
|
|
|
|
|
+ unpublishingIds.value.add(id);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ unpublishingIds.value.delete(id);
|
|
|
|
|
+ }
|
|
|
|
|
+ triggerRef(unpublishingIds);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function openCreate() {
|
|
|
|
|
+ const result = await editApi
|
|
|
|
|
+ .setData({
|
|
|
|
|
+ workroomId: workroomId.value,
|
|
|
|
|
+ category: 'clinical_skill',
|
|
|
|
|
+ } as ContinuingEducationVO)
|
|
|
|
|
+ .open<{ id?: string }>(Promise.withResolvers());
|
|
|
|
|
+ if (result?.id) {
|
|
|
|
|
+ pageNum.value = 1;
|
|
|
|
|
+ await loadList();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatDate(value?: string) {
|
|
|
|
|
+ if (!value) return '长期';
|
|
|
|
|
+ return String(value).slice(0, 10);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openView(row: ContinuingEducationVO) {
|
|
|
|
|
+ Modal.info({
|
|
|
|
|
+ title: row.projectName,
|
|
|
|
|
+ width: 640,
|
|
|
|
|
+ okText: '关闭',
|
|
|
|
|
+ content: h('div', { class: 'pt-2 text-sm leading-7 text-foreground/85' }, [
|
|
|
|
|
+ h('p', `项目编号:${row.projectNumber ?? '-'}`),
|
|
|
|
|
+ h(
|
|
|
|
|
+ 'p',
|
|
|
|
|
+ `继续教育分类:${getContinuingEducationCategoryLabel(row.category)}`,
|
|
|
|
|
+ ),
|
|
|
|
|
+ h('p', `学分:${row.credits ?? '-'}分`),
|
|
|
|
|
+ h('p', `项目负责人:${row.leader ?? '-'}`),
|
|
|
|
|
+ h('p', `开始日期:${formatDate(row.startDate)}`),
|
|
|
|
|
+ h('p', `结束日期:${formatDate(row.endDate)}`),
|
|
|
|
|
+ h(
|
|
|
|
|
+ 'p',
|
|
|
|
|
+ `报名情况:${row.registeredCount ?? 0}/${row.maxParticipants ?? 0}人`,
|
|
|
|
|
+ ),
|
|
|
|
|
+ h('p', `报名截止:${formatDate(row.registrationDeadline)}`),
|
|
|
|
|
+ h('p', `状态:${getContinuingEducationStatusLabel(row.status)}`),
|
|
|
|
|
+ ]),
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openManageRegistration(row: ContinuingEducationVO) {
|
|
|
|
|
+ message.info(`管理报名:${row.projectName ?? ''}`);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleDelete(row: ContinuingEducationVO) {
|
|
|
|
|
+ if (!row.id || isDeleting(row.id)) return;
|
|
|
|
|
+ setDeleting(row.id, true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ await invokeMethod(deleteContinuingEducationMethod(row), { force: true });
|
|
|
|
|
+ message.success('删除成功');
|
|
|
|
|
+ if (items.value.length <= 1 && pageNum.value > 1) {
|
|
|
|
|
+ pageNum.value -= 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ await loadList();
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setDeleting(row.id, false);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleUnpublish(row: ContinuingEducationVO) {
|
|
|
|
|
+ if (!row.id || isUnpublishing(row.id)) return;
|
|
|
|
|
+ setUnpublishing(row.id, true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ await invokeMethod(unpublishContinuingEducationMethod(row), {
|
|
|
|
|
+ force: true,
|
|
|
|
|
+ });
|
|
|
|
|
+ message.success('已取消发布');
|
|
|
|
|
+ await loadList();
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setUnpublishing(row.id, false);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function applySearch(value: string) {
|
|
|
|
|
+ const next = value.trim();
|
|
|
|
|
+ if (next === searchKeyword.value) return;
|
|
|
|
|
+ searchKeyword.value = next;
|
|
|
|
|
+ pageNum.value = 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+watchDebounced(keyword, applySearch, { debounce: 300 });
|
|
|
|
|
+
|
|
|
|
|
+function onSearch() {
|
|
|
|
|
+ applySearch(keyword.value);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onPageChange(page: number, size: number) {
|
|
|
|
|
+ pageNum.value = page;
|
|
|
|
|
+ pageSize.value = size;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+watch(category, () => {
|
|
|
|
|
+ pageNum.value = 1;
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+watch(workroomId, () => {
|
|
|
|
|
+ pageNum.value = 1;
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ [pageNum, pageSize, searchKeyword, category, workroomId],
|
|
|
|
|
+ () => {
|
|
|
|
|
+ void loadList();
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: true },
|
|
|
|
|
+);
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<template>
|
|
|
|
|
+ <Page auto-content-height title="继续教育项目管理">
|
|
|
|
|
+ <Edit />
|
|
|
|
|
+
|
|
|
|
|
+ <template #extra>
|
|
|
|
|
+ <Button type="primary" @click="openCreate()">
|
|
|
|
|
+ <Plus class="size-4" />
|
|
|
|
|
+ 创建项目
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex h-full flex-col gap-4">
|
|
|
|
|
+ <div class="flex flex-wrap items-center gap-3">
|
|
|
|
|
+ <Input.Search
|
|
|
|
|
+ v-model:value="keyword"
|
|
|
|
|
+ allow-clear
|
|
|
|
|
+ class="min-w-[240px] flex-1"
|
|
|
|
|
+ placeholder="搜索项目名称、编号或组织者..."
|
|
|
|
|
+ @search="onSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Select
|
|
|
|
|
+ v-model:value="category"
|
|
|
|
|
+ :options="categoryOptions"
|
|
|
|
|
+ class="w-36"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Spin :spinning="loading">
|
|
|
|
|
+ <div class="min-h-48">
|
|
|
|
|
+ <Empty
|
|
|
|
|
+ v-if="!loading && items.length === 0"
|
|
|
|
|
+ :image="Empty.PRESENTED_IMAGE_SIMPLE"
|
|
|
|
|
+ description="暂无继续教育项目"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Button type="primary" @click="openCreate()">创建项目</Button>
|
|
|
|
|
+ </Empty>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-else class="flex flex-col gap-4">
|
|
|
|
|
+ <ContinuingEducationCard
|
|
|
|
|
+ v-for="item in items"
|
|
|
|
|
+ :key="item.id"
|
|
|
|
|
+ :data="item"
|
|
|
|
|
+ :deleting="isDeleting(item.id)"
|
|
|
|
|
+ :unpublishing="isUnpublishing(item.id)"
|
|
|
|
|
+ @manage-registration="openManageRegistration"
|
|
|
|
|
+ @view="openView"
|
|
|
|
|
+ @delete="handleDelete"
|
|
|
|
|
+ @unpublish="handleUnpublish"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Spin>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="total > 0"
|
|
|
|
|
+ class="mt-auto flex flex-wrap items-center justify-between gap-3 border-t pt-4"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="text-sm text-foreground/70">共 {{ total }} 条记录</span>
|
|
|
|
|
+ <Pagination
|
|
|
|
|
+ :current="pageNum"
|
|
|
|
|
+ :page-size="pageSize"
|
|
|
|
|
+ :total="total"
|
|
|
|
|
+ :show-size-changer="false"
|
|
|
|
|
+ show-less-items
|
|
|
|
|
+ @change="onPageChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Page>
|
|
|
|
|
+</template>
|