| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- <template>
- <div :class="prefixCls" class="relative flex h-full min-h-0 w-full flex-col overflow-hidden bg-light-400">
- <div class="case-header-bar flex items-center justify-between">
- <AppLogo :showTitle="true" />
- <div class="flex items-center gap-3">
- <span v-if="accountDisplayName" class="text-sm text-gray-600">{{ accountDisplayName }}</span>
- <Button type="text" size="small" @click="handleLogout">退出登录</Button>
- </div>
- </div>
- <div class="container relative mx-auto flex h-full min-h-0 flex-1 flex-col py-2">
- <div class="flex h-full min-h-0 flex-col">
- <div class="flex items-start justify-between gap-4">
- <div>
- <h1 class="text-2xl">案件管理</h1>
- <p class="mt-2 text-gray-600 dark:text-gray-400">管理您的所有案件</p>
- </div>
- </div>
- <div class="mb-6">
- <Input
- v-model:value="searchKeyword"
- placeholder="请输入案件名称进行搜索"
- size="large"
- allowClear
- autocomplete="off"
- @change="handleSearch"
- >
- <template #prefix>
- <SearchOutlined />
- </template>
- </Input>
- </div>
- <div class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden pt-4">
- <div class="grid grid-cols-1 gap-6 pb-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
- <Card
- class="case-card add-card hover:shadow-lg transition-shadow duration-300 cursor-pointer border-dashed"
- @click="handleAdd"
- >
- <div class="flex flex-col items-center justify-center py-8">
- <PlusOutlined class="text-4xl text-gray-400 mb-4" />
- <span class="text-gray-500 text-lg">点击创建案件</span>
- </div>
- </Card>
- <Card
- size="small"
- v-for="item in filteredCaseList"
- :key="item.id"
- class="case-card hover:shadow-lg transition-shadow duration-300"
- >
- <template #title>
- <div class="flex items-center justify-between">
- <span class="font-semibold text-lg truncate flex-1">{{ item.name }}</span>
- <div class="flex items-center">
- <Button type="link" size="small" color="primary" :skill="true" @click="handleEdit(item)">
- <template #icon>
- <EditOutlined />
- </template>
- </Button>
- <Button type="link" size="small" danger :skill="true" @click="handleDelete(item.id!)">
- <template #icon>
- <DeleteOutlined />
- </template>
- </Button>
- </div>
- </div>
- </template>
- <div>
- <div class="mb-3">
- <span class="text-gray-500 dark:text-gray-400 text-sm">案件类型:</span>
- <Tag :color="getTypeColor(item.type)">{{ item.type }}</Tag>
- </div>
- <div class="mb-3">
- <span class="text-gray-500 dark:text-gray-400 text-sm">创建时间:</span>
- <span class="text-gray-700 dark:text-gray-300 text-sm">{{ item.createTime }}</span>
- </div>
- <div>
- <span class="text-gray-500 dark:text-gray-400 text-sm">描述:</span>
- <span class="case-card__remark text-gray-700 dark:text-gray-300 text-sm" :title="item.remark || '-'">
- {{ item.remark || '-' }}
- </span>
- </div>
- </div>
- <template #actions>
- <div class="flex justify-center">
- <Button type="primary" :skill="true" @click="handleOpen(item.id!)">
- <template #icon>
- <FolderOpenOutlined />
- </template>
- 打开案件
- </Button>
- </div>
- </template>
- </Card>
- </div>
- </div>
- </div>
- </div>
- <div class="case-footer flex-shrink-0 border-t border-gray-200 bg-white px-6 py-2.5">
- <div class="mx-auto flex max-w-7xl items-center justify-between text-xs text-gray-400">
- <span>© 2026 中数矩阵(成都)科技有限公司</span>
- <span>联系电话:18908093240</span>
- </div>
- </div>
- <CaseForm @register="registerModal" @success="handleSuccess" @openCase="handleOpenCreatedCase" />
- <Modal
- centered
- v-model:open="deleteModalVisible"
- title="确认删除案件吗?"
- :confirmLoading="deleteLoading"
- @ok="confirmDelete"
- @cancel="cancelDelete"
- >
- <div class="py-4">
- <p class="m-0 text-sm text-gray-500">
- 删除后该案件的数据库文件与工作空间会被一并清理,且不可恢复。
- </p>
- </div>
- </Modal>
- </div>
- </template>
- <script lang="ts" setup>
- import { computed, onMounted, ref, toRaw } from 'vue';
- import { Card, Button, Tag, Input, Modal, Tooltip } from 'ant-design-vue';
- import {
- DeleteOutlined,
- FolderOpenOutlined,
- SearchOutlined,
- PlusOutlined,
- EditOutlined,
- } from '@ant-design/icons-vue';
- import { useDesign } from '@/core/hooks/web/useDesign';
- import { useMessage } from '@/core/hooks/web/useMessage';
- import { useUserStore } from '@/core/store/modules/case';
- import { useAccountStore } from '@/core/store/modules/account';
- import { useRouter } from 'vue-router';
- import { PageEnum } from '@/core/enums/pageEnum';
- import { useModal } from '@/core/components/Modal';
- import { AppLogo, AppSearch } from '@/core/components/Application';
- import { list, deleteById } from '@/plat/api/case/caseApi';
- import type { CaseInfo } from '@/plat/api/case/caseApi';
- import CaseForm from './CaseForm.vue';
- const { showMessage, notification, createConfirm } = useMessage();
- const { prefixCls } = useDesign('case-list');
- const userStore = useUserStore();
- const accountStore = useAccountStore();
- const router = useRouter();
- const accountDisplayName = computed(() => accountStore.getDisplayName);
- /**
- * 退出登录。
- *
- * <p>案件列表页没有 Layout Header,所以入口放在本页顶部。
- * 注意与「退出当前案件」(Header 里的 LogoutButton)语义不同:这里会注销 token。
- */
- function handleLogout() {
- createConfirm({
- iconType: 'warning',
- title: '提示',
- content: '确认退出登录?退出后需要重新输入账号密码。',
- onOk: async () => {
- userStore.resetState();
- await accountStore.logout();
- await router.replace({ path: PageEnum.BASE_AUTHORIZATION });
- },
- });
- }
- const caseList = ref<CaseInfo[]>([]);
- const searchKeyword = ref('');
- const deleteModalVisible = ref(false);
- const deleteLoading = ref(false);
- const currentDeleteId = ref<number>(0);
- const [registerModal, { openModal }] = useModal();
- async function loadCaseList() {
- const data = await list();
- caseList.value = data || [];
- }
- onMounted(() => {
- loadCaseList();
- });
- function getCaseSortTime(item: CaseInfo) {
- const rawTime = item.createTime?.trim();
- if (!rawTime) return 0;
- const timestamp = new Date(rawTime.replace(/-/g, '/')).getTime();
- return Number.isNaN(timestamp) ? 0 : timestamp;
- }
- const getLogoWidth = computed(() => {
- return {};
- });
- const filteredCaseList = computed(() => {
- const sortedList = [...caseList.value].sort((left, right) => {
- const timeDiff = getCaseSortTime(right) - getCaseSortTime(left);
- if (timeDiff !== 0) return timeDiff;
- return Number(right.id || 0) - Number(left.id || 0);
- });
- if (!searchKeyword.value) {
- return sortedList;
- }
- const keyword = searchKeyword.value.toLowerCase();
- return sortedList.filter((item) => item.name.toLowerCase().includes(keyword));
- });
- function handleSearch() {
- console.log('搜索关键词:', searchKeyword.value);
- }
- function getTypeColor(type: string): string {
- const colorMap: Record<string, string> = {
- 刑事案件: 'red',
- 民事案件: 'blue',
- 行政案件: 'orange',
- 经济案件: 'green',
- 知识产权: 'purple',
- 劳动争议: 'cyan',
- };
- return colorMap[type] || 'default';
- }
- async function handleDelete(id: number) {
- currentDeleteId.value = id;
- deleteModalVisible.value = true;
- }
- async function confirmDelete() {
- deleteLoading.value = true;
- try {
- // 案件已归属个人,后端不再校验案件密码
- await deleteById(currentDeleteId.value);
- const index = caseList.value.findIndex((item) => item.id === currentDeleteId.value);
- if (index > -1) {
- caseList.value.splice(index, 1);
- }
- deleteModalVisible.value = false;
- } catch (error) {
- // 错误提示由 defHttp 统一弹出
- } finally {
- deleteLoading.value = false;
- }
- }
- function cancelDelete() {
- deleteModalVisible.value = false;
- currentDeleteId.value = 0;
- }
- /** 打开案件:案件归属当前用户,无需再输案件密码 */
- async function handleOpen(id: number) {
- await doOpenCase(id);
- }
- async function doOpenCase(id: number) {
- try {
- const matchedCase = caseList.value.find((item) => Number(item.id) === Number(id));
- if (matchedCase?.name) {
- userStore.setPageCache('currentCaseName', matchedCase.name);
- }
- const res = await userStore.openCase(toRaw({ id }));
- if (!res?.name && matchedCase?.name) {
- userStore.setCaseInfo({
- ...(userStore.getCaseInfo || {}),
- ...res,
- id,
- name: matchedCase.name,
- });
- }
- notification.success({
- message: '打开成功',
- description: res?.name || matchedCase?.name || '',
- duration: 2,
- });
- return true;
- } catch (error: any) {
- const err: string = error?.toString?.() ?? '';
- if (error?.code === 'ECONNABORTED' && err.indexOf('timeout of') !== -1) {
- showMessage('请求超时');
- } else if (err.indexOf('Network Error') !== -1) {
- showMessage('网络异常');
- } else if (error?.code === 'ERR_BAD_RESPONSE') {
- showMessage('请求失败');
- }
- console.log(error);
- return false;
- }
- }
- function handleAdd() {
- openModal(true, { isEdit: false, caseList: toRaw(caseList.value) });
- }
- function handleEdit(item: CaseInfo) {
- openModal(true, { isEdit: true, ...item, caseList: toRaw(caseList.value) });
- }
- function handleSuccess() {
- loadCaseList();
- }
- async function handleOpenCreatedCase(payload: { id: number; onFinished?: (success: boolean) => void }) {
- const success = await doOpenCase(payload.id);
- payload.onFinished?.(success);
- }
- </script>
- <style lang="less">
- @prefix-cls: ~'jeesite-case-list';
- .@{prefix-cls} {
- min-height: 100%;
- display: flex;
- flex-direction: column;
- .case-header-bar {
- height: 48px;
- padding: 0 12px 0 0;
- background: #fff;
- -webkit-app-region: drag;
- .jeesite-app-logo {
- -webkit-app-region: no-drag;
- &__title {
- display: inline-flex;
- align-items: baseline;
- gap: 6px;
- font-size: 18px;
- font-weight: bold;
- color: #555;
- font-family: Arial, 'Microsoft YaHei', sans-serif;
- cursor: default;
- user-select: none;
- transition: all 0.5s;
- }
- }
- .jeesite-app-logo img {
- width: 32px;
- height: 32px;
- margin-right: 2px;
- }
- .jeesite-header-action-button {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- border-radius: 10px;
- color: #333;
- font-size: 18px;
- cursor: pointer;
- transition: background-color 0.2s ease;
- -webkit-app-region: no-drag;
- &:hover {
- background-color: #f6f6f6;
- }
- }
- }
- .case-card {
- border-radius: 12px;
- transition: all 0.3s ease;
- &:hover {
- transform: translateY(-4px);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
- }
- &.add-card {
- border: 2px dashed #d9d9d9;
- background: #fafafa;
- min-height: 229px;
- display: flex;
- align-items: center;
- justify-content: center;
- .ant-card-body {
- padding: 0;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- &:hover {
- border-color: #3f61ee;
- background: #e6f7ff;
- .anticon-plus {
- color: #3f61ee;
- }
- }
- }
- .ant-card-head {
- border-bottom: 1px solid #f0f0f0;
- padding: 16px;
- .ant-card-head-title {
- padding: 0;
- }
- }
- .ant-card-body {
- padding: 10px;
- }
- .case-card__remark {
- display: inline-block;
- max-width: calc(100% - 52px);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- vertical-align: bottom;
- }
- .ant-card-actions {
- border-top: 1px solid #f0f0f0;
- background: #fafafa;
- }
- }
- }
- html[data-theme='dark'] {
- .@{prefix-cls} {
- background-color: #141414;
- .case-card {
- background: #1f1f1f;
- border-color: #303030;
- &.add-card {
- border-color: #434343;
- background: #1f1f1f;
- .ant-card-body {
- padding: 0;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- &:hover {
- border-color: #177ddc;
- background: #111b26;
- }
- }
- .ant-card-head {
- border-bottom-color: #303030;
- }
- .ant-card-body {
- color: rgba(255, 255, 255, 0.85);
- }
- .ant-card-actions {
- border-top-color: #303030;
- background: #141414;
- }
- &:hover {
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
- }
- }
- }
- }
- </style>
|