case.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. <template>
  2. <div :class="prefixCls" class="relative flex h-full min-h-0 w-full flex-col overflow-hidden bg-light-400">
  3. <div class="case-header-bar flex items-center justify-between">
  4. <AppLogo :showTitle="true" />
  5. <div class="flex items-center">
  6. <WindowMinimize />
  7. <WindowMaximize />
  8. <WindowClose />
  9. </div>
  10. </div>
  11. <div class="container relative mx-auto flex h-full min-h-0 flex-1 flex-col py-2">
  12. <div class="flex h-full min-h-0 flex-col">
  13. <div class="flex items-start justify-between gap-4">
  14. <div>
  15. <h1 class="text-2xl">案件管理</h1>
  16. <p class="mt-2 text-gray-600 dark:text-gray-400">管理您的所有案件</p>
  17. </div>
  18. </div>
  19. <div class="mb-6">
  20. <Input
  21. v-model:value="searchKeyword"
  22. placeholder="请输入案件名称进行搜索"
  23. size="large"
  24. allowClear
  25. autocomplete="off"
  26. @change="handleSearch"
  27. >
  28. <template #prefix>
  29. <SearchOutlined />
  30. </template>
  31. </Input>
  32. </div>
  33. <div class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden pt-4">
  34. <div class="grid grid-cols-1 gap-6 pb-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
  35. <Card
  36. class="case-card add-card hover:shadow-lg transition-shadow duration-300 cursor-pointer border-dashed"
  37. @click="handleAdd"
  38. >
  39. <div class="flex flex-col items-center justify-center py-8">
  40. <PlusOutlined class="text-4xl text-gray-400 mb-4" />
  41. <span class="text-gray-500 text-lg">点击创建案件</span>
  42. </div>
  43. </Card>
  44. <Card
  45. size="small"
  46. v-for="item in filteredCaseList"
  47. :key="item.id"
  48. class="case-card hover:shadow-lg transition-shadow duration-300"
  49. >
  50. <template #title>
  51. <div class="flex items-center justify-between">
  52. <span class="font-semibold text-lg truncate flex-1">{{ item.name }}</span>
  53. <div class="flex items-center">
  54. <Button type="link" size="small" color="primary" :skill="true" @click="handleEdit(item)">
  55. <template #icon>
  56. <EditOutlined />
  57. </template>
  58. </Button>
  59. <Button type="link" size="small" danger :skill="true" @click="handleDelete(item.id!)">
  60. <template #icon>
  61. <DeleteOutlined />
  62. </template>
  63. </Button>
  64. </div>
  65. </div>
  66. </template>
  67. <div>
  68. <div class="mb-3">
  69. <span class="text-gray-500 dark:text-gray-400 text-sm">案件类型:</span>
  70. <Tag :color="getTypeColor(item.type)">{{ item.type }}</Tag>
  71. </div>
  72. <div class="mb-3">
  73. <span class="text-gray-500 dark:text-gray-400 text-sm">创建时间:</span>
  74. <span class="text-gray-700 dark:text-gray-300 text-sm">{{ item.createTime }}</span>
  75. </div>
  76. <div>
  77. <span class="text-gray-500 dark:text-gray-400 text-sm">描述:</span>
  78. <span class="case-card__remark text-gray-700 dark:text-gray-300 text-sm" :title="item.remark || '-'">
  79. {{ item.remark || '-' }}
  80. </span>
  81. </div>
  82. </div>
  83. <template #actions>
  84. <div class="flex justify-center">
  85. <Button type="primary" :skill="true" @click="handleOpen(item.id!)">
  86. <template #icon>
  87. <FolderOpenOutlined />
  88. </template>
  89. 打开案件
  90. </Button>
  91. </div>
  92. </template>
  93. </Card>
  94. </div>
  95. </div>
  96. </div>
  97. </div>
  98. <div class="case-footer flex-shrink-0 border-t border-gray-200 bg-white px-6 py-2.5">
  99. <div class="mx-auto flex max-w-7xl items-center justify-between text-xs text-gray-400">
  100. <span>© 2026 重庆恒时乐科科技有限公司 | 向量探索团队</span>
  101. <span>联系电话:18908093240</span>
  102. </div>
  103. </div>
  104. <CaseForm @register="registerModal" @success="handleSuccess" @openCase="handleOpenCreatedCase" />
  105. <Modal
  106. centered
  107. v-model:open="deleteModalVisible"
  108. title="确认删除案件吗?"
  109. @ok="confirmDelete"
  110. @cancel="cancelDelete"
  111. :okButtonProps="{ disabled: !deletePassword }"
  112. >
  113. <div class="py-4">
  114. <InputPassword
  115. v-model:value="deletePassword"
  116. placeholder="请输入案件密码"
  117. autocomplete="new-password"
  118. @pressEnter="confirmDelete"
  119. />
  120. </div>
  121. </Modal>
  122. <Modal
  123. centered
  124. v-model:open="openPwdModalVisible"
  125. title="请输入案件密码"
  126. :confirmLoading="openPwdLoading"
  127. @ok="confirmOpenCase"
  128. @cancel="cancelOpenCase"
  129. :okButtonProps="{ disabled: !openCasePassword }"
  130. >
  131. <div class="py-4">
  132. <InputPassword
  133. v-model:value="openCasePassword"
  134. placeholder="请输入案件密码"
  135. autocomplete="new-password"
  136. @pressEnter="confirmOpenCase"
  137. style="width: 100%"
  138. />
  139. </div>
  140. </Modal>
  141. </div>
  142. </template>
  143. <script lang="ts" setup>
  144. import { computed, onMounted, ref, toRaw } from 'vue';
  145. import { Card, Button, Tag, Input, InputPassword, Modal, Tooltip } from 'ant-design-vue';
  146. import {
  147. DeleteOutlined,
  148. FolderOpenOutlined,
  149. SearchOutlined,
  150. PlusOutlined,
  151. EditOutlined,
  152. } from '@ant-design/icons-vue';
  153. import { useDesign } from '@jeesite/core/hooks/web/useDesign';
  154. import { useMessage } from '@jeesite/core/hooks/web/useMessage';
  155. import { useUserStore } from '@jeesite/core/store/modules/case';
  156. import { useModal } from '@jeesite/core/components/Modal';
  157. import { AppLogo, AppSearch } from '@jeesite/core/components/Application';
  158. import { WindowMinimize, WindowMaximize, WindowClose } from '@jeesite/core/layouts/default/header/components';
  159. import { list, deleteById } from '@jeesite/plat/api/case/caseApi';
  160. import type { CaseInfo } from '@jeesite/plat/api/case/caseApi';
  161. import CaseForm from './CaseForm.vue';
  162. const { showMessage, notification } = useMessage();
  163. const { prefixCls } = useDesign('case-list');
  164. const userStore = useUserStore();
  165. const caseList = ref<CaseInfo[]>([]);
  166. const searchKeyword = ref('');
  167. const deletePassword = ref('');
  168. const deleteModalVisible = ref(false);
  169. const currentDeleteId = ref<number>(0);
  170. const openPwdModalVisible = ref(false);
  171. const openPwdLoading = ref(false);
  172. const openCasePassword = ref('');
  173. const currentOpeningCaseId = ref<number>(0);
  174. const [registerModal, { openModal }] = useModal();
  175. async function loadCaseList() {
  176. const data = await list();
  177. caseList.value = data || [];
  178. }
  179. onMounted(() => {
  180. loadCaseList();
  181. });
  182. function getCaseSortTime(item: CaseInfo) {
  183. const rawTime = item.createTime?.trim();
  184. if (!rawTime) return 0;
  185. const timestamp = new Date(rawTime.replace(/-/g, '/')).getTime();
  186. return Number.isNaN(timestamp) ? 0 : timestamp;
  187. }
  188. const getLogoWidth = computed(() => {
  189. return {};
  190. });
  191. const filteredCaseList = computed(() => {
  192. const sortedList = [...caseList.value].sort((left, right) => {
  193. const timeDiff = getCaseSortTime(right) - getCaseSortTime(left);
  194. if (timeDiff !== 0) return timeDiff;
  195. return Number(right.id || 0) - Number(left.id || 0);
  196. });
  197. if (!searchKeyword.value) {
  198. return sortedList;
  199. }
  200. const keyword = searchKeyword.value.toLowerCase();
  201. return sortedList.filter((item) => item.name.toLowerCase().includes(keyword));
  202. });
  203. function handleSearch() {
  204. console.log('搜索关键词:', searchKeyword.value);
  205. }
  206. function getTypeColor(type: string): string {
  207. const colorMap: Record<string, string> = {
  208. 刑事案件: 'red',
  209. 民事案件: 'blue',
  210. 行政案件: 'orange',
  211. 经济案件: 'green',
  212. 知识产权: 'purple',
  213. 劳动争议: 'cyan',
  214. };
  215. return colorMap[type] || 'default';
  216. }
  217. async function handleDelete(id: number) {
  218. currentDeleteId.value = id;
  219. deletePassword.value = '';
  220. deleteModalVisible.value = true;
  221. }
  222. async function confirmDelete() {
  223. if (!deletePassword.value) {
  224. showMessage('请输入案件密码');
  225. return;
  226. }
  227. try {
  228. await deleteById(currentDeleteId.value, deletePassword.value);
  229. const index = caseList.value.findIndex((item) => item.id === currentDeleteId.value);
  230. if (index > -1) {
  231. caseList.value.splice(index, 1);
  232. }
  233. deleteModalVisible.value = false;
  234. } catch (error) {}
  235. }
  236. function cancelDelete() {
  237. deleteModalVisible.value = false;
  238. deletePassword.value = '';
  239. currentDeleteId.value = 0;
  240. }
  241. function handleOpen(id: number) {
  242. currentOpeningCaseId.value = id;
  243. openCasePassword.value = '';
  244. openPwdModalVisible.value = true;
  245. }
  246. async function confirmOpenCase() {
  247. if (!openCasePassword.value) {
  248. showMessage('请输入案件密码');
  249. return;
  250. }
  251. openPwdLoading.value = true;
  252. await doOpenCase(currentOpeningCaseId.value, openCasePassword.value);
  253. openPwdModalVisible.value = false;
  254. openPwdLoading.value = false;
  255. }
  256. function cancelOpenCase() {
  257. openPwdModalVisible.value = false;
  258. openCasePassword.value = '';
  259. currentOpeningCaseId.value = 0;
  260. }
  261. async function doOpenCase(id: number, pwd: string) {
  262. try {
  263. const matchedCase = caseList.value.find((item) => Number(item.id) === Number(id));
  264. if (matchedCase?.name) {
  265. userStore.setPageCache('currentCaseName', matchedCase.name);
  266. }
  267. const res = await userStore.openCase(toRaw({ pwd, id }));
  268. if (!res?.name && matchedCase?.name) {
  269. userStore.setCaseInfo({
  270. ...(userStore.getCaseInfo || {}),
  271. ...res,
  272. id,
  273. name: matchedCase.name,
  274. });
  275. }
  276. notification.success({
  277. message: '打开成功',
  278. description: res?.name || matchedCase?.name || '',
  279. duration: 2,
  280. });
  281. return true;
  282. } catch (error: any) {
  283. const err: string = error?.toString?.() ?? '';
  284. if (error?.code === 'ECONNABORTED' && err.indexOf('timeout of') !== -1) {
  285. showMessage('请求超时');
  286. } else if (err.indexOf('Network Error') !== -1) {
  287. showMessage('网络异常');
  288. } else if (error?.code === 'ERR_BAD_RESPONSE') {
  289. showMessage('请求失败');
  290. }
  291. console.log(error);
  292. return false;
  293. }
  294. }
  295. function handleAdd() {
  296. openModal(true, { isEdit: false, caseList: toRaw(caseList.value) });
  297. }
  298. function handleEdit(item: CaseInfo) {
  299. openModal(true, { isEdit: true, ...item, caseList: toRaw(caseList.value) });
  300. }
  301. function handleSuccess() {
  302. loadCaseList();
  303. }
  304. async function handleOpenCreatedCase(payload: { id: number; pwd: string; onFinished?: (success: boolean) => void }) {
  305. const success = await doOpenCase(payload.id, payload.pwd);
  306. payload.onFinished?.(success);
  307. }
  308. </script>
  309. <style lang="less">
  310. @prefix-cls: ~'jeesite-case-list';
  311. .@{prefix-cls} {
  312. min-height: 100%;
  313. display: flex;
  314. flex-direction: column;
  315. .case-header-bar {
  316. height: 48px;
  317. padding: 0 12px 0 0;
  318. background: #fff;
  319. -webkit-app-region: drag;
  320. .jeesite-app-logo {
  321. -webkit-app-region: no-drag;
  322. &__title {
  323. display: inline-flex;
  324. align-items: baseline;
  325. gap: 6px;
  326. font-size: 18px;
  327. font-weight: bold;
  328. color: #555;
  329. font-family: Arial, 'Microsoft YaHei', sans-serif;
  330. cursor: default;
  331. user-select: none;
  332. transition: all 0.5s;
  333. }
  334. }
  335. .jeesite-app-logo img {
  336. width: 32px;
  337. height: 32px;
  338. margin-right: 2px;
  339. }
  340. .jeesite-header-action-button {
  341. display: inline-flex;
  342. align-items: center;
  343. justify-content: center;
  344. width: 36px;
  345. height: 36px;
  346. border-radius: 10px;
  347. color: #333;
  348. font-size: 18px;
  349. cursor: pointer;
  350. transition: background-color 0.2s ease;
  351. -webkit-app-region: no-drag;
  352. &:hover {
  353. background-color: #f6f6f6;
  354. }
  355. }
  356. }
  357. .case-card {
  358. border-radius: 12px;
  359. transition: all 0.3s ease;
  360. &:hover {
  361. transform: translateY(-4px);
  362. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  363. }
  364. &.add-card {
  365. border: 2px dashed #d9d9d9;
  366. background: #fafafa;
  367. min-height: 229px;
  368. display: flex;
  369. align-items: center;
  370. justify-content: center;
  371. .ant-card-body {
  372. padding: 0;
  373. width: 100%;
  374. display: flex;
  375. align-items: center;
  376. justify-content: center;
  377. }
  378. &:hover {
  379. border-color: #3f61ee;
  380. background: #e6f7ff;
  381. .anticon-plus {
  382. color: #3f61ee;
  383. }
  384. }
  385. }
  386. .ant-card-head {
  387. border-bottom: 1px solid #f0f0f0;
  388. padding: 16px;
  389. .ant-card-head-title {
  390. padding: 0;
  391. }
  392. }
  393. .ant-card-body {
  394. padding: 10px;
  395. }
  396. .case-card__remark {
  397. display: inline-block;
  398. max-width: calc(100% - 52px);
  399. overflow: hidden;
  400. text-overflow: ellipsis;
  401. white-space: nowrap;
  402. vertical-align: bottom;
  403. }
  404. .ant-card-actions {
  405. border-top: 1px solid #f0f0f0;
  406. background: #fafafa;
  407. }
  408. }
  409. }
  410. html[data-theme='dark'] {
  411. .@{prefix-cls} {
  412. background-color: #141414;
  413. .case-card {
  414. background: #1f1f1f;
  415. border-color: #303030;
  416. &.add-card {
  417. border-color: #434343;
  418. background: #1f1f1f;
  419. .ant-card-body {
  420. padding: 0;
  421. width: 100%;
  422. display: flex;
  423. align-items: center;
  424. justify-content: center;
  425. }
  426. &:hover {
  427. border-color: #177ddc;
  428. background: #111b26;
  429. }
  430. }
  431. .ant-card-head {
  432. border-bottom-color: #303030;
  433. }
  434. .ant-card-body {
  435. color: rgba(255, 255, 255, 0.85);
  436. }
  437. .ant-card-actions {
  438. border-top-color: #303030;
  439. background: #141414;
  440. }
  441. &:hover {
  442. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  443. }
  444. }
  445. }
  446. }
  447. </style>