Commit 3b3884f8 authored by Andy-bubu's avatar Andy-bubu

feat: 统一鑫元审计材料中心设计系统

parent ccd2f097
......@@ -74,6 +74,9 @@ export default defineConfig({
}
},
esbuildMinifyIIFE: true,
jsMinifierOptions: {
target: ['chrome80', 'es2020'],
},
define: {
STREAMS_API_URL: '/api',
STREAMS_PARSER_URL: '/parser',
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -8,8 +8,12 @@
"start": "npm run dev"
},
"dependencies": {
"@ant-design/icons": "^6.3.2",
"@headlessui/react": "^2.2.10",
"@heroicons/react": "^2.2.0",
"@heroui-pro/react": "^1.0.0-beta.7",
"@heroui/react": "^3.2.2",
"@heroui/styles": "^3.2.2",
"@tailwindcss/postcss": "^4.2.0",
"@umijs/max": "^4.5.3",
"clsx": "^2.1.1",
......@@ -18,15 +22,15 @@
"echarts": "^6.1.0",
"echarts-for-react": "^3.0.6",
"jszip": "^3.10.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"tailwindcss": "^4.2.0",
"umi-request": "^1.4.0",
"umi-request-progress": "^1.1.0"
},
"devDependencies": {
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"typescript": "^5.0.3"
}
}
This diff is collapsed.
......@@ -20,17 +20,17 @@ export function Alert({ size = 'md', className, children, ...props }) {
<Headless.Dialog {...props}>
<Headless.DialogBackdrop
transition
className="fixed inset-0 flex w-screen justify-center overflow-y-auto bg-zinc-950/15 px-2 py-2 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-6 sm:py-8 lg:px-8 lg:py-16"
className="rc-dialog-backdrop fixed inset-0 flex w-screen justify-center overflow-y-auto px-4 py-4 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-10 sm:py-10"
/>
<div className="fixed inset-0 w-screen overflow-y-auto pt-6 sm:pt-0">
<div className="grid min-h-full grid-rows-[1fr_auto_1fr] justify-items-center p-8 sm:grid-rows-[1fr_auto_3fr] sm:p-4">
<div className="fixed inset-0 w-screen overflow-y-auto px-4 py-4 sm:px-10 sm:py-10">
<div className="grid min-h-full place-items-center">
<Headless.DialogPanel
transition
className={clsx(
className,
sizes[size],
'row-start-2 w-full rounded-2xl bg-white p-8 shadow-lg ring-1 ring-zinc-950/10 sm:rounded-2xl sm:p-6 forced-colors:outline',
'max-h-[calc(100vh-32px)] w-full overflow-hidden rounded-[var(--rc-modal-radius)] bg-[var(--overlay,var(--rc-bg-surface))] p-[var(--rc-modal-padding)] shadow-[var(--rc-shadow-overlay)] forced-colors:outline',
'transition duration-100 will-change-transform data-closed:opacity-0 data-enter:ease-out data-closed:data-enter:scale-95 data-leave:ease-in'
)}
>
......@@ -48,7 +48,7 @@ export function AlertTitle({ className, ...props }) {
{...props}
className={clsx(
className,
'text-center text-base/6 font-semibold text-balance text-zinc-950 sm:text-left sm:text-sm/6 sm:text-wrap'
'text-center text-base/6 font-medium text-balance text-[var(--rc-text-primary)] sm:text-left sm:text-wrap'
)}
/>
)
......@@ -59,13 +59,13 @@ export function AlertDescription({ className, ...props }) {
<Headless.Description
as={Text}
{...props}
className={clsx(className, 'mt-2 text-center text-pretty sm:text-left')}
className={clsx(className, 'mt-2 text-center text-pretty text-[var(--rc-text-secondary)] sm:text-left')}
/>
)
}
export function AlertBody({ className, ...props }) {
return <div {...props} className={clsx(className, 'mt-4')} />
return <div {...props} className={clsx(className, 'mt-2')} />
}
export function AlertActions({ className, ...props }) {
......@@ -74,7 +74,7 @@ export function AlertActions({ className, ...props }) {
{...props}
className={clsx(
className,
'mt-6 flex flex-col-reverse items-center justify-end gap-3 *:w-full sm:mt-4 sm:flex-row sm:*:w-auto'
'mt-5 flex flex-col-reverse items-center justify-end gap-2 *:w-full sm:flex-row sm:*:w-auto'
)}
/>
)
......
This diff is collapsed.
......@@ -43,7 +43,7 @@ const base = [
// Basic layout
'relative isolate flex size-4.5 items-center justify-center rounded-[0.3125rem] sm:size-4',
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(0.3125rem-1px)] before:bg-white before:shadow-sm',
'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(0.3125rem-1px)] before:bg-[var(--rc-field-background)]',
// Background color when checked
'group-data-checked:before:bg-(--checkbox-checked-bg)',
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
......@@ -51,13 +51,13 @@ const base = [
// Background color applied to control in dark mode
,
// Border
'border border-zinc-950/15 group-data-checked:border-transparent group-data-hover:group-data-checked:border-transparent group-data-hover:border-zinc-950/30 group-data-checked:bg-(--checkbox-checked-border)',
'border border-[var(--rc-checkbox-border,var(--rc-field-border))] group-data-checked:border-transparent group-data-hover:group-data-checked:border-transparent group-data-hover:border-[var(--rc-field-border-hover)] group-data-checked:bg-(--checkbox-checked-border)',
,
// Inner highlight shadow
'after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)] after:shadow-[inset_0_1px_--theme(--color-white/15%)]',
'after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)]',
,
// Focus ring
'group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-[var(--pm-brand)]',
'group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-[var(--rc-brand-primary)]',
// 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',
......@@ -68,19 +68,21 @@ const base = [
]
const colors = {
xinyuan:
'[--checkbox-check:var(--rc-checkbox-check-color)] [--checkbox-checked-bg:var(--rc-checkbox-checked-background)] [--checkbox-checked-border:var(--rc-checkbox-checked-border)]',
'dark/zinc': [
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
'[--checkbox-check:var(--rc-checkbox-check-color)] [--checkbox-checked-bg:var(--rc-checkbox-checked-background)] [--checkbox-checked-border:var(--rc-checkbox-checked-border)]',
,
],
'dark/white': [
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
'[--checkbox-check:var(--rc-checkbox-check-color)] [--checkbox-checked-bg:var(--rc-checkbox-checked-background)] [--checkbox-checked-border:var(--rc-checkbox-checked-border)]',
,
],
white:
'[--checkbox-check:var(--color-zinc-900)] [--checkbox-checked-bg:var(--color-white)] [--checkbox-checked-border:var(--color-zinc-950)]/15',
dark: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
zinc: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-600)] [--checkbox-checked-border:var(--color-zinc-700)]/90',
red: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-red-600)] [--checkbox-checked-border:var(--color-red-700)]/90',
dark: '[--checkbox-check:var(--rc-checkbox-check-color)] [--checkbox-checked-bg:var(--rc-checkbox-checked-background)] [--checkbox-checked-border:var(--rc-checkbox-checked-border)]',
zinc: '[--checkbox-check:var(--rc-checkbox-check-color)] [--checkbox-checked-bg:var(--rc-checkbox-checked-background)] [--checkbox-checked-border:var(--rc-checkbox-checked-border)]',
red: '[--checkbox-check:var(--rc-checkbox-check-color)] [--checkbox-checked-bg:var(--rc-checkbox-checked-background)] [--checkbox-checked-border:var(--rc-checkbox-checked-border)]',
orange:
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-orange-500)] [--checkbox-checked-border:var(--color-orange-600)]/90',
amber:
......@@ -95,7 +97,7 @@ const colors = {
teal: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-teal-600)] [--checkbox-checked-border:var(--color-teal-700)]/90',
cyan: '[--checkbox-check:var(--color-cyan-950)] [--checkbox-checked-bg:var(--color-cyan-300)] [--checkbox-checked-border:var(--color-cyan-400)]/80',
sky: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-sky-500)] [--checkbox-checked-border:var(--color-sky-600)]/80',
blue: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-blue-600)] [--checkbox-checked-border:var(--color-blue-700)]/90',
blue: '[--checkbox-check:var(--rc-checkbox-check-color)] [--checkbox-checked-bg:var(--rc-checkbox-checked-background)] [--checkbox-checked-border:var(--rc-checkbox-checked-border)]',
indigo:
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-indigo-500)] [--checkbox-checked-border:var(--color-indigo-600)]/90',
violet:
......@@ -108,7 +110,7 @@ const colors = {
rose: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-rose-500)] [--checkbox-checked-border:var(--color-rose-600)]/90',
}
export function Checkbox({ color = 'dark/zinc', className, ...props }) {
export function Checkbox({ color = 'xinyuan', className, ...props }) {
return (
<Headless.Checkbox
data-slot="control"
......
......@@ -20,18 +20,20 @@ export function Dialog({ size = 'lg', className, children, ...props }) {
<Headless.Dialog {...props}>
<Headless.DialogBackdrop
transition
className="fixed inset-0 z-50 flex w-screen justify-center overflow-y-auto bg-zinc-950/25 px-2 py-2 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-6 sm:py-8 lg:px-8 lg:py-16"
className="rc-dialog-backdrop fixed inset-0 z-50 flex w-screen justify-center overflow-y-auto px-4 py-4 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-10 sm:py-10"
/>
<div className="fixed inset-0 z-50 w-screen overflow-y-auto pt-6 sm:pt-0">
<div className="grid min-h-full grid-rows-[1fr_auto] justify-items-center sm:grid-rows-[1fr_auto_3fr] sm:p-4">
<div className="fixed inset-0 z-50 w-screen overflow-y-auto px-4 py-4 sm:px-10 sm:py-10">
<div className="grid min-h-full place-items-center">
<Headless.DialogPanel
transition
className={clsx(
className,
sizes[size],
'row-start-2 w-full min-w-0 rounded-t-3xl bg-white p-(--gutter) shadow-lg ring-1 ring-zinc-950/10 [--gutter:--spacing(8)] sm:mb-auto sm:rounded-2xl forced-colors:outline',
'transition duration-100 will-change-transform data-closed:translate-y-12 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:data-closed:translate-y-0 sm:data-closed:data-enter:scale-95'
'rc-dialog-panel',
`rc-dialog-panel--${size}`,
'max-h-[calc(100vh-32px)] w-full min-w-0 overflow-hidden rounded-[var(--rc-modal-radius)] bg-[var(--overlay,var(--rc-bg-surface))] p-[var(--rc-modal-padding)] shadow-[var(--rc-shadow-overlay)] forced-colors:outline',
'transition duration-100 will-change-transform data-closed:translate-y-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in data-closed:data-enter:scale-95'
)}
>
{children}
......@@ -46,17 +48,17 @@ export function DialogTitle({ className, ...props }) {
return (
<Headless.DialogTitle
{...props}
className={clsx(className, 'text-lg/6 font-semibold text-balance text-zinc-950 sm:text-base/6')}
className={clsx(className, 'rc-dialog-title text-base/6 font-medium text-balance text-[var(--rc-text-primary)]')}
/>
)
}
export function DialogDescription({ className, ...props }) {
return <Headless.Description as={Text} {...props} className={clsx(className, 'mt-2 text-pretty')} />
return <Headless.Description as={Text} {...props} className={clsx(className, 'rc-dialog-description mt-2 text-pretty text-[var(--rc-text-secondary)]')} />
}
export function DialogBody({ className, ...props }) {
return <div {...props} className={clsx(className, 'mt-6')} />
return <div {...props} className={clsx(className, 'rc-dialog-body mt-2 max-h-[calc(100vh-180px)] overflow-y-auto')} />
}
export function DialogActions({ className, ...props }) {
......@@ -65,7 +67,8 @@ export function DialogActions({ className, ...props }) {
{...props}
className={clsx(
className,
'mt-8 flex flex-col-reverse items-center justify-end gap-3 *:w-full sm:flex-row sm:*:w-auto'
'rc-dialog-actions',
'mt-5 flex flex-col-reverse items-center justify-end gap-2 *:w-full sm:flex-row sm:*:w-auto'
)}
/>
)
......
......@@ -34,11 +34,11 @@ export const Input = forwardRef(function Input(
// Basic layout
'relative block w-full',
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)] before:bg-white before:shadow-sm',
'before:absolute before:inset-0 before:rounded-lg before:bg-[var(--rc-field-background)] before:shadow-[var(--field-shadow,0_1px_2px_rgb(15_23_42_/_4%))]',
// 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-[var(--pm-brand)]',
'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(--rc-brand-primary)]/20',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
])}
......@@ -64,11 +64,11 @@ export const Input = forwardRef(function Input(
'[&::-webkit-datetime-edit-meridiem-field]:p-0',
],
// Basic layout
'relative block w-full appearance-none rounded-lg px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2.5)-1px)] sm:px-[calc(--spacing(3)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
'relative block h-9 w-full appearance-none rounded-lg px-3 py-[calc(--spacing(1.5)-1px)]',
// Typography
'text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6',
'text-sm/6 text-[var(--rc-text-primary)] placeholder:text-[var(--rc-text-muted)]',
// Border
'border border-zinc-950/10 data-hover:border-zinc-950/20',
'border border-[var(--rc-field-border)] data-hover:border-[var(--rc-field-border-hover)] focus:border-[var(--rc-brand-primary)]',
// Background color
'bg-transparent',
// Hide default focus styles
......@@ -76,7 +76,7 @@ export const Input = forwardRef(function Input(
// Invalid state
'data-invalid:border-red-500 data-invalid:data-hover:border-red-500',
// Disabled state
'data-disabled:border-zinc-950/20',
'data-disabled:border-[var(--rc-field-border)]',
// System icons
,
])}
......
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import React, { forwardRef } from 'react'
import FormSelect from '@/components/FormSelect'
import { forwardRef } from 'react'
export const Select = forwardRef(function Select({ className, multiple, ...props }, ref) {
return (
<span
data-slot="control"
className={clsx([
className,
// Basic layout
'group relative block w-full',
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)] before:bg-white before:shadow-sm',
// 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-[var(--pm-brand)]',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
])}
>
<Headless.Select
ref={ref}
multiple={multiple}
{...props}
className={clsx([
// Basic layout
'relative block w-full appearance-none rounded-lg py-[calc(--spacing(2.5)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
// Horizontal padding
multiple
? 'px-[calc(--spacing(3.5)-1px)] sm:px-[calc(--spacing(3)-1px)]'
: 'pr-[calc(--spacing(10)-1px)] pl-[calc(--spacing(3.5)-1px)] sm:pr-[calc(--spacing(9)-1px)] sm:pl-[calc(--spacing(3)-1px)]',
// Options (multi-select)
'[&_optgroup]:font-semibold',
// Typography
'text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6',
// Border
'border border-zinc-950/10 data-hover:border-zinc-950/20',
// Background color
'bg-transparent',
// Hide default focus styles
'focus:outline-hidden',
// Invalid state
'data-invalid:border-red-500 data-invalid:data-hover:border-red-500',
// Disabled state
'data-disabled:border-zinc-950/20 data-disabled:opacity-100',
])}
/>
{!multiple && (
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
className="size-5 stroke-zinc-500 group-has-data-disabled:stroke-zinc-600 sm:size-4 forced-colors:stroke-[CanvasText]"
viewBox="0 0 16 16"
aria-hidden="true"
fill="none"
>
<path d="M5.75 10.75L8 13L10.25 10.75" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.25 5.25L8 3L5.75 5.25" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
)}
</span>
)
return <FormSelect ref={ref} className={className} multiple={multiple} {...props} />
})
......@@ -27,7 +27,7 @@ export function Table({
{...props}
className={clsx(
className,
'-mx-(--gutter) overflow-x-auto whitespace-nowrap',
'overflow-x-auto whitespace-nowrap rounded-[var(--rc-data-grid-radius)] bg-[var(--rc-bg-surface)]',
)}
>
<div
......@@ -36,7 +36,7 @@ export function Table({
!bleed && 'sm:px-(--gutter)',
)}
>
<table className="min-w-full text-left text-sm/6 text-zinc-950">
<table className="min-w-full border-separate border-spacing-0 text-left text-sm/6 text-[var(--rc-data-grid-body-color)]">
{children}
</table>
</div>
......@@ -47,7 +47,7 @@ export function Table({
}
export function TableHead({ className, ...props }) {
return <thead {...props} className={clsx(className, 'text-zinc-500')} />;
return <thead {...props} className={clsx(className, 'bg-[var(--rc-data-grid-header-background)] text-[var(--rc-data-grid-header-color)]')} />;
}
export function TableBody(props) {
......@@ -67,13 +67,12 @@ export function TableRow({ href, target, title, className, ...props }) {
<TableRowContext.Provider value={{ href, target, title }}>
<tr
{...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-[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',
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-[var(--rc-brand-primary)]',
striped && 'even:bg-[var(--rc-bg-subtle)]/40',
href && 'hover:bg-[var(--rc-data-grid-row-hover-background)]',
)}
/>
</TableRowContext.Provider>
......@@ -88,8 +87,8 @@ export function TableHeader({ className, ...props }) {
{...props}
className={clsx(
className,
'border-b border-b-zinc-950/10 px-4 py-2 font-medium first:pl-(--gutter,--spacing(2)) last:pr-(--gutter,--spacing(2))',
grid && 'border-l border-l-zinc-950/5 first:border-l-0',
'h-9 border-b border-b-[var(--rc-border-default)] px-4 py-2 text-xs font-medium first:pl-(--gutter,--spacing(2)) last:pr-(--gutter,--spacing(2)) first:rounded-tl-[var(--rc-data-grid-radius)] last:rounded-tr-[var(--rc-data-grid-radius)]',
grid && 'border-l border-l-[var(--rc-border-default)] first:border-l-0',
!bleed && 'sm:first:pl-4 sm:last:pr-4',
)}
/>
......@@ -108,9 +107,9 @@ export function TableCell({ className, children, ...props }) {
className={clsx(
className,
'relative px-4 first:pl-(--gutter,--spacing(2)) last:pr-(--gutter,--spacing(2))',
!striped && 'border-b border-zinc-950/5',
grid && 'border-l border-l-zinc-950/5 first:border-l-0',
dense ? 'py-2.5' : 'py-4',
!striped && 'border-b border-[var(--rc-border-default)]/70',
grid && 'border-l border-l-[var(--rc-border-default)] first:border-l-0',
dense ? 'py-2.5' : 'py-3',
!bleed && 'sm:first:pl-4 sm:last:pr-4',
)}
>
......
......@@ -11,11 +11,11 @@ export const Textarea = forwardRef(function Textarea({ className, resizable = tr
// Basic layout
'relative block w-full',
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)] before:bg-white before:shadow-sm',
'before:absolute before:inset-0 before:rounded-lg before:bg-[var(--rc-field-background)] before:shadow-[var(--field-shadow,0_1px_2px_rgb(15_23_42_/_4%))]',
// 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-[var(--pm-brand)]',
'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(--rc-brand-primary)]/20',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
])}
......@@ -27,9 +27,9 @@ export const Textarea = forwardRef(function Textarea({ className, resizable = tr
// Basic layout
'relative block h-full w-full appearance-none rounded-lg px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2.5)-1px)] sm:px-[calc(--spacing(3)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
// Typography
'text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6',
'text-sm/6 text-[var(--rc-text-primary)] placeholder:text-[var(--rc-text-muted)]',
// Border
'border border-zinc-950/10 data-hover:border-zinc-950/20',
'border border-[var(--rc-field-border)] data-hover:border-[var(--rc-field-border-hover)] focus:border-[var(--rc-brand-primary)]',
// Background color
'bg-transparent',
// Hide default focus styles
......@@ -37,7 +37,7 @@ export const Textarea = forwardRef(function Textarea({ className, resizable = tr
// Invalid state
'data-invalid:border-red-500 data-invalid:data-hover:border-red-500',
// Disabled state
'disabled:border-zinc-950/20',
'disabled:border-[var(--rc-field-border)]',
// Resizable
resizable ? 'resize-y' : 'resize-none',
])}
......
......@@ -28,17 +28,17 @@ export function Alert({
<Headless.Dialog {...props}>
<Headless.DialogBackdrop
transition
className="fixed inset-0 flex w-screen justify-center overflow-y-auto bg-zinc-950/15 px-2 py-2 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-6 sm:py-8 lg:px-8 lg:py-16 dark:bg-zinc-950/50"
className="rc-dialog-backdrop fixed inset-0 flex w-screen justify-center overflow-y-auto px-4 py-4 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-10 sm:py-10"
/>
<div className="fixed inset-0 w-screen overflow-y-auto pt-6 sm:pt-0">
<div className="grid min-h-full grid-rows-[1fr_auto_1fr] justify-items-center p-8 sm:grid-rows-[1fr_auto_3fr] sm:p-4">
<div className="fixed inset-0 w-screen overflow-y-auto px-4 py-4 sm:px-10 sm:py-10">
<div className="grid min-h-full place-items-center">
<Headless.DialogPanel
transition
className={clsx(
className,
sizes[size],
'row-start-2 w-full rounded-2xl bg-white p-8 shadow-lg ring-1 ring-zinc-950/10 sm:rounded-2xl sm:p-6 dark:bg-zinc-900 dark:ring-white/10 forced-colors:outline',
'max-h-[calc(100vh-32px)] w-full overflow-hidden rounded-[var(--rc-modal-radius)] bg-[var(--overlay,var(--rc-bg-surface))] p-[var(--rc-modal-padding)] shadow-[var(--rc-shadow-overlay)] forced-colors:outline',
'transition duration-100 will-change-transform data-closed:opacity-0 data-enter:ease-out data-closed:data-enter:scale-95 data-leave:ease-in'
)}
>
......@@ -59,7 +59,7 @@ export function AlertTitle({
{...props}
className={clsx(
className,
'text-center text-base/6 font-semibold text-balance text-zinc-950 sm:text-left sm:text-sm/6 sm:text-wrap dark:text-white'
'text-center text-base/6 font-medium text-balance text-[var(--rc-text-primary)] sm:text-left sm:text-wrap'
)}
/>
)
......@@ -73,13 +73,13 @@ export function AlertDescription({
<Headless.Description
as={Text}
{...props}
className={clsx(className, 'mt-2 text-center text-pretty sm:text-left')}
className={clsx(className, 'mt-2 text-center text-pretty text-[var(--rc-text-secondary)] sm:text-left')}
/>
)
}
export function AlertBody({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
return <div {...props} className={clsx(className, 'mt-4')} />
return <div {...props} className={clsx(className, 'mt-2')} />
}
export function AlertActions({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
......@@ -88,7 +88,7 @@ export function AlertActions({ className, ...props }: React.ComponentPropsWithou
{...props}
className={clsx(
className,
'mt-6 flex flex-col-reverse items-center justify-end gap-3 *:w-full sm:mt-4 sm:flex-row sm:*:w-auto'
'mt-5 flex flex-col-reverse items-center justify-end gap-2 *:w-full sm:flex-row sm:*:w-auto'
)}
/>
)
......
......@@ -28,18 +28,18 @@ export function Dialog({
<Headless.Dialog {...props}>
<Headless.DialogBackdrop
transition
className="fixed inset-0 z-50 flex w-screen justify-center overflow-y-auto bg-zinc-950/25 px-2 py-2 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-6 sm:py-8 lg:px-8 lg:py-16 dark:bg-zinc-950/50"
className="rc-dialog-backdrop fixed inset-0 z-50 flex w-screen justify-center overflow-y-auto px-4 py-4 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-10 sm:py-10"
/>
<div className="fixed inset-0 z-50 w-screen overflow-y-auto pt-6 sm:pt-0">
<div className="grid min-h-full grid-rows-[1fr_auto] justify-items-center sm:grid-rows-[1fr_auto_3fr] sm:p-4">
<div className="fixed inset-0 z-50 w-screen overflow-y-auto px-4 py-4 sm:px-10 sm:py-10">
<div className="grid min-h-full place-items-center">
<Headless.DialogPanel
transition
className={clsx(
className,
sizes[size],
'row-start-2 w-full min-w-0 rounded-t-3xl bg-white p-(--gutter) shadow-lg ring-1 ring-zinc-950/10 [--gutter:--spacing(8)] sm:mb-auto sm:rounded-2xl dark:bg-zinc-900 dark:ring-white/10 forced-colors:outline',
'transition duration-100 will-change-transform data-closed:translate-y-12 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:data-closed:translate-y-0 sm:data-closed:data-enter:scale-95'
'max-h-[calc(100vh-32px)] w-full min-w-0 overflow-hidden rounded-[var(--rc-modal-radius)] bg-[var(--overlay,var(--rc-bg-surface))] p-[var(--rc-modal-padding)] shadow-[var(--rc-shadow-overlay)] forced-colors:outline',
'transition duration-100 will-change-transform data-closed:translate-y-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in data-closed:data-enter:scale-95'
)}
>
{children}
......@@ -57,7 +57,7 @@ export function DialogTitle({
return (
<Headless.DialogTitle
{...props}
className={clsx(className, 'text-lg/6 font-semibold text-balance text-zinc-950 sm:text-base/6 dark:text-white')}
className={clsx(className, 'text-base/6 font-medium text-balance text-[var(--rc-text-primary)]')}
/>
)
}
......@@ -66,11 +66,11 @@ export function DialogDescription({
className,
...props
}: { className?: string } & Omit<Headless.DescriptionProps<typeof Text>, 'as' | 'className'>) {
return <Headless.Description as={Text} {...props} className={clsx(className, 'mt-2 text-pretty')} />
return <Headless.Description as={Text} {...props} className={clsx(className, 'mt-2 text-pretty text-[var(--rc-text-secondary)]')} />
}
export function DialogBody({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
return <div {...props} className={clsx(className, 'mt-6')} />
return <div {...props} className={clsx(className, 'mt-2 max-h-[calc(100vh-180px)] overflow-y-auto')} />
}
export function DialogActions({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
......@@ -79,7 +79,7 @@ export function DialogActions({ className, ...props }: React.ComponentPropsWitho
{...props}
className={clsx(
className,
'mt-8 flex flex-col-reverse items-center justify-end gap-3 *:w-full sm:flex-row sm:*:w-auto'
'mt-5 flex flex-col-reverse items-center justify-end gap-2 *:w-full sm:flex-row sm:*:w-auto'
)}
/>
)
......
import BankOutlined from '@ant-design/icons/BankOutlined';
import BarChartOutlined from '@ant-design/icons/BarChartOutlined';
import AppstoreOutlined from '@ant-design/icons/AppstoreOutlined';
import CalendarOutlined from '@ant-design/icons/CalendarOutlined';
import CheckOutlined from '@ant-design/icons/CheckOutlined';
import CheckCircleOutlined from '@ant-design/icons/CheckCircleOutlined';
import DownloadOutlined from '@ant-design/icons/DownloadOutlined';
import DownOutlined from '@ant-design/icons/DownOutlined';
import ClockCircleOutlined from '@ant-design/icons/ClockCircleOutlined';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import DollarOutlined from '@ant-design/icons/DollarOutlined';
import DatabaseOutlined from '@ant-design/icons/DatabaseOutlined';
import EditOutlined from '@ant-design/icons/EditOutlined';
import EyeOutlined from '@ant-design/icons/EyeOutlined';
import ExportOutlined from '@ant-design/icons/ExportOutlined';
import FileTextOutlined from '@ant-design/icons/FileTextOutlined';
import FolderOpenOutlined from '@ant-design/icons/FolderOpenOutlined';
import PaperClipOutlined from '@ant-design/icons/PaperClipOutlined';
import LeftOutlined from '@ant-design/icons/LeftOutlined';
import LogoutOutlined from '@ant-design/icons/LogoutOutlined';
import InboxOutlined from '@ant-design/icons/InboxOutlined';
import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
import MenuFoldOutlined from '@ant-design/icons/MenuFoldOutlined';
import MenuUnfoldOutlined from '@ant-design/icons/MenuUnfoldOutlined';
import MoreOutlined from '@ant-design/icons/MoreOutlined';
import MoonOutlined from '@ant-design/icons/MoonOutlined';
import OrderedListOutlined from '@ant-design/icons/OrderedListOutlined';
import PieChartOutlined from '@ant-design/icons/PieChartOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import ProjectOutlined from '@ant-design/icons/ProjectOutlined';
import ReloadOutlined from '@ant-design/icons/ReloadOutlined';
import RightOutlined from '@ant-design/icons/RightOutlined';
import RiseOutlined from '@ant-design/icons/RiseOutlined';
import SearchOutlined from '@ant-design/icons/SearchOutlined';
import SendOutlined from '@ant-design/icons/SendOutlined';
import StarOutlined from '@ant-design/icons/StarOutlined';
import SunOutlined from '@ant-design/icons/SunOutlined';
import TeamOutlined from '@ant-design/icons/TeamOutlined';
import UserOutlined from '@ant-design/icons/UserOutlined';
import UploadOutlined from '@ant-design/icons/UploadOutlined';
import WarningOutlined from '@ant-design/icons/WarningOutlined';
function iconStyle(size, style) {
return size === undefined ? style : { fontSize: size, ...style };
}
function withSize(Icon) {
return function AppIcon({ size, style, ...props }) {
return (
<Icon aria-hidden="true" {...props} style={iconStyle(size, style)} />
);
};
}
export const Bank = withSize(BankOutlined);
export const Archive = withSize(InboxOutlined);
export const AppGrid = withSize(AppstoreOutlined);
export const BarChart = withSize(BarChartOutlined);
export const Calendar = withSize(CalendarOutlined);
export const Check = withSize(CheckOutlined);
export const ChevronDown = withSize(DownOutlined);
export const ChevronRight = withSize(RightOutlined);
export const Clock = withSize(ClockCircleOutlined);
export const Close = withSize(CloseOutlined);
export const Delete = withSize(DeleteOutlined);
export const Dollar = withSize(DollarOutlined);
export const Database = withSize(DatabaseOutlined);
export const Edit = withSize(EditOutlined);
export const Eye = withSize(EyeOutlined);
export const OpenExternal = withSize(ExportOutlined);
export const FileText = withSize(FileTextOutlined);
export const DocumentTextIcon = FileText;
export const Info = withSize(InfoCircleOutlined);
export const Folder = withSize(FolderOpenOutlined);
export const LogOut = withSize(LogoutOutlined);
export const MenuFold = withSize(MenuFoldOutlined);
export const MenuUnfold = withSize(MenuUnfoldOutlined);
export const More = withSize(MoreOutlined);
export const Moon = withSize(MoonOutlined);
export const Project = withSize(ProjectOutlined);
export const Search = withSize(SearchOutlined);
export const Send = withSize(SendOutlined);
export const Sparkles = withSize(StarOutlined);
export const Sun = withSize(SunOutlined);
export const Team = withSize(TeamOutlined);
export const User = withSize(UserOutlined);
export const Upload = withSize(UploadOutlined);
export const Back = withSize(LeftOutlined);
export const ArrowDownTrayIcon = withSize(DownloadOutlined);
export const ArrowTopRightOnSquareIcon = OpenExternal;
export const ArrowPathIcon = withSize(ReloadOutlined);
export const ArrowTrendingUpIcon = withSize(RiseOutlined);
export const ArrowUpTrayIcon = Upload;
export const BanknotesIcon = withSize(DollarOutlined);
export const BuildingOffice2Icon = Bank;
export const CalendarDaysIcon = Calendar;
export const ChartBarSquareIcon = BarChart;
export const ChartPieIcon = withSize(PieChartOutlined);
export const CheckCircleIcon = withSize(CheckCircleOutlined);
export const ChevronDownIcon = ChevronDown;
export const ChevronRightIcon = ChevronRight;
export const CircleStackIcon = Database;
export const ClipboardDocumentIcon = FileText;
export const ClipboardDocumentListIcon = withSize(OrderedListOutlined);
export const ClockIcon = Clock;
export const ExclamationCircleIcon = withSize(WarningOutlined);
export const ExclamationTriangleIcon = withSize(WarningOutlined);
export const FolderIcon = Folder;
export const FolderOpenIcon = Folder;
export const InformationCircleIcon = Info;
export const MagnifyingGlassIcon = Search;
export const PaperClipIcon = withSize(PaperClipOutlined);
export const PlusIcon = withSize(PlusOutlined);
export const ShieldCheckIcon = withSize(CheckCircleOutlined);
export const ShieldExclamationIcon = withSize(WarningOutlined);
export const SparklesIcon = Sparkles;
export const UserGroupIcon = Team;
export const XMarkIcon = Close;
This diff is collapsed.
import { Label, ListBox, Select } from '@heroui/react';
import clsx from 'clsx';
import { useMemo } from 'react';
import { ChevronDown } from './AppIcons';
const EMPTY_OPTION_KEY = '__rc_filter_empty__';
function normalizeOption(option) {
if (option && typeof option === 'object') {
return {
label: String(option.label ?? option.value ?? ''),
value: String(option.value ?? ''),
disabled: Boolean(option.disabled),
};
}
return {
label: String(option ?? ''),
value: String(option ?? ''),
disabled: false,
};
}
export default function FilterSelect({
'aria-label': ariaLabel,
className,
disabled = false,
emptyText = '暂无可选项',
onChange,
options = [],
placeholder = '请选择',
value = '',
}) {
const normalizedOptions = useMemo(
() => options.map(normalizeOption),
[options],
);
const selectedValue = String(value ?? '');
const selectedOption = normalizedOptions.find(
(option) => option.value === selectedValue,
);
const selectedKey = selectedValue === '' ? EMPTY_OPTION_KEY : selectedValue;
return (
<Select
aria-label={ariaLabel || placeholder}
className={clsx('rc-filter-select', className)}
fullWidth
isDisabled={disabled}
selectedKey={selectedKey}
variant="secondary"
onSelectionChange={(key) => {
const nextValue = key === EMPTY_OPTION_KEY ? '' : String(key ?? '');
onChange?.(nextValue);
}}
>
<Select.Trigger className="rc-filter-select__trigger">
<Select.Value className="rc-filter-select__value">
<span
className={clsx(
'block min-w-0 truncate',
!selectedOption && 'rc-filter-select__placeholder',
)}
title={selectedOption?.label || placeholder}
>
{selectedOption?.label || placeholder}
</span>
</Select.Value>
<ChevronDown className="rc-filter-select__indicator" />
</Select.Trigger>
<Select.Popover
className="rc-filter-select__popover"
placement="bottom start"
>
<ListBox
aria-label={ariaLabel || placeholder}
className="rc-filter-select__listbox"
>
{normalizedOptions.length > 0 ? (
normalizedOptions.map((option) => (
<ListBox.Item
key={option.value || EMPTY_OPTION_KEY}
id={option.value || EMPTY_OPTION_KEY}
className="rc-filter-select__option"
isDisabled={option.disabled}
textValue={option.label}
>
<Label
className="rc-filter-select__option-label"
title={option.label}
>
{option.label}
</Label>
<ListBox.ItemIndicator className="rc-filter-select__option-indicator" />
</ListBox.Item>
))
) : (
<div className="rc-filter-select__empty">{emptyText}</div>
)}
</ListBox>
</Select.Popover>
</Select>
);
}
import { Calendar as HeroCalendar } from '@heroui/react/calendar';
import { DateField } from '@heroui/react/date-field';
import { DatePicker } from '@heroui/react/date-picker';
import { TimeField } from '@heroui/react/time-field';
import { parseDate, parseDateTime, parseTime } from '@internationalized/date';
import clsx from 'clsx';
import { useMemo } from 'react';
import { Calendar as CalendarIcon } from './AppIcons';
function parseValue(value, mode) {
if (!value) {
return null;
}
try {
return mode === 'datetime'
? parseDateTime(String(value).slice(0, 16))
: parseDate(String(value).slice(0, 10));
} catch {
return null;
}
}
function formatValue(value, mode) {
if (!value) {
return '';
}
const rawValue = value.toString();
if (mode === 'date') {
return rawValue.slice(0, 10);
}
if (rawValue.includes('T')) {
return rawValue.slice(0, 16);
}
return `${rawValue.slice(0, 10)}T00:00`;
}
function parseTimeValue(value) {
if (!value || !String(value).includes('T')) {
return null;
}
try {
return parseTime(String(value).slice(11, 16));
} catch {
return null;
}
}
function formatTimeValue(value) {
return value ? value.toString().slice(0, 5) : '00:00';
}
function combineDateAndTime(value, dateValue, timeValue) {
const dateText = dateValue
? dateValue.toString().slice(0, 10)
: String(value || '').slice(0, 10);
if (!dateText) {
return '';
}
return `${dateText}T${formatTimeValue(timeValue)}`;
}
function emitDateChange(onChange, name, nextValue) {
onChange?.({
target: {
name,
value: nextValue,
},
});
}
export default function FormDatePicker({
'aria-label': ariaLabel,
className,
disabled = false,
granularity,
max,
min,
mode = 'date',
name,
onChange,
readOnly = false,
required = false,
value,
...props
}) {
const resolvedMode = mode === 'datetime' ? 'datetime' : 'date';
const resolvedGranularity =
granularity || (resolvedMode === 'datetime' ? 'minute' : 'day');
const parsedValue = useMemo(
() => parseValue(value, resolvedMode),
[resolvedMode, value],
);
const minValue = useMemo(() => parseValue(min, resolvedMode), [min, resolvedMode]);
const maxValue = useMemo(() => parseValue(max, resolvedMode), [max, resolvedMode]);
const timeValue = useMemo(() => parseTimeValue(value), [value]);
return (
<DatePicker
{...props}
aria-label={ariaLabel || '选择日期'}
className={clsx('rc-form-date-picker', className)}
granularity={resolvedGranularity}
isDisabled={disabled}
isReadOnly={readOnly}
isRequired={required}
maxValue={maxValue || undefined}
minValue={minValue || undefined}
value={parsedValue}
onChange={(nextValue) =>
emitDateChange(onChange, name, formatValue(nextValue, resolvedMode))
}
>
<DateField.Group
className="rc-form-date-picker__group"
fullWidth
variant="secondary"
>
<DateField.Input className="rc-form-date-picker__input">
{(segment) => (
<DateField.Segment
className="rc-form-date-picker__segment"
segment={segment}
/>
)}
</DateField.Input>
<DateField.Suffix className="rc-form-date-picker__suffix">
<DatePicker.Trigger
className="rc-form-date-picker__trigger"
aria-label="打开日期选择"
>
<DatePicker.TriggerIndicator>
<CalendarIcon />
</DatePicker.TriggerIndicator>
</DatePicker.Trigger>
</DateField.Suffix>
</DateField.Group>
<DatePicker.Popover
className="rc-form-date-picker__popover"
placement="bottom start"
>
<HeroCalendar className="rc-form-date-picker__calendar">
<HeroCalendar.Header>
<HeroCalendar.NavButton slot="previous" />
<HeroCalendar.Heading />
<HeroCalendar.NavButton slot="next" />
</HeroCalendar.Header>
<HeroCalendar.Grid>
<HeroCalendar.GridHeader>
{(day) => <HeroCalendar.HeaderCell>{day}</HeroCalendar.HeaderCell>}
</HeroCalendar.GridHeader>
<HeroCalendar.GridBody>
{(date) => <HeroCalendar.Cell date={date} />}
</HeroCalendar.GridBody>
</HeroCalendar.Grid>
</HeroCalendar>
{resolvedMode === 'datetime' ? (
<div className="rc-form-date-picker__time-panel">
<span className="rc-form-date-picker__time-label">时间</span>
<TimeField
aria-label="选择时间"
className="rc-form-date-picker__time-field"
granularity="minute"
hideTimeZone
hourCycle={24}
value={timeValue}
onChange={(nextTime) =>
emitDateChange(
onChange,
name,
combineDateAndTime(value, parsedValue, nextTime),
)
}
>
<TimeField.Group
className="rc-form-date-picker__time-group"
variant="secondary"
>
<TimeField.Input className="rc-form-date-picker__time-input">
{(segment) => (
<TimeField.Segment
className="rc-form-date-picker__segment"
segment={segment}
/>
)}
</TimeField.Input>
</TimeField.Group>
</TimeField>
</div>
) : null}
</DatePicker.Popover>
</DatePicker>
);
}
import { Label, ListBox, Select } from '@heroui/react';
import clsx from 'clsx';
import { Children, Fragment, isValidElement, useMemo } from 'react';
import { ChevronDown } from './AppIcons';
const EMPTY_OPTION_KEY = '__rc_form_select_empty__';
function extractText(node) {
if (node === null || node === undefined || typeof node === 'boolean') {
return '';
}
if (typeof node === 'string' || typeof node === 'number') {
return String(node);
}
if (Array.isArray(node)) {
return node.map(extractText).join('');
}
if (isValidElement(node)) {
return extractText(node.props.children);
}
return '';
}
function normalizeOptions(children) {
const options = [];
Children.forEach(children, (child) => {
if (!isValidElement(child)) {
return;
}
if (child.type === Fragment || child.type === 'optgroup') {
options.push(...normalizeOptions(child.props.children));
return;
}
if (child.type !== 'option') {
return;
}
options.push({
disabled: Boolean(child.props.disabled),
label: extractText(child.props.children),
value: String(child.props.value ?? ''),
});
});
return options;
}
function toOptionKey(value) {
return value === '' ? EMPTY_OPTION_KEY : String(value);
}
function fromOptionKey(key) {
return key === EMPTY_OPTION_KEY ? '' : String(key ?? '');
}
export default function FormSelect({
'aria-label': ariaLabel,
children,
className,
disabled = false,
multiple = false,
name,
onChange,
placeholder,
required = false,
value,
...props
}) {
const options = useMemo(() => normalizeOptions(children), [children]);
const emptyOption = options.find((option) => option.value === '');
const resolvedPlaceholder = placeholder || emptyOption?.label || '请选择';
const selectedValues = multiple
? (Array.isArray(value) ? value : []).map(String)
: [String(value ?? '')];
const selectedOptions = options.filter(
(option) => option.value !== '' && selectedValues.includes(option.value),
);
const displayText = multiple
? selectedOptions.length <= 2
? selectedOptions.map((option) => option.label).join('、')
: `${selectedOptions
.slice(0, 2)
.map((option) => option.label)
.join('、')} 等 ${selectedOptions.length} 项`
: options.find((option) => option.value === selectedValues[0])?.label ||
resolvedPlaceholder;
const hasSelection = multiple
? selectedOptions.length > 0
: selectedValues[0] !== '';
const emitChange = (nextValues) => {
const normalizedValues = nextValues.map(fromOptionKey);
onChange?.({
target: {
name,
selectedOptions: normalizedValues.map((nextValue) => ({
value: nextValue,
})),
value: normalizedValues[0] || '',
},
});
};
const sharedProps = {
...props,
'aria-label': ariaLabel || resolvedPlaceholder,
className: clsx('rc-form-select rc-filter-select', className),
fullWidth: true,
isDisabled: disabled,
isRequired: required,
selectionMode: multiple ? 'multiple' : 'single',
variant: 'secondary',
};
if (multiple) {
sharedProps.selectedKeys = new Set(selectedValues.map(toOptionKey));
sharedProps.onSelectionChange = (keys) => {
const nextKeys =
keys === 'all' ? options.map((option) => option.value) : [...keys];
emitChange(nextKeys);
};
} else {
sharedProps.selectedKey = toOptionKey(selectedValues[0]);
sharedProps.onSelectionChange = (key) => emitChange([key]);
}
return (
<Select {...sharedProps}>
<Select.Trigger className="rc-form-select__trigger rc-filter-select__trigger">
<Select.Value className="rc-form-select__value rc-filter-select__value">
<span
className={clsx(
'block min-w-0 truncate',
!hasSelection && 'rc-filter-select__placeholder',
)}
title={displayText}
>
{displayText}
</span>
</Select.Value>
<ChevronDown className="rc-filter-select__indicator" />
</Select.Trigger>
<Select.Popover
className="rc-form-select__popover rc-filter-select__popover"
placement="bottom start"
>
<ListBox
aria-label={ariaLabel || resolvedPlaceholder}
className="rc-filter-select__listbox"
selectionMode={multiple ? 'multiple' : 'single'}
>
{options.map((option) => (
<ListBox.Item
key={toOptionKey(option.value)}
id={toOptionKey(option.value)}
className="rc-form-select__option rc-filter-select__option"
isDisabled={option.disabled}
textValue={option.label}
>
<Label
className="rc-filter-select__option-label"
title={option.label}
>
{option.label}
</Label>
<ListBox.ItemIndicator className="rc-filter-select__option-indicator" />
</ListBox.Item>
))}
</ListBox>
</Select.Popover>
</Select>
);
}
import {
Back,
Check,
Clock,
Close,
Delete,
Edit,
Eye,
More,
Send,
} from '@/components/AppIcons';
import { Button, Dropdown, Label, Tooltip } from '@heroui/react';
import clsx from 'clsx';
import { Children, Fragment, isValidElement } from 'react';
const STICKY_ACTION_BASE_HEADER_CLASS =
'sticky right-0 z-10 bg-[var(--rc-data-grid-header-background)] text-left';
const STICKY_ACTION_BASE_CELL_CLASS =
'sticky right-0 z-[1] bg-[var(--rc-bg-surface)] group-hover:bg-[var(--rc-data-grid-row-hover-background)] hover:z-40 focus-within:z-40';
const STICKY_FIRST_COLUMN_BASE_HEADER_CLASS =
'sticky left-0 z-20 bg-[var(--rc-data-grid-header-background)] text-left';
const STICKY_FIRST_COLUMN_BASE_CELL_CLASS =
'sticky left-0 z-[2] bg-[var(--rc-bg-surface)] group-hover:bg-[var(--rc-data-grid-row-hover-background)] hover:z-30 focus-within:z-30';
const ACTION_COLUMN_WIDTHS = {
icon: 'w-[4.5rem] min-w-[4.5rem]',
single: 'w-[5rem] min-w-[5rem]',
double: 'w-[7rem] min-w-[7rem]',
compact: 'w-[9rem] min-w-[9rem]',
medium: 'w-[11rem] min-w-[11rem]',
wide: 'w-[14rem] min-w-[14rem]',
xwide: 'w-[18rem] min-w-[18rem]',
};
export const PROJECT_MANAGER_LIST_TABLE_CLASS =
'rounded-lg border border-zinc-200 bg-white text-sm text-zinc-700 [&_thead]:bg-zinc-50/70 [&_thead_th]:border-b [&_thead_th]:border-zinc-200 [&_thead_th]:px-4 [&_thead_th]:py-3 [&_thead_th]:text-sm [&_thead_th]:font-semibold [&_thead_th]:text-zinc-500 [&_tbody_td]:px-4 [&_tbody_td]:text-sm [&_tbody_td]:text-zinc-700';
export const PROJECT_MANAGER_LIST_ROW_CLASS =
'group border-b border-zinc-100 transition-colors [&>td]:bg-zinc-50 [&>td]:py-3.5 hover:[&>td]:bg-zinc-100';
export const PROJECT_MANAGER_LIST_EDITABLE_CONTROL_CLASS =
'[&_input]:h-9 [&_input]:rounded-md [&_input]:border-zinc-200 [&_input]:bg-zinc-50/60 [&_input]:text-sm [&_input]:font-normal [&_input]:text-zinc-600 [&_input]:placeholder-zinc-400 [&_input]:shadow-none [&_input:disabled]:bg-zinc-50 [&_input:disabled]:text-zinc-400 [&_select]:h-9 [&_select]:rounded-md [&_select]:border-zinc-200 [&_select]:bg-zinc-50/60 [&_select]:text-sm [&_select]:text-zinc-600 [&_select]:shadow-none [&_select:disabled]:bg-zinc-50 [&_select:disabled]:text-zinc-400';
const ACTION_TONE_CLASSES = {
approve: 'pm-table-action--approve',
delete: 'pm-table-action--danger',
edit: 'pm-table-action--default',
history: 'pm-table-action--default',
reject: 'pm-table-action--danger',
revoke: 'pm-table-action--warning',
submit: 'pm-table-action--primary',
view: 'pm-table-action--default',
};
const ACTION_COLOR_TONE = {
blue: 'submit',
green: 'approve',
red: 'delete',
};
function resolveActionTone({ color, icon, label, outline, tone }) {
if (tone) {
return tone;
}
const iconTone = Object.entries(TABLE_ACTION_ICONS).find(
([, ActionIcon]) => ActionIcon === icon,
)?.[0];
if (iconTone) {
return iconTone;
}
if (/删除|移除|作废|清退|delete|remove|trash/i.test(String(label || ''))) {
return 'delete';
}
if (/驳回|拒绝|reject/i.test(String(label || ''))) {
return 'reject';
}
return ACTION_COLOR_TONE[color] || (outline ? 'edit' : 'view');
}
export function getStickyActionHeaderClass(size = 'medium') {
return clsx(
STICKY_ACTION_BASE_HEADER_CLASS,
ACTION_COLUMN_WIDTHS[size] || ACTION_COLUMN_WIDTHS.medium,
);
}
export function getStickyActionCellClass(size = 'medium') {
return clsx(
STICKY_ACTION_BASE_CELL_CLASS,
ACTION_COLUMN_WIDTHS[size] || ACTION_COLUMN_WIDTHS.medium,
);
}
export function getStickyFirstColumnHeaderClass(className) {
return clsx(STICKY_FIRST_COLUMN_BASE_HEADER_CLASS, className);
}
export function getStickyFirstColumnCellClass(className) {
return clsx(STICKY_FIRST_COLUMN_BASE_CELL_CLASS, className);
}
export const TABLE_ACTION_ICONS = {
approve: Check,
delete: Delete,
edit: Edit,
history: Clock,
reject: Close,
revoke: Back,
submit: Send,
view: Eye,
};
function flattenActions(children) {
const actions = [];
Children.forEach(children, (child) => {
if (!child) {
return;
}
if (isValidElement(child) && child.type === Fragment) {
actions.push(...flattenActions(child.props.children));
return;
}
actions.push(child);
});
return actions;
}
export function TableActionGroup({ className, children, ...props }) {
const actions = flattenActions(children);
const shouldCollapseOverflow = actions.length >= 4;
const visibleActions = shouldCollapseOverflow ? actions.slice(0, 2) : actions;
const overflowActions = shouldCollapseOverflow ? actions.slice(2) : [];
return (
<div
{...props}
className={clsx(
'icon-actions flex flex-nowrap items-center justify-end gap-1.5 whitespace-nowrap',
className,
)}
>
{visibleActions}
{overflowActions.length > 0 ? (
<Dropdown>
<Tooltip delay={0}>
<Button
isIconOnly
size="sm"
variant="ghost"
className="pm-table-action pm-table-action--default inline-flex size-8 items-center justify-center rounded-full"
aria-label="更多操作"
title="更多操作"
>
<More size={16} />
</Button>
<Tooltip.Content>更多操作</Tooltip.Content>
</Tooltip>
<Dropdown.Popover className="rc-dropdown-popover min-w-36" placement="bottom end">
<Dropdown.Menu
aria-label="更多操作"
onAction={(key) => overflowActions[Number(key)]?.props?.onClick?.()}
>
{overflowActions.map((action, index) => {
const label = action?.props?.label || `操作 ${index + 3}`;
const Icon = action?.props?.icon;
return (
<Dropdown.Item
id={String(index)}
isDisabled={action?.props?.disabled}
key={`${label}-${index}`}
className="rc-dropdown-option"
textValue={label}
>
<span className="flex items-center gap-2">
{Icon ? <Icon className="size-4" aria-hidden="true" /> : null}
<Label>{label}</Label>
</span>
</Dropdown.Item>
);
})}
</Dropdown.Menu>
</Dropdown.Popover>
</Dropdown>
) : null}
</div>
);
}
export function TableActionButton({
className,
color,
disabled,
icon: Icon,
label,
onClick,
outline = false,
tone,
...props
}) {
const resolvedTone = resolveActionTone({
color,
icon: Icon,
label,
outline,
tone,
});
return (
<Tooltip delay={0}>
<Button
{...props}
isIconOnly
aria-label={label}
title={label}
isDisabled={disabled}
size="sm"
variant="ghost"
onClick={onClick}
className={clsx(
'pm-table-action inline-flex size-8 items-center justify-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-50',
ACTION_TONE_CLASSES[resolvedTone] || ACTION_TONE_CLASSES.view,
className,
)}
>
<Icon size={16} />
</Button>
<Tooltip.Content>{label}</Tooltip.Content>
</Tooltip>
);
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import clsx from 'clsx';
import FilterSelect from '@/components/FilterSelect';
import FormDatePicker from '@/components/FormDatePicker';
import FormSelect from '@/components/FormSelect';
import { useEffect, useMemo, useState } from 'react';
import {
buildDemoTasks,
......@@ -670,7 +673,7 @@ function buildLocalTaskRecord(values, currentTask) {
function ToolbarCard({ children }) {
return (
<div className="rounded-2xl border border-zinc-200/80 bg-white p-4 shadow-sm">
<div className="rc-workspace-module border border-zinc-200/80 bg-white p-4 shadow-sm">
{children}
</div>
);
......@@ -689,7 +692,7 @@ function StatCard({ label, value, hint, tone = 'default' }) {
return (
<div
className={clsx(
'rounded-2xl border bg-gradient-to-br p-5 shadow-sm',
'rc-workspace-module border bg-gradient-to-br p-5 shadow-sm',
toneClass,
)}
>
......@@ -700,23 +703,6 @@ function StatCard({ label, value, hint, tone = 'default' }) {
);
}
function FilterSelect({ value, onChange, options, placeholder = '全部' }) {
return (
<select
value={value}
onChange={(event) => onChange(event.target.value)}
className="h-10 rounded-xl border border-zinc-200 bg-white px-3 text-sm text-zinc-700 outline-none transition focus:border-sky-400"
>
<option value="">{placeholder}</option>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
}
function TabButton({ active, children, onClick }) {
return (
<button
......@@ -798,7 +784,7 @@ function ProjectGanttBoard({
);
return (
<div className="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm">
<div className="rc-workspace-module overflow-hidden border border-zinc-200 bg-white shadow-sm">
<div className="border-b border-zinc-200 px-5 py-4">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
......@@ -1052,7 +1038,7 @@ function ProjectCalendarBoard({
};
return (
<div className="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm">
<div className="rc-workspace-module overflow-hidden border border-zinc-200 bg-white shadow-sm">
<div className="flex items-center justify-between border-b border-zinc-200 px-5 py-4">
<div>
<div className="text-lg font-semibold text-zinc-900">个人项目日历</div>
......@@ -1143,7 +1129,7 @@ function ProjectCalendarBoard({
function ProjectListPanel({ tasks, selectedTaskId, onSelectTask, onEditTask }) {
return (
<div className="rounded-2xl border border-zinc-200 bg-white shadow-sm">
<div className="rc-workspace-module border border-zinc-200 bg-white shadow-sm">
<div className="border-b border-zinc-200 px-5 py-4">
<div className="text-lg font-semibold text-zinc-900">项目列表</div>
<div className="mt-1 text-sm text-zinc-500">
......@@ -1291,10 +1277,9 @@ function ProjectTaskDialog({
<label className="space-y-2 text-sm text-zinc-600">
<span className="font-medium">优先级</span>
<select
<FormSelect
value={formValues.priority}
onChange={(event) => updateField('priority', event.target.value)}
className="h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 outline-none transition focus:border-sky-400"
>
<option value="">请选择优先级</option>
{PRIORITY_OPTIONS.map((option) => (
......@@ -1302,48 +1287,40 @@ function ProjectTaskDialog({
{option.label}
</option>
))}
</select>
</FormSelect>
</label>
<label className="space-y-2 text-sm text-zinc-600">
<span className="font-medium">计划开始日期</span>
<input
type="date"
<FormDatePicker
value={formValues.planned_start_date}
onChange={(event) => updateField('planned_start_date', event.target.value)}
className="h-11 w-full rounded-xl border border-zinc-200 px-3 outline-none transition focus:border-sky-400"
required
/>
</label>
<label className="space-y-2 text-sm text-zinc-600">
<span className="font-medium">计划结束日期</span>
<input
type="date"
<FormDatePicker
value={formValues.planned_end_date}
onChange={(event) => updateField('planned_end_date', event.target.value)}
className="h-11 w-full rounded-xl border border-zinc-200 px-3 outline-none transition focus:border-sky-400"
required
/>
</label>
<label className="space-y-2 text-sm text-zinc-600">
<span className="font-medium">实际开始日期</span>
<input
type="date"
<FormDatePicker
value={formValues.actual_start_date}
onChange={(event) => updateField('actual_start_date', event.target.value)}
className="h-11 w-full rounded-xl border border-zinc-200 px-3 outline-none transition focus:border-sky-400"
/>
</label>
<label className="space-y-2 text-sm text-zinc-600">
<span className="font-medium">实际完成日期</span>
<input
type="date"
<FormDatePicker
value={formValues.actual_finish_date}
onChange={(event) => updateField('actual_finish_date', event.target.value)}
className="h-11 w-full rounded-xl border border-zinc-200 px-3 outline-none transition focus:border-sky-400"
/>
</label>
</div>
......@@ -1360,10 +1337,9 @@ function ProjectTaskDialog({
<div className="mt-5 grid gap-4 md:grid-cols-3">
<label className="space-y-2 text-sm text-zinc-600">
<span className="font-medium">项目状态</span>
<select
<FormSelect
value={formValues.project_status}
onChange={(event) => updateField('project_status', event.target.value)}
className="h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 outline-none transition focus:border-sky-400"
required
>
{PROJECT_STATUS_OPTIONS.map((option) => (
......@@ -1371,15 +1347,14 @@ function ProjectTaskDialog({
{option.label}
</option>
))}
</select>
</FormSelect>
</label>
<label className="space-y-2 text-sm text-zinc-600">
<span className="font-medium">结果状态</span>
<select
<FormSelect
value={formValues.result_status}
onChange={(event) => updateField('result_status', event.target.value)}
className="h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 outline-none transition focus:border-sky-400"
>
<option value="">自动判定 / 手动补充</option>
{RESULT_STATUS_OPTIONS.map((option) => (
......@@ -1387,7 +1362,7 @@ function ProjectTaskDialog({
{option.label}
</option>
))}
</select>
</FormSelect>
</label>
<label className="space-y-2 text-sm text-zinc-600">
......@@ -1683,7 +1658,7 @@ export default function ProjectBookingWorkspace({ currentUsername }) {
<button
type="button"
onClick={openCreateDialog}
className="rounded-2xl bg-[linear-gradient(135deg,#0b75b6_0%,#1095d3_55%,#18b6f6_100%)] px-5 py-2.5 text-sm font-medium text-white shadow-[0_16px_40px_rgba(17,128,196,0.2)] transition hover:brightness-105"
className="rounded-2xl bg-[linear-gradient(135deg,var(--rc-brand-dark)_0%,var(--rc-brand-primary)_55%,var(--rc-brand-light)_100%)] px-5 py-2.5 text-sm font-medium text-white shadow-[var(--rc-shadow-brand-glow)] transition hover:brightness-105"
>
新建项目
</button>
......
@import 'tailwindcss';
@import '@heroui/styles/css';
@import './design-system/heroui-pro-baseline.css';
@import './design-system/xinyuan-portable.css';
@import './design-system/tokens.css';
@theme {
--font-sans:
......@@ -7,34 +11,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;
--color-sky-50: #fff7f7;
--color-sky-100: #fde8e8;
--color-sky-200: #f8c9c9;
--color-sky-300: #f0a3a3;
--color-sky-400: #d86666;
--color-sky-500: #b83a3a;
--color-sky-600: #9e1b1b;
--color-sky-700: #7e1515;
--color-sky-800: #641313;
--color-sky-900: #4f1010;
--color-blue-50: #fff7f7;
--color-blue-100: #fde8e8;
--color-blue-200: #f8c9c9;
--color-blue-300: #f0a3a3;
--color-blue-400: #d86666;
--color-blue-500: #b83a3a;
--color-blue-600: #9e1b1b;
--color-blue-700: #7e1515;
--color-blue-800: #641313;
--color-blue-900: #4f1010;
--color-cyan-50: #fff7f7;
--color-cyan-100: #fde8e8;
--color-cyan-200: #f8c9c9;
--color-cyan-300: #f0a3a3;
--color-cyan-400: #d86666;
--color-cyan-500: #b83a3a;
--color-cyan-600: #9e1b1b;
--color-cyan-700: #7e1515;
}
:root {
......
......@@ -4,6 +4,7 @@ import authService from '@/services/authService';
// 使用 .umirc.ts 中定义的全局变量
const baseUrl = STREAMS_API_URL;
const routerBase = '/audit_material_manager';
function createRequestWithPrefix(prefix = baseUrl) {
const requestInstance = extend({
......@@ -40,7 +41,9 @@ function createRequestWithPrefix(prefix = baseUrl) {
console.warn('[request] authentication required (401)');
// 跳转到登录页(可选,根据业务需求调整)
location.href = '/login?redirect=' + encodeURIComponent(location.href);
location.href = `${routerBase}/login?redirect=${encodeURIComponent(
location.href,
)}`;
} else if (response?.status === 403) {
authService.clearUser();
console.warn('[request] forbidden (403)');
......
......@@ -4,6 +4,7 @@ import { createContext, useEffect, useMemo, useState } from 'react';
import { history, Navigate, Outlet, useLocation } from 'umi';
export const UserContext = createContext(null);
const ROUTER_BASE = '/audit_material_manager';
function getStoredUser() {
const value = authService.getCurrentUser();
......@@ -91,10 +92,14 @@ export default function AuthWrapper() {
}
if (!userInfo || !authService.getToken()) {
const redirectPath = location.pathname.startsWith(ROUTER_BASE)
? location.pathname.slice(ROUTER_BASE.length) || '/'
: location.pathname;
return (
<Navigate
to={`/login?redirect=${encodeURIComponent(
location.pathname + location.search,
redirectPath + location.search,
)}`}
replace
/>
......
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