Commit ccd2f097 authored by David Yang's avatar David Yang

feat: 更新配置

parent a455bb75
No preview for this file type
{
"0 debug pnpm:scope": {
"selected": 1
},
"1 error pnpm": {
"code": "ELIFECYCLE",
"errno": "ENOENT",
"syscall": "spawn",
"file": "sh",
"pkgid": "@",
"stage": "dev",
"script": "max dev",
"err": {
"name": "pnpm",
"message": "@ dev: `max dev`\nspawn ENOENT",
"code": "ELIFECYCLE",
"stack": "pnpm: @ dev: `max dev`\nspawn ENOENT\n at ChildProcess.<anonymous> (/snapshot/dist/pnpm.cjs)\n at ChildProcess.emit (events.js:400:28)\n at maybeClose (internal/child_process.js:1088:16)\n at Process.ChildProcess._handle.onexit (internal/child_process.js:296:5)"
}
},
"2 warn pnpm:global": " Local package.json exists, but node_modules missing, did you mean to install?"
}
\ No newline at end of file
lockfileVersion: 5.4
specifiers:
'@headlessui/react': ^2.2.10
'@heroicons/react': ^2.2.0
'@tailwindcss/postcss': ^4.2.0
'@types/react': ^18.0.33
'@types/react-dom': ^18.0.11
'@umijs/max': ^4.5.3
clsx: ^2.1.1
crypto-js: ^4.2.0
dayjs: ^1.11.20
echarts: ^6.1.0
echarts-for-react: ^3.0.6
jszip: ^3.10.1
react: ^18.3.1
react-dom: ^18.3.1
tailwindcss: ^4.2.0
......@@ -16,11 +21,16 @@ specifiers:
umi-request-progress: ^1.1.0
dependencies:
'@headlessui/react': 2.2.10_nnrd3gsncyragczmpvfhocinkq
'@heroicons/react': 2.2.0_react@18.3.1
'@tailwindcss/postcss': 4.2.4
'@umijs/max': 4.6.51_jmpe7imvoulfyscogwj5klffma
clsx: 2.1.1
crypto-js: 4.2.0
dayjs: 1.11.20
echarts: 6.1.0
echarts-for-react: 3.0.6_echarts@6.1.0+react@18.3.1
jszip: 3.10.1
react: 18.3.1
react-dom: 18.3.1_react@18.3.1
tailwindcss: 4.2.4
......@@ -1533,12 +1543,25 @@ packages:
resolution: {integrity: sha512-jktYRmZwmau63adUG3GKOAVCofBXkk55S/zQ94XOorAHhwqFIOFAy1rSp2N0Wp6/tGbe9V3u/ExlGZypyY17rg==}
dev: false
/@floating-ui/core/1.8.0:
resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
dependencies:
'@floating-ui/utils': 0.2.12
dev: false
/@floating-ui/dom/0.4.5:
resolution: {integrity: sha512-b+prvQgJt8pieaKYMSJBXHxX/DYwdLsAWxKYqnO5dO2V4oo/TYBZJAUQCVNjTWWsrs6o4VDrNcP9+E70HAhJdw==}
dependencies:
'@floating-ui/core': 0.6.2
dev: false
/@floating-ui/dom/1.8.0:
resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
dependencies:
'@floating-ui/core': 1.8.0
'@floating-ui/utils': 0.2.12
dev: false
/@floating-ui/react-dom-interactions/0.3.1_sxhxcuqpgk7ol3pcxkqnid5o2i:
resolution: {integrity: sha512-tP2KEh7EHJr5hokSBHcPGojb+AorDNUf0NYfZGg/M+FsMvCOOsSEeEF0O1NDfETIzDnpbHnCs0DuvCFhSMSStg==}
deprecated: Package renamed to @floating-ui/react
......@@ -1567,6 +1590,34 @@ packages:
- '@types/react'
dev: false
/@floating-ui/react-dom/2.1.9_nnrd3gsncyragczmpvfhocinkq:
resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@floating-ui/dom': 1.8.0
react: 18.3.1
react-dom: 18.3.1_react@18.3.1
dev: false
/@floating-ui/react/0.26.28_nnrd3gsncyragczmpvfhocinkq:
resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@floating-ui/react-dom': 2.1.9_nnrd3gsncyragczmpvfhocinkq
'@floating-ui/utils': 0.2.12
react: 18.3.1
react-dom: 18.3.1_react@18.3.1
tabbable: 6.5.0
dev: false
/@floating-ui/utils/0.2.12:
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
dev: false
/@formatjs/intl-displaynames/1.2.10:
resolution: {integrity: sha512-GROA2RP6+7Ouu0WnHFF78O5XIU7pBfI19WM1qm93l6MFWibUk67nCfVCK3VAYJkLy8L8ZxjkYT11VIAfvSz8wg==}
dependencies:
......@@ -1597,6 +1648,22 @@ packages:
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
dev: false
/@headlessui/react/2.2.10_nnrd3gsncyragczmpvfhocinkq:
resolution: {integrity: sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==}
engines: {node: '>=10'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc
dependencies:
'@floating-ui/react': 0.26.28_nnrd3gsncyragczmpvfhocinkq
'@react-aria/focus': 3.22.1_nnrd3gsncyragczmpvfhocinkq
'@react-aria/interactions': 3.28.1_nnrd3gsncyragczmpvfhocinkq
'@tanstack/react-virtual': 3.14.8_nnrd3gsncyragczmpvfhocinkq
react: 18.3.1
react-dom: 18.3.1_react@18.3.1
use-sync-external-store: 1.6.0_react@18.3.1
dev: false
/@heroicons/react/2.2.0_react@18.3.1:
resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==}
peerDependencies:
......@@ -1668,6 +1735,24 @@ packages:
- supports-color
dev: false
/@internationalized/date/3.12.2:
resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==}
dependencies:
'@swc/helpers': 0.5.15
dev: false
/@internationalized/number/3.6.7:
resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==}
dependencies:
'@swc/helpers': 0.5.15
dev: false
/@internationalized/string/3.2.9:
resolution: {integrity: sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==}
dependencies:
'@swc/helpers': 0.5.15
dev: false
/@isaacs/cliui/8.0.2:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
......@@ -2077,6 +2162,39 @@ packages:
react-dom: 18.3.1_react@18.3.1
dev: false
/@react-aria/focus/3.22.1_nnrd3gsncyragczmpvfhocinkq:
resolution: {integrity: sha512-CPxtkyrBi/HYY5P3lE/57sQ6qfa0lN8E55TOm89H0kNGv0lKt+/0zP7lWERzBjRr5IxBVrQX4gFEowBN52LPaA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
dependencies:
'@swc/helpers': 0.5.15
react: 18.3.1
react-aria: 3.50.0_nnrd3gsncyragczmpvfhocinkq
react-dom: 18.3.1_react@18.3.1
dev: false
/@react-aria/interactions/3.28.1_nnrd3gsncyragczmpvfhocinkq:
resolution: {integrity: sha512-Bqb+HrD5I5MHS2SKBhISYqo2SW8Y2dfzgF/Y1lIJq7xqLxheo9vzxPGEHhz+XzkgGfoqEJx8A6a3C7uiqS3HWA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
dependencies:
'@react-types/shared': 3.36.0_react@18.3.1
'@swc/helpers': 0.5.15
react: 18.3.1
react-aria: 3.50.0_nnrd3gsncyragczmpvfhocinkq
react-dom: 18.3.1_react@18.3.1
dev: false
/@react-types/shared/3.36.0_react@18.3.1:
resolution: {integrity: sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
dependencies:
react: 18.3.1
dev: false
/@sinclair/typebox/0.27.10:
resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
dev: false
......@@ -2447,6 +2565,21 @@ packages:
use-sync-external-store: 1.6.0_react@18.3.1
dev: false
/@tanstack/react-virtual/3.14.8_nnrd3gsncyragczmpvfhocinkq:
resolution: {integrity: sha512-O39GJQpAYEJcIu3uN1//YtmhjSEOyw75vg9CKCatBDPiD5hKtZQoJHfferyrB/LdOD3UWaoMLWtdEjarwIwdDw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
dependencies:
'@tanstack/virtual-core': 3.17.6
react: 18.3.1
react-dom: 18.3.1_react@18.3.1
dev: false
/@tanstack/virtual-core/3.17.6:
resolution: {integrity: sha512-h0/Ebo18CkOrChlQIhNtQkM5ySUnh/GumQ/D1st3hG2HWUPEF+ILUc2k29UtivCi/9G7w7G3/f7Xyd5cCFbKBw==}
dev: false
/@types/babel__core/7.20.5:
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
dependencies:
......@@ -4692,6 +4825,10 @@ packages:
randomfill: 1.0.4
dev: false
/crypto-js/4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
dev: false
/css-blank-pseudo/3.0.3_postcss@8.5.14:
resolution: {integrity: sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==}
engines: {node: ^12 || ^14 || >=16}
......@@ -5252,6 +5389,25 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: false
/echarts-for-react/3.0.6_echarts@6.1.0+react@18.3.1:
resolution: {integrity: sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==}
peerDependencies:
echarts: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
react: ^15.0.0 || >=16.0.0
dependencies:
echarts: 6.1.0
fast-deep-equal: 3.1.3
react: 18.3.1
size-sensor: 1.0.3
dev: false
/echarts/6.1.0:
resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==}
dependencies:
tslib: 2.3.0
zrender: 6.1.0
dev: false
/ee-first/1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
dev: false
......@@ -6665,6 +6821,10 @@ packages:
dev: false
optional: true
/immediate/3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
dev: false
/immer/8.0.4:
resolution: {integrity: sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ==}
dev: false
......@@ -7321,6 +7481,15 @@ packages:
object.values: 1.2.1
dev: false
/jszip/3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
dev: false
/keyv/4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
dependencies:
......@@ -7421,6 +7590,12 @@ packages:
type-check: 0.4.0
dev: false
/lie/3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
dependencies:
immediate: 3.0.6
dev: false
/lightningcss-android-arm64/1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
......@@ -9755,6 +9930,25 @@ packages:
react-dom: 18.3.1_react@18.3.1
dev: false
/react-aria/3.50.0_nnrd3gsncyragczmpvfhocinkq:
resolution: {integrity: sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
dependencies:
'@internationalized/date': 3.12.2
'@internationalized/number': 3.6.7
'@internationalized/string': 3.2.9
'@react-types/shared': 3.36.0_react@18.3.1
'@swc/helpers': 0.5.15
aria-hidden: 1.2.6
clsx: 2.1.1
react: 18.3.1
react-dom: 18.3.1_react@18.3.1
react-stately: 3.48.0_react@18.3.1
use-sync-external-store: 1.6.0_react@18.3.1
dev: false
/react-dom/18.3.1_react@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
......@@ -9891,6 +10085,20 @@ packages:
react: 18.3.1
dev: false
/react-stately/3.48.0_react@18.3.1:
resolution: {integrity: sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
dependencies:
'@internationalized/date': 3.12.2
'@internationalized/number': 3.6.7
'@internationalized/string': 3.2.9
'@react-types/shared': 3.36.0_react@18.3.1
'@swc/helpers': 0.5.15
react: 18.3.1
use-sync-external-store: 1.6.0_react@18.3.1
dev: false
/react/18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
......@@ -10508,6 +10716,10 @@ packages:
resolution: {integrity: sha512-9SQdmsyz4HSP+3gs6PJzhkaMEg+6zTlu9oxIghnwUX3eq+ajq4ft5egl0iyR55LAmO/UwvU8NgIWs/ZyQMa6dw==}
dev: false
/size-sensor/1.0.3:
resolution: {integrity: sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==}
dev: false
/slash/3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
......@@ -11003,6 +11215,10 @@ packages:
resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
dev: false
/tabbable/6.5.0:
resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==}
dev: false
/table/6.9.0:
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
engines: {node: '>=10.0.0'}
......@@ -11126,6 +11342,10 @@ packages:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: false
/tslib/2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
dev: false
/tslib/2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
dev: false
......@@ -11742,3 +11962,9 @@ packages:
/zod/3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
dev: false
/zrender/6.1.0:
resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==}
dependencies:
tslib: 2.3.0
dev: false
......@@ -56,8 +56,9 @@
- `material_title`:材料标题
- `file_asset`:文件对象
- `source_app`:来源应用,可选
- `source_system`:来源系统,可选
- `archive_category`:分类,可选
- `source_system`:来源系统,可选;字段类型为 `select`,候选项读取 Subject 字段配置的 `settings.options`
- `archive_category`:分类,可选;字段类型为 `select`,候选项读取 Subject 字段配置的 `settings.options`
- `archive_year`:所属周期/年份,可选,建议保存四位年份字符串,避免大量同名材料在不同年度间产生视觉混淆
- `uploader` / `uploaded_at`:上传人、上传时间
- `remark`:备注,可选
- `tag`:用户自定义分组标签
......@@ -75,13 +76,13 @@
2. 选择本地文件;若选择的是 `ZIP` 压缩包,可进一步选择上传模式:
- `整包上传`:将压缩包本身作为 1 份材料处理
- `解压后逐文件上传`:按压缩包内文件逐个建档
3. 用户补充标题、来源应用、来源系统、标签、分类、提供部门、提供时间、是否涉密、是否审计部材料等信息
3. 用户补充标题、所属周期/年份、来源应用、来源系统、标签、分类、提供部门、提供时间、是否涉密、是否审计部材料等信息;来源系统和材料分类必须从配置下拉中选择
4. 先调用上传接口上传二进制文件
5. 上传成功后,得到 `{name, type, size, url}` 文件对象
6. 再创建 `am_material` 记录:
- 普通文件或 `整包上传`:创建 1 条记录
- `解压后逐文件上传`:压缩包内有多少个文件,就创建多少条记录
7. 当使用 `解压后逐文件上传` 时,上传表单中的来源应用、来源系统、分类、标签、提供信息、合规属性等字段作为每个文件的通用属性一并写入
7. 当使用 `解压后逐文件上传` 时,上传表单中的所属周期/年份、来源应用、来源系统、分类、标签、提供信息、合规属性等字段作为每个文件的通用属性一并写入
8. 材料进入统一材料池,可被其他审计应用统一查看
### 4.2 在其他业务应用上传材料
......@@ -94,13 +95,13 @@
### 4.3 在材料中心统一查看与管理
1. 用户进入 `材料归档`
2. 在“搜索材料”区域按字段手填筛选条件,支持模糊搜索:
2. 在“搜索材料”区域按字段填写筛选条件;文本字段支持模糊搜索,来源系统和材料分类使用配置下拉筛选:
- 材料标题:匹配 `metadata.material_title`
- 所属周期/年份:匹配 `metadata.archive_year`
- 文件名:匹配 `metadata.file_asset.name`
- 材料分类:匹配 `metadata.archive_category`
- 标签分组:匹配 `metadata.tag`
- 材料分类:从 `archive_category.settings.options` 选择并匹配 `metadata.archive_category`
- 来源应用:匹配 `metadata.source_app`
- 来源系统:匹配 `metadata.source_system`
- 来源系统:从 `source_system.settings.options` 选择并匹配 `metadata.source_system`
- 提供部门:匹配 `metadata.provide_department`
- 备注:匹配 `metadata.remark`
- 是否涉密:匹配 `metadata.is_confidential`
......@@ -109,22 +110,22 @@
4. 列表默认每页 20 条,支持切换为 50 或 100 条
5. 用户通过上一页 / 下一页进行分页浏览
6. 在“全部文件”视图下,直接以列表展示当前筛选条件下的全部材料
7. 切换到某个分类标签后,系统将分类名称回填到“材料分类”搜索字段,并重新从第一页加载模糊匹配结果
7. 切换到某个分类标签后,系统将分类存储值回填到“材料分类”筛选字段,并重新从第一页加载匹配结果
8. `未分类` 仅表示 `metadata.archive_category` 为空、空字符串或缺失,不作为真实分类值写回查询条件
9. 列表支持当前页批量勾选,并提供“全选当前页”能力
10. 勾选后可执行以下批量操作:
- 批量下载/导出:勾选 1 份材料时直接下载原文件;勾选多份材料时,前端读取各材料 `metadata.file_asset.url` 并打包为 `ZIP` 压缩包下载
- 批量修改:按字段选择是否覆盖来源应用、来源系统、分类、提供部门、提供时间、是否涉密、是否审计部材料
- 批量修改:按字段选择是否覆盖所属周期/年份、来源应用、来源系统、分类、提供部门、提供时间、是否涉密、是否审计部材料;来源系统或材料分类启用覆盖后从对应配置下拉选择
- 批量打标签:统一覆盖 `metadata.tag`
- 批量删除:对已勾选材料执行软删除
11. 点击某份材料后,以弹框方式打开详情
12. 弹框默认进入查看态,字段不可直接编辑,可查看:
- 文件信息与打开或下载入口
- 标签与分类
- 所属周期/年份、标签与分类
- 来源信息
- 提供部门、提供时间
- 涉密标记、审计部材料标记
13. 用户点击“编辑”后进入编辑态,可修改材料标题、来源应用、来源系统、分类、标签、提供信息与合规属性
13. 用户点击“编辑”后进入编辑态,可修改材料标题、所属周期/年份、来源应用、来源系统、分类、标签、提供信息与合规属性;来源系统和材料分类从配置下拉选择
14. 编辑态下点击“取消编辑”放弃未保存修改并恢复当前材料原值
15. 编辑态下点击“保存修改”后调用材料更新接口,保存成功后生效并回到查看态
16. 详情弹框提供单个删除入口,删除前需二次确认,确认后对当前材料执行软删除并关闭弹框
......@@ -193,9 +194,10 @@
建议至少包含:
- 上传按钮
- 标签筛选
- 年份筛选
- 来源应用筛选
- 来源系统筛选
- 来源系统下拉筛选,选项来自 `am_material.source_system.settings.options`
- 材料分类下拉筛选,选项来自 `am_material.archive_category.settings.options`
- 分类切换标签页
- 材料列表表格
- 当前页批量勾选工具条
......@@ -224,6 +226,7 @@
建议展示:
- 材料标题
- 所属周期/年份
- 文件名 / 类型 / 大小
- 标签
- 分类
......@@ -259,16 +262,19 @@
- 字段搜索采用展开式表单,不再使用聚合关键词搜索
- 可使用以下过滤条件:
- `metadata.material_title`
- `metadata.archive_year`
- `metadata.file_asset.name`
- `metadata.source_app`
- `metadata.source_system`
- `metadata.archive_category`
- `metadata.tag`
- `metadata.provide_department`
- `metadata.remark`
- `metadata.is_confidential`
- `metadata.is_audit_dept_material`
- 文本字段按 `$regex` + `$options: i` 组织为模糊匹配
- 除来源系统和材料分类外,文本字段按 `$regex` + `$options: i` 组织为模糊匹配
- 来源系统筛选值来自 `settings.options`,按选项存储值查询 `metadata.source_system`
- 材料分类筛选值来自 `settings.options`,按选项存储值查询 `metadata.archive_category`
- `metadata.archive_year` 按文本字段处理,用户输入 `2025` 时可检索到该年度材料
- 布尔字段采用“全部 / 是 / 否”三态筛选,选择“全部”时不进入查询条件
- 多个搜索字段同时填写时按 AND 关系组合
......@@ -310,13 +316,18 @@
- 创建 `am_material` 失败时,需要提示用户重试,并避免出现“文件已传但列表无记录”的静默失败
- ZIP 拆分上传若中途失败,需要明确提示已成功创建的记录条数以及失败文件名,便于用户补传
- 标签允许自由输入,但需控制重复标签与空标签
- 分类和标签用于支持用户自定义分组
- 材料分类由 Subject 选项配置统一维护,标签继续支持用户自定义分组
- 批量下载/导出依赖 `metadata.file_asset.url` 可被当前浏览器访问;若跨域或文件失效导致拉取失败,多文件 ZIP 中会跳过失败项并提示用户
- 批量修改仅允许覆盖用户显式勾选的字段,未勾选字段必须保留原值,避免误覆盖
- 批量删除前必须二次确认,默认仍按软删除处理
- 当前版本的批量勾选范围限定在“当前筛选结果的当前页”,切换分页后需要重新确认勾选结果
- `provide_time` 提交时统一转为 `ISO 8601`
- 布尔字段 `is_confidential`、`is_audit_dept_material` 需要以 true/false 写入 `metadata`
- `archive_year` 写入 `metadata.archive_year`,建议使用四位年份字符串;为空时不进入年份搜索条件
- `source_system` 的搜索、上传、详情编辑和批量修改控件均读取 Subject 字段的 `settings.options`
- `archive_category` 的搜索、上传、详情编辑和批量修改控件均读取 Subject 字段的 `settings.options`
- Subject 配置加载期间来源系统和材料分类控件显示加载状态;加载失败或未配置选项时禁用对应控件并提示用户刷新或联系管理员补充配置
- 当前配置选项为:投资交易系统、估值系统、注册登记系统、OA系统、财务系统、产品系统、投研系统、外部导出、其他业务系统
---
......
......@@ -2,6 +2,7 @@ import {
batchRemoveRecords,
countRecords,
createRecord,
loadSubject,
queryRecords,
removeRecord,
updateRecord,
......@@ -40,14 +41,6 @@ export const SOURCE_APP_SUGGESTIONS = [
{ value: '中台', label: '中台' },
];
export const SOURCE_SYSTEM_SUGGESTIONS = [
{ value: 'audit_material_manage', label: '审计材料管理系统' },
{ value: 'project_manager_xy', label: '审计项目管理系统' },
{ value: 'fund_audit', label: '数智报告平台' },
{ value: 'aml_tables', label: '反洗钱数据表' },
{ value: 'middle_platform', label: '中台' },
];
const ARCHIVE_IGNORE_PATTERNS = [/^__MACOSX\//, /\/\.DS_Store$/i, /^\.DS_Store$/i];
const MATERIAL_EXPORT_ZIP_NAME_PREFIX = '审计材料批量导出';
const MATERIAL_SEARCH_FIELDS = [
......@@ -56,6 +49,11 @@ const MATERIAL_SEARCH_FIELDS = [
recordPath: 'metadata.material_title',
valueGetter: (item) => item.materialTitle,
},
{
filterKey: 'archiveYear',
recordPath: 'metadata.archive_year',
valueGetter: (item) => item.archiveYear,
},
{
filterKey: 'fileName',
recordPath: 'metadata.file_asset.name',
......@@ -124,6 +122,7 @@ const DEMO_MATERIALS = [
source_app: 'project_manager_xy',
source_system: 'project_manager_xy',
archive_category: '审计证据',
archive_year: '2026',
uploader: {
_id: '69b12a47e92b1de77c94a9f5',
display_name: '杨康',
......@@ -150,6 +149,7 @@ const DEMO_MATERIALS = [
source_app: 'audit_material_center',
source_system: 'audit_material_manage',
archive_category: '项目资料',
archive_year: '2026',
uploader: {
_id: '69b12a47e92b1de77c94a9f5',
display_name: '杨康',
......@@ -176,6 +176,7 @@ const DEMO_MATERIALS = [
source_app: 'issue_rectification',
source_system: 'project_manager_xy',
archive_category: '整改资料',
archive_year: '2025',
uploader: {
_id: '69b12a47e92b1de77c94a9f6',
display_name: '整改专员',
......@@ -202,6 +203,7 @@ const DEMO_MATERIALS = [
source_app: 'audit_material_center',
source_system: 'audit_material_manage',
archive_category: '项目资料',
archive_year: '2025',
uploader: {
_id: '69b12a47e92b1de77c94a9f7',
display_name: '审计经理',
......@@ -228,6 +230,7 @@ const DEMO_MATERIALS = [
source_app: 'project_manager_xy',
source_system: 'project_manager_xy',
archive_category: '审计证据',
archive_year: '2024',
uploader: {
_id: '69b12a47e92b1de77c94a9f8',
display_name: '现场组',
......@@ -358,6 +361,7 @@ export function normalizeMaterialRecord(record = {}) {
const sourceApp = metadata.source_app || '';
const sourceSystem = metadata.source_system || '';
const category = metadata.archive_category || '';
const archiveYear = metadata.archive_year || '';
const tag = metadata.tag || '';
const provideTime = metadata.provide_time || '';
......@@ -377,6 +381,8 @@ export function normalizeMaterialRecord(record = {}) {
sourceSystem,
sourceSystemLabel: getSourceSystemLabel(sourceSystem),
archiveCategory: category || UNCATEGORIZED_CATEGORY_LABEL,
archiveYear,
archiveYearLabel: archiveYear || '未设置年份',
tag,
remark: metadata.remark || '',
provideDepartment: metadata.provide_department || '',
......@@ -421,15 +427,6 @@ function normalizeSourceAppFilter(value = '') {
return matchedOption?.value || normalizedValue;
}
function normalizeSourceSystemFilter(value = '') {
const normalizedValue = normalizeFilterValue(value);
const matchedOption = SOURCE_SYSTEM_SUGGESTIONS.find(
(item) => item.value === normalizedValue || item.label === normalizedValue,
);
return matchedOption?.value || normalizedValue;
}
function includesFuzzy(value = '', keyword = '') {
const normalizedKeyword = normalizeFilterValue(keyword).toLowerCase();
return (
......@@ -460,8 +457,17 @@ function applyClientFilters(materials, filters = {}) {
? !String(item.rawMetadata?.archive_category || '').trim()
: true);
const matchesSearchFields = MATERIAL_SEARCH_FIELDS.every(
({ filterKey, valueGetter }) =>
includesFuzzy(valueGetter(item), filters[filterKey]),
({ filterKey, valueGetter }) => {
if (filterKey === 'archiveCategory') {
return (
!archiveCategory ||
isUncategorizedCategory(archiveCategory) ||
normalizeFilterValue(valueGetter(item)) === archiveCategory
);
}
return includesFuzzy(valueGetter(item), filters[filterKey]);
},
);
const matchesBooleanFields = MATERIAL_BOOLEAN_SEARCH_FIELDS.every(
({ filterKey, valueGetter }) => {
......@@ -528,8 +534,6 @@ function buildRecordFilter(filters = {}) {
const value =
filterKey === 'sourceApp'
? normalizeSourceAppFilter(filters[filterKey])
: filterKey === 'sourceSystem'
? normalizeSourceSystemFilter(filters[filterKey])
: normalizeFilterValue(filters[filterKey]);
if (!value || filterKey === 'archiveCategory') {
return;
......@@ -562,7 +566,7 @@ function buildRecordFilter(filters = {}) {
{ 'metadata.archive_category': { $exists: false } },
],
}
: { 'metadata.archive_category': buildFuzzyFilter(archiveCategory) },
: { 'metadata.archive_category': archiveCategory },
);
}
......@@ -632,6 +636,11 @@ export function buildMaterialDashboard(materials = [], options = {}) {
(item) => item.provideDepartment,
(item) => item.provideDepartment,
);
const archiveYearGroups = buildGroup(
materials.filter((item) => item.archiveYear),
(item) => item.archiveYear,
(item) => item.archiveYear,
);
const recentMaterials = [...materials]
.sort((left, right) =>
......@@ -664,6 +673,9 @@ export function buildMaterialDashboard(materials = [], options = {}) {
if (!item.rawMetadata?.archive_category) {
attentionReasons.push('缺少材料分类');
}
if (!item.archiveYear) {
attentionReasons.push('缺少所属周期/年份');
}
if (!item.tag) {
attentionReasons.push('缺少标签分组');
}
......@@ -710,6 +722,7 @@ export function buildMaterialDashboard(materials = [], options = {}) {
categoryGroups,
tagGroups,
provideDepartmentGroups,
archiveYearGroups,
recentMaterials,
attentionMaterials,
sourceSuggestions: [
......@@ -721,20 +734,86 @@ export function buildMaterialDashboard(materials = [], options = {}) {
label: item.label,
})),
],
sourceSystemSuggestions: [
...SOURCE_SYSTEM_SUGGESTIONS,
...sourceSystemGroups
.filter((item) => !SOURCE_SYSTEM_LABEL_MAP[item.key])
.map((item) => ({
value: item.key,
label: item.label,
})),
],
sourceSystemSuggestions: sourceSystemGroups.map((item) => ({
value: item.key,
label: item.label,
})),
categorySuggestions: categoryGroups.map((item) => item.label),
tagSuggestions: tagGroups.map((item) => item.label),
archiveYearSuggestions: archiveYearGroups.map((item) => item.label),
};
}
function normalizeSelectOptions(options = []) {
if (!Array.isArray(options)) {
return [];
}
return options
.map((option) => {
if (typeof option === 'string') {
const value = option.trim();
return { value, label: value };
}
const value = String(
option?.value ?? option?.label ?? option?.title ?? '',
).trim();
const label = String(
option?.label ?? option?.title ?? option?.value ?? '',
).trim();
return { value, label };
})
.filter((option) => option.value && option.label);
}
function extractSubjectFields(subject) {
const candidate = subject?.data || subject?.result || subject?.subject || subject;
return candidate?.fields || candidate?.metadata?.fields || [];
}
export async function loadMaterialSelectFieldOptions() {
try {
const subject = await loadSubject(MATERIAL_SUBJECT);
const fields = extractSubjectFields(subject);
const sourceSystemField = fields.find(
(field) => (field?.name || field?.alias || field?.key) === 'source_system',
);
const archiveCategoryField = fields.find(
(field) =>
(field?.name || field?.alias || field?.key) === 'archive_category',
);
const sourceSystemOptions = normalizeSelectOptions(
sourceSystemField?.settings?.options,
);
const archiveCategoryOptions = normalizeSelectOptions(
archiveCategoryField?.settings?.options,
);
return {
sourceSystemOptions,
sourceSystemError:
sourceSystemOptions.length > 0
? ''
: 'am_material.source_system 未配置 settings.options',
archiveCategoryOptions,
archiveCategoryError:
archiveCategoryOptions.length > 0
? ''
: 'am_material.archive_category 未配置 settings.options',
};
} catch (error) {
const message =
error?.message || '材料字段选项加载失败,请检查 Subject 配置后重试。';
return {
sourceSystemOptions: [],
sourceSystemError: message,
archiveCategoryOptions: [],
archiveCategoryError: message,
};
}
}
async function queryMaterialRecords(filters = {}) {
const filter = buildRecordFilter(filters);
......@@ -959,6 +1038,7 @@ function buildMaterialPayload({
source_app: String(values.sourceApp || '').trim(),
source_system: String(values.sourceSystem || '').trim(),
archive_category: String(values.archiveCategory || '').trim(),
archive_year: String(values.archiveYear || '').trim(),
uploader:
currentUser?._id || currentUser?.username || currentUser?.id || '',
uploaded_at: dayjs().toISOString(),
......@@ -1078,6 +1158,7 @@ export async function updateMaterialRecord(material, values) {
source_app: String(values.sourceApp || '').trim(),
source_system: String(values.sourceSystem || '').trim(),
archive_category: String(values.archiveCategory || '').trim(),
archive_year: String(values.archiveYear || '').trim(),
uploader:
material.rawMetadata?.uploader ||
material.uploaderValue ||
......
......@@ -55,10 +55,10 @@ import {
inferTitleFromFile,
isZipFile,
loadMaterialPage,
loadMaterialSelectFieldOptions,
removeMaterialRecord,
UNCATEGORIZED_CATEGORY_LABEL,
SOURCE_APP_SUGGESTIONS,
SOURCE_SYSTEM_SUGGESTIONS,
updateMaterialRecord,
} from '../materialService';
......@@ -69,9 +69,9 @@ const PAGE_SIZE_OPTIONS = [20, 50, 100];
function buildEmptyMaterialSearchForm() {
return {
materialTitle: '',
archiveYear: '',
fileName: '',
archiveCategory: '',
tag: '',
sourceApp: '',
sourceSystem: '',
provideDepartment: '',
......@@ -167,6 +167,11 @@ function MaterialListRow({ material, onOpen, checked, onToggle }) {
<TableCell className="font-medium text-zinc-950">
<div className="max-w-[340px] truncate">{material.materialTitle}</div>
</TableCell>
<TableCell className="text-zinc-600">
<Badge tone={material.archiveYear ? 'amber' : 'default'}>
{material.archiveYearLabel}
</Badge>
</TableCell>
<TableCell className="text-zinc-600">
<div className="max-w-[240px] truncate">{material.fileName}</div>
<div className="mt-1 text-xs text-zinc-500">
......@@ -190,6 +195,7 @@ function MaterialListRow({ material, onOpen, checked, onToggle }) {
function buildEmptyUploadForm() {
return {
materialTitle: '',
archiveYear: '',
sourceApp: '',
sourceSystem: '',
archiveCategory: '',
......@@ -204,6 +210,7 @@ function buildEmptyUploadForm() {
function buildEmptyBatchEditForm() {
return {
archiveYear: '',
sourceApp: '',
sourceSystem: '',
archiveCategory: '',
......@@ -216,6 +223,7 @@ function buildEmptyBatchEditForm() {
function buildEmptyBatchFieldSelection() {
return {
archiveYear: false,
sourceApp: false,
sourceSystem: false,
archiveCategory: false,
......@@ -246,6 +254,12 @@ function formatDateTimeLocalInput(value) {
return new Date(date.getTime() - timezoneOffset).toISOString().slice(0, 16);
}
function getMaterialArchiveCategoryValue(material) {
const archiveCategory =
material?.rawMetadata?.archive_category || material?.archiveCategory || '';
return archiveCategory === UNCATEGORIZED_CATEGORY_LABEL ? '' : archiveCategory;
}
function buildDetailForm(material) {
if (!material) {
return buildEmptyUploadForm();
......@@ -253,11 +267,11 @@ function buildDetailForm(material) {
return {
materialTitle: material.materialTitle || '',
archiveYear: material.rawMetadata?.archive_year || material.archiveYear || '',
sourceApp: material.rawMetadata?.source_app || material.sourceApp || '',
sourceSystem:
material.rawMetadata?.source_system || material.sourceSystem || '',
archiveCategory:
material.rawMetadata?.archive_category || material.archiveCategory || '',
archiveCategory: getMaterialArchiveCategoryValue(material),
tag: material.rawMetadata?.tag || material.tag || '',
provideDepartment:
material.rawMetadata?.provide_department || material.provideDepartment || '',
......@@ -269,19 +283,18 @@ function buildDetailForm(material) {
};
}
function FormSection({ eyebrow, title, description, children }) {
function FormSection({ title, description, children }) {
return (
<section className="rounded-[24px] border border-zinc-200 bg-zinc-50/70 p-4 sm:p-5">
<div className="max-w-2xl">
<div className="text-[11px] font-semibold tracking-[0.24em] text-zinc-500">
{eyebrow}
</div>
<div className="mt-2 text-lg font-semibold tracking-tight text-zinc-950">
<div className="text-lg font-semibold tracking-tight text-zinc-950">
{title}
</div>
<p className="mt-1 text-sm leading-6 text-zinc-500">{description}</p>
{description ? (
<p className="mt-1 text-sm leading-6 text-zinc-500">{description}</p>
) : null}
</div>
<div className="mt-5">{children}</div>
<div className={description ? 'mt-5' : 'mt-4'}>{children}</div>
</section>
);
}
......@@ -295,6 +308,57 @@ function FieldLabel({ children, required = false }) {
);
}
function ConfiguredSelect({
value,
options,
loading,
error,
fieldLabel = '选项',
emptyLabel = '请选择(可选)',
disabled = false,
showError = false,
...props
}) {
const hasUnconfiguredValue =
Boolean(value) &&
!loading &&
!error &&
!options.some((option) => option.value === value);
return (
<div>
<Select
{...props}
value={value}
disabled={disabled || loading || Boolean(error)}
>
<option value="">
{loading
? `正在加载${fieldLabel}...`
: error
? `${fieldLabel}配置不可用`
: emptyLabel}
</option>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
{showError && error ? (
<div className="mt-2 text-xs text-rose-600" role="alert">
{error} 请刷新页面重试,或联系管理员补充字段配置。
</div>
) : null}
{hasUnconfiguredValue ? (
<div className="mt-2 text-xs text-amber-700">
当前值“{value}”不在可选配置中;如不重新选择,保存时将保留原值。
</div>
) : null}
</div>
);
}
function sortMaterialsByTime(materials) {
return [...materials].sort((left, right) =>
String(right.uploadedAt || right.updatedAt).localeCompare(
......@@ -343,6 +407,15 @@ export default function ResourceBookingPage() {
const [exporting, setExporting] = useState(false);
const [error, setError] = useState('');
const [notice, setNotice] = useState('');
const [sourceSystemOptions, setSourceSystemOptions] = useState([]);
const [sourceSystemOptionsLoading, setSourceSystemOptionsLoading] =
useState(true);
const [sourceSystemOptionsError, setSourceSystemOptionsError] = useState('');
const [archiveCategoryOptions, setArchiveCategoryOptions] = useState([]);
const [archiveCategoryOptionsLoading, setArchiveCategoryOptionsLoading] =
useState(true);
const [archiveCategoryOptionsError, setArchiveCategoryOptionsError] =
useState('');
const [isDemoMode, setIsDemoMode] = useState(false);
const [isUploadOpen, setIsUploadOpen] = useState(false);
const [isDetailOpen, setIsDetailOpen] = useState(false);
......@@ -369,6 +442,33 @@ export default function ResourceBookingPage() {
setIsDetailEditing(false);
}, [activeMaterial]);
useEffect(() => {
let cancelled = false;
async function run() {
setSourceSystemOptionsLoading(true);
setArchiveCategoryOptionsLoading(true);
const result = await loadMaterialSelectFieldOptions();
if (cancelled) {
return;
}
setSourceSystemOptions(result.sourceSystemOptions || []);
setSourceSystemOptionsError(result.sourceSystemError || '');
setSourceSystemOptionsLoading(false);
setArchiveCategoryOptions(result.archiveCategoryOptions || []);
setArchiveCategoryOptionsError(result.archiveCategoryError || '');
setArchiveCategoryOptionsLoading(false);
}
run();
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
let cancelled = false;
......@@ -428,22 +528,7 @@ export default function ResourceBookingPage() {
[],
);
}, [dashboard.sourceSuggestions]);
const sourceSystemOptions = useMemo(() => {
const dynamicOptions = (dashboard.sourceSystemSuggestions || []).filter(Boolean);
return [...SOURCE_SYSTEM_SUGGESTIONS, ...dynamicOptions].reduce(
(accumulator, item) => {
if (!item?.value || accumulator.some((current) => current.value === item.value)) {
return accumulator;
}
accumulator.push(item);
return accumulator;
},
[],
);
}, [dashboard.sourceSystemSuggestions]);
const tagSuggestions = dashboard.tagSuggestions || [];
const archiveCategorySuggestions = dashboard.categorySuggestions || [];
const archiveYearSuggestions = dashboard.archiveYearSuggestions || [];
const categoryTabs = useMemo(
() => [
{
......@@ -793,6 +878,9 @@ export default function ResourceBookingPage() {
try {
await batchUpdateMaterialRecords(selectedMaterials, (material) => ({
materialTitle: material.materialTitle,
archiveYear: batchEditFields.archiveYear
? batchEditForm.archiveYear
: material.rawMetadata?.archive_year || material.archiveYear || '',
sourceApp: batchEditFields.sourceApp
? batchEditForm.sourceApp
: material.rawMetadata?.source_app || material.sourceApp || '',
......@@ -801,9 +889,7 @@ export default function ResourceBookingPage() {
: material.rawMetadata?.source_system || material.sourceSystem || '',
archiveCategory: batchEditFields.archiveCategory
? batchEditForm.archiveCategory
: material.rawMetadata?.archive_category ||
material.archiveCategory ||
'',
: getMaterialArchiveCategoryValue(material),
tag: material.rawMetadata?.tag || material.tag || '',
provideDepartment: batchEditFields.provideDepartment
? batchEditForm.provideDepartment
......@@ -853,11 +939,11 @@ export default function ResourceBookingPage() {
try {
await batchUpdateMaterialRecords(selectedMaterials, (material) => ({
materialTitle: material.materialTitle,
archiveYear: material.rawMetadata?.archive_year || material.archiveYear || '',
sourceApp: material.rawMetadata?.source_app || material.sourceApp || '',
sourceSystem:
material.rawMetadata?.source_system || material.sourceSystem || '',
archiveCategory:
material.rawMetadata?.archive_category || material.archiveCategory || '',
archiveCategory: getMaterialArchiveCategoryValue(material),
tag: String(batchTagForm.tag || '').trim(),
provideDepartment:
material.rawMetadata?.provide_department || material.provideDepartment || '',
......@@ -947,50 +1033,28 @@ export default function ResourceBookingPage() {
return (
<div className="space-y-6">
<Panel className="relative overflow-hidden p-6 sm:p-8">
<div className="absolute inset-0 bg-[linear-gradient(90deg,rgba(255,241,242,0.96)_0%,rgba(255,255,255,0.97)_48%,rgba(255,245,245,0.94)_100%),radial-gradient(circle_at_top_left,rgba(225,29,72,0.16),transparent_38%),radial-gradient(circle_at_top_right,rgba(251,113,133,0.12),transparent_32%)]" />
<div className="relative">
<div className="inline-flex rounded-full border border-rose-200 bg-rose-50 px-3 py-1 text-xs font-semibold tracking-[0.2em] text-rose-700">
MATERIAL WORKSPACE
</div>
<div className="mt-5 flex flex-col gap-6 xl:flex-row xl:items-end xl:justify-between">
<div className="max-w-3xl">
<h1 className="text-3xl font-semibold tracking-tight text-zinc-950 sm:text-4xl">
同类材料放一块看,切换起来也要足够顺手
</h1>
<p className="mt-3 text-sm leading-7 text-zinc-600 sm:text-base">
现在支持“全部文件”列表总览,也支持按分类切换浏览。你可以先从分类层面扫一遍,再点开弹框处理单份材料的细节。
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<Button
color="blue"
className="cursor-pointer"
onClick={() => {
setError('');
setNotice('');
setIsUploadOpen(true);
}}
>
<ArrowUpTrayIcon className="size-4" />
上传材料
</Button>
<Button
outline
className="cursor-pointer"
onClick={resetMaterialSearch}
>
<SparklesIcon className="size-4" />
重置筛选
</Button>
</div>
<Panel className="p-5 sm:p-6">
<div>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<h1 className="text-2xl font-semibold text-zinc-950">材料归档</h1>
<Button
color="blue"
className="cursor-pointer"
onClick={() => {
setError('');
setNotice('');
setIsUploadOpen(true);
}}
>
<ArrowUpTrayIcon className="size-4" />
上传材料
</Button>
</div>
{error ? (
<div
className={clsx(
'mt-6 rounded-2xl border px-4 py-3 text-sm',
'mt-4 rounded-2xl border px-4 py-3 text-sm',
isDemoMode
? 'border-amber-200 bg-amber-50 text-amber-700'
: 'border-rose-200 bg-rose-50 text-rose-700',
......@@ -1001,7 +1065,7 @@ export default function ResourceBookingPage() {
) : null}
{!error && notice ? (
<div className="mt-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
<div className="mt-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
{notice}
</div>
) : null}
......@@ -1009,11 +1073,19 @@ export default function ResourceBookingPage() {
</Panel>
<Panel className="p-5 sm:p-6">
<div>
<div className="mb-3 flex items-center gap-2 text-sm font-medium text-zinc-600">
<div className="mb-4 flex items-center justify-between gap-4">
<div className="flex items-center gap-2 text-sm font-medium text-zinc-600">
<MagnifyingGlassIcon className="size-4" />
搜索材料
</div>
<Button
outline
className="cursor-pointer"
onClick={resetMaterialSearch}
>
<SparklesIcon className="size-4" />
重置筛选
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
......@@ -1032,48 +1104,47 @@ export default function ResourceBookingPage() {
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">文件名</div>
<div className="mb-2 text-sm font-medium text-zinc-600">所属周期/年份</div>
<Input
value={materialSearchForm.fileName}
list="archive-year-search-suggestions"
value={materialSearchForm.archiveYear}
onChange={(event) =>
updateMaterialSearchField('fileName', event.target.value)
updateMaterialSearchField('archiveYear', event.target.value)
}
placeholder="例如:底稿.pdf"
placeholder="例如:2025"
/>
<datalist id="archive-year-search-suggestions">
{archiveYearSuggestions.map((item) => (
<option key={item} value={item} />
))}
</datalist>
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">材料分类</div>
<div className="mb-2 text-sm font-medium text-zinc-600">文件名</div>
<Input
list="archive-category-search-suggestions"
value={materialSearchForm.archiveCategory}
value={materialSearchForm.fileName}
onChange={(event) =>
updateMaterialSearchField('archiveCategory', event.target.value)
updateMaterialSearchField('fileName', event.target.value)
}
placeholder="例如:审计证据"
placeholder="例如:底稿.pdf"
/>
<datalist id="archive-category-search-suggestions">
{archiveCategorySuggestions.map((item) => (
<option key={item} value={item} />
))}
</datalist>
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">标签分组</div>
<Input
list="tag-search-suggestions"
value={materialSearchForm.tag}
<div className="mb-2 text-sm font-medium text-zinc-600">材料分类</div>
<ConfiguredSelect
value={materialSearchForm.archiveCategory}
options={archiveCategoryOptions}
loading={archiveCategoryOptionsLoading}
error={archiveCategoryOptionsError}
fieldLabel="材料分类"
emptyLabel="全部材料分类"
showError
onChange={(event) =>
updateMaterialSearchField('tag', event.target.value)
updateMaterialSearchField('archiveCategory', event.target.value)
}
placeholder="例如:项目一期"
/>
<datalist id="tag-search-suggestions">
{tagSuggestions.map((item) => (
<option key={item} value={item} />
))}
</datalist>
</div>
<div>
......@@ -1097,21 +1168,18 @@ export default function ResourceBookingPage() {
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">来源系统</div>
<Input
list="source-system-search-suggestions"
<ConfiguredSelect
value={materialSearchForm.sourceSystem}
options={sourceSystemOptions}
loading={sourceSystemOptionsLoading}
error={sourceSystemOptionsError}
fieldLabel="来源系统"
emptyLabel="全部来源系统"
showError
onChange={(event) =>
updateMaterialSearchField('sourceSystem', event.target.value)
}
placeholder="例如:审计材料管理系统 / 中台"
/>
<datalist id="source-system-search-suggestions">
{sourceSystemOptions.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</datalist>
</div>
<div>
......@@ -1210,7 +1278,7 @@ export default function ResourceBookingPage() {
) : visibleMaterials.length === 0 ? (
<EmptyStatePage
title="当前分类下没有可展示的材料"
description="你可以切换到“全部文件”查看汇总,或者调整搜索、来源和标签筛选条件。"
description="你可以切换到“全部文件”查看汇总,或者调整搜索和来源筛选条件。"
/>
) : (
<div className="space-y-6">
......@@ -1310,6 +1378,7 @@ export default function ResourceBookingPage() {
/>
</TableHeader>
<TableHeader>材料标题</TableHeader>
<TableHeader>所属周期/年份</TableHeader>
<TableHeader>文件信息</TableHeader>
<TableHeader>分类 / 标签</TableHeader>
<TableHeader>来源应用</TableHeader>
......@@ -1430,6 +1499,9 @@ export default function ResourceBookingPage() {
归档标签
</div>
<div className="mt-2 flex flex-wrap gap-2">
<Badge tone={activeMaterial.archiveYear ? 'amber' : 'default'}>
{activeMaterial.archiveYearLabel}
</Badge>
<Badge tone="sky">{activeMaterial.archiveCategory}</Badge>
{activeMaterial.tag ? (
<Badge tone="emerald">{activeMaterial.tag}</Badge>
......@@ -1479,9 +1551,7 @@ export default function ResourceBookingPage() {
: '当前处于查看态,字段已锁定,点击编辑后可维护材料信息。'}
</div>
<FormSection
eyebrow="BASIC INFO"
title="基础信息"
description="这里维护材料的主展示信息和材料池检索字段。"
>
<div className="grid gap-4 sm:grid-cols-2">
<div>
......@@ -1497,6 +1567,21 @@ export default function ResourceBookingPage() {
}
/>
</div>
<div>
<FieldLabel>所属周期/年份</FieldLabel>
<Input
list="archive-year-suggestions"
value={detailForm.archiveYear}
disabled={!isDetailEditing || saving}
onChange={(event) =>
setDetailForm((prev) => ({
...prev,
archiveYear: event.target.value,
}))
}
placeholder="例如:2025"
/>
</div>
<div>
<FieldLabel>来源应用</FieldLabel>
<Input
......@@ -1514,9 +1599,12 @@ export default function ResourceBookingPage() {
</div>
<div>
<FieldLabel>来源系统</FieldLabel>
<Input
list="source-system-suggestions"
<ConfiguredSelect
value={detailForm.sourceSystem}
options={sourceSystemOptions}
loading={sourceSystemOptionsLoading}
error={sourceSystemOptionsError}
fieldLabel="来源系统"
disabled={!isDetailEditing || saving}
onChange={(event) =>
setDetailForm((prev) => ({
......@@ -1524,14 +1612,17 @@ export default function ResourceBookingPage() {
sourceSystem: event.target.value,
}))
}
placeholder="可选"
/>
</div>
<div>
<FieldLabel>材料分类</FieldLabel>
<Input
list="archive-category-suggestions"
<ConfiguredSelect
value={detailForm.archiveCategory}
options={archiveCategoryOptions}
loading={archiveCategoryOptionsLoading}
error={archiveCategoryOptionsError}
fieldLabel="材料分类"
emptyLabel="请选择材料分类(可选)"
disabled={!isDetailEditing || saving}
onChange={(event) =>
setDetailForm((prev) => ({
......@@ -1539,7 +1630,6 @@ export default function ResourceBookingPage() {
archiveCategory: event.target.value,
}))
}
placeholder="可选"
/>
</div>
<div>
......@@ -1561,9 +1651,7 @@ export default function ResourceBookingPage() {
</FormSection>
<FormSection
eyebrow="PROVIDE INFO"
title="提供信息"
description="对应对象文档中的提供部门与提供时间,方便后续核对材料来源链路。"
>
<div className="grid gap-4 sm:grid-cols-2">
<div>
......@@ -1598,9 +1686,7 @@ export default function ResourceBookingPage() {
</FormSection>
<FormSection
eyebrow="COMPLIANCE"
title="合规属性"
description="布尔类字段改用 Catalyst Checkbox,对应对象定义中的合规标识。"
>
<div className="grid gap-4 sm:grid-cols-2">
<CheckboxField className="rounded-2xl border border-zinc-200 bg-white px-4 py-3">
......@@ -1655,13 +1741,8 @@ export default function ResourceBookingPage() {
<option key={item.value} value={item.value} />
))}
</datalist>
<datalist id="source-system-suggestions">
{sourceSystemOptions.map((item) => (
<option key={item.value} value={item.value} />
))}
</datalist>
<datalist id="archive-category-suggestions">
{archiveCategorySuggestions.map((item) => (
<datalist id="archive-year-suggestions">
{archiveYearSuggestions.map((item) => (
<option key={item} value={item} />
))}
</datalist>
......@@ -1732,11 +1813,45 @@ export default function ResourceBookingPage() {
</div>
<FormSection
eyebrow="BATCH FIELDS"
title="批量覆盖字段"
description="先勾选要更新的字段,再填写统一值,避免误把空值覆盖到所有材料。"
>
<div className="grid gap-4 sm:grid-cols-2">
<CheckboxField className="rounded-2xl border border-zinc-200 bg-white px-4 py-3">
<Checkbox
color="sky"
checked={batchEditFields.archiveYear}
onChange={(checked) =>
setBatchEditFields((prev) => ({
...prev,
archiveYear: Boolean(checked),
}))
}
/>
<div>
<div data-slot="label" className="text-sm font-medium text-zinc-900">
批量更新所属周期/年份
</div>
<div data-slot="description" className="text-sm text-zinc-500">
勾选后可统一覆盖所有已选材料的年度归属。
</div>
</div>
</CheckboxField>
<div>
<FieldLabel>所属周期/年份</FieldLabel>
<Input
list="archive-year-suggestions"
value={batchEditForm.archiveYear}
onChange={(event) =>
setBatchEditForm((prev) => ({
...prev,
archiveYear: event.target.value,
}))
}
placeholder="可留空,用于统一清空"
/>
</div>
<CheckboxField className="rounded-2xl border border-zinc-200 bg-white px-4 py-3">
<Checkbox
color="sky"
......@@ -1794,16 +1909,20 @@ export default function ResourceBookingPage() {
</CheckboxField>
<div>
<FieldLabel>来源系统</FieldLabel>
<Input
list="source-system-suggestions"
<ConfiguredSelect
value={batchEditForm.sourceSystem}
options={sourceSystemOptions}
loading={sourceSystemOptionsLoading}
error={sourceSystemOptionsError}
fieldLabel="来源系统"
emptyLabel="留空以统一清除来源系统"
disabled={!batchEditFields.sourceSystem}
onChange={(event) =>
setBatchEditForm((prev) => ({
...prev,
sourceSystem: event.target.value,
}))
}
placeholder="可留空,用于统一清空"
/>
</div>
......@@ -1829,16 +1948,20 @@ export default function ResourceBookingPage() {
</CheckboxField>
<div>
<FieldLabel>材料分类</FieldLabel>
<Input
list="archive-category-suggestions"
<ConfiguredSelect
value={batchEditForm.archiveCategory}
options={archiveCategoryOptions}
loading={archiveCategoryOptionsLoading}
error={archiveCategoryOptionsError}
fieldLabel="材料分类"
emptyLabel="留空以统一清除材料分类"
disabled={!batchEditFields.archiveCategory}
onChange={(event) =>
setBatchEditForm((prev) => ({
...prev,
archiveCategory: event.target.value,
}))
}
placeholder="可留空,用于统一清空"
/>
</div>
......@@ -1913,7 +2036,6 @@ export default function ResourceBookingPage() {
</FormSection>
<FormSection
eyebrow="COMPLIANCE"
title="合规属性"
description="布尔字段也按“先选择,再覆盖”的方式批量修改。"
>
......@@ -2083,7 +2205,6 @@ export default function ResourceBookingPage() {
<DialogBody>
<div className="space-y-6">
<FormSection
eyebrow="FILE PICKER"
title="上传文件"
description="先完成文件选择,系统会自动尝试用文件名回填材料标题。"
>
......@@ -2146,7 +2267,6 @@ export default function ResourceBookingPage() {
{selectedFileIsZip ? (
<FormSection
eyebrow="UPLOAD MODE"
title="压缩包处理方式"
description="同一个 ZIP 可以选择直接归档,或拆成多条材料记录。拆分时,下方属性会作为每个文件的通用属性。"
>
......@@ -2198,7 +2318,6 @@ export default function ResourceBookingPage() {
) : null}
<FormSection
eyebrow="BASIC INFO"
title="基础信息"
description={
selectedFileIsZip && uploadForm.uploadMode === ARCHIVE_UPLOAD_MODE.EXTRACT
......@@ -2240,6 +2359,20 @@ export default function ResourceBookingPage() {
}
/>
</div>
<div>
<FieldLabel>所属周期/年份</FieldLabel>
<Input
list="archive-year-suggestions"
value={uploadForm.archiveYear}
onChange={(event) =>
setUploadForm((prev) => ({
...prev,
archiveYear: event.target.value,
}))
}
placeholder="可选,例如 2025"
/>
</div>
<div>
<FieldLabel>来源应用</FieldLabel>
<Input
......@@ -2256,30 +2389,35 @@ export default function ResourceBookingPage() {
</div>
<div>
<FieldLabel>来源系统</FieldLabel>
<Input
list="source-system-suggestions"
<ConfiguredSelect
value={uploadForm.sourceSystem}
options={sourceSystemOptions}
loading={sourceSystemOptionsLoading}
error={sourceSystemOptionsError}
fieldLabel="来源系统"
onChange={(event) =>
setUploadForm((prev) => ({
...prev,
sourceSystem: event.target.value,
}))
}
placeholder="可选,例如 审计材料管理系统"
/>
</div>
<div>
<FieldLabel>材料分类</FieldLabel>
<Input
list="archive-category-suggestions"
<ConfiguredSelect
value={uploadForm.archiveCategory}
options={archiveCategoryOptions}
loading={archiveCategoryOptionsLoading}
error={archiveCategoryOptionsError}
fieldLabel="材料分类"
emptyLabel="请选择材料分类(可选)"
onChange={(event) =>
setUploadForm((prev) => ({
...prev,
archiveCategory: event.target.value,
}))
}
placeholder="可选,例如 审计证据"
/>
</div>
<div className="sm:col-span-2">
......@@ -2300,7 +2438,6 @@ export default function ResourceBookingPage() {
</FormSection>
<FormSection
eyebrow="PROVIDE INFO"
title="提供信息"
description="与对象文档保持一致,用于记录材料由谁、何时提供。"
>
......@@ -2335,7 +2472,6 @@ export default function ResourceBookingPage() {
</FormSection>
<FormSection
eyebrow="COMPLIANCE"
title="合规属性"
description="布尔字段统一使用 Checkbox,方便快速录入与后续筛查。"
>
......@@ -2384,7 +2520,6 @@ export default function ResourceBookingPage() {
</FormSection>
<FormSection
eyebrow="REMARKS"
title="补充说明"
description="这里汇总当前录入的材料属性,便于提交前快速确认。"
>
......@@ -2400,6 +2535,9 @@ export default function ResourceBookingPage() {
uploadForm.archiveCategory
? `材料分类:${uploadForm.archiveCategory}`
: '',
uploadForm.archiveYear
? `所属周期/年份:${uploadForm.archiveYear}`
: '',
uploadForm.tag ? `标签分组:${uploadForm.tag}` : '',
]
.filter(Boolean)
......@@ -2418,13 +2556,8 @@ export default function ResourceBookingPage() {
<option key={item.value} value={item.value} />
))}
</datalist>
<datalist id="source-system-suggestions">
{sourceSystemOptions.map((item) => (
<option key={item.value} value={item.value} />
))}
</datalist>
<datalist id="archive-category-suggestions">
{archiveCategorySuggestions.map((item) => (
<datalist id="archive-year-suggestions">
{archiveYearSuggestions.map((item) => (
<option key={item} value={item} />
))}
</datalist>
......
......@@ -43,12 +43,33 @@
| 材料标题 | `material_title` | `text` | 是 | 否 | 否 | 无 | 主展示字段 |
| 文件对象 | `file_asset` | `file` | 是 | 否 | 否 | 无 | 上传接口返回对象 |
| 来源应用 | `source_app` | `text` | 否 | 否 | 否 | 无 | 材料最初来源 |
| 材料分类 | `archive_category` | `text` | 否 | 否 | 否 | 无 | 用户自定义分类 |
| 来源系统 | `source_system` | `select` | 否 | 否 | 否 | 无 | 选项必须配置在 `settings.options` |
| 材料分类 | `archive_category` | `select` | 否 | 否 | 否 | 无 | 选项必须配置在 `settings.options` |
| 上传人 | `uploader` | `user` | 是 | 否 | 否 | 无 | 当前登录人 |
| 上传时间 | `uploaded_at` | `date` | 是 | 否 | 否 | 无 | 创建材料时间 |
| 备注 | `remark` | `text` | 否 | 否 | 否 | 无 | 说明字段 |
| 标签 | `tag` | `text` | 否 | 否 | 否 | 无 | 用户自定义分组标签 |
### 3.2.1 `source_system.settings.options`
| 显示值 | 存储值 |
| --- | --- |
| 投资交易系统 | `投资交易系统` |
| 估值系统 | `估值系统` |
| 注册登记系统 | `注册登记系统` |
| OA系统 | `OA系统` |
| 财务系统 | `财务系统` |
| 产品系统 | `产品系统` |
| 投研系统 | `投研系统` |
| 外部导出 | `外部导出` |
| 其他业务系统 | `其他业务系统` |
### 3.2.2 `archive_category.settings.options`
- 材料分类候选项以线上 Subject 字段配置为准
- 前端仅从 `archive_category.settings.options` 读取显示值和存储值
- 不在页面代码中硬编码或根据历史材料临时生成可编辑选项
## 3.3 推荐初始记录规则
- `title` 默认取 `material_title`
......@@ -60,8 +81,11 @@
1. 能成功创建一条本地上传材料
2. `file_asset.url` 能正常回显
3. 可以按 `metadata.source_app` 查询
4. 可以按 `metadata.archive_category` 查询
5. 可以按 `metadata.tag` 查询
4. 来源系统下拉只展示 `source_system.settings.options` 中的选项
5. 可以按 `metadata.source_system` 查询
6. 材料分类下拉只展示 `archive_category.settings.options` 中的选项
7. 可以按 `metadata.archive_category` 查询
8. 可以按 `metadata.tag` 查询
---
......
......@@ -173,8 +173,9 @@
"url": "https://oss.example.com/audit/xxx.pdf"
},
"source_app": "中台",
"source_system": "middle_platform",
"source_system": "投资交易系统",
"archive_category": "试试水",
"archive_year": "2026",
"uploader": "69b12a47e92b1de77c94a9f5",
"uploaded_at": "2026-05-09T08:30:00.000Z",
"tag": "项目一期"
......@@ -193,8 +194,9 @@
"page_size": 20,
"filter": {
"metadata.source_app": "project_manager_xy",
"metadata.source_system": "project_manager_xy",
"metadata.source_system": "投资交易系统",
"metadata.archive_category": "审计证据",
"metadata.archive_year": "2026",
"metadata.tag": "项目一期"
},
"sort": "-created_at"
......@@ -222,8 +224,9 @@
"url": "https://oss.streams7.com/xinyuan/3WCqMeKoiqOc.xlsx"
},
"source_app": "中台",
"source_system": "middle_platform",
"source_system": "投资交易系统",
"archive_category": "试试水",
"archive_year": "2026",
"uploader": "64b6575b80bdbd9b3d65d09b",
"uploaded_at": "2026-05-08T16:00:00.000Z",
"tag": "ttt"
......
......@@ -94,15 +94,16 @@ type Record = {
说明:
- `title` 建议默认取文件名去后缀,或取用户录入的材料标题
- `archive_category` 和 `tag` 用于支持用户自定义分类和分组
- `archive_category` 使用 Subject 配置维护分类选项,`tag` 用于用户自定义分组
| 字段标签 | 字段别名 | 字段类型 | 是否主键 | 是否多值 | 是否必填 | 说明 |
| --- | --- | --- | --- | --- | --- | --- |
| 材料标题 | `material_title` | text | 否 | 否 | 是 | 业务展示主标题 |
| 文件对象 | `file_asset` | file | 否 | 否 | 是 | 上传接口返回的文件对象 |
| 来源应用 | `source_app` | text | 否 | 否 | 否 | 材料最初产生于哪个应用 |
| 来源系统 | `source_system` | text | 否 | 否 | 否 | 材料最初产生于哪个系统 |
| 材料分类 | `archive_category` | text | 否 | 否 | 否 | 用户自定义分类 |
| 来源系统 | `source_system` | select | 否 | 否 | 否 | 材料最初产生于哪个系统;选项读取 `settings.options` |
| 材料分类 | `archive_category` | select | 否 | 否 | 否 | 选项读取 `settings.options` |
| 所属周期/年份 | `archive_year` | text | 否 | 否 | 否 | 材料归属年度或周期,建议存四位年份字符串,如 `2025` |
| 上传人 | `uploader` | user | 否 | 否 | 是 | 当前登录用户 |
| 上传时间 | `uploaded_at` | date | 否 | 否 | 是 | 创建主档时间 |
| 备注 | `remark` | text | 否 | 否 | 否 | 材料补充说明 |
......@@ -112,7 +113,29 @@ type Record = {
| 是否涉密 | `is_confidential` | boolean | 否 | 否 | 否 | |
| 是否审计部材料 | `is_audit_dept_material` | boolean | 否 | 否 | 否 | |
### 3.1.1 metadata 示例
### 3.1.1 `source_system` 选项配置
`source_system` 必须使用以下 `settings.options`,前端只读取配置生成下拉,不得写死选项:
```json
[
{ "label": "投资交易系统", "value": "投资交易系统" },
{ "label": "估值系统", "value": "估值系统" },
{ "label": "注册登记系统", "value": "注册登记系统" },
{ "label": "OA系统", "value": "OA系统" },
{ "label": "财务系统", "value": "财务系统" },
{ "label": "产品系统", "value": "产品系统" },
{ "label": "投研系统", "value": "投研系统" },
{ "label": "外部导出", "value": "外部导出" },
{ "label": "其他业务系统", "value": "其他业务系统" }
]
```
### 3.1.2 `archive_category` 选项配置
`archive_category` 必须使用 Subject 字段配置中的 `settings.options`。前端只读取线上配置生成下拉,不在代码或文档中重复维护候选项。
### 3.1.3 metadata 示例
```json
{
......@@ -124,8 +147,9 @@ type Record = {
"url": "https://oss.example.com/audit/meeting-note.pdf"
},
"source_app": "project_manager_xy",
"source_system": "project_manager_xy",
"source_system": "投资交易系统",
"archive_category": "审计证据",
"archive_year": "2026",
"uploader": "69b12a47e92b1de77c94a9f5",
"uploaded_at": "2026-05-09T08:30:00.000Z",
"remark": "会议纪要已补签字页",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment