style(ui): 优化组件样式和代码格式

This commit is contained in:
Jeff Gu committed 2026-04-12 17:15:13 +08:00
1 parent 45fd61e285
commit 37cbaa4283
9 files changed
+152 -160

No files matched your search

+19 -29
View File
@@ -1,23 +1,20 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:shadow-md disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer active:scale-[0.97] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:shadow-md disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer active:scale-[0.97] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow-md", default: "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-md",
destructive: destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: "border border-input hover:bg-accent hover:text-accent-foreground hover:border-accent-foreground/20",
outline: secondary: "border border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
"border border-input shadow-sm hover:bg-accent hover:text-accent-foreground hover:border-accent-foreground/20",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
success: "bg-success text-success-foreground shadow-sm hover:bg-success/90", success: "bg-success text-success-foreground hover:bg-success/90",
}, },
size: { size: {
default: "h-10 px-4 py-2", default: "h-10 px-4 py-2",
@@ -30,27 +27,20 @@ const buttonVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
VariantProps<typeof buttonVariants> { asChild?: boolean;
asChild?: boolean
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
<Comp },
className={cn(buttonVariants({ variant, size, className }))} );
ref={ref} Button.displayName = "Button";
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants } export { Button, buttonVariants };
+1 -1
View File
@@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"rounded-md border bg-card text-card-foreground shadow-soft transition-shadow duration-200", "rounded-md bg-card text-card-foreground shadow-soft transition-shadow duration-200",
className className
)} )}
{...props} {...props}
+1 -1
View File
@@ -35,7 +35,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-2xl duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-2xl duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className
)} )}
{...props} {...props}
+9 -9
View File
@@ -1,5 +1,5 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>( const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
@@ -7,15 +7,15 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-input px-3 py-2 text-sm shadow-sm transition-all duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:border-primary/50 hover:border-muted-foreground/30 disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full rounded-md border border-input bg-card px-3 py-2 text-sm transition-all duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-primary/50 hover:border-muted-foreground/30 disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} },
) );
Input.displayName = "Input" Input.displayName = "Input";
export { Input } export { Input };
+1 -1
View File
@@ -18,7 +18,7 @@ interface SelectOption {
} }
const TRIGGER_BASE_CLASS = const TRIGGER_BASE_CLASS =
'group relative flex h-10 w-full items-center rounded-md border border-input bg-transparent px-3 py-2 pr-10 text-sm text-foreground shadow-sm outline-none transition-all duration-200 hover:border-muted-foreground/30 focus-visible:border-primary/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50' 'group relative flex h-10 w-full items-center rounded-md border border-input bg-transparent px-3 py-2 pr-10 text-sm text-foreground transition-all duration-200 hover:border-muted-foreground/30 focus-visible:border-primary/50 disabled:cursor-not-allowed disabled:opacity-50'
const CONTENT_BASE_CLASS = const CONTENT_BASE_CLASS =
'absolute z-50 mt-1 w-full overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-lg' 'absolute z-50 mt-1 w-full overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-lg'
+1 -1
View File
@@ -8,7 +8,7 @@ const Textarea = React.forwardRef<
return ( return (
<textarea <textarea
className={cn( className={cn(
"flex min-h-[80px] w-full rounded-md border border-input px-3 py-2 text-sm shadow-sm transition-all duration-200 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:border-primary/50 hover:border-muted-foreground/30 disabled:cursor-not-allowed disabled:opacity-50", "flex min-h-[80px] w-full rounded-md border border-input px-3 py-2 text-sm transition-all duration-200 placeholder:text-muted-foreground focus-visible:border-primary/50 hover:border-muted-foreground/30 disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
ref={ref} ref={ref}
+10 -10
View File
@@ -55,23 +55,23 @@
} }
.dark { .dark {
--color-background: hsl(224 71% 4%); --color-background: #0e0f11;
--color-foreground: hsl(210 20% 98%); --color-foreground: hsl(210 20% 98%);
--color-card: hsl(224 50% 8%); --color-card: #131416;
--color-card-foreground: hsl(210 20% 98%); --color-card-foreground: hsl(210 20% 98%);
--color-popover: hsl(224 50% 8%); --color-popover: #0e0f11;
--color-popover-foreground: hsl(210 20% 98%); --color-popover-foreground: hsl(210 20% 98%);
--color-primary: hsl(234 89% 67%); --color-primary: #1c1e20;
--color-primary-foreground: hsl(0 0% 100%); --color-primary-foreground: hsl(0 0% 100%);
--color-secondary: hsl(224 40% 12%); --color-secondary: #1c1e20;
--color-secondary-foreground: hsl(210 20% 98%); --color-secondary-foreground: hsl(210 20% 98%);
--color-muted: hsl(224 40% 12%); --color-muted: #1c1e20;
--color-muted-foreground: hsl(220 9% 56%); --color-muted-foreground: hsl(220 9% 56%);
--color-accent: hsl(224 40% 12%); --color-accent: #191b1e;
--color-accent-foreground: hsl(210 20% 98%); --color-accent-foreground: hsl(210 20% 98%);
--color-destructive: hsl(0 72% 51%); --color-destructive: hsl(0 72% 51%);
@@ -86,13 +86,13 @@
--color-info: hsl(199 89% 42%); --color-info: hsl(199 89% 42%);
--color-info-foreground: hsl(0 0% 98%); --color-info-foreground: hsl(0 0% 98%);
--color-border: hsl(224 30% 16%); --color-border: #1c1d20;
--color-input: hsl(224 30% 16%); --color-input: #1c1d20;
--color-sidebar: hsl(224 71% 3%); --color-sidebar: hsl(224 71% 3%);
--color-sidebar-foreground: hsl(210 20% 98%); --color-sidebar-foreground: hsl(210 20% 98%);
--color-sidebar-muted: hsl(224 30% 10%); --color-sidebar-muted: hsl(224 30% 10%);
--color-sidebar-accent: hsl(234 89% 67%); --color-sidebar-accent: #363738;
} }
@layer base { @layer base {
+1 -1
View File
@@ -1245,7 +1245,7 @@ export function Channels({ createMode = false, editRoute = false }: { createMode
</div> </div>
<div className=""> <div className="">
<Label className="text-sm">渠道状态</Label> <Label className="text-sm">渠道状态</Label>
<div className="h-10 flex items-center gap-3 mb-2 rounded-md px-3 border border-input shadow-sm"> <div className="h-10 flex items-center gap-3 mb-2 rounded-md px-3 border border-input hover:border-muted-foreground/30">
<span <span
className={cn( className={cn(
"text-sm font-medium", "text-sm font-medium",
+109 -107
View File
@@ -1,64 +1,55 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { apiClient } from '@/api/client' import { apiClient } from "@/api/client";
import { Channel, PricingConfig } from '@/types' import { Channel, PricingConfig } from "@/types";
import { AutoCompleteInput, type AutoCompleteOption } from '@/components/ui/autocomplete' import { AutoCompleteInput, type AutoCompleteOption } from "@/components/ui/autocomplete";
import { Button } from '@/components/ui/button' import { Button } from "@/components/ui/button";
import { Input } from '@/components/ui/input' import { Input } from "@/components/ui/input";
import { Label } from '@/components/ui/label' import { Label } from "@/components/ui/label";
import { Textarea } from '@/components/ui/textarea' import { Textarea } from "@/components/ui/textarea";
import { useToast } from '@/components/ui/use-toast' import { useToast } from "@/components/ui/use-toast";
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils";
import { getUniqueModelNamesFromChannels } from '@/lib/channel-models' import { getUniqueModelNamesFromChannels } from "@/lib/channel-models";
import { import { Plus, RefreshCw, Trash2, FileJson, FileText, DollarSign, Check, Info, Search } from "lucide-react";
Plus, import { PageContainer } from "@/components/ui/page-container";
RefreshCw, import { Card } from "@/components/ui/card";
Trash2,
FileJson,
FileText,
DollarSign,
Check,
Info,
Search,
} from 'lucide-react'
import { PageContainer } from '@/components/ui/page-container'
type EditMode = 'table' | 'json' type EditMode = "table" | "json";
export function Pricing() { export function Pricing() {
const [editMode, setEditMode] = useState<EditMode>('table') const [editMode, setEditMode] = useState<EditMode>("table");
const [jsonValue, setJsonValue] = useState('') const [jsonValue, setJsonValue] = useState("");
const [pricingRows, setPricingRows] = useState< const [pricingRows, setPricingRows] = useState<
Array<{ model: string; input: number; output: number; cache: number; request: number }> Array<{ model: string; input: number; output: number; cache: number; request: number }>
>([]) >([]);
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState("");
const [modelOptions, setModelOptions] = useState<AutoCompleteOption[]>([]) const [modelOptions, setModelOptions] = useState<AutoCompleteOption[]>([]);
const { addToast } = useToast() const { addToast } = useToast();
const queryClient = useQueryClient() const queryClient = useQueryClient();
const { data, isLoading, isFetching, refetch } = useQuery({ const { data, isLoading, isFetching, refetch } = useQuery({
queryKey: ['pricing'], queryKey: ["pricing"],
queryFn: async () => { queryFn: async () => {
const response = await apiClient.getPricing() const response = await apiClient.getPricing();
return response.data as PricingConfig return response.data as PricingConfig;
}, },
}) });
useEffect(() => { useEffect(() => {
const loadChannelModels = async () => { const loadChannelModels = async () => {
try { try {
const response = await apiClient.getChannels() const response = await apiClient.getChannels();
const channels = response.data as Channel[] const channels = response.data as Channel[];
const modelNames = getUniqueModelNamesFromChannels(channels) const modelNames = getUniqueModelNamesFromChannels(channels);
setModelOptions(modelNames.map((model) => ({ value: model }))) setModelOptions(modelNames.map((model) => ({ value: model })));
} catch (error) { } catch (error) {
console.error('Failed to load channels for pricing suggestions:', error) console.error("Failed to load channels for pricing suggestions:", error);
}
} }
};
loadChannelModels() loadChannelModels();
}, []) }, []);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -68,35 +59,35 @@ export function Pricing() {
output: pricing.output, output: pricing.output,
cache: pricing.cache || 0, cache: pricing.cache || 0,
request: pricing.request || 0, request: pricing.request || 0,
})) }));
setPricingRows(rows) setPricingRows(rows);
setJsonValue(JSON.stringify(data, null, 2)) setJsonValue(JSON.stringify(data, null, 2));
} }
}, [data]) }, [data]);
const saveMutation = useMutation({ const saveMutation = useMutation({
mutationFn: async (config: PricingConfig) => { mutationFn: async (config: PricingConfig) => {
return apiClient.savePricing(config) return apiClient.savePricing(config);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['pricing'] }) queryClient.invalidateQueries({ queryKey: ["pricing"] });
addToast('定价配置已保存', 'success') addToast("定价配置已保存", "success");
}, },
onError: (error: Error) => { onError: (error: Error) => {
addToast('保存失败:' + error.message, 'error') addToast("保存失败:" + error.message, "error");
}, },
}) });
const handleSave = () => { const handleSave = () => {
let config: PricingConfig let config: PricingConfig;
if (editMode === 'table') { if (editMode === "table") {
if (pricingRows.length === 0) { if (pricingRows.length === 0) {
addToast('请至少添加一个模型定价', 'error') addToast("请至少添加一个模型定价", "error");
return return;
} }
config = {} config = {};
pricingRows.forEach((row) => { pricingRows.forEach((row) => {
if (row.model) { if (row.model) {
config[row.model] = { config[row.model] = {
@@ -104,24 +95,24 @@ export function Pricing() {
output: row.output || 0, output: row.output || 0,
cache: row.cache || 0, cache: row.cache || 0,
request: row.request || 0, request: row.request || 0,
};
} }
} });
})
} else { } else {
try { try {
config = JSON.parse(jsonValue) config = JSON.parse(jsonValue);
} catch { } catch {
addToast('JSON格式错误', 'error') addToast("JSON格式错误", "error");
return return;
} }
} }
saveMutation.mutate(config) saveMutation.mutate(config);
} };
const toggleEditMode = () => { const toggleEditMode = () => {
if (editMode === 'table') { if (editMode === "table") {
const config: PricingConfig = {} const config: PricingConfig = {};
pricingRows.forEach((row) => { pricingRows.forEach((row) => {
if (row.model) { if (row.model) {
config[row.model] = { config[row.model] = {
@@ -129,46 +120,50 @@ export function Pricing() {
output: row.output || 0, output: row.output || 0,
cache: row.cache || 0, cache: row.cache || 0,
request: row.request || 0, request: row.request || 0,
};
} }
} });
}) setJsonValue(JSON.stringify(config, null, 2));
setJsonValue(JSON.stringify(config, null, 2)) setEditMode("json");
setEditMode('json')
} else { } else {
try { try {
const config = JSON.parse(jsonValue) const config = JSON.parse(jsonValue);
const rows = Object.entries(config).map(([model, pricing]: [string, unknown]) => ({ const rows = Object.entries(config).map(([model, pricing]: [string, unknown]) => ({
model, model,
input: (pricing as { input: number }).input || 0, input: (pricing as { input: number }).input || 0,
output: (pricing as { output: number }).output || 0, output: (pricing as { output: number }).output || 0,
cache: (pricing as { cache?: number }).cache || 0, cache: (pricing as { cache?: number }).cache || 0,
request: (pricing as { request?: number }).request || 0, request: (pricing as { request?: number }).request || 0,
})) }));
setPricingRows(rows) setPricingRows(rows);
setEditMode('table') setEditMode("table");
} catch { } catch {
addToast('JSON格式错误', 'error') addToast("JSON格式错误", "error");
}
} }
} }
};
const addRow = () => { const addRow = () => {
setPricingRows([...pricingRows, { model: '', input: 0, output: 0, cache: 0, request: 0 }]) setPricingRows([...pricingRows, { model: "", input: 0, output: 0, cache: 0, request: 0 }]);
} };
const removeRow = (index: number) => { const removeRow = (index: number) => {
setPricingRows(pricingRows.filter((_, i) => i !== index)) setPricingRows(pricingRows.filter((_, i) => i !== index));
} };
const updateRow = (index: number, field: 'model' | 'input' | 'output' | 'cache' | 'request', value: string | number) => { const updateRow = (
const newRows = [...pricingRows] index: number,
;(newRows[index] as any)[field] = value field: "model" | "input" | "output" | "cache" | "request",
setPricingRows(newRows) value: string | number,
} ) => {
const newRows = [...pricingRows];
(newRows[index] as any)[field] = value;
setPricingRows(newRows);
};
const filteredRows = pricingRows const filteredRows = pricingRows
.map((row, index) => ({ ...row, _i: index })) .map((row, index) => ({ ...row, _i: index }))
.filter((row) => row.model.toLowerCase().includes(searchQuery.toLowerCase())) .filter((row) => row.model.toLowerCase().includes(searchQuery.toLowerCase()));
const pricingModelOptions = [ const pricingModelOptions = [
...pricingRows ...pricingRows
@@ -176,7 +171,7 @@ export function Pricing() {
.filter((model) => model.length > 0) .filter((model) => model.length > 0)
.map((model) => ({ value: model })), .map((model) => ({ value: model })),
...modelOptions, ...modelOptions,
].filter((option, index, options) => options.findIndex((candidate) => candidate.value === option.value) === index) ].filter((option, index, options) => options.findIndex((candidate) => candidate.value === option.value) === index);
return ( return (
<PageContainer <PageContainer
@@ -188,8 +183,8 @@ export function Pricing() {
<RefreshCw className={cn("h-4 w-4", isFetching && "animate-spin")} /> <RefreshCw className={cn("h-4 w-4", isFetching && "animate-spin")} />
</Button> </Button>
<Button variant="outline" size="sm" onClick={toggleEditMode}> <Button variant="outline" size="sm" onClick={toggleEditMode}>
{editMode === 'table' ? <FileJson className="h-4 w-4" /> : <FileText className="h-4 w-4" />} {editMode === "table" ? <FileJson className="h-4 w-4" /> : <FileText className="h-4 w-4" />}
<span className="hidden sm:inline ml-1">{editMode === 'table' ? 'JSON' : '表格'}</span> <span className="hidden sm:inline ml-1">{editMode === "table" ? "JSON" : "表格"}</span>
</Button> </Button>
<Button size="sm" onClick={handleSave} disabled={saveMutation.isPending}> <Button size="sm" onClick={handleSave} disabled={saveMutation.isPending}>
<Check className="h-4 w-4 mr-1" /> <Check className="h-4 w-4 mr-1" />
@@ -198,7 +193,7 @@ export function Pricing() {
</div> </div>
} }
> >
{editMode === 'table' ? ( {editMode === "table" ? (
isLoading ? ( isLoading ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
@@ -207,17 +202,19 @@ export function Pricing() {
</div> </div>
</div> </div>
) : pricingRows.length === 0 ? ( ) : pricingRows.length === 0 ? (
<div className="rounded-md border bg-card shadow-sm flex flex-col items-center justify-center py-16"> <Card className="flex flex-col items-center justify-center py-16">
<div className="w-14 h-14 rounded-2xl bg-primary/10 flex items-center justify-center mb-4"> <div className="w-14 h-14 rounded-2xl bg-primary/10 flex items-center justify-center mb-4">
<DollarSign className="h-7 w-7 text-primary" /> <DollarSign className="h-7 w-7 text-primary" />
</div> </div>
<h3 className="font-semibold text-lg mb-1">暂无定价配置</h3> <h3 className="font-semibold text-lg mb-1">暂无定价配置</h3>
<p className="text-muted-foreground text-sm text-center max-w-sm mb-6">可调整倍率控制输入/输出成本,或者设置按次扣费。</p> <p className="text-muted-foreground text-sm text-center max-w-sm mb-6">
可调整倍率控制输入/输出成本,或者设置按次扣费。
</p>
<Button onClick={addRow}> <Button onClick={addRow}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
添加模型 添加模型
</Button> </Button>
</div> </Card>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{/* Search */} {/* Search */}
@@ -234,9 +231,12 @@ export function Pricing() {
)} )}
{/* Table */} {/* Table */}
<div className="rounded-md border bg-card"> <Card>
{/* Header */} {/* Header */}
<div className="grid gap-2 px-4 py-2.5 border-b bg-muted/30" style={{ gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr 40px' }}> <div
className="grid gap-2 px-4 py-2.5 border-b bg-muted/30"
style={{ gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr 40px" }}
>
<span className="text-xs font-medium text-muted-foreground">模型名称</span> <span className="text-xs font-medium text-muted-foreground">模型名称</span>
<span className="text-xs font-medium text-muted-foreground">输入倍率</span> <span className="text-xs font-medium text-muted-foreground">输入倍率</span>
<span className="text-xs font-medium text-muted-foreground">输出倍率</span> <span className="text-xs font-medium text-muted-foreground">输出倍率</span>
@@ -248,10 +248,14 @@ export function Pricing() {
{/* Rows */} {/* Rows */}
<div className="divide-y"> <div className="divide-y">
{filteredRows.map((row) => ( {filteredRows.map((row) => (
<div key={row._i} className="grid gap-2 px-4 py-2 items-center hover:bg-muted/20 transition-colors" style={{ gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr 40px' }}> <div
key={row._i}
className="grid gap-2 px-4 py-2 items-center hover:bg-muted/20 transition-colors"
style={{ gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr 40px" }}
>
<AutoCompleteInput <AutoCompleteInput
value={row.model} value={row.model}
onChange={(value) => updateRow(row._i, 'model', value)} onChange={(value) => updateRow(row._i, "model", value)}
placeholder="模型名称" placeholder="模型名称"
inputClassName="font-mono text-sm h-8" inputClassName="font-mono text-sm h-8"
options={pricingModelOptions} options={pricingModelOptions}
@@ -260,7 +264,7 @@ export function Pricing() {
<Input <Input
type="number" type="number"
value={row.input} value={row.input}
onChange={(e) => updateRow(row._i, 'input', parseFloat(e.target.value) || 0)} onChange={(e) => updateRow(row._i, "input", parseFloat(e.target.value) || 0)}
step="0.001" step="0.001"
min="0" min="0"
className="font-mono text-sm h-8" className="font-mono text-sm h-8"
@@ -268,7 +272,7 @@ export function Pricing() {
<Input <Input
type="number" type="number"
value={row.output} value={row.output}
onChange={(e) => updateRow(row._i, 'output', parseFloat(e.target.value) || 0)} onChange={(e) => updateRow(row._i, "output", parseFloat(e.target.value) || 0)}
step="0.001" step="0.001"
min="0" min="0"
className="font-mono text-sm h-8" className="font-mono text-sm h-8"
@@ -276,7 +280,7 @@ export function Pricing() {
<Input <Input
type="number" type="number"
value={row.cache} value={row.cache}
onChange={(e) => updateRow(row._i, 'cache', parseFloat(e.target.value) || 0)} onChange={(e) => updateRow(row._i, "cache", parseFloat(e.target.value) || 0)}
step="0.001" step="0.001"
min="0" min="0"
className="font-mono text-sm h-8" className="font-mono text-sm h-8"
@@ -284,7 +288,7 @@ export function Pricing() {
<Input <Input
type="number" type="number"
value={row.request} value={row.request}
onChange={(e) => updateRow(row._i, 'request', parseFloat(e.target.value) || 0)} onChange={(e) => updateRow(row._i, "request", parseFloat(e.target.value) || 0)}
step="0.001" step="0.001"
min="0" min="0"
className="font-mono text-sm h-8" className="font-mono text-sm h-8"
@@ -303,11 +307,9 @@ export function Pricing() {
</div> </div>
{filteredRows.length === 0 && searchQuery && ( {filteredRows.length === 0 && searchQuery && (
<div className="py-8 text-center text-sm text-muted-foreground"> <div className="py-8 text-center text-sm text-muted-foreground">没有匹配的模型</div>
没有匹配的模型
</div>
)} )}
</div> </Card>
{/* Add */} {/* Add */}
<button <button
@@ -338,5 +340,5 @@ export function Pricing() {
</div> </div>
)} )}
</PageContainer> </PageContainer>
) );
} }