Commit 5ac06b5a authored by David Yang's avatar David Yang

feat: 完善审计材料中心

parent 58f53090
......@@ -13,6 +13,9 @@
"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",
......@@ -10230,6 +10233,36 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/echarts": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
"integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.1.0"
}
},
"node_modules/echarts-for-react": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz",
"integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"size-sensor": "^1.0.1"
},
"peerDependencies": {
"echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0",
"react": "^15.0.0 || >=16.0.0"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
......@@ -12462,6 +12495,12 @@
"node": ">=0.10.0"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/immer": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.4.tgz",
......@@ -13608,6 +13647,54 @@
"node": ">=4.0"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/jszip/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jszip/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/jszip/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/jszip/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
......@@ -13725,6 +13812,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
......@@ -18491,6 +18587,12 @@
"integrity": "sha512-9SQdmsyz4HSP+3gs6PJzhkaMEg+6zTlu9oxIghnwUX3eq+ajq4ft5egl0iyR55LAmO/UwvU8NgIWs/ZyQMa6dw==",
"license": "MIT"
},
"node_modules/size-sensor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz",
"integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==",
"license": "ISC"
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
......@@ -21161,6 +21263,21 @@
"peerDependencies": {
"zod": "^3.18.0"
}
},
"node_modules/zrender": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
"integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
}
}
}
......@@ -15,6 +15,9 @@
"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",
......
......@@ -43,7 +43,7 @@ export const AvatarButton = forwardRef(function AvatarButton(
let classes = clsx(
className,
square ? 'rounded-[20%]' : 'rounded-full',
'relative inline-grid focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500'
'relative inline-grid focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-[var(--pm-brand)]'
)
return typeof props.href === 'string' ? (
......
......@@ -54,7 +54,7 @@ export const BadgeButton = forwardRef(function BadgeButton(
) {
let classes = clsx(
className,
'group relative inline-flex rounded-md focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500'
'group relative inline-flex rounded-md focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-[var(--pm-brand)]'
)
return typeof props.href === 'string' ? (
......
......@@ -57,7 +57,7 @@ const base = [
'after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)] after:shadow-[inset_0_1px_--theme(--color-white/15%)]',
,
// Focus ring
'group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-blue-500',
'group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-[var(--pm-brand)]',
// Disabled state
'group-data-disabled:opacity-50',
'group-data-disabled:border-zinc-950/25 group-data-disabled:bg-zinc-950/5 group-data-disabled:[--checkbox-check:var(--color-zinc-950)]/50 group-data-disabled:before:bg-transparent',
......
......@@ -38,7 +38,7 @@ export function Combobox({
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
,
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-blue-500',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-[var(--pm-brand)]',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
// Invalid state
......@@ -133,7 +133,7 @@ export function ComboboxOption({ children, className, ...props }) {
// Typography
'text-base/6 text-zinc-950 sm:text-sm/6 forced-colors:text-[CanvasText]',
// Focus
'outline-hidden data-focus:bg-blue-500 data-focus:text-white',
'outline-hidden data-focus:bg-[var(--pm-brand)] data-focus:text-white',
// Forced colors mode
'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]',
// Disabled
......
......@@ -51,7 +51,7 @@ export function DropdownItem({ className, ...props }) {
// Text styles
'text-left text-base/6 text-zinc-950 sm:text-sm/6 forced-colors:text-[CanvasText]',
// Focus
'data-focus:bg-blue-500 data-focus:text-white',
'data-focus:bg-[var(--pm-brand)] data-focus:text-white',
// Disabled state
'data-disabled:opacity-50',
// Forced colors mode
......
......@@ -38,7 +38,7 @@ export const Input = forwardRef(function Input(
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
,
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-blue-500',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-[var(--pm-brand)]',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
])}
......
......@@ -22,7 +22,7 @@ export function Listbox({ className, placeholder, autoFocus, 'aria-label': ariaL
// Hide default focus styles
'focus:outline-hidden',
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset data-focus:after:ring-2 data-focus:after:ring-blue-500',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset data-focus:after:ring-2 data-focus:after:ring-[var(--pm-brand)]',
// Disabled state
'data-disabled:opacity-50 data-disabled:before:bg-zinc-950/5 data-disabled:before:shadow-none',
])}
......@@ -115,7 +115,7 @@ export function ListboxOption({ children, className, ...props }) {
// Typography
'text-base/6 text-zinc-950 sm:text-sm/6 forced-colors:text-[CanvasText]',
// Focus
'outline-hidden data-focus:bg-blue-500 data-focus:text-white',
'outline-hidden data-focus:bg-[var(--pm-brand)] data-focus:text-white',
// Forced colors mode
'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]',
// Disabled
......
......@@ -61,7 +61,7 @@ const base = [
// Indicator color (dark mode)
,
// Focus ring
'group-data-focus:outline group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-blue-500',
'group-data-focus:outline group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-[var(--pm-brand)]',
// Disabled state
'group-data-disabled:opacity-50',
'group-data-disabled:border-zinc-950/25 group-data-disabled:bg-zinc-950/5 group-data-disabled:[--radio-checked-indicator:var(--color-zinc-950)]/50 group-data-disabled:before:bg-transparent',
......
......@@ -15,7 +15,7 @@ export const Select = forwardRef(function Select({ className, multiple, ...props
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
,
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset has-data-focus:after:ring-2 has-data-focus:after:ring-blue-500',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset has-data-focus:after:ring-2 has-data-focus:after:ring-[var(--pm-brand)]',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
])}
......
......@@ -148,7 +148,7 @@ export function Switch({ color = 'dark/zinc', className, ...props }) {
// Checked
'data-checked:bg-(--switch-bg) data-checked:ring-(--switch-bg-ring)',
// Focus
'focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500',
'focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-[var(--pm-brand)]',
// Hover
'data-hover:ring-black/15 data-hover:data-checked:ring-(--switch-bg-ring)',
,
......
......@@ -70,7 +70,7 @@ export function TableRow({ href, target, title, className, ...props }) {
className={clsx(
className,
href &&
'has-[[data-row-link][data-focus]]:outline-2 has-[[data-row-link][data-focus]]:-outline-offset-2 has-[[data-row-link][data-focus]]:outline-blue-500',
'has-[[data-row-link][data-focus]]:outline-2 has-[[data-row-link][data-focus]]:-outline-offset-2 has-[[data-row-link][data-focus]]:outline-[var(--pm-brand)]',
striped && 'even:bg-zinc-950/2.5',
href && striped && 'hover:bg-zinc-950/5',
href && !striped && 'hover:bg-zinc-950/2.5',
......
......@@ -15,7 +15,7 @@ export const Textarea = forwardRef(function Textarea({ className, resizable = tr
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
,
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-blue-500',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-[var(--pm-brand)]',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
])}
......
:root {
--pm-brand: #009cd9;
--pm-brand-strong: #007bb8;
--pm-brand: #c81e3a;
--pm-brand-strong: #9f1239;
--pm-surface: #ffffff;
--pm-bg: #eef5f9;
--pm-bg: #fff1f2;
--pm-sidebar: #0f172a;
--pm-sidebar-soft: #1e293b;
--pm-text: #0f172a;
......@@ -17,7 +17,7 @@ body,
body {
margin: 0;
background: linear-gradient(180deg, #f8fbfd 0%, #eef5f9 100%);
background: linear-gradient(180deg, #fff7f7 0%, #fff1f2 100%);
color: var(--pm-text);
font-family:
"PingFang SC",
......
......@@ -32,11 +32,11 @@ export default function LoginPage() {
const backgroundStyle = useMemo(() => {
const overlay =
'linear-gradient(135deg, rgba(5, 23, 41, 0.92) 0%, rgba(14, 62, 90, 0.9) 50%, rgba(0, 122, 181, 0.82) 100%)';
'linear-gradient(135deg, rgba(28, 10, 16, 0.94) 0%, rgba(98, 20, 38, 0.9) 52%, rgba(190, 24, 93, 0.82) 100%)';
if (!site?.background) {
return {
backgroundImage: `${overlay}, radial-gradient(circle at top, #12385d 0%, #081a2c 55%, #040b15 100%)`,
backgroundImage: `${overlay}, radial-gradient(circle at top, #57111f 0%, #21070f 55%, #120308 100%)`,
};
}
......@@ -96,12 +96,12 @@ export default function LoginPage() {
return (
<div
className="min-h-screen bg-[#061321] px-4 py-6 text-white md:px-8"
className="min-h-screen bg-[#18060d] px-4 py-6 text-white md:px-8"
style={backgroundStyle}
>
<div className="mx-auto flex min-h-[calc(100vh-3rem)] w-full max-w-7xl overflow-hidden rounded-[32px] border border-white/10 bg-white/6 shadow-[0_28px_120px_rgba(0,0,0,0.38)] backdrop-blur-sm">
<section className="relative hidden w-[52%] flex-col justify-between overflow-hidden px-10 py-12 lg:flex">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.2),transparent_36%),radial-gradient(circle_at_bottom_right,rgba(49,196,255,0.3),transparent_32%)]" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.2),transparent_36%),radial-gradient(circle_at_bottom_right,rgba(244,63,94,0.28),transparent_32%)]" />
<div className="relative z-10 max-w-xl">
<p className="text-xs font-medium uppercase tracking-[0.45em] text-white/60">
Streams Style Login
......@@ -137,7 +137,7 @@ export default function LoginPage() {
<section className="flex flex-1 items-center justify-center bg-white/92 px-6 py-10 text-slate-900 sm:px-10">
<div className="w-full max-w-md">
<div className="mb-8">
<p className="text-sm font-medium uppercase tracking-[0.35em] text-sky-600/80">
<p className="text-sm font-medium uppercase tracking-[0.35em] text-rose-600/80">
Sign In
</p>
<h2 className="mt-3 text-3xl font-semibold text-slate-900">
......@@ -154,7 +154,7 @@ export default function LoginPage() {
账号
</label>
<input
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-sky-500 focus:bg-white"
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-500 focus:bg-white"
value={identity}
onChange={(event) => setIdentity(event.target.value)}
disabled={loading}
......@@ -169,7 +169,7 @@ export default function LoginPage() {
</label>
<input
type="password"
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-sky-500 focus:bg-white"
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-500 focus:bg-white"
value={password}
onChange={(event) => setPassword(event.target.value)}
disabled={loading}
......@@ -184,7 +184,7 @@ export default function LoginPage() {
</label>
<div className="flex gap-3">
<input
className="min-w-0 flex-1 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-sky-500 focus:bg-white"
className="min-w-0 flex-1 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-rose-500 focus:bg-white"
value={captchaCode}
onChange={(event) => setCaptchaCode(event.target.value)}
disabled={loading}
......@@ -198,7 +198,7 @@ export default function LoginPage() {
refreshCaptcha();
}}
disabled={loading || captchaLoading}
className="flex h-[56px] w-[148px] shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-slate-200 bg-white px-2 text-xs text-slate-500 transition hover:border-sky-300 hover:text-sky-600 disabled:cursor-not-allowed disabled:opacity-70"
className="flex h-[56px] w-[148px] shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-slate-200 bg-white px-2 text-xs text-slate-500 transition hover:border-rose-300 hover:text-rose-600 disabled:cursor-not-allowed disabled:opacity-70"
>
{captchaData?.image ? (
<span
......@@ -221,7 +221,7 @@ export default function LoginPage() {
<button
type="submit"
disabled={loading || captchaLoading}
className="flex w-full items-center justify-center rounded-2xl bg-[linear-gradient(135deg,#0b75b6_0%,#1095d3_55%,#18b6f6_100%)] px-4 py-3 text-sm font-medium text-white shadow-[0_16px_40px_rgba(17,128,196,0.28)] transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-70"
className="flex w-full items-center justify-center rounded-2xl bg-[linear-gradient(135deg,#9f1239_0%,#be123c_52%,#e11d48_100%)] px-4 py-3 text-sm font-medium text-white shadow-[0_16px_40px_rgba(190,24,93,0.28)] transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-70"
>
{loading ? '登录中...' : '登录'}
</button>
......
export default function EmptyStatePage({ title, description }) {
return (
<section className="relative overflow-hidden rounded-[32px] border border-white/70 bg-white/90 p-8 shadow-[0_30px_80px_rgba(15,23,42,0.08)]">
<div className="absolute -right-20 -top-20 h-56 w-56 rounded-full bg-sky-100 blur-3xl" />
<div className="absolute bottom-0 left-0 h-40 w-40 rounded-full bg-cyan-50 blur-3xl" />
<div className="absolute -right-20 -top-20 h-56 w-56 rounded-full bg-rose-100 blur-3xl" />
<div className="absolute bottom-0 left-0 h-40 w-40 rounded-full bg-rose-50 blur-3xl" />
<div className="relative">
<div className="inline-flex rounded-full border border-sky-200 bg-sky-50 px-3 py-1 text-xs font-semibold tracking-[0.2em] text-sky-700">
<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">
COMING SOON
</div>
<h2 className="mt-6 text-3xl font-semibold text-slate-900">{title}</h2>
......
import rsmLogo from '@/assets/rsm_logo.svg';
import rsmLogoSmall from '@/assets/rsm_logo_small.svg';
import xyLogo from '@/assets/xy-logo.png';
import xyLogoSmall from '@/assets/xy-logo_small.png';
import { UserContext } from '@/wrapper/Auth';
import {
ArchiveBoxIcon,
......@@ -234,8 +234,8 @@ export default function ProjectManagerLayout() {
)}
>
<img
src={sidebarCollapsed ? rsmLogoSmall : rsmLogo}
alt="RSM Logo"
src={sidebarCollapsed ? xyLogoSmall : xyLogo}
alt="XY Logo"
className={clsx(
'shrink-0 object-contain',
sidebarCollapsed
......@@ -302,8 +302,8 @@ export default function ProjectManagerLayout() {
<div className="px-5 pt-6">
<div className="flex items-center gap-2">
<img
src={rsmLogo}
alt="RSM Logo"
src={xyLogo}
alt="XY Logo"
className="h-5 w-auto max-w-[128px] shrink-0 object-contain"
/>
<h1 className="text-base font-semibold tracking-[0.02em] text-white">
......
......@@ -31,9 +31,9 @@
- 提供全局检索入口
- `材料归档`
- 上传材料
- 编辑标题、标签、分类、备注
- 编辑标题、标签、分类
- 查看材料列表与详情
- 支持“全部文件”汇总视图与按分类切换浏览
- 支持“全部文件”列表视图与按分类切换浏览
- 材料详情使用弹框查看与编辑
---
......@@ -51,7 +51,11 @@
- `source_app`:来源应用,可选
- `archive_category`:分类,可选
- `uploader` / `uploaded_at`:上传人、上传时间
- `remark`:备注,可选
- `tag`:用户自定义分组标签
- `provide_department` / `provide_time`:材料提供部门、提供时间
- `is_confidential`:是否涉密
- `is_audit_dept_material`:是否审计部材料
---
......@@ -60,12 +64,17 @@
### 4.1 在材料中心上传本地材料
1. 用户进入 `材料归档`
2. 选择本地文件
3. 先调用上传接口上传二进制文件
4. 上传成功后,得到 `{name, type, size, url}` 文件对象
5. 再创建一条 `am_material` 记录
6. 用户补充标题、标签、分类、备注等信息,其中来源应用和分类允许留空
7. 材料进入统一材料池,可被其他审计应用统一查看
2. 选择本地文件;若选择的是 `ZIP` 压缩包,可进一步选择上传模式:
- `整包上传`:将压缩包本身作为 1 份材料处理
- `解压后逐文件上传`:按压缩包内文件逐个建档
3. 用户补充标题、标签、分类、提供部门、提供时间、是否涉密、是否审计部材料等信息
4. 先调用上传接口上传二进制文件
5. 上传成功后,得到 `{name, type, size, url}` 文件对象
6. 再创建 `am_material` 记录:
- 普通文件或 `整包上传`:创建 1 条记录
- `解压后逐文件上传`:压缩包内有多少个文件,就创建多少条记录
7. 当使用 `解压后逐文件上传` 时,上传表单中的来源应用、分类、标签、提供信息、合规属性等字段作为每个文件的通用属性一并写入
8. 材料进入统一材料池,可被其他审计应用统一查看
### 4.2 在其他业务应用上传材料
......@@ -79,14 +88,26 @@
1. 用户进入 `材料归档`
2. 按关键词、标签、来源应用筛选材料
3. 页面提供“全部文件”与“分类切换”两种浏览方式
4. 在“全部文件”视图下,直接平铺展示当前筛选条件下的全部材料
5. 切换到某个分类标签后,只查看该分类下的材料
5. 点击某份材料后,以弹框方式打开详情
6. 弹框内查看:
4. 列表默认每页 20 条,支持切换为 50 或 100 条
5. 用户通过上一页 / 下一页进行分页浏览
6. 在“全部文件”视图下,直接以列表展示当前筛选条件下的全部材料
7. 切换到某个分类标签后,只查看该分类下的材料
8. `未分类` 仅表示 `metadata.archive_category` 为空、空字符串或缺失,不作为真实分类值写回查询条件
9. 列表支持当前页批量勾选,并提供“全选当前页”能力
10. 勾选后可执行以下批量操作:
- 批量修改:按字段选择是否覆盖来源应用、分类、提供部门、提供时间、是否涉密、是否审计部材料
- 批量打标签:统一覆盖 `metadata.tag`
- 批量删除:对已勾选材料执行软删除
11. 点击某份材料后,以弹框方式打开详情
12. 弹框内查看:
- 文件预览或下载入口
- 标签与分类
- 来源信息
7. 管理员或有权限用户可在弹框中修改分类、标签、备注
- 提供部门、提供时间
- 涉密标记、审计部材料标记
13. 管理员或有权限用户可在弹框中修改分类、标签
14. 详情弹框提供单个删除入口,删除前需二次确认,确认后对当前材料执行软删除并关闭弹框
15. 详情弹框中的编辑区域按“基础信息 / 提供信息 / 合规属性”三段式维护,减少字段混排
### 4.4 删除或归档材料
......@@ -129,7 +150,17 @@
- 本周新增材料数
- 来源应用分布
- 分类分布
- 提供信息完整度
- 涉密材料 / 审计部材料概览
- 最近上传材料
- 待完善材料提醒
当前工作台推荐采用“图表 + 风险清单”布局:
- 来源应用分布使用柱状图,便于快速判断材料是否已从各业务应用顺利汇入
- 分类分布使用环形图,便于观察常用分类与未分类占比
- 提供与合规信息使用双层环图或类似图形,突出补录完成度、涉密材料、审计部材料情况
- 最近上传与待完善材料以列表方式承接,方便后续直接进入归档页处理
### 6.2 材料归档
......@@ -140,7 +171,14 @@
- 来源应用筛选
- 分类切换标签页
- 材料列表表格
- 当前页批量勾选工具条
- 材料详情弹框
- 批量修改弹框
- 批量打标签弹框
- 创建与编辑弹框中的分区式表单:
- 基础信息区
- 提供信息区
- 合规属性区
### 6.3 材料详情
......@@ -151,6 +189,8 @@
- 标签
- 分类
- 来源应用
- 提供部门 / 提供时间
- 是否涉密 / 是否审计部材料
- 上传人 / 上传时间
- 下载或预览入口
......@@ -165,6 +205,10 @@
2. 创建材料主档:
- `POST /record`
- `subject = am_material`
3. ZIP 上传约定:
- 选择 `整包上传` 时,按普通文件流程调用 1 次 `POST /upload` 和 1 次 `POST /record`
- 选择 `解压后逐文件上传` 时,前端先在本地解析 ZIP,再对压缩包内每个文件分别调用 `POST /upload` 和 `POST /record`
- 拆分上传时,当前表单填写的公共属性需复制到每一条新建材料记录的 `metadata`
### 7.2 查询材料池
......@@ -175,13 +219,29 @@
- `metadata.archive_category`
- `metadata.tag`
### 7.3 大数据量分页策略
### 7.3 批量管理材料
- 批量修改:
- 前端先基于当前页勾选结果拿到材料记录
- 再按勾选字段逐条调用 `PUT /record/:record`
- 所有业务字段仍写入 `metadata`
- 批量删除:
- 使用批量删除接口对当前勾选材料执行软删除
- 请求体按 `subject = am_material` 与 `filter.name.$in = [recordName...]` 组织
- 单个删除:
- 在材料详情弹框内调用 `DELETE /record/:name`
- 默认软删除,`:name` 使用材料记录 `name` 或 `_id`
- 批量打标签:
- 本质上属于批量修改的一种
- 统一覆盖 `metadata.tag`
### 7.4 大数据量分页策略
- 当材料数量超过 1000 份时,不应一次性全量拉取
- 归档页首屏只加载一页数据,建议每页 36 条左右
- 用户滚动到列表底部时,自动请求下一页
- 当用户切换分类时,按当前分类重新从第一页加载
- “全部文件”视图按页平铺追加结果
- 归档页首屏只加载一页数据,默认每页 20 条,可切换为 50 或 100 条
- 用户通过分页按钮切换页码,不再使用滚动加载
- 当用户切换分类、关键词、来源或页大小时,按当前条件重新从第一页加载
- “全部文件”视图按页展示结果
- 总数统计通过 `count` 接口单独获取,不依赖前端已加载条数推断
---
......@@ -190,8 +250,14 @@
- 上传文件失败时,不创建 `am_material`
- 创建 `am_material` 失败时,需要提示用户重试,并避免出现“文件已传但列表无记录”的静默失败
- ZIP 拆分上传若中途失败,需要明确提示已成功创建的记录条数以及失败文件名,便于用户补传
- 标签允许自由输入,但需控制重复标签与空标签
- 分类和标签用于支持用户自定义分组
- 批量修改仅允许覆盖用户显式勾选的字段,未勾选字段必须保留原值,避免误覆盖
- 批量删除前必须二次确认,默认仍按软删除处理
- 当前版本的批量勾选范围限定在“当前筛选结果的当前页”,切换分页后需要重新确认勾选结果
- `provide_time` 提交时统一转为 `ISO 8601`
- 布尔字段 `is_confidential`、`is_audit_dept_material` 需要以 true/false 写入 `metadata`
---
......
......@@ -41,9 +41,7 @@
| 字段标题 | 字段别名 | 类型 | 必填 | 多值 | 主键 | 默认值 | 备注 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 材料标题 | `material_title` | `text` | 是 | 否 | 否 | 无 | 主展示字段 |
| 材料编号 | `material_code` | `text` | 否 | 否 | 否 | 无 | 一期先不设主键,避免卡流程 |
| 文件对象 | `file_asset` | `file` | 是 | 否 | 否 | 无 | 上传接口返回对象 |
| 文件摘要 | `file_hash` | `text` | 否 | 否 | 否 | 无 | 后续做去重时使用 |
| 来源应用 | `source_app` | `text` | 否 | 否 | 否 | 无 | 材料最初来源 |
| 材料分类 | `archive_category` | `text` | 否 | 否 | 否 | 无 | 用户自定义分类 |
| 上传人 | `uploader` | `user` | 是 | 否 | 否 | 无 | 当前登录人 |
......
......@@ -166,19 +166,16 @@
"content_type": "text",
"metadata": {
"material_title": "审计底稿",
"material_code": "AM-2026-001",
"file_asset": {
"name": "审计底稿.pdf",
"type": "application/pdf",
"size": 248120,
"url": "https://oss.example.com/audit/xxx.pdf"
},
"file_hash": "sha256:xxxx",
"source_app": "中台",
"archive_category": "试试水",
"uploader": "69b12a47e92b1de77c94a9f5",
"uploaded_at": "2026-05-09T08:30:00.000Z",
"remark": "统一材料池文件",
"tag": "项目一期"
}
}
......@@ -216,19 +213,16 @@
"content_type": "text",
"metadata": {
"material_title": "测试1",
"material_code": "111",
"file_asset": {
"name": "项目管理_字段.xlsx",
"type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"size": 9888,
"url": "https://oss.streams7.com/xinyuan/3WCqMeKoiqOc.xlsx"
},
"file_hash": "test",
"source_app": "中台",
"archive_category": "试试水",
"uploader": "64b6575b80bdbd9b3d65d09b",
"uploaded_at": "2026-05-08T16:00:00.000Z",
"remark": "测试",
"tag": "ttt"
},
"subject": "am_material"
......
......@@ -99,34 +99,38 @@ type Record = {
| 字段标签 | 字段别名 | 字段类型 | 是否主键 | 是否多值 | 是否必填 | 说明 |
| --- | --- | --- | --- | --- | --- | --- |
| 材料标题 | `material_title` | text | 否 | 否 | 是 | 业务展示主标题 |
| 材料编号 | `material_code` | text | 否 | 否 | 否 | 如后续需要编码管理再启用 |
| 文件对象 | `file_asset` | file | 否 | 否 | 是 | 上传接口返回的文件对象 |
| 文件摘要 | `file_hash` | text | 否 | 否 | 否 | 用于后续查重,可先留空 |
| 来源应用 | `source_app` | text | 否 | 否 | 否 | 材料最初产生于哪个应用 |
| 材料分类 | `archive_category` | text | 否 | 否 | 否 | 用户自定义分类 |
| 上传人 | `uploader` | user | 否 | 否 | 是 | 当前登录用户 |
| 上传时间 | `uploaded_at` | date | 否 | 否 | 是 | 创建主档时间 |
| 备注 | `remark` | text | 否 | 否 | 否 | 管理说明 |
| 备注 | `remark` | text | 否 | 否 | 否 | 材料补充说明 |
| 标签 | `tag` | text | 否 | 否 | 否 | 用户自定义分组标签 |
| 提供部门 | `provide_department` | text | 否 | 否 | 否 | |
| 提供时间 | `provide_time` | date | 否 | 否 | 否 | |
| 是否涉密 | `is_confidential` | boolean | 否 | 否 | 否 | |
| 是否审计部材料 | `is_audit_dept_material` | boolean | 否 | 否 | 否 | |
### 3.1.1 metadata 示例
```json
{
"material_title": "审计访谈会议纪要",
"material_code": "AM-2026-001",
"file_asset": {
"name": "审计访谈会议纪要.pdf",
"type": "application/pdf",
"size": 248120,
"url": "https://oss.example.com/audit/meeting-note.pdf"
},
"file_hash": "sha256:xxxx",
"source_app": "project_manager_xy",
"archive_category": "审计证据",
"uploader": "69b12a47e92b1de77c94a9f5",
"uploaded_at": "2026-05-09T08:30:00.000Z",
"remark": "上传后可被各审计应用统一查看",
"tag": "项目一期"
"remark": "会议纪要已补签字页",
"tag": "项目一期",
"provide_department": "审计部",
"provide_time": "2026-05-08T16:00:00.000Z",
"is_confidential": false,
"is_audit_dept_material": true
}
```
......@@ -7,6 +7,34 @@
"Helvetica Neue",
Arial,
sans-serif;
--color-sky-50: #fff1f2;
--color-sky-100: #ffe4e6;
--color-sky-200: #fecdd3;
--color-sky-300: #fda4af;
--color-sky-400: #fb7185;
--color-sky-500: #f43f5e;
--color-sky-600: #e11d48;
--color-sky-700: #be123c;
--color-sky-800: #9f1239;
--color-sky-900: #881337;
--color-blue-50: #fff1f2;
--color-blue-100: #ffe4e6;
--color-blue-200: #fecdd3;
--color-blue-300: #fda4af;
--color-blue-400: #fb7185;
--color-blue-500: #f43f5e;
--color-blue-600: #e11d48;
--color-blue-700: #be123c;
--color-blue-800: #9f1239;
--color-blue-900: #881337;
--color-cyan-50: #fff1f2;
--color-cyan-100: #ffe4e6;
--color-cyan-200: #fecdd3;
--color-cyan-300: #fda4af;
--color-cyan-400: #fb7185;
--color-cyan-500: #f43f5e;
--color-cyan-600: #e11d48;
--color-cyan-700: #be123c;
}
:root {
......
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