@spacecorexdev/core 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/package.json +2 -36
  2. package/src/api/_base.ts +243 -0
  3. package/src/api/index.ts +17 -0
  4. package/src/api/project.ts +1345 -0
  5. package/src/api/user.ts +1174 -0
  6. package/src/contract.ts +50 -0
  7. package/src/hooks/index.ts +2 -0
  8. package/{dist/hooks/index.js → src/hooks/useLongPress.ts} +53 -49
  9. package/src/hooks/useOnlineStatus.ts +22 -0
  10. package/src/index.ts +14 -0
  11. package/src/types/id.ts +2 -0
  12. package/src/types/index.ts +5 -0
  13. package/src/types/project/base.ts +63 -0
  14. package/src/types/project/contract.ts +79 -0
  15. package/src/types/project/contractStatus.tsx +15 -0
  16. package/src/types/project/index.ts +5 -0
  17. package/src/types/project/member.ts +46 -0
  18. package/src/types/project/progress.ts +43 -0
  19. package/src/types/projectRole.ts +17 -0
  20. package/src/types/theme.ts +1 -0
  21. package/src/types/user/index.ts +1 -0
  22. package/src/types/user/role.ts +7 -0
  23. package/src/ui/CommentTimeline/index.tsx +330 -0
  24. package/src/ui/FileManager/FileContextMenu.tsx +628 -0
  25. package/src/ui/FileManager/FileGridView.tsx +529 -0
  26. package/src/ui/FileManager/FileTableView.tsx +405 -0
  27. package/src/ui/FileManager/FileToolbar.tsx +119 -0
  28. package/src/ui/FileManager/FileVersionHistoryModal.tsx +175 -0
  29. package/src/ui/FileManager/MoveCopyToProjectModal.tsx +119 -0
  30. package/src/ui/FileManager/ShareToWechatModal.tsx +419 -0
  31. package/src/ui/FileManager/hooks/useFileManagerActions.ts +2532 -0
  32. package/src/ui/FileManager/hooks/useFileManagerState.ts +419 -0
  33. package/src/ui/FileManager/index.tsx +580 -0
  34. package/src/ui/FileManager/projectFileManagerApi.ts +57 -0
  35. package/src/ui/FileManager/types.ts +184 -0
  36. package/src/ui/FileManager/utils/fileDeltaWorker.ts +193 -0
  37. package/src/ui/FileManager/utils/fileTree.ts +178 -0
  38. package/src/ui/FileManager/utils/fileUpload.ts +714 -0
  39. package/src/ui/FileManager/utils/fileUtils.ts +65 -0
  40. package/src/ui/InvoiceRecordsList/index.tsx +813 -0
  41. package/src/ui/MyFileSelectorModal.tsx +261 -0
  42. package/src/ui/ProjectSelector/index.tsx +472 -0
  43. package/src/ui/ProjectSelector/projectCache.ts +40 -0
  44. package/src/ui/RoleSelector/index.tsx +473 -0
  45. package/src/ui/RoleSelector/roleCache.ts +40 -0
  46. package/src/ui/UIAlert.tsx +399 -0
  47. package/src/ui/UIAlertExample.tsx +129 -0
  48. package/src/ui/UIConfirm.tsx +335 -0
  49. package/src/ui/UIConfirmExample.tsx +165 -0
  50. package/src/ui/UIDatePicker.tsx +146 -0
  51. package/src/ui/UIDateRangePicker.tsx +38 -0
  52. package/src/ui/UIFilePreview/ExcelEditor/index.tsx +617 -0
  53. package/src/ui/UIFilePreview/ExcelEditor/types.ts +24 -0
  54. package/src/ui/UIFilePreview/ExportDropdown.css +85 -0
  55. package/src/ui/UIFilePreview/ExportDropdown.tsx +109 -0
  56. package/src/ui/UIFilePreview/IndexedDBUtils.ts +564 -0
  57. package/src/ui/UIFilePreview/MarkdownEditor/exportMarkdownPdf.ts +576 -0
  58. package/src/ui/UIFilePreview/MarkdownEditor/index.tsx +677 -0
  59. package/src/ui/UIFilePreview/MarkdownEditor/markdownEditor.css +1005 -0
  60. package/src/ui/UIFilePreview/MarkdownEditor/types.ts +15 -0
  61. package/src/ui/UIFilePreview/PPTXEditor/index.tsx +602 -0
  62. package/src/ui/UIFilePreview/RichDocEditor/exportRDocMarkdown.ts +77 -0
  63. package/src/ui/UIFilePreview/RichDocEditor/exportRDocPdf.ts +374 -0
  64. package/src/ui/UIFilePreview/RichDocEditor/index.tsx +730 -0
  65. package/src/ui/UIFilePreview/RichDocEditor/richdoc-editor.css +59 -0
  66. package/src/ui/UIFilePreview/UIFilePreview.tsx +611 -0
  67. package/src/ui/UIFilePreview/WordEditor/index.tsx +3933 -0
  68. package/src/ui/UIFilePreview/WordEditor/style.ts +1213 -0
  69. package/src/ui/UIFilePreview/WordEditor/word-editor.css +999 -0
  70. package/src/ui/UIFilePreview/filePreview.css +193 -0
  71. package/src/ui/UIFilePreview/filetypes/archive.tsx +66 -0
  72. package/src/ui/UIFilePreview/filetypes/cad.tsx +287 -0
  73. package/src/ui/UIFilePreview/filetypes/excel.tsx +355 -0
  74. package/src/ui/UIFilePreview/filetypes/image.tsx +20 -0
  75. package/src/ui/UIFilePreview/filetypes/imageLoader.ts +63 -0
  76. package/src/ui/UIFilePreview/filetypes/pdf.tsx +156 -0
  77. package/src/ui/UIFilePreview/filetypes/ppt.tsx +535 -0
  78. package/src/ui/UIFilePreview/filetypes/pptExportPdf.ts +523 -0
  79. package/src/ui/UIFilePreview/filetypes/video.tsx +482 -0
  80. package/src/ui/UIFilePreview/filetypes/word.tsx +245 -0
  81. package/src/ui/UIFilePreview/filetypes/wordExportMarkdown.ts +86 -0
  82. package/src/ui/UIFilePreview/filetypes/wordExportPdf.tsx +427 -0
  83. package/src/ui/UIFilePreview/index.ts +35 -0
  84. package/src/ui/UIFilePreview/pdfPagination.ts +143 -0
  85. package/src/ui/UIFilePreview/previewUtils.tsx +531 -0
  86. package/src/ui/UIFileUpload.tsx +382 -0
  87. package/src/ui/UIImageUpload.tsx +195 -0
  88. package/src/ui/UILoading.tsx +255 -0
  89. package/src/ui/UILoadingExample.tsx +139 -0
  90. package/src/ui/UISelect.tsx +105 -0
  91. package/src/ui/UIToast.tsx +381 -0
  92. package/src/ui/UIToastExample.tsx +69 -0
  93. package/src/ui/UIToastSingle.tsx +279 -0
  94. package/src/ui/UIViewToggle.tsx +64 -0
  95. package/src/ui/UserSelector/index.tsx +583 -0
  96. package/src/ui/index.ts +84 -0
  97. package/src/ui/upload/image.ts +135 -0
  98. package/src/ui/upload/index.ts +73 -0
  99. package/src/ui/upload/submitFile.ts +106 -0
  100. package/src/ui/upload/submitForm.ts +124 -0
  101. package/src/ui/upload/upload.err.ts +15 -0
  102. package/src/ui/upload/upload.ts +250 -0
  103. package/src/utils/CacheDepartmentInfo.ts +372 -0
  104. package/src/utils/CacheFileContent.ts +315 -0
  105. package/src/utils/CachePermissionInfo.ts +235 -0
  106. package/src/utils/CacheRoleInfo.ts +303 -0
  107. package/src/utils/CacheUserInfo.ts +818 -0
  108. package/src/utils/CacheViewToggleInfo.ts +182 -0
  109. package/src/utils/MessageContentUtils.ts +268 -0
  110. package/src/utils/clipboard.ts +70 -0
  111. package/src/utils/companyEmail.ts +19 -0
  112. package/src/utils/forbidDebugger.ts +59 -0
  113. package/src/utils/formatters.ts +94 -0
  114. package/src/utils/htmlUtils.tsx +59 -0
  115. package/src/utils/index.ts +18 -0
  116. package/src/utils/keyboard.ts +114 -0
  117. package/src/utils/mammothApi.ts +16 -0
  118. package/src/utils/polyfills.ts +928 -0
  119. package/src/utils/regex.ts +14 -0
  120. package/src/utils/storage.ts +95 -0
  121. package/src/utils/workerBootstrap.ts +87 -0
  122. package/dist/api/index.js +0 -1553
  123. package/dist/contract.js +0 -5
  124. package/dist/index.js +0 -4492
  125. package/dist/types/index.js +0 -49
  126. package/dist/ui/index.css +0 -1876
  127. package/dist/ui/index.js +0 -22245
  128. package/dist/utils/index.js +0 -4296
  129. /package/{dist → src}/styles/ant-modal.css +0 -0
  130. /package/{dist → src}/styles/ant-spin.css +0 -0
  131. /package/{dist → src}/styles/ant-table.css +0 -0
  132. /package/{dist → src}/styles/index.css +0 -0
@@ -0,0 +1,813 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Table, Select, Tag, Button, Input, DatePicker, Modal, Descriptions, Form, Menu, message } from 'antd';
3
+ import { DownloadOutlined, EditOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
4
+ import dayjs from 'dayjs';
5
+ import type { Dayjs } from 'dayjs';
6
+ import { UIFilePreview } from '../UIFilePreview/index';
7
+ import { ResponseCode } from '../../api/_base';
8
+ import { CacheUserInfo } from '../../utils/CacheUserInfo';
9
+
10
+ const { Option } = Select;
11
+ const { RangePicker } = DatePicker;
12
+ const { TextArea } = Input;
13
+
14
+ /**
15
+ * 发票类型
16
+ */
17
+ export enum InvoiceType {
18
+ /** 其他 */
19
+ 其他 = '其他',
20
+ /** 增值税普通发票 */
21
+ 增值税普通发票 = '‌增值税普通发票',
22
+ /** 增值税专用发票 */
23
+ 增值税专用发票 = '‌增值税专用发票',
24
+ /** 国有金融、保险、邮政、电信、铁路、航空、存单等 */
25
+ 专业发票 = '专业发票',
26
+ /** 机动车销售统一发票 */
27
+ 机动车发票 = '机动车发票',
28
+ /** 二手车销售统一发票 */
29
+ 二手车发票 = '二手车发票',
30
+ }
31
+
32
+ /**
33
+ * 发票状态
34
+ */
35
+ export enum ApprovalInvoiceStatus {
36
+ /** 已报销 */
37
+ 已报销 = '已报销',
38
+ /** 待报销 */
39
+ 待报销 = '待报销',
40
+ /** 作废 */
41
+ 作废 = '作废',
42
+ }
43
+
44
+ interface InvoiceRecord {
45
+ id: string;
46
+ instanceId: string;
47
+ invoiceFilePath: string;
48
+ invoiceNo: string;
49
+ invoiceCode: string;
50
+ invoiceType: string;
51
+ invoiceDate: string;
52
+ amountWithoutTax: number;
53
+ taxAmount: number;
54
+ totalAmount: number;
55
+ buyerName: string;
56
+ buyerTaxNo: string;
57
+ buyerAddressPhone: string;
58
+ buyerBankAccount: string;
59
+ sellerName: string;
60
+ sellerTaxNo: string;
61
+ sellerAddressPhone: string;
62
+ sellerBankAccount: string;
63
+ status: string;
64
+ summary: string;
65
+ remark: string;
66
+ drawer: string;
67
+ reviewer: string;
68
+ payee: string;
69
+ createTime: Date;
70
+ createBy: string;
71
+ updateTime: Date;
72
+ updateBy: string;
73
+ }
74
+
75
+ const statusColors: Record<string, string> = {
76
+ '已报销': 'green',
77
+ '待报销': 'orange',
78
+ '作废': 'red',
79
+ };
80
+
81
+ const statusText: Record<string, string> = {
82
+ '已报销': '已报销',
83
+ '待报销': '待报销',
84
+ '作废': '作废',
85
+ };
86
+
87
+ /** 发票 API —— 由使用方注入(approval 模块的 approver.approvalinvoice),避免 core 依赖模块专属 API */
88
+ export interface InvoiceRecordsApi {
89
+ getInvoiceListByProject: (params: any) => Promise<any>;
90
+ getInvoiceList: (params: any) => Promise<any>;
91
+ updateInvoice: (data: any) => Promise<any>;
92
+ }
93
+
94
+ interface InvoiceRecordsListProps {
95
+ /** 项目ID:传入时仅展示该项目关联的发票记录(项目“费用”页签使用),不传则展示全部发票记录 */
96
+ projectId?: string;
97
+ /** 发票接口实现,由壳层/模块注入 */
98
+ invoiceApi: InvoiceRecordsApi;
99
+ }
100
+
101
+ export const InvoiceRecordsList: React.FC<InvoiceRecordsListProps> = ({ projectId, invoiceApi }) => {
102
+ const isProjectMode = !!projectId;
103
+ const [invoices, setInvoices] = useState<InvoiceRecord[]>([]);
104
+ const [total, setTotal] = useState(0);
105
+ const [loading, setLoading] = useState(false);
106
+ const [previewVisible, setPreviewVisible] = useState(false);
107
+ const [previewFileUrl, setPreviewFileUrl] = useState<string | null>(null);
108
+ const [previewFileName, setPreviewFileName] = useState<string | undefined>(undefined);
109
+ const [detailVisible, setDetailVisible] = useState(false);
110
+ const [selectedInvoice, setSelectedInvoice] = useState<InvoiceRecord | null>(null);
111
+ const [editVisible, setEditVisible] = useState(false);
112
+ const [editForm] = Form.useForm();
113
+ const [userMap, setUserMap] = useState<Map<string, string>>(new Map());
114
+ const [contextMenu, setContextMenu] = useState<{ visible: boolean; x: number; y: number; record: InvoiceRecord | null }>({
115
+ visible: false, x: 0, y: 0, record: null,
116
+ });
117
+
118
+ const [filters, setFilters] = useState({
119
+ instanceId: '',
120
+ invoiceNo: '',
121
+ buyerName: '',
122
+ sellerName: '',
123
+ createBy: '',
124
+ status: '',
125
+ fromDate: null as Dayjs | null,
126
+ toDate: null as Dayjs | null,
127
+ });
128
+
129
+ // 符合筛选条件的汇总金额(后端返回,单位:分).
130
+ const [sumTotalAmount, setSumTotalAmount] = useState(0);
131
+ const [sumTaxAmount, setSumTaxAmount] = useState(0);
132
+
133
+ // 申请人筛选选项(用户搜索).
134
+ const [userOptions, setUserOptions] = useState<{ value: string; label: string }[]>([]);
135
+ const userSearchTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
136
+
137
+ const [pagination, setPagination] = useState({
138
+ pageNum: 1,
139
+ pageSize: 10,
140
+ });
141
+
142
+ useEffect(() => {
143
+ fetchInvoices();
144
+ }, [filters, pagination, projectId]);
145
+
146
+ const fetchInvoices = async () => {
147
+ setLoading(true);
148
+ try {
149
+ const params: any = {
150
+ pageNum: pagination.pageNum,
151
+ pageSize: pagination.pageSize,
152
+ };
153
+
154
+ if (filters.invoiceNo) params.invoiceNo = filters.invoiceNo;
155
+ if (filters.buyerName) params.buyerName = filters.buyerName;
156
+ if (filters.sellerName) params.sellerName = filters.sellerName;
157
+ if (filters.createBy) params.createBy = filters.createBy;
158
+ if (filters.status) params.status = filters.status;
159
+ if (filters.fromDate) params.fromDate = filters.fromDate.format('YYYY-MM-DD');
160
+ if (filters.toDate) params.toDate = filters.toDate.format('YYYY-MM-DD');
161
+
162
+ const res = isProjectMode
163
+ ? await invoiceApi.getInvoiceListByProject({ ...params, projectId: projectId! })
164
+ : await invoiceApi.getInvoiceList({ ...params, instanceId: filters.instanceId });
165
+
166
+ if (res?.code === ResponseCode.SUCCESS && res.data) {
167
+ setInvoices(res.data.list);
168
+ setTotal(res.data.total);
169
+ setSumTotalAmount(res.data.sumTotalAmount || 0);
170
+ setSumTaxAmount(res.data.sumTaxAmount || 0);
171
+
172
+ const userIds = new Set<string>();
173
+ res.data.list.forEach(invoice => {
174
+ if (invoice.createBy) userIds.add(invoice.createBy);
175
+ if (invoice.updateBy) userIds.add(invoice.updateBy);
176
+ });
177
+
178
+ if (userIds.size > 0) {
179
+ const users = await CacheUserInfo.getUserInfos({ userIds: Array.from(userIds) });
180
+ const newUserMap = new Map<string, string>();
181
+ users.forEach((user, id) => {
182
+ newUserMap.set(id, user.nickname);
183
+ });
184
+ setUserMap(newUserMap);
185
+ }
186
+ }
187
+ } catch (error) {
188
+ console.error('获取发票列表失败', error);
189
+ } finally {
190
+ setLoading(false);
191
+ }
192
+ };
193
+
194
+ const handleResetFilters = () => {
195
+ setFilters({
196
+ instanceId: '',
197
+ invoiceNo: '',
198
+ buyerName: '',
199
+ sellerName: '',
200
+ createBy: '',
201
+ status: '',
202
+ fromDate: null,
203
+ toDate: null,
204
+ });
205
+ setPagination({ pageNum: 1, pageSize: 10 });
206
+ };
207
+
208
+ // 加载申请人筛选选项;有关键词时按昵称搜索,否则取前100个用户.
209
+ const loadUserOptions = async (keywords?: string) => {
210
+ try {
211
+ const users = await CacheUserInfo.getUserInfos(keywords ? { keywords } : {});
212
+ const opts = Array.from(users.entries()).map(([id, user]) => ({
213
+ value: id,
214
+ label: user.nickname || user.username || id,
215
+ }));
216
+ setUserOptions(opts);
217
+ } catch (error) {
218
+ console.error('获取申请人列表失败', error);
219
+ }
220
+ };
221
+
222
+ useEffect(() => {
223
+ loadUserOptions();
224
+ }, []);
225
+
226
+ const handlePreviewFile = (record: InvoiceRecord) => {
227
+ setSelectedInvoice(record);
228
+ setDetailVisible(true);
229
+ };
230
+
231
+ const currentIndex = selectedInvoice
232
+ ? invoices.findIndex(item => item.id === selectedInvoice.id)
233
+ : -1;
234
+
235
+ const handlePrevInvoice = () => {
236
+ if (currentIndex > 0) {
237
+ setSelectedInvoice(invoices[currentIndex - 1]);
238
+ }
239
+ };
240
+
241
+ const handleNextInvoice = () => {
242
+ if (currentIndex >= 0 && currentIndex < invoices.length - 1) {
243
+ setSelectedInvoice(invoices[currentIndex + 1]);
244
+ }
245
+ };
246
+
247
+ const handleContextMenu = (e: React.MouseEvent, record: InvoiceRecord) => {
248
+ e.preventDefault();
249
+ setContextMenu({ visible: true, x: e.clientX, y: e.clientY, record });
250
+ };
251
+
252
+ const closeContextMenu = () => {
253
+ setContextMenu(prev => prev.visible ? { ...prev, visible: false } : prev);
254
+ };
255
+
256
+ useEffect(() => {
257
+ if (!contextMenu.visible) return;
258
+ const handleClick = () => closeContextMenu();
259
+ const handleContextMenuDoc = (e: MouseEvent) => {
260
+ e.preventDefault();
261
+ };
262
+ document.addEventListener('click', handleClick);
263
+ document.addEventListener('contextmenu', handleContextMenuDoc);
264
+ return () => {
265
+ document.removeEventListener('click', handleClick);
266
+ document.removeEventListener('contextmenu', handleContextMenuDoc);
267
+ };
268
+ }, [contextMenu.visible]);
269
+
270
+ const handleViewFile = (record: InvoiceRecord) => {
271
+ if (record.invoiceFilePath) {
272
+ const ext = record.invoiceFilePath.split('.').pop()?.toLowerCase() || 'pdf';
273
+ setPreviewFileName(`${record.invoiceNo}.${ext}`);
274
+ setPreviewFileUrl(import.meta.env.VITE_API_BASE + '/private/invoices/' + record.invoiceFilePath);
275
+ setPreviewVisible(true);
276
+ }
277
+ };
278
+
279
+ const handleDownloadFile = async (record: InvoiceRecord) => {
280
+ if (!record.invoiceFilePath) return;
281
+
282
+ try {
283
+ const response = await fetch(import.meta.env.VITE_API_BASE +'/private/invoices/' + record.invoiceFilePath);
284
+ const blob = await response.blob();
285
+ const ext = record.invoiceFilePath.split('.').pop()?.toLowerCase() || 'pdf';
286
+ const fileName = `${record.invoiceNo}.${ext}`;
287
+
288
+ const url = window.URL.createObjectURL(blob);
289
+ const a = document.createElement('a');
290
+ a.href = url;
291
+ a.download = fileName;
292
+ document.body.appendChild(a);
293
+ a.click();
294
+ document.body.removeChild(a);
295
+ window.URL.revokeObjectURL(url);
296
+ } catch (error) {
297
+ console.error('下载发票失败', error);
298
+ }
299
+ };
300
+
301
+ const handleEdit = (record: InvoiceRecord) => {
302
+ setSelectedInvoice(record);
303
+ editForm.setFieldsValue({
304
+ invoiceNo: record.invoiceNo,
305
+ invoiceCode: record.invoiceCode,
306
+ invoiceType: record.invoiceType,
307
+ invoiceDate: record.invoiceDate ? dayjs(record.invoiceDate) : null,
308
+ // 金额存储单位为分,表单显示单位为元
309
+ amountWithoutTax: record.amountWithoutTax != null ? record.amountWithoutTax / 100 : undefined,
310
+ taxAmount: record.taxAmount != null ? record.taxAmount / 100 : undefined,
311
+ totalAmount: record.totalAmount != null ? record.totalAmount / 100 : undefined,
312
+ buyerName: record.buyerName,
313
+ buyerTaxNo: record.buyerTaxNo,
314
+ buyerAddressPhone: record.buyerAddressPhone,
315
+ buyerBankAccount: record.buyerBankAccount,
316
+ sellerName: record.sellerName,
317
+ sellerTaxNo: record.sellerTaxNo,
318
+ sellerAddressPhone: record.sellerAddressPhone,
319
+ sellerBankAccount: record.sellerBankAccount,
320
+ status: record.status,
321
+ summary: record.summary,
322
+ remark: record.remark,
323
+ drawer: record.drawer,
324
+ reviewer: record.reviewer,
325
+ payee: record.payee,
326
+ });
327
+ setEditVisible(true);
328
+ setDetailVisible(false);
329
+ };
330
+
331
+ const handleSubmitEdit = async () => {
332
+ try {
333
+ const values = await editForm.validateFields();
334
+
335
+ if (!selectedInvoice) return;
336
+
337
+ // 表单金额单位为元,存储单位为分
338
+ const yuanToCents = (v: unknown) =>
339
+ v === undefined || v === null || v === '' ? undefined : Math.round(Number(v) * 100);
340
+
341
+ const submitData = {
342
+ ...values,
343
+ totalAmount: yuanToCents(values.totalAmount),
344
+ amountWithoutTax: yuanToCents(values.amountWithoutTax),
345
+ taxAmount: yuanToCents(values.taxAmount),
346
+ invoiceDate: values.invoiceDate ? values.invoiceDate.format('YYYY-MM-DD') : undefined,
347
+ };
348
+
349
+ const res = await invoiceApi.updateInvoice({
350
+ id: selectedInvoice.id,
351
+ ...submitData,
352
+ });
353
+
354
+ if (res.code === ResponseCode.SUCCESS) {
355
+ message.success('发票信息更新成功');
356
+ setEditVisible(false);
357
+ fetchInvoices();
358
+ } else {
359
+ message.error(res.message || '更新失败');
360
+ }
361
+ } catch (error) {
362
+ console.error('编辑发票失败', error);
363
+ message.error('编辑发票失败');
364
+ }
365
+ };
366
+
367
+ const columns = [
368
+ {
369
+ title: '开票日期',
370
+ dataIndex: 'invoiceDate',
371
+ key: 'invoiceDate',
372
+ width: 120,
373
+ },
374
+ {
375
+ title: '发票号码',
376
+ dataIndex: 'invoiceNo',
377
+ key: 'invoiceNo',
378
+ width: 180,
379
+ ellipsis: true,
380
+ },
381
+ {
382
+ title: '发票代码',
383
+ dataIndex: 'invoiceCode',
384
+ key: 'invoiceCode',
385
+ width: 140,
386
+ ellipsis: true,
387
+ },
388
+ {
389
+ title: '发票类型',
390
+ dataIndex: 'invoiceType',
391
+ key: 'invoiceType',
392
+ width: 120,
393
+ render: (type: InvoiceType) => (
394
+ <Tag color="blue" className="text-xs">
395
+ {type}
396
+ </Tag>
397
+ ),
398
+ },
399
+ {
400
+ title: '金额(含税)',
401
+ dataIndex: 'totalAmount',
402
+ key: 'totalAmount',
403
+ width: 120,
404
+ render: (amount: number) => (
405
+ <span className="text-green-600">¥{(amount/100).toFixed(2)}</span>
406
+ ),
407
+ },
408
+ {
409
+ title: '不含税金额',
410
+ dataIndex: 'amountWithoutTax',
411
+ key: 'amountWithoutTax',
412
+ width: 120,
413
+ render: (amount: number) => (
414
+ <span>¥{(amount/100).toFixed(2)}</span>
415
+ ),
416
+ },
417
+ {
418
+ title: '税额',
419
+ dataIndex: 'taxAmount',
420
+ key: 'taxAmount',
421
+ width: 100,
422
+ render: (amount: number) => (
423
+ <span>¥{(amount/100).toFixed(2)}</span>
424
+ ),
425
+ },
426
+ {
427
+ title: '购方名称',
428
+ dataIndex: 'buyerName',
429
+ key: 'buyerName',
430
+ width: 200,
431
+ ellipsis: true,
432
+ },
433
+ {
434
+ title: '销方名称',
435
+ dataIndex: 'sellerName',
436
+ key: 'sellerName',
437
+ width: 200,
438
+ ellipsis: true,
439
+ },
440
+ {
441
+ title: '状态',
442
+ dataIndex: 'status',
443
+ key: 'status',
444
+ width: 100,
445
+ render: (status: ApprovalInvoiceStatus) => (
446
+ <Tag color={statusColors[status]} className="text-xs">
447
+ {statusText[status]}
448
+ </Tag>
449
+ ),
450
+ },
451
+ ];
452
+
453
+ const detailItems = selectedInvoice ? [
454
+ { key: 'invoiceNo', label: '发票号码', children: selectedInvoice.invoiceNo },
455
+ { key: 'invoiceCode', label: '发票代码', children: selectedInvoice.invoiceCode },
456
+ { key: 'invoiceType', label: '发票类型', children: <Tag color="blue">{selectedInvoice.invoiceType}</Tag> },
457
+ { key: 'invoiceDate', label: '开票日期', children: selectedInvoice.invoiceDate },
458
+ { key: 'totalAmount', label: '金额(含税)', children: <span className="text-green-600">¥{(selectedInvoice.totalAmount/100).toFixed(2)}</span> },
459
+ { key: 'amountWithoutTax', label: '不含税金额', children: `¥${(selectedInvoice.amountWithoutTax/100).toFixed(2)}` },
460
+ { key: 'taxAmount', label: '税额', children: `¥${(selectedInvoice.taxAmount/100).toFixed(2)}` },
461
+ { key: 'buyerName', label: '购方名称', children: selectedInvoice.buyerName },
462
+ { key: 'buyerTaxNo', label: '购方税号', children: selectedInvoice.buyerTaxNo },
463
+ { key: 'buyerAddressPhone', label: '购方地址电话', children: selectedInvoice.buyerAddressPhone },
464
+ { key: 'buyerBankAccount', label: '购方开户行及账号', children: selectedInvoice.buyerBankAccount },
465
+ { key: 'sellerName', label: '销方名称', children: selectedInvoice.sellerName },
466
+ { key: 'sellerTaxNo', label: '销方税号', children: selectedInvoice.sellerTaxNo },
467
+ { key: 'sellerAddressPhone', label: '销方地址电话', children: selectedInvoice.sellerAddressPhone },
468
+ { key: 'sellerBankAccount', label: '销方开户行及账号', children: selectedInvoice.sellerBankAccount },
469
+ { key: 'status', label: '状态', children: <Tag color={statusColors[selectedInvoice.status]}>{statusText[selectedInvoice.status]}</Tag> },
470
+ { key: 'summary', label: '摘要', children: selectedInvoice.summary },
471
+ { key: 'remark', label: '备注', children: selectedInvoice.remark || '-' },
472
+ { key: 'drawer', label: '开票人', children: selectedInvoice.drawer || '-' },
473
+ { key: 'reviewer', label: '复核人', children: selectedInvoice.reviewer || '-' },
474
+ { key: 'payee', label: '收款人', children: selectedInvoice.payee || '-' },
475
+ { key: 'createBy', label: '申请人', children: userMap.get(selectedInvoice.createBy) || selectedInvoice.createBy },
476
+ { key: 'createTime', label: '申请时间', children: selectedInvoice.createTime ? dayjs(selectedInvoice.createTime).format('YYYY-MM-DD HH:mm:ss') : '-' },
477
+ { key: 'updateBy', label: '更新人', children: userMap.get(selectedInvoice.updateBy) || selectedInvoice.updateBy || '-' },
478
+ { key: 'updateTime', label: '更新时间', children: selectedInvoice.updateTime ? dayjs(selectedInvoice.updateTime).format('YYYY-MM-DD HH:mm:ss') : '-' }
479
+ ] : [];
480
+
481
+ return (
482
+ <div className="space-y-5 p-3 sm:space-y-6 sm:p-6">
483
+ <div className="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
484
+ <div>
485
+ <h2 className="mb-1 text-xl font-semibold text-foreground">{isProjectMode ? '费用发票' : '发票列表'}</h2>
486
+ <p className="text-sm text-muted">{isProjectMode ? '查看该项目关联的所有发票记录' : '查看和管理所有发票记录'}</p>
487
+ </div>
488
+ <div className="flex items-center gap-3">
489
+ <span className="text-sm text-muted">共 {total} 条记录</span>
490
+ <span className="text-sm text-muted">
491
+ 总金额(含税):<span className="font-medium text-green-600">¥{(sumTotalAmount / 100).toFixed(2)}</span>
492
+ </span>
493
+ <span className="text-sm text-muted">
494
+ 总税额:<span className="font-medium text-green-600">¥{(sumTaxAmount / 100).toFixed(2)}</span>
495
+ </span>
496
+ </div>
497
+ </div>
498
+
499
+ <div className="grid grid-cols-2 gap-3 md:grid-cols-4 lg:grid-cols-5">
500
+ <Input
501
+ placeholder="发票号码"
502
+ value={filters.invoiceNo}
503
+ onChange={(e) => {
504
+ setFilters(prev => ({ ...prev, invoiceNo: e.target.value }));
505
+ setPagination({ pageNum: 1, pageSize: pagination.pageSize });
506
+ }}
507
+ className="w-full"
508
+ />
509
+ <Input
510
+ placeholder="购方名称"
511
+ value={filters.buyerName}
512
+ onChange={(e) => {
513
+ setFilters(prev => ({ ...prev, buyerName: e.target.value }));
514
+ setPagination({ pageNum: 1, pageSize: pagination.pageSize });
515
+ }}
516
+ className="w-full"
517
+ />
518
+ <Input
519
+ placeholder="销方名称"
520
+ value={filters.sellerName}
521
+ onChange={(e) => {
522
+ setFilters(prev => ({ ...prev, sellerName: e.target.value }));
523
+ setPagination({ pageNum: 1, pageSize: pagination.pageSize });
524
+ }}
525
+ className="w-full"
526
+ />
527
+ <Select
528
+ placeholder="申请人"
529
+ allowClear
530
+ showSearch
531
+ filterOption={false}
532
+ options={userOptions}
533
+ value={filters.createBy || undefined}
534
+ onSearch={(kw) => {
535
+ if (userSearchTimer.current) clearTimeout(userSearchTimer.current);
536
+ userSearchTimer.current = setTimeout(() => loadUserOptions(kw), 300);
537
+ }}
538
+ onChange={(val) => {
539
+ setFilters(prev => ({ ...prev, createBy: val || '' }));
540
+ setPagination({ pageNum: 1, pageSize: pagination.pageSize });
541
+ }}
542
+ className="w-full"
543
+ />
544
+ <Select
545
+ placeholder="状态"
546
+ allowClear
547
+ value={filters.status || undefined}
548
+ onChange={(val) => {
549
+ setFilters(prev => ({ ...prev, status: val || '' }));
550
+ setPagination({ pageNum: 1, pageSize: pagination.pageSize });
551
+ }}
552
+ className="w-full"
553
+ >
554
+ <Option value="已报销">{statusText['已报销']}</Option>
555
+ <Option value="待报销">{statusText['待报销']}</Option>
556
+ <Option value="作废">{statusText['作废']}</Option>
557
+ </Select>
558
+
559
+ <RangePicker
560
+ placeholder={['开始日期', '结束日期']}
561
+ value={filters.fromDate && filters.toDate ? [filters.fromDate, filters.toDate] : undefined}
562
+ onChange={(_, dates) => {
563
+ const from = dates && dates[0] ? dayjs(dates[0]) : null;
564
+ const to = dates && dates[1] ? dayjs(dates[1]) : null;
565
+ setFilters(prev => ({
566
+ ...prev,
567
+ fromDate: from,
568
+ toDate: to,
569
+ }));
570
+ setPagination({ pageNum: 1, pageSize: pagination.pageSize });
571
+ }}
572
+ />
573
+ </div>
574
+
575
+ <div className="flex justify-end">
576
+ <Button
577
+ type="primary"
578
+ onClick={handleResetFilters}
579
+ >
580
+ 重置筛选
581
+ </Button>
582
+ </div>
583
+
584
+ <div className="overflow-x-auto">
585
+ <Table
586
+ scroll={{ x: 1400 }}
587
+ columns={columns}
588
+ dataSource={invoices}
589
+ rowKey="id"
590
+ loading={loading}
591
+ onRow={(record) => ({
592
+ onClick: (e) => {
593
+ if (e.ctrlKey) return;
594
+ handlePreviewFile(record);
595
+ },
596
+ onContextMenu: (e) => handleContextMenu(e, record),
597
+ style: { cursor: 'pointer' },
598
+ })}
599
+ pagination={{
600
+ current: pagination.pageNum,
601
+ pageSize: pagination.pageSize,
602
+ total,
603
+ showSizeChanger: true,
604
+ showQuickJumper: true,
605
+ showTotal: (total, range) => (
606
+ <span className="text-sm text-muted">
607
+ 第 {range[0]}-{range[1]} 条,共 {total} 条
608
+ </span>
609
+ ),
610
+ onChange: (page, pageSize) => {
611
+ setPagination({ pageNum: page, pageSize });
612
+ },
613
+ }}
614
+ size="middle"
615
+ className="border-0"
616
+ />
617
+ </div>
618
+
619
+ <UIFilePreview
620
+ fileUrl={previewFileUrl}
621
+ fileName={previewFileName}
622
+ visible={previewVisible}
623
+ onClose={() => setPreviewVisible(false)}
624
+ editable={false}
625
+ uploadPercent={undefined}
626
+ />
627
+
628
+ <Modal
629
+ title={
630
+ <div className="flex items-center justify-between pr-8">
631
+ <span>发票详情</span>
632
+ {currentIndex >= 0 && invoices.length > 0 && (
633
+ <span className="text-sm font-normal text-muted">
634
+ 第 {currentIndex + 1} / {invoices.length} 条
635
+ </span>
636
+ )}
637
+ </div>
638
+ }
639
+ open={detailVisible}
640
+ onCancel={() => setDetailVisible(false)}
641
+ footer={[
642
+ <Button
643
+ key="prev"
644
+ icon={<LeftOutlined />}
645
+ disabled={currentIndex <= 0}
646
+ onClick={handlePrevInvoice}
647
+ >
648
+ 上一条
649
+ </Button>,
650
+ <Button
651
+ key="next"
652
+ disabled={currentIndex < 0 || currentIndex >= invoices.length - 1}
653
+ onClick={handleNextInvoice}
654
+ >
655
+ 下一条
656
+ <RightOutlined />
657
+ </Button>,
658
+ !isProjectMode && (
659
+ <Button key="edit" icon={<EditOutlined />} onClick={() => handleEdit(selectedInvoice!)}>
660
+ 编辑
661
+ </Button>
662
+ ),
663
+ selectedInvoice?.invoiceFilePath ? (
664
+ <React.Fragment key="file-actions">
665
+ <Button key="download" icon={<DownloadOutlined />} onClick={() => handleDownloadFile(selectedInvoice!)}>
666
+ 下载发票
667
+ </Button>
668
+ <Button key="view" type="primary" onClick={() => handleViewFile(selectedInvoice!)}>
669
+ 查看发票文件
670
+ </Button>
671
+ </React.Fragment>
672
+ ) : null,
673
+ <Button key="back" onClick={() => setDetailVisible(false)}>
674
+ 关闭
675
+ </Button>,
676
+ ].filter(Boolean)}
677
+ width={700}
678
+ >
679
+ <Descriptions
680
+ bordered
681
+ column={2}
682
+ size="small"
683
+ items={detailItems}
684
+ />
685
+ </Modal>
686
+
687
+ <Modal
688
+ title="编辑发票信息"
689
+ open={editVisible}
690
+ onCancel={() => setEditVisible(false)}
691
+ footer={[
692
+ <Button key="back" onClick={() => setEditVisible(false)}>
693
+ 取消
694
+ </Button>,
695
+ <Button key="submit" type="primary" onClick={handleSubmitEdit}>
696
+ 保存
697
+ </Button>,
698
+ ]}
699
+ width={800}
700
+ destroyOnHidden
701
+ >
702
+ <Form form={editForm} layout="vertical" size="small">
703
+ <div className="grid grid-cols-2 gap-4">
704
+ <Form.Item label="发票号码" name="invoiceNo">
705
+ <Input />
706
+ </Form.Item>
707
+ <Form.Item label="发票代码" name="invoiceCode">
708
+ <Input />
709
+ </Form.Item>
710
+ <Form.Item label="发票类型" name="invoiceType">
711
+ <Select>
712
+ {Object.values(InvoiceType).map(type => (
713
+ <Option key={type} value={type}>{type}</Option>
714
+ ))}
715
+ </Select>
716
+ </Form.Item>
717
+ <Form.Item label="开票日期" name="invoiceDate">
718
+ <DatePicker style={{ width: '100%' }} />
719
+ </Form.Item>
720
+ <Form.Item label="金额(含税)(元)" name="totalAmount">
721
+ <Input type="number" step="0.01" prefix="¥" />
722
+ </Form.Item>
723
+ <Form.Item label="不含税金额(元)" name="amountWithoutTax">
724
+ <Input type="number" step="0.01" prefix="¥" />
725
+ </Form.Item>
726
+ <Form.Item label="税额(元)" name="taxAmount">
727
+ <Input type="number" step="0.01" prefix="¥" />
728
+ </Form.Item>
729
+ <Form.Item label="状态" name="status">
730
+ <Select>
731
+ {Object.values(ApprovalInvoiceStatus).map(status => (
732
+ <Option key={status} value={status}>{status}</Option>
733
+ ))}
734
+ </Select>
735
+ </Form.Item>
736
+ <Form.Item label="购方名称" name="buyerName">
737
+ <Input />
738
+ </Form.Item>
739
+ <Form.Item label="购方税号" name="buyerTaxNo">
740
+ <Input />
741
+ </Form.Item>
742
+ <Form.Item label="购方地址电话" name="buyerAddressPhone">
743
+ <Input />
744
+ </Form.Item>
745
+ <Form.Item label="购方开户行及账号" name="buyerBankAccount">
746
+ <Input />
747
+ </Form.Item>
748
+ <Form.Item label="销方名称" name="sellerName">
749
+ <Input />
750
+ </Form.Item>
751
+ <Form.Item label="销方税号" name="sellerTaxNo">
752
+ <Input />
753
+ </Form.Item>
754
+ <Form.Item label="销方地址电话" name="sellerAddressPhone">
755
+ <Input />
756
+ </Form.Item>
757
+ <Form.Item label="销方开户行及账号" name="sellerBankAccount">
758
+ <Input />
759
+ </Form.Item>
760
+ <Form.Item label="开票人" name="drawer">
761
+ <Input />
762
+ </Form.Item>
763
+ <Form.Item label="复核人" name="reviewer">
764
+ <Input />
765
+ </Form.Item>
766
+ <Form.Item label="收款人" name="payee">
767
+ <Input />
768
+ </Form.Item>
769
+ <Form.Item label="摘要" name="summary">
770
+ <Input />
771
+ </Form.Item>
772
+ <Form.Item label="备注" name="remark">
773
+ <TextArea rows={3} />
774
+ </Form.Item>
775
+ </div>
776
+ </Form>
777
+ </Modal>
778
+
779
+ {contextMenu.visible && contextMenu.record && (
780
+ <div
781
+ className="fixed z-50"
782
+ style={{ left: contextMenu.x, top: contextMenu.y, minWidth: 160 }}
783
+ onClick={(e) => e.stopPropagation()}
784
+ >
785
+ <Menu
786
+ items={[
787
+ {
788
+ key: 'download',
789
+ label: '下载发票',
790
+ icon: <DownloadOutlined />,
791
+ disabled: !contextMenu.record.invoiceFilePath,
792
+ onClick: () => {
793
+ handleDownloadFile(contextMenu.record!);
794
+ closeContextMenu();
795
+ },
796
+ },
797
+ ]}
798
+ mode="vertical"
799
+ style={{
800
+ borderRadius: 6,
801
+ boxShadow: '0 2px 8px rgb(var(--shadow-color) / 0.15)',
802
+ background: 'rgb(var(--color-surface))',
803
+ border: '1px solid rgb(var(--color-border))',
804
+ padding: '4px 0',
805
+ fontSize: '14px',
806
+ minWidth: 160,
807
+ }}
808
+ />
809
+ </div>
810
+ )}
811
+ </div>
812
+ );
813
+ };