|
|
@@ -0,0 +1,284 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import type { WinawardLevel, WinawardVO } 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 {
|
|
|
+ deleteWinawardMethod,
|
|
|
+ getWinawardLevelLabel,
|
|
|
+ listWinawardMethod,
|
|
|
+ WINAWARD_LEVEL_OPTIONS,
|
|
|
+} from '#/api/outcome';
|
|
|
+import { useWorkroomStore } from '#/stores';
|
|
|
+
|
|
|
+import WinawardCard from './components/WinawardCard.vue';
|
|
|
+import WinawardEdit from './modules/WinawardEdit.vue';
|
|
|
+
|
|
|
+const workroomStore = useWorkroomStore();
|
|
|
+const { workroomId } = storeToRefs(workroomStore);
|
|
|
+
|
|
|
+const keyword = ref('');
|
|
|
+const searchKeyword = ref('');
|
|
|
+const level = ref<'' | WinawardLevel>('');
|
|
|
+const year = ref('');
|
|
|
+const pageNum = ref(1);
|
|
|
+const pageSize = ref(20);
|
|
|
+const loading = ref(false);
|
|
|
+const pageData = ref<{ items: WinawardVO[]; total: number }>({
|
|
|
+ total: 0,
|
|
|
+ items: [],
|
|
|
+});
|
|
|
+
|
|
|
+const deletingIds = shallowRef(new Set<string>());
|
|
|
+
|
|
|
+const levelOptions = [
|
|
|
+ { label: '全部级别', value: '' },
|
|
|
+ ...WINAWARD_LEVEL_OPTIONS.map(({ label, value }) => ({ label, value })),
|
|
|
+];
|
|
|
+
|
|
|
+const yearOptions = computed(() => {
|
|
|
+ const currentYear = new Date().getFullYear();
|
|
|
+ const years = [{ label: '全部年份', value: '' }];
|
|
|
+ for (let y = currentYear; y >= currentYear - 10; y -= 1) {
|
|
|
+ years.push({ label: `${y}年`, value: String(y) });
|
|
|
+ }
|
|
|
+ return years;
|
|
|
+});
|
|
|
+
|
|
|
+async function loadList() {
|
|
|
+ if (!workroomId.value) {
|
|
|
+ pageData.value = { total: 0, items: [] };
|
|
|
+ loading.value = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ pageData.value = await invokeMethod(
|
|
|
+ listWinawardMethod(pageNum.value, pageSize.value, {
|
|
|
+ $filters: [],
|
|
|
+ $sorts: [],
|
|
|
+ keyword: searchKeyword.value || undefined,
|
|
|
+ workroomId: workroomId.value,
|
|
|
+ level: level.value || undefined,
|
|
|
+ year: year.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: WinawardEdit,
|
|
|
+});
|
|
|
+
|
|
|
+function isDeleting(id?: string) {
|
|
|
+ return id ? deletingIds.value.has(id) : false;
|
|
|
+}
|
|
|
+
|
|
|
+function setDeleting(id: string, value: boolean) {
|
|
|
+ if (value) {
|
|
|
+ deletingIds.value.add(id);
|
|
|
+ } else {
|
|
|
+ deletingIds.value.delete(id);
|
|
|
+ }
|
|
|
+ triggerRef(deletingIds);
|
|
|
+}
|
|
|
+
|
|
|
+async function openCreate() {
|
|
|
+ const result = await editApi
|
|
|
+ .setData({
|
|
|
+ workroomId: workroomId.value,
|
|
|
+ level: 'national',
|
|
|
+ } as WinawardVO)
|
|
|
+ .open<{ id?: string }>(Promise.withResolvers());
|
|
|
+ if (result?.id) {
|
|
|
+ pageNum.value = 1;
|
|
|
+ await loadList();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function openEdit(row: WinawardVO) {
|
|
|
+ const result = await editApi
|
|
|
+ .setData(row)
|
|
|
+ .open<{ id?: string }>(Promise.withResolvers());
|
|
|
+ if (result?.id) {
|
|
|
+ await loadList();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openView(row: WinawardVO) {
|
|
|
+ if (row.fileUrl) {
|
|
|
+ window.open(row.fileUrl, '_blank');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const meta = [
|
|
|
+ getWinawardLevelLabel(row.level),
|
|
|
+ row.awardee ? `获奖人:${row.awardee}` : '',
|
|
|
+ row.issuer ? `颁发机构:${row.issuer}` : '',
|
|
|
+ row.awardDate ? `获奖时间:${row.awardDate}` : '',
|
|
|
+ ]
|
|
|
+ .filter(Boolean)
|
|
|
+ .join(' · ');
|
|
|
+
|
|
|
+ Modal.info({
|
|
|
+ title: row.name,
|
|
|
+ width: 640,
|
|
|
+ okText: '关闭',
|
|
|
+ content: h('div', { class: 'pt-2' }, [
|
|
|
+ row.project
|
|
|
+ ? h(
|
|
|
+ 'p',
|
|
|
+ { class: 'mb-2 text-sm text-foreground/70' },
|
|
|
+ `项目:${row.project}`,
|
|
|
+ )
|
|
|
+ : null,
|
|
|
+ meta ? h('p', { class: 'mb-3 text-sm text-foreground/70' }, meta) : null,
|
|
|
+ h(
|
|
|
+ 'div',
|
|
|
+ { class: 'whitespace-pre-wrap text-sm leading-7 text-foreground/85' },
|
|
|
+ row.profile || '暂无简介',
|
|
|
+ ),
|
|
|
+ ]),
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+async function handleDelete(row: WinawardVO) {
|
|
|
+ if (!row.id || isDeleting(row.id)) return;
|
|
|
+ setDeleting(row.id, true);
|
|
|
+ try {
|
|
|
+ await invokeMethod(deleteWinawardMethod(row), { force: true });
|
|
|
+ message.success('删除成功');
|
|
|
+ if (items.value.length <= 1 && pageNum.value > 1) {
|
|
|
+ pageNum.value -= 1;
|
|
|
+ }
|
|
|
+ await loadList();
|
|
|
+ } finally {
|
|
|
+ setDeleting(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([level, year], () => {
|
|
|
+ pageNum.value = 1;
|
|
|
+});
|
|
|
+
|
|
|
+watch(workroomId, () => {
|
|
|
+ pageNum.value = 1;
|
|
|
+});
|
|
|
+
|
|
|
+watch(
|
|
|
+ [pageNum, pageSize, searchKeyword, level, year, 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="level" :options="levelOptions" class="w-36" />
|
|
|
+ <Select v-model:value="year" :options="yearOptions" 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">
|
|
|
+ <WinawardCard
|
|
|
+ v-for="item in items"
|
|
|
+ :key="item.id"
|
|
|
+ :data="item"
|
|
|
+ :deleting="isDeleting(item.id)"
|
|
|
+ @edit="openEdit"
|
|
|
+ @view="openView"
|
|
|
+ @delete="handleDelete"
|
|
|
+ />
|
|
|
+ </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>
|