'feat:暗色模式样式修改'

This commit is contained in:
zjy committed 2026-06-02 11:01:07 +08:00
1 parent ee8e2b3225
commit 372d4f7933
23 files changed
+594 -269

No files matched your search

+28 -28
View File
@@ -1,6 +1,6 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
@@ -14,7 +14,7 @@ const buttonVariants = cva(
destructive:
"cursor-pointer bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
"cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-[#213D5E] dark:bg-[#022248] dark:text-[#CCD2DA] dark:hover:border-[#213D5E] dark:hover:bg-[#0a2f58] dark:hover:text-[#CCD2DA] dark:[&_svg]:text-[#CCD2DA]",
secondary:
"cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
@@ -37,27 +37,27 @@ const buttonVariants = cva(
},
);
const Button = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}
>(({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-slot="button"
{...(variant !== undefined && { "data-variant": variant })}
{...(size !== undefined && { "data-size": size })}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
});
Button.displayName = "Button";
export { Button, buttonVariants };
const Button = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}
>(({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-slot="button"
{...(variant !== undefined && { "data-variant": variant })}
{...(size !== undefined && { "data-size": size })}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
});
Button.displayName = "Button";
export { Button, buttonVariants };
+3 -3
View File
@@ -34,7 +34,7 @@ const emptyMediaVariants = cva(
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6 dark:text-[#55A6FE] dark:bg-[#022248]",
},
},
defaultVariants: {
@@ -62,7 +62,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
className={cn("text-lg font-medium tracking-tight dark:text-[#AACEFF]", className)}
{...props}
/>
);
@@ -73,7 +73,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
<div
data-slot="empty-description"
className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 dark:text-[#55A6FE]",
className,
)}
{...props}
+10 -1
View File
@@ -3,6 +3,14 @@ import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
const usesDarkNativePicker =
type === "number" ||
type === "date" ||
type === "datetime-local" ||
type === "time" ||
type === "month" ||
type === "week";
return (
<input
type={type}
@@ -10,7 +18,8 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
className={cn(
"h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm",
"border-input placeholder:text-muted-foreground",
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] rounded-[4px] text-[16px]",
"dark:border-[#1B477E] dark:bg-[#0D2D54] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] dark:rounded-[4px] dark:text-[16px] md:dark:text-[16px]",
usesDarkNativePicker && "dark:[color-scheme:dark]",
"file:text-foreground selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
+98 -93
View File
@@ -24,69 +24,70 @@ function SelectValue({
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}
>(({ className, size = "default", children, ...props }, ref) => {
return (
<SelectPrimitive.Trigger
ref={ref}
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
});
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}
>(({ className, size = "default", children, ...props }, ref) => {
return (
<SelectPrimitive.Trigger
ref={ref}
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEE2] dark:data-[placeholder]:text-[#566B7F] dark:hover:bg-[#022248] dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
});
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "item-aligned", align = "center", ...props }, ref) => {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
});
SelectContent.displayName = SelectPrimitive.Content.displayName;
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "item-aligned", align = "center", ...props }, ref) => {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
});
SelectContent.displayName = SelectPrimitive.Content.displayName;
function SelectLabel({
className,
@@ -95,40 +96,44 @@ function SelectLabel({
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
className={cn(
"text-muted-foreground px-2 py-1.5 text-xs dark:text-[#566B7F]",
className,
)}
{...props}
/>
);
}
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => {
return (
<SelectPrimitive.Item
ref={ref}
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
});
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => {
return (
<SelectPrimitive.Item
ref={ref}
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"dark:text-[#AACEFF] dark:focus:bg-[#0062D9] dark:focus:text-white dark:data-[highlighted]:bg-[#0062D9] dark:data-[highlighted]:text-white dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus:[&_svg:not([class*='text-'])]:text-white dark:data-[highlighted]:[&_svg:not([class*='text-'])]:text-white",
className,
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
});
SelectItem.displayName = SelectPrimitive.Item.displayName;
function SelectSeparator({
className,
@@ -151,7 +156,7 @@ function SelectScrollUpButton({
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
"flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]",
className,
)}
{...props}
@@ -169,7 +174,7 @@ function SelectScrollDownButton({
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
"flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]",
className,
)}
{...props}
+1 -1
View File
@@ -13,7 +13,7 @@ function Switch({
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-[#55A6FE] dark:data-[state=checked]:bg-[#0062D9] inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
+10 -6
View File
@@ -32,7 +32,8 @@ const tabsListVariants = cva(
variant: {
default:
"bg-muted dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-hidden dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
line: "gap-1 bg-transparent",
line:
"gap-1 bg-transparent dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-visible dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
},
},
defaultVariants: {
@@ -70,13 +71,16 @@ const TabsTrigger = React.forwardRef<
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"dark:text-muted-foreground dark:hover:text-foreground",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background data-[state=active]:text-foreground",
"dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-input dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-input/30 dark:group-data-[variant=line]/tabs-list:data-[state=active]:text-foreground",
"dark:group-data-[variant=default]/tabs-list:relative dark:group-data-[variant=default]/tabs-list:!h-full dark:group-data-[variant=default]/tabs-list:self-stretch dark:group-data-[variant=default]/tabs-list:flex-none dark:group-data-[variant=default]/tabs-list:rounded-none dark:group-data-[variant=default]/tabs-list:border-0 dark:group-data-[variant=default]/tabs-list:bg-transparent dark:group-data-[variant=default]/tabs-list:px-4 dark:group-data-[variant=default]/tabs-list:!py-0 dark:group-data-[variant=default]/tabs-list:!leading-none dark:group-data-[variant=default]/tabs-list:shadow-none dark:group-data-[variant=default]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=default]/tabs-list:[&_svg]:text-[#CCD2DA]",
"dark:group-data-[variant=line]/tabs-list:relative dark:group-data-[variant=line]/tabs-list:!h-full dark:group-data-[variant=line]/tabs-list:self-stretch dark:group-data-[variant=line]/tabs-list:flex-none dark:group-data-[variant=line]/tabs-list:rounded-none dark:group-data-[variant=line]/tabs-list:border-0 dark:group-data-[variant=line]/tabs-list:bg-transparent dark:group-data-[variant=line]/tabs-list:px-4 dark:group-data-[variant=line]/tabs-list:!py-0 dark:group-data-[variant=line]/tabs-list:!leading-none dark:group-data-[variant=line]/tabs-list:text-[#CCD2DA] dark:group-data-[variant=line]/tabs-list:shadow-none dark:group-data-[variant=line]/tabs-list:hover:text-[#CCD2DA] dark:group-data-[variant=line]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=line]/tabs-list:[&_svg]:text-[#CCD2DA]",
"dark:group-data-[variant=line]/tabs-list:not(:first-child):border-l dark:group-data-[variant=line]/tabs-list:not(:first-child):border-[#1d3961] dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-l-0",
"dark:group-data-[variant=line]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=line]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=line]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF]",
"dark:group-data-[variant=default]/tabs-list:flex dark:group-data-[variant=default]/tabs-list:min-h-0 dark:group-data-[variant=default]/tabs-list:!h-full dark:group-data-[variant=default]/tabs-list:flex-1 dark:group-data-[variant=default]/tabs-list:self-stretch dark:group-data-[variant=default]/tabs-list:rounded-none dark:group-data-[variant=default]/tabs-list:border-0 dark:group-data-[variant=default]/tabs-list:bg-transparent dark:group-data-[variant=default]/tabs-list:px-4 dark:group-data-[variant=default]/tabs-list:!py-0 dark:group-data-[variant=default]/tabs-list:!leading-none dark:group-data-[variant=default]/tabs-list:text-[#CCD2DA] dark:group-data-[variant=default]/tabs-list:shadow-none dark:group-data-[variant=default]/tabs-list:hover:text-[#CCD2DA] dark:group-data-[variant=default]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=default]/tabs-list:[&_svg]:text-[#CCD2DA]",
"dark:group-data-[variant=default]/tabs-list:not(:first-child):border-l dark:group-data-[variant=default]/tabs-list:not(:first-child):border-[#1d3961] dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-l-0",
"dark:group-data-[variant=default]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=default]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=default]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:hover:text-[#CCD2DA]",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
"dark:group-data-[variant=default]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=default]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=default]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:hover:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF]",
"group-data-[variant=line]/tabs-list:after:pointer-events-none group-data-[variant=line]/tabs-list:after:absolute group-data-[variant=line]/tabs-list:after:inset-x-2 group-data-[variant=line]/tabs-list:after:bottom-[-5px] group-data-[variant=line]/tabs-list:after:h-0.5 group-data-[variant=line]/tabs-list:after:bg-foreground group-data-[variant=line]/tabs-list:after:opacity-0 group-data-[variant=line]/tabs-list:after:transition-opacity group-data-[variant=line]/tabs-list:after:content-[''] group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
"dark:group-data-[variant=line]/tabs-list:after:inset-x-0 dark:group-data-[variant=line]/tabs-list:after:bottom-0 dark:group-data-[variant=line]/tabs-list:after:z-[2] dark:group-data-[variant=line]/tabs-list:after:h-[2px] dark:group-data-[variant=line]/tabs-list:after:bg-[#FFFFFF]",
className,
)}
{...props}
@@ -6,6 +6,7 @@ import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useTheme } from "@/contexts/ThemeContext";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
@@ -90,7 +91,7 @@ function V1SectionField({
return (
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground">{label}</span>
<span className="text-xs font-medium text-muted-foreground dark:text-[#AACEE2]">{label}</span>
{!editing && (
<Button size="icon" variant="ghost" className="size-6" onClick={() => { setDraft(value); setEditing(true); }}>
<Pencil className="size-3" />
@@ -154,7 +155,7 @@ function V1FactItem({
}
return (
<div className="rounded border p-2 space-y-1 group">
<div className="rounded border p-2 space-y-1 group ">
{editing ? (
<div className="space-y-2">
<Textarea value={draftContent} onChange={(e) => setDraftContent(e.target.value)} className="min-h-[64px] text-sm" autoFocus />
@@ -196,7 +197,7 @@ function V1FactItem({
</Button>
</div>
</div>
<p className="text-sm whitespace-pre-wrap">{String(fact.content ?? "")}</p>
<p className="text-sm whitespace-pre-wrap dark:text-[#AACEE2]">{String(fact.content ?? "")}</p>
{fact.sourceError ? <p className="text-xs text-red-600 bg-red-50 rounded px-2 py-1">避免:{String(fact.sourceError)}</p> : null}
</>
)}
@@ -206,6 +207,7 @@ function V1FactItem({
// 新增事实表单
function V1AddFactForm({ agent, userId }: { agent: string | null; userId: string }) {
const { theme } = useTheme();
const [open, setOpen] = useState(false);
const [content, setContent] = useState("");
const [category, setCategory] = useState("context");
@@ -222,7 +224,12 @@ function V1AddFactForm({ agent, userId }: { agent: string | null; userId: string
if (!open) {
return (
<Button variant="outline" size="sm" className="mt-1 w-full" onClick={() => setOpen(true)}>
<Button
variant={theme === "dark" ? "default" : "outline"}
size="sm"
className="mt-1 w-full"
onClick={() => setOpen(true)}
>
<Plus className="size-3" />新增事实
</Button>
);
@@ -276,8 +283,8 @@ export function V1BucketView({
return (
<div className="space-y-4">
{/* 用户画像字段 */}
<div className="rounded-md border p-3 space-y-3">
<h4 className="text-xs font-semibold text-muted-foreground">用户画像</h4>
<div className="rounded-md border p-3 space-y-3 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
<h4 className="text-xs font-semibold text-muted-foreground dark:text-white">用户画像</h4>
{allCtxFields.map((field) => (
<V1SectionField
key={field}
@@ -291,8 +298,8 @@ export function V1BucketView({
</div>
{/* 历史摘要字段 */}
<div className="rounded-md border p-3 space-y-3">
<h4 className="text-xs font-semibold text-muted-foreground">历史摘要</h4>
<div className="rounded-md border p-3 space-y-3 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
<h4 className="text-xs font-semibold text-muted-foreground dark:text-white">历史摘要</h4>
{allHistoryFields.map((field) => (
<V1SectionField
key={field}
@@ -306,8 +313,8 @@ export function V1BucketView({
</div>
{/* 事实记录 */}
<div className="rounded-md border p-3 space-y-2">
<h4 className="text-xs font-semibold text-muted-foreground">事实记录({factsRaw.length} 条)</h4>
<div className="rounded-md border p-3 space-y-2 dark:border-[#213D5E] dark:bg-[#022248]">
<h4 className="text-xs font-semibold text-muted-foreground dark:text-white">事实记录({factsRaw.length} 条)</h4>
{factsRaw.map((fact, i) => (
<V1FactItem key={String(fact.id ?? i)} fact={fact} agent={agent} userId={userId} />
))}
@@ -179,7 +179,7 @@ function UserAuditPanel({ userId }: { userId: string }) {
<ScrollArea className="min-h-0 flex-1">
<div className="p-4">
{!threadId && (
<div className="text-muted-foreground flex flex-col items-center gap-2 py-16 text-sm">
<div className="text-muted-foreground flex flex-col items-center gap-2 py-16 text-sm dark:text-[#AACEF8]">
<MessageSquareIcon className="size-6" />
请从左侧选择一个会话
</div>
@@ -253,7 +253,7 @@ function AgentMemoryPanel({ userId }: { userId: string }) {
<SelectTrigger className="h-8 w-48" size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent position="popper" side="bottom" sideOffset={4}>
{/* 默认助手始终显示,对应全局记忆文件 */}
<SelectItem value="default">默认助手</SelectItem>
{otherBuckets.map((b) => (
@@ -224,16 +224,16 @@ export function TagManagementPage() {
<table className="w-full text-sm">
<thead className="bg-muted/50">
<tr>
<th className="text-muted-foreground px-4 py-3 text-left font-medium">
<th className="text-muted-foreground px-4 py-3 text-left font-medium dark:text-white">
{t.adminTags.name}
</th>
<th className="text-muted-foreground px-4 py-3 text-left font-medium">
<th className="text-muted-foreground px-4 py-3 text-left font-medium dark:text-white">
{t.adminTags.tagDescription}
</th>
<th className="text-muted-foreground px-4 py-3 text-right font-medium">
<th className="text-muted-foreground px-4 py-3 text-right font-medium dark:text-white">
{t.adminTags.usageCount}
</th>
<th className="text-muted-foreground px-4 py-3 text-right font-medium">
<th className="text-muted-foreground px-4 py-3 text-right font-medium dark:text-white">
{t.adminTags.actions}
</th>
</tr>
@@ -1,6 +1,6 @@
"use client";
import { Download, Loader2 } from "lucide-react";
import { AlertCircle,CheckCircle2, Download, Loader2 } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
@@ -29,6 +29,7 @@ import {
} from "@/core/tool-metrics/api";
import { useToolMetrics, useToolMetricsSummary } from "@/core/tool-metrics/hooks";
import { cn } from "@/lib/utils";
import { useTheme } from "@/contexts/ThemeContext";
const PAGE_SIZE = 50;
@@ -66,6 +67,7 @@ function categoryLabel(category?: string | null): string {
}
export function ToolMetricsPanel() {
const { theme } = useTheme();
const [page, setPage] = useState(0);
const [tool, setTool] = useState("");
const [user, setUser] = useState("");
@@ -79,6 +81,31 @@ export function ToolMetricsPanel() {
const [until, setUntil] = useState("");
const [exporting, setExporting] = useState(false);
const [detailRow, setDetailRow] = useState<ToolMetric | null>(null);
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
const successToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
} as const;
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
const errorToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
} as const;
const darkToastOpts =
theme === "dark"
? { success: successToastDarkOptions, error: errorToastDarkOptions }
: { success: undefined, error: undefined };
const filters = useMemo<ToolMetricsFilters>(() => {
const out: ToolMetricsFilters = {};
@@ -117,9 +144,9 @@ export function ToolMetricsPanel() {
setExporting(true);
try {
await downloadToolMetricsExcel(filters);
toast.success("已导出为 Excel");
toast.success("已导出为 Excel",darkToastOpts.success);
} catch (e) {
toast.error(e instanceof Error ? e.message : "导出失败");
toast.error(e instanceof Error ? e.message : "导出失败",darkToastOpts.error);
} finally {
setExporting(false);
}
@@ -167,7 +194,7 @@ export function ToolMetricsPanel() {
</div>
{/* Summary strip */}
<div className="flex flex-wrap items-center gap-2 rounded-xl border bg-white p-4">
<div className="flex flex-wrap items-center gap-2 rounded-xl border bg-white p-4 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
<div className="mr-4 flex flex-col">
<span className="text-muted-foreground text-xs">总调用</span>
<span className="text-lg font-semibold tabular-nums">
@@ -213,7 +240,7 @@ export function ToolMetricsPanel() {
{/* Per-skill breakdown — answers "was skill X invoked, and did it succeed". */}
{summary && summary.by_skill.length > 0 && (
<div className="rounded-xl border bg-white p-4">
<div className="rounded-xl border bg-white p-4 ">
<h3 className="mb-2 text-sm font-medium">各技能调用情况</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
@@ -258,7 +285,7 @@ export function ToolMetricsPanel() {
</div>
)}
{summary && kind === "skill" && summary.by_skill.length === 0 && (
<div className="text-muted-foreground rounded-xl border bg-white p-4 text-sm">
<div className="text-muted-foreground rounded-xl border bg-white p-4 text-sm dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
当前筛选范围内没有检测到任何技能调用记录。技能只有在被 Agent 通过
<code className="bg-muted mx-1 rounded px-1">skill_view</code>
读取(即真正"调用")时才会被记录;仅在系统提示里被列出、但未被读取的技能不会计入。
@@ -266,9 +293,9 @@ export function ToolMetricsPanel() {
)}
{/* Filters */}
<div className="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-3 lg:grid-cols-6">
<div className="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-3 lg:grid-cols-6 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">调用类型</label>
<label className="text-muted-foreground text-xs dark:text-white">调用类型</label>
<Select
value={kind}
onValueChange={(value) => {
@@ -276,17 +303,17 @@ export function ToolMetricsPanel() {
setPage(0);
}}
>
<SelectTrigger>
<SelectTrigger className="dark:bg-[#0D2D54] dark:hover:bg-[#0D2D54]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent position="popper" side="bottom" sideOffset={4}>
<SelectItem value="skill">技能(Agent Skill)</SelectItem>
<SelectItem value="all">全部工具调用</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">技能名称</label>
<label className="text-muted-foreground text-xs dark:text-white">技能名称</label>
<Input
value={skill}
placeholder="按技能名筛选"
@@ -297,7 +324,7 @@ export function ToolMetricsPanel() {
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">工具名</label>
<label className="text-muted-foreground text-xs dark:text-white">工具名</label>
<Input
value={tool}
placeholder="按工具名筛选"
@@ -308,7 +335,7 @@ export function ToolMetricsPanel() {
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">用户名</label>
<label className="text-muted-foreground text-xs dark:text-white">用户名</label>
<Input
value={user}
placeholder="@ 前部分"
@@ -319,7 +346,7 @@ export function ToolMetricsPanel() {
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">状态</label>
<label className="text-muted-foreground text-xs dark:text-white">状态</label>
<Select
value={status}
onValueChange={(value) => {
@@ -327,10 +354,10 @@ export function ToolMetricsPanel() {
setPage(0);
}}
>
<SelectTrigger>
<SelectTrigger className="w-full dark:bg-[#0D2D54] dark:hover:bg-[#0D2D54]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent position="popper" side="bottom" sideOffset={4}>
<SelectItem value="all">全部</SelectItem>
<SelectItem value="success">成功</SelectItem>
<SelectItem value="error">失败</SelectItem>
@@ -338,7 +365,7 @@ export function ToolMetricsPanel() {
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">错误类型</label>
<label className="text-muted-foreground text-xs dark:text-white">错误类型</label>
<Select
value={category}
onValueChange={(value) => {
@@ -346,10 +373,10 @@ export function ToolMetricsPanel() {
setPage(0);
}}
>
<SelectTrigger>
<SelectTrigger className="w-full dark:bg-[#0D2D54] dark:hover:bg-[#0D2D54]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent position="popper" side="bottom" sideOffset={4}>
<SelectItem value="all">全部</SelectItem>
{TOOL_ERROR_CATEGORIES.map((cat) => (
<SelectItem key={cat} value={cat}>
@@ -360,7 +387,7 @@ export function ToolMetricsPanel() {
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">开始时间</label>
<label className="text-muted-foreground text-xs dark:text-white">开始时间</label>
<Input
type="datetime-local"
value={since}
@@ -371,7 +398,7 @@ export function ToolMetricsPanel() {
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">结束时间</label>
<label className="text-muted-foreground text-xs dark:text-white">结束时间</label>
<Input
type="datetime-local"
value={until}
@@ -389,10 +416,10 @@ export function ToolMetricsPanel() {
</div>
{/* Table */}
<div className="rounded-xl border bg-white">
<div className="rounded-xl border bg-white dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
<div className="overflow-x-auto">
<table className="w-full min-w-[960px] text-sm">
<thead className="bg-muted/60 text-muted-foreground">
<thead className="bg-muted/60 text-muted-foreground dark:text-white">
<tr>
<Th>时间</Th>
<Th>用户</Th>
@@ -229,7 +229,7 @@ export function MemorySettingsPage() {
<SelectTrigger className="h-8 w-44" size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent position="popper" side="bottom" sideOffset={4}>
<SelectItem value="default">
{m.workNotesDefaultAgent}
</SelectItem>
@@ -490,7 +490,7 @@ function MemoryV1View({
return (
<div className="space-y-3">
{/* 说明横幅 */}
<div className="rounded-md border border-dashed px-4 py-3 text-sm text-muted-foreground">
<div className="rounded-md border border-dashed px-4 py-3 text-sm text-muted-foreground dark:border-[#213D5E] dark:bg-[#022248]">
V1 模式:记忆由系统在对话结束后自动提取,无需手动管理。
</div>
@@ -509,13 +509,13 @@ function MemoryV1View({
{/* 事实记录 Tab */}
<TabsContent value="facts" className="mt-3">
<div className="rounded-lg border p-4 space-y-3">
<div className="rounded-lg border p-4 space-y-3 dark:border-[#213D5E] dark:bg-[#022248]">
<div className="flex items-center justify-between">
<h3 className="text-base font-medium">事实记录</h3>
<span className="text-xs text-muted-foreground">{memory.memory_usage}</span>
</div>
{memory.facts.length === 0 ? (
<div className="rounded-md border border-dashed p-3 text-sm text-muted-foreground">
<div className="rounded-md border border-dashed p-3 text-sm text-muted-foreground ">
暂无事实记录,对话后系统会自动提取。
</div>
) : (
@@ -542,21 +542,21 @@ function MemoryV1View({
</div>
<p className="text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(fact.content)}</p>
{fact.sourceError && (
<p className="text-xs text-red-600 bg-red-50 rounded px-2 py-1">
<p className="rounded bg-red-50 px-2 py-1 text-xs text-red-600 dark:bg-[#311629] dark:text-[#EE212C]">
避免:{desensitize(fact.sourceError)}
</p>
)}
</div>
))}
</div>
)}
)}
</div>
</TabsContent>
{/* 用户画像 & 历史摘要 Tab */}
<TabsContent value="profile" className="mt-3 space-y-4">
{/* 用户画像 (userContext) */}
<div className="rounded-lg border p-4 space-y-2">
<div className="rounded-lg border p-4 space-y-2 dark:border-[#213D5E] dark:bg-[#022248]">
<h3 className="text-base font-medium">用户画像</h3>
{ctxEntries.length === 0 ? (
<p className="text-sm text-muted-foreground">暂无画像信息,对话后系统会自动归纳。</p>
@@ -575,7 +575,7 @@ function MemoryV1View({
</div>
{/* 历史摘要 (history) */}
<div className="rounded-lg border p-4 space-y-2">
<div className="rounded-lg border p-4 space-y-2 dark:border-[#213D5E] dark:bg-[#022248]">
<h3 className="text-base font-medium">历史摘要</h3>
{historyEntries.length === 0 ? (
<p className="text-sm text-muted-foreground">暂无历史摘要。</p>
@@ -9,6 +9,7 @@ import {
ClockIcon,
FileArchive,
GlobeIcon,
InfoIcon,
Loader2,
LockIcon,
PinIcon,
@@ -54,6 +55,7 @@ import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-di
import { TagChipList } from "@/components/workspace/tags/tag-chip";
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
import { useTheme } from "@/contexts/ThemeContext";
import { getStoredAuth } from "@/core/auth";
import { useI18n } from "@/core/i18n/hooks";
import type { ValidateSkillUploadResponse } from "@/core/skills/api";
@@ -88,8 +90,42 @@ export function SkillSettingsPage({ onClose }: { onClose?: () => void } = {}) {
const PAGE_SIZE = 10;
/** 重新扫描成功 toast:仅暗色模式使用,浅色走 Sonner 默认样式 */
const rescanSuccessToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
} as const;
/** 重新扫描无变更 toast:仅暗色模式使用,浅色走 Sonner 默认样式 */
const rescanInfoToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#704812] !bg-[#202222] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <InfoIcon className="size-4 shrink-0 text-[#FF9900]" />,
} as const;
/** 重新扫描失败 toast:仅暗色模式使用,浅色走 Sonner 默认样式 */
const rescanErrorToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
} as const;
function SkillSettingsList({ onClose }: { onClose?: () => void }) {
const { t } = useI18n();
const { theme } = useTheme();
const router = useRouter();
const [filter, setFilter] = useState<string>("mine");
const [installOpen, setInstallOpen] = useState(false);
@@ -174,13 +210,21 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
"{count}",
String(result.adopted.length),
),
theme === "dark" ? rescanSuccessToastDarkOptions : undefined,
);
} else {
toast.info(t.settings.skills.rescanSkillsNoop);
toast.info(
t.settings.skills.rescanSkillsNoop,
theme === "dark" ? rescanInfoToastDarkOptions : undefined,
);
}
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
toast.error(
err instanceof Error ? err.message : String(err),
theme === "dark" ? rescanErrorToastDarkOptions : undefined,
);
}
};
return (
<div className="flex w-full flex-col gap-4">
@@ -398,7 +442,7 @@ export function SkillRow({
return (
<>
<Item className="w-full" variant="outline">
<Item className="w-full dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white" variant="outline">
<ItemContent
className="cursor-pointer"
onClick={() => setDetailOpen(true)}
@@ -425,10 +425,15 @@ function ConfigCard() {
})
}
>
<SelectTrigger className="max-w-[20rem]">
<SelectTrigger
className={cn(
"max-w-[20rem]",
"dark:border-[#1B477E] dark:bg-[#0D2D54] dark:text-[#AACEFF] dark:data-[placeholder]:text-[#566B7F] dark:hover:bg-[#0D2D54]",
)}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent position="popper" side="bottom" sideOffset={4}>
<SelectItem value={DEFAULT_MODEL_VALUE}>
{tt.curatorModelDefault}
</SelectItem>
@@ -9,16 +9,16 @@ function DraftWaitingPlaceholder() {
return (
<div className="h-full flex flex-col items-center justify-center gap-5 select-none">
<div className="relative">
<div className="w-16 h-16 rounded-2xl bg-muted/60 flex items-center justify-center">
<FileText className="w-8 h-8 text-muted-foreground/40" />
<div className="w-16 h-16 rounded-2xl bg-muted/60 flex items-center justify-center dark:bg-[#022248] ">
<FileText className="w-8 h-8 text-muted-foreground/40 dark:text-[#55A6FE] " />
</div>
<div className="absolute -bottom-1.5 -right-1.5 w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center">
<PenLine className="w-3.5 h-3.5 text-primary/60" />
<div className="absolute -bottom-1.5 -right-1.5 w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center dark:bg-[#102D5C] ">
<PenLine className="w-3.5 h-3.5 text-primary/60 dark:text-[#2580FF] " />
</div>
</div>
<div className="text-center space-y-1.5 max-w-[200px]">
<p className="text-sm font-medium text-foreground/60">草稿将在此处显示</p>
<p className="text-[11px] text-muted-foreground leading-relaxed">
<p className="text-sm font-medium text-foreground/60 dark:text-[#AACEFF]">草稿将在此处显示</p>
<p className="text-[11px] text-muted-foreground leading-relaxed dark:text-[#55A6FE]">
完成写作流程后,AI 生成的内容将实时出现在这里
</p>
</div>
@@ -25,7 +25,9 @@ interface AIWritingFormProps {
const INPUT_CLASS =
'flex h-12 w-full rounded-xl border border-[#e8dedd] bg-white px-4 py-2 text-sm shadow-sm ' +
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 ' +
'focus-visible:ring-ring disabled:opacity-50';
'focus-visible:ring-ring disabled:opacity-50 ' +
'dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] ' +
'dark:[color-scheme:dark] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30';
const DEFAULT_KEYWORD_COUNT = 1;
const DEFAULT_AUDIENCE = '普通读者';
@@ -117,16 +119,18 @@ export function AIWritingForm({
return (
<form onSubmit={handleSubmit} className="space-y-4 px-4 py-4">
<div className="rounded-2xl border border-[#ebe3e2] bg-[#fcf8f8] p-4">
<div className="flex items-center gap-2 border-b border-[#ece5e4] pb-3">
<PenLine className="size-4 text-[#8b6f6f]" />
<h3 className="text-[18px] font-semibold leading-none text-[#2f2f2f]">写作配置选项</h3>
<div className="rounded-2xl border border-[#ebe3e2] bg-[#fcf8f8] p-4 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white [&_h3]:dark:text-white [&_label]:dark:text-white">
<div className="flex items-center gap-2 border-b border-[#ece5e4] pb-3 dark:border-[#213D5E]">
<PenLine className="size-4 text-[#8b6f6f] dark:text-white" />
<h3 className="text-[18px] font-semibold leading-none text-[#2f2f2f] dark:text-white">
写作配置选项
</h3>
</div>
<div className="mt-4 space-y-2">
<div className="flex items-center justify-between">
<label className={FIELD_LABEL_CLASS}>写作意图</label>
<span className="text-sm text-muted-foreground">
<span className="text-sm text-muted-foreground dark:text-white/70">
{intent.length} / {MAX_INTENT_LENGTH} 字
</span>
</div>
@@ -152,7 +156,7 @@ export function AIWritingForm({
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
<SelectValue placeholder={typesLoading ? '加载中…' : '选择类型'} />
</SelectTrigger>
<SelectContent>
<SelectContent position="popper" side="bottom" sideOffset={4}>
{articleTypes.map((t) => (
<SelectItem key={t.key} value={t.key} className="text-sm">
{t.label}
@@ -196,7 +200,7 @@ export function AIWritingForm({
}
/>
</SelectTrigger>
<SelectContent>
<SelectContent position="popper" side="bottom" sideOffset={4}>
{models.map((m) => (
<SelectItem key={m.name} value={m.name} className="text-sm">
{m.display_name || m.name}
+38 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { BotIcon, RefreshCwIcon, SearchIcon } from "lucide-react";
import { AlertCircle, BotIcon, CheckCircle2, RefreshCwIcon, SearchIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
@@ -22,9 +22,37 @@ import {
import { AgentCard } from "@/components/workspace/agents/agent-card";
import { useAdminSyncAgentSkills, useAllAgentsAdmin } from "@/core/agents";
import type { AgentSkillsSyncResult } from "@/core/agents/api";
import { useTheme } from "@/contexts/ThemeContext";
import { desensitize } from "@/lib/desensitize";
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
const successToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
} as const;
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
const errorToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
} as const;
export default function AdminAgentsPage() {
const { theme } = useTheme();
const darkToastOpts =
theme === "dark"
? { success: successToastDarkOptions, error: errorToastDarkOptions }
: { success: undefined, error: undefined };
const [search, setSearch] = useState("");
const { agents, isLoading } = useAllAgentsAdmin(true);
const syncSkills = useAdminSyncAgentSkills();
@@ -39,14 +67,21 @@ export default function AdminAgentsPage() {
setSyncResult(result);
setSyncDialogOpen(true);
if (result.modified === 0) {
toast.success("所有智能体的技能均有效,无需清理");
toast.success(
"所有智能体的技能均有效,无需清理",
darkToastOpts.success,
);
} else {
toast.success(
`已清理 ${result.modified} 个智能体中的失效技能引用`,
darkToastOpts.success,
);
}
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
toast.error(
err instanceof Error ? err.message : String(err),
darkToastOpts.error,
);
}
}
@@ -17,6 +17,7 @@ import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useTheme } from "@/contexts/ThemeContext";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
@@ -70,6 +71,7 @@ const emptyForm = {
};
export default function AdminScheduledTasksPage() {
const { theme } = useTheme();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
// ?task= carries the task to re-select when returning from a run detail.
@@ -227,7 +229,10 @@ export default function AdminScheduledTasksPage() {
onChange={(event) => setSearchInput(event.target.value)}
placeholder="按任务名称搜索"
/>
<Button type="submit" variant="outline">
<Button
type="submit"
variant={theme === "dark" ? "default" : "outline"}
>
<Search className="size-4" />
搜索
</Button>
+3 -3
View File
@@ -84,10 +84,10 @@ export default function AdminSkillsPage() {
)}
{!isLoading && !error && filteredSkills.length === 0 && (
<div className="flex h-64 flex-col items-center justify-center gap-3 text-center">
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full">
<SparklesIcon className="text-muted-foreground h-7 w-7" />
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full dark:bg-[#022248]">
<SparklesIcon className="text-muted-foreground h-7 w-7 dark:text-[#AACEFF]" />
</div>
<p className="text-muted-foreground text-sm">
<p className="text-muted-foreground text-sm dark:text-[#AACEFF]">
当前没有可管理的技能。
</p>
</div>
+41 -14
View File
@@ -1,6 +1,6 @@
"use client";
import { Download, Loader2 } from "lucide-react";
import { AlertCircle,CheckCircle2, Download, Loader2 } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
@@ -22,6 +22,7 @@ import {
} from "@/core/llm-metrics/api";
import { useLlmMetrics } from "@/core/llm-metrics/hooks";
import { cn } from "@/lib/utils";
import { useTheme } from "@/contexts/ThemeContext";
const PAGE_SIZE = 50;
@@ -74,6 +75,7 @@ function speedColor(tps?: number | null): string {
/** The original LLM-call metrics view, now one tab of the metrics page. */
function LlmMetricsPanel() {
const { theme } = useTheme();
const [page, setPage] = useState(0);
const [model, setModel] = useState("");
const [user, setUser] = useState("");
@@ -81,6 +83,31 @@ function LlmMetricsPanel() {
const [since, setSince] = useState("");
const [until, setUntil] = useState("");
const [exporting, setExporting] = useState(false);
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
const successToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
} as const;
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
const errorToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
} as const;
const darkToastOpts =
theme === "dark"
? { success: successToastDarkOptions, error: errorToastDarkOptions }
: { success: undefined, error: undefined };
const filters = useMemo<LlmMetricsFilters>(() => {
const out: LlmMetricsFilters = {};
@@ -103,9 +130,9 @@ function LlmMetricsPanel() {
setExporting(true);
try {
await downloadLlmMetricsExcel(filters);
toast.success("已导出为 Excel");
toast.success("已导出为 Excel",darkToastOpts.success);
} catch (e) {
toast.error(e instanceof Error ? e.message : "导出失败");
toast.error(e instanceof Error ? e.message : "导出失败",darkToastOpts.error);
} finally {
setExporting(false);
}
@@ -267,9 +294,9 @@ function LlmMetricsPanel() {
</Button>
</div>
<div className="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-5">
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">模型</label>
<div className="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
<div className="flex flex-col gap-1 ">
<label className="text-muted-foreground text-xs dark:text-white">模型</label>
<Input
value={model}
placeholder="按模型名筛选"
@@ -280,7 +307,7 @@ function LlmMetricsPanel() {
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">用户名</label>
<label className="text-muted-foreground text-xs dark:text-white">用户名</label>
<Input
value={user}
placeholder="按用户名匹配(@ 前部分)"
@@ -291,7 +318,7 @@ function LlmMetricsPanel() {
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">状态</label>
<label className="text-muted-foreground text-xs dark:text-white">状态</label>
<Select
value={status}
onValueChange={(value) => {
@@ -299,10 +326,10 @@ function LlmMetricsPanel() {
setPage(0);
}}
>
<SelectTrigger>
<SelectTrigger className="w-full dark:bg-[#0D2D54] dark:hover:bg-[#0D2D54]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent position="popper" side="bottom" sideOffset={4}>
<SelectItem value="all">全部</SelectItem>
<SelectItem value="success">成功</SelectItem>
<SelectItem value="error">失败</SelectItem>
@@ -310,7 +337,7 @@ function LlmMetricsPanel() {
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">开始时间</label>
<label className="text-muted-foreground text-xs dark:text-white">开始时间</label>
<Input
type="datetime-local"
value={since}
@@ -321,7 +348,7 @@ function LlmMetricsPanel() {
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-muted-foreground text-xs">结束时间</label>
<label className="text-muted-foreground text-xs dark:text-white">结束时间</label>
<Input
type="datetime-local"
value={until}
@@ -338,10 +365,10 @@ function LlmMetricsPanel() {
</div>
</div>
<div className="rounded-xl border bg-white">
<div className="rounded-xl border bg-white dark:border-[#213D5E] dark:bg-[#02244C] ">
<div className="overflow-x-auto">
<table className="w-full min-w-[1080px] text-sm">
<thead className="bg-muted/60 text-muted-foreground">
<thead className="bg-muted/60 text-muted-foreground dark:text-white">
<tr>
{visibleColumns.map((col) => (
<Th
@@ -1,8 +1,9 @@
"use client";
import { Loader2Icon, ShieldIcon } from "lucide-react";
import { AlertCircle,CheckCircle2, Loader2Icon, ShieldIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { useTheme } from "@/contexts/ThemeContext";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -12,13 +13,36 @@ import {
useUpdatePrivateSquareSettings,
} from "@/core/system-settings/hooks";
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
const successToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
} as const;
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
const errorToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
} as const;
export default function PrivateSquareSettingsPage() {
const { data, isLoading, error } = usePrivateSquareStatus();
const update = useUpdatePrivateSquareSettings();
const { theme } = useTheme();
const [enabled, setEnabled] = useState(false);
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
// The status response never contains the password itself, so re-syncing
// ``enabled`` from a refetch wouldn't clobber any in-progress edit — but we
// still gate the seed on a one-shot ref to match the prompt-prefix page's
@@ -31,10 +55,14 @@ export default function PrivateSquareSettingsPage() {
setEnabled(data.enabled);
initializedRef.current = true;
}, [data]);
const darkToastOpts =
theme === "dark"
? { success: successToastDarkOptions, error: errorToastDarkOptions }
: { success: undefined, error: undefined };
const handleSave = async () => {
if (password && password !== confirm) {
toast.error("两次输入的密码不一致");
toast.error("两次输入的密码不一致", darkToastOpts.error);
return;
}
try {
@@ -48,9 +76,9 @@ export default function PrivateSquareSettingsPage() {
});
setPassword("");
setConfirm("");
toast.success("已保存");
toast.success("已保存",darkToastOpts.success);
} catch (e) {
toast.error(e instanceof Error ? e.message : "保存失败");
toast.error(e instanceof Error ? e.message : "保存失败",darkToastOpts.error);
}
};
@@ -60,9 +88,9 @@ export default function PrivateSquareSettingsPage() {
}
try {
await update.mutateAsync({ password: "" });
toast.success("已清除密码");
toast.success("已清除密码",darkToastOpts.success);
} catch (e) {
toast.error(e instanceof Error ? e.message : "清除失败");
toast.error(e instanceof Error ? e.message : "清除失败", darkToastOpts.error);
}
};
@@ -85,7 +113,7 @@ export default function PrivateSquareSettingsPage() {
加载失败:{error.message}
</div>
) : (
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5">
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
<div className="flex items-center justify-between gap-4">
<div>
<div className="text-sm font-medium">启用私密广场</div>
@@ -1,6 +1,6 @@
"use client";
import { Loader2Icon } from "lucide-react";
import { Loader2Icon,AlertCircle,CheckCircle2, } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
@@ -12,11 +12,33 @@ import {
usePromptPrefixSettings,
useUpdatePromptPrefixSettings,
} from "@/core/system-settings/hooks";
import { useTheme } from "@/contexts/ThemeContext";
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
const successToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
} as const;
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
const errorToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
} as const;
export default function PromptPrefixSettingsPage() {
const { data, isLoading, error } = usePromptPrefixSettings();
const update = useUpdatePromptPrefixSettings();
const { theme } = useTheme();
const [enabled, setEnabled] = useState(false);
const [switchLabel, setSwitchLabel] = useState("");
const [promptPrefix, setPromptPrefix] = useState("");
@@ -34,13 +56,18 @@ export default function PromptPrefixSettingsPage() {
initializedRef.current = true;
}, [data]);
const handleSave = async () => {
const handleSave = async () => {
const darkToastOpts =
theme === "dark"
? { success: successToastDarkOptions, error: errorToastDarkOptions }
: { success: undefined, error: undefined };
if (enabled && !switchLabel.trim()) {
toast.error("开启时必须填写开关名称");
toast.error("开启时必须填写开关名称",darkToastOpts.error);
return;
}
if (enabled && !promptPrefix.trim()) {
toast.error("开启时必须填写拼接的前缀内容");
toast.error("开启时必须填写拼接的前缀内容",darkToastOpts.error);
return;
}
try {
@@ -49,9 +76,9 @@ export default function PromptPrefixSettingsPage() {
switch_label: switchLabel,
prompt_prefix: promptPrefix,
});
toast.success("已保存");
toast.success("已保存",darkToastOpts.success);
} catch (e) {
toast.error(e instanceof Error ? e.message : "保存失败");
toast.error(e instanceof Error ? e.message : "保存失败",darkToastOpts.error);
}
};
@@ -74,7 +101,7 @@ export default function PromptPrefixSettingsPage() {
加载失败:{error.message}
</div>
) : (
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5">
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
<div className="flex items-center justify-between gap-4">
<div>
<div className="text-sm font-medium">向用户暴露开关</div>
@@ -2,6 +2,8 @@
import { useState } from "react";
import {
AlertCircle,
CheckCircle2,
GripVerticalIcon,
Loader2Icon,
PencilIcon,
@@ -11,6 +13,7 @@ import {
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { useTheme } from "@/contexts/ThemeContext";
import {
Dialog,
DialogContent,
@@ -34,7 +37,30 @@ type DialogState =
| { mode: "edit"; question: RecommendedQuestion }
| null;
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
const successToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
} as const;
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
const errorToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
} as const;
export default function RecommendedQuestionsPage() {
const { theme } = useTheme();
const { questions, isLoading, error } = useRecommendedQuestions();
const createMutation = useCreateRecommendedQuestion();
const updateMutation = useUpdateRecommendedQuestion();
@@ -70,7 +96,7 @@ export default function RecommendedQuestionsPage() {
const title = formTitle.trim();
const content = formContent.trim();
if (!title || !content) {
toast.error("标题和内容不能为空");
toast.error("标题和内容不能为空", darkToastOpts.error);
return;
}
@@ -81,17 +107,20 @@ export default function RecommendedQuestionsPage() {
content,
sort_order: questions.length,
});
toast.success("问题已添加");
toast.success("问题已添加", darkToastOpts.success);
} else if (dialog?.mode === "edit") {
await updateMutation.mutateAsync({
id: dialog.question.id,
body: { title, content },
});
toast.success("问题已更新");
toast.success("问题已更新", darkToastOpts.success);
}
closeDialog();
} catch (e) {
toast.error(e instanceof Error ? e.message : "操作失败");
toast.error(
e instanceof Error ? e.message : "操作失败",
darkToastOpts.error,
);
}
}
@@ -99,9 +128,12 @@ export default function RecommendedQuestionsPage() {
try {
await deleteMutation.mutateAsync(q.id);
setDeleteTarget(null);
toast.success("问题已删除");
toast.success("问题已删除", darkToastOpts.success);
} catch (e) {
toast.error(e instanceof Error ? e.message : "删除失败");
toast.error(
e instanceof Error ? e.message : "删除失败",
darkToastOpts.error,
);
}
}
@@ -128,7 +160,7 @@ export default function RecommendedQuestionsPage() {
try {
await reorderMutation.mutateAsync(next.map((q) => q.id));
} catch {
toast.error("排序保存失败");
toast.error("排序保存失败", darkToastOpts.error);
}
}
@@ -138,6 +170,11 @@ export default function RecommendedQuestionsPage() {
deleteMutation.isPending ||
reorderMutation.isPending;
const darkToastOpts =
theme === "dark"
? { success: successToastDarkOptions, error: errorToastDarkOptions }
: { success: undefined, error: undefined };
return (
<div className="mx-auto flex w-full max-w-2xl flex-col gap-6 px-6 py-10">
<div className="flex items-center justify-between">
@@ -162,15 +199,15 @@ export default function RecommendedQuestionsPage() {
<p>加载失败:{error.message}</p>
</div>
) : questions.length === 0 ? (
<div className="text-muted-foreground flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-sm">
<div className="text-muted-foreground flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-sm dark:text-[#7FACF2] dark:!border-[#213F62]">
<p>暂无推荐问题</p>
<Button variant="outline" size="sm" onClick={openAdd}>
<Button size="sm" onClick={openAdd}>
<PlusIcon className="mr-1.5 size-4" />
添加第一个问题
</Button>
</div>
) : (
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 ">
{questions.map((q, index) => (
<div
key={q.id}
@@ -182,18 +219,18 @@ export default function RecommendedQuestionsPage() {
setDragIndex(null);
setDragOverIndex(null);
}}
className={`flex items-start gap-3 rounded-xl border bg-white px-4 py-3.5 transition-colors ${
className={`flex items-start gap-3 rounded-xl border bg-white px-4 py-3.5 transition-colors dark:bg-[#022248] dark:!border-[#213D5E] ${
dragOverIndex === index && dragIndex !== index
? "border-blue-400 bg-blue-50"
? "border-blue-400 bg-blue-50 dark:border-[#2580FF] dark:bg-[#0D2D54]"
: "border-[#e4e4e7]"
}`}
>
<GripVerticalIcon className="text-muted-foreground/40 mt-0.5 size-4 shrink-0 cursor-grab active:cursor-grabbing" />
<GripVerticalIcon className="text-muted-foreground/40 mt-0.5 size-4 shrink-0 cursor-grab active:cursor-grabbing dark:text-[#566B7F]" />
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-[#18181b]">
<div className="truncate text-sm font-medium text-[#18181b] dark:text-white">
{q.title}
</div>
<div className="text-muted-foreground mt-0.5 line-clamp-2 text-xs">
<div className="text-muted-foreground mt-0.5 line-clamp-2 text-xs dark:text-[#7FACF2]">
{q.content}
</div>
</div>
+80 -19
View File
@@ -2,8 +2,10 @@
import { useEffect, useMemo, useRef, useState } from "react";
import {
AlertCircle,
BellOff,
BellRing,
CheckCircle2,
Download,
Edit3,
Eye,
@@ -62,9 +64,32 @@ import {
statusLabel,
statusVariant,
} from "@/components/workspace/scheduled-tasks/schedule-builder";
import { useTheme } from "@/contexts/ThemeContext";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
const successToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
} as const;
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
const errorToastDarkOptions = {
classNames: {
toast:
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
title: "!text-[#FFFFFF]",
description: "!text-[#FFFFFF]",
},
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
} as const;
function formatDate(value?: string | null) {
return value ? new Date(value).toLocaleString() : "暂无";
}
@@ -88,6 +113,11 @@ const emptyForm = {
};
export default function ScheduledTasksPage() {
const { theme } = useTheme();
const darkToastOpts =
theme === "dark"
? { success: successToastDarkOptions, error: errorToastDarkOptions }
: { success: undefined, error: undefined };
const navigate = useNavigate();
const [searchParams] = useSearchParams();
// When returning from a run-detail page the task id is carried in ?task=,
@@ -207,7 +237,7 @@ export default function ScheduledTasksPage() {
require_markdown: form.require_markdown,
});
setSelectedTaskId(task.task_id);
toast.success("任务已创建");
toast.success("任务已创建", darkToastOpts.success);
} else if (editing) {
await actions.update.mutateAsync({
taskId: editing.task_id,
@@ -221,11 +251,14 @@ export default function ScheduledTasksPage() {
require_markdown: form.require_markdown,
},
});
toast.success("任务已更新");
toast.success("任务已更新", darkToastOpts.success);
}
closeDialog();
} catch (error) {
toast.error(error instanceof Error ? error.message : "保存失败");
toast.error(
error instanceof Error ? error.message : "保存失败",
darkToastOpts.error,
);
}
};
@@ -233,16 +266,22 @@ export default function ScheduledTasksPage() {
try {
await downloadScheduledTaskRun(runId);
} catch (error) {
toast.error(error instanceof Error ? error.message : "下载失败");
toast.error(
error instanceof Error ? error.message : "下载失败",
darkToastOpts.error,
);
}
};
const handleDeleteRun = async (runId: string) => {
try {
await actions.removeRun.mutateAsync(runId);
toast.success("执行记录已删除");
toast.success("执行记录已删除", darkToastOpts.success);
} catch (error) {
toast.error(error instanceof Error ? error.message : "删除失败");
toast.error(
error instanceof Error ? error.message : "删除失败",
darkToastOpts.error,
);
}
};
@@ -273,14 +312,18 @@ export default function ScheduledTasksPage() {
/>
<Button
variant="outline"
className="dark:bg-[#0D2D54] dark:hover:!bg-[#2580FF] dark:hover:text-white"
onClick={async () => {
try {
await actions.updateDeliveryProfile.mutateAsync({
element_user_id: elementUserId || null,
});
toast.success("Element 配置已保存");
toast.success("Element 配置已保存", darkToastOpts.success);
} catch (error) {
toast.error(error instanceof Error ? error.message : "保存失败");
toast.error(
error instanceof Error ? error.message : "保存失败",
darkToastOpts.error,
);
}
}}
>
@@ -437,7 +480,7 @@ export default function ScheduledTasksPage() {
onClick={(event) => {
event.stopPropagation();
actions.trigger.mutate(task.task_id);
toast.success("已触发执行");
toast.success("已触发执行", darkToastOpts.success);
}}
>
<RefreshCw className="size-4" />
@@ -451,13 +494,19 @@ export default function ScheduledTasksPage() {
try {
if (task.published) {
await actions.unpublish.mutateAsync(task.task_id);
toast.success("已取消发布(已订阅者保留)");
toast.success(
"已取消发布(已订阅者保留)",
darkToastOpts.success,
);
} else {
await actions.publish.mutateAsync(task.task_id);
toast.success("已发布到广场");
toast.success("已发布到广场", darkToastOpts.success);
}
} catch (error) {
toast.error(error instanceof Error ? error.message : "操作失败");
toast.error(
error instanceof Error ? error.message : "操作失败",
darkToastOpts.error,
);
}
}}
>
@@ -488,9 +537,15 @@ export default function ScheduledTasksPage() {
taskId: task.task_id,
notifyElement: !notifyOn,
});
toast.success(notifyOn ? "已关闭 Element 通知" : "已开启 Element 通知");
toast.success(
notifyOn ? "已关闭 Element 通知" : "已开启 Element 通知",
darkToastOpts.success,
);
} catch (error) {
toast.error(error instanceof Error ? error.message : "操作失败");
toast.error(
error instanceof Error ? error.message : "操作失败",
darkToastOpts.error,
);
}
}}
>
@@ -505,9 +560,12 @@ export default function ScheduledTasksPage() {
event.stopPropagation();
try {
await actions.unsubscribe.mutateAsync(task.task_id);
toast.success("已取消收藏");
toast.success("已取消收藏", darkToastOpts.success);
} catch (error) {
toast.error(error instanceof Error ? error.message : "操作失败");
toast.error(
error instanceof Error ? error.message : "操作失败",
darkToastOpts.error,
);
}
}}
>
@@ -581,7 +639,7 @@ export default function ScheduledTasksPage() {
<SelectTrigger className="h-7 w-28 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectContent position="popper" side="bottom" sideOffset={4}>
<SelectItem value="all">全部时间</SelectItem>
<SelectItem value="today">今天</SelectItem>
<SelectItem value="7d">近 7 天</SelectItem>
@@ -747,12 +805,15 @@ export default function ScheduledTasksPage() {
taskId: subscribeTarget.task_id,
notifyElement: subscribeNotify,
});
toast.success("已收藏到我的任务");
toast.success("已收藏到我的任务", darkToastOpts.success);
setSubscribeTarget(null);
setSubscribeNotify(false);
setActiveTab("mine");
} catch (error) {
toast.error(error instanceof Error ? error.message : "收藏失败");
toast.error(
error instanceof Error ? error.message : "收藏失败",
darkToastOpts.error,
);
}
}}
>