feat(canvas): 模型选择 - 支持彩色展示标签并优化目录与积分交互

This commit is contained in:
ddCat
2026-09-21 11:11:10 +08:00
parent ea6f02b42f
commit 4f9e79f67f
31 changed files with 410 additions and 63 deletions

View File

@@ -2,6 +2,7 @@ import type { ModelCapabilityChoice } from "@/components/model-protocol-picker";
import { defaultModelCapabilityConfig, normalizeModelCapabilityConfig, type ModelCapabilityConfig } from "@/lib/model-capabilities";
import { modelProtocolSupportsTokenBilling, type ModelProtocolDefinition } from "@/lib/model-protocols";
import type { ChannelModel } from "@/services/api/wallet";
import type { ModelTag } from "@/lib/model-tags";
import { defaultPriceTier, legacyPriceTierToForm, priceTierToForm, type PriceTierFormValues } from "./channel-model-price-tier-form";
export type ChannelModelFormValues = {
@@ -9,6 +10,7 @@ export type ChannelModelFormValues = {
providerModelKey?: string;
displayName?: string;
channelLabel?: string;
tags: ModelTag[];
description?: string;
icon?: string;
capability: ModelCapabilityChoice;
@@ -33,6 +35,7 @@ export function initialChannelModelValues(item: ChannelModel | null, protocols:
providerModelKey: upstreamModel,
displayName: item?.displayName || "",
channelLabel: item?.channelLabel || "",
tags: item?.tags?.map((tag) => ({ ...tag })) || [],
description: item?.description || "",
icon: item?.icon || "",
capability,

View File

@@ -13,6 +13,7 @@ import { createAdminChannelModel, testAdminChannelModel, updateAdminChannelModel
import type { ModelChannel } from "@/stores/use-config-store";
import { defaultPriceTier, normalizeUpstreamModelKey, priceTierPayloadFromForm } from "./channel-model-price-tier-form";
import { PriceTierFields } from "./channel-model-price-tier-fields";
import { ChannelModelTagsEditor } from "./channel-model-tags-editor";
import { changeChannelModelCapability, editorSectionForField, initialChannelModelValues, validateChannelModelPrices, validateChannelModelProtocol, type ChannelModelFormValues as FormValues, type EditorSection } from "./channel-model-editor-form";
export function ChannelModelEditor({
@@ -112,6 +113,7 @@ export function ChannelModelEditor({
providerModelKey: upstreamModel,
displayName: values.displayName?.trim() || values.modelKey.trim(),
channelLabel: values.channelLabel?.trim() || "",
tags: (values.tags || []).map((tag) => ({ text: tag.text.trim(), color: tag.color })),
description: values.description?.trim() || "",
icon: values.icon?.trim() || "",
capability: values.capability,
@@ -275,7 +277,8 @@ export function ChannelModelEditor({
description="命中的请求会优先使用价格档的上游 ID;修改上方上游模型 ID 时,只有与旧值相同的档位会自动跟随更新,其余保持不变。"
/>
) : null}
<Form.Item name="description" label="模型描述" extra="在创作端二级渠道选项悬浮或聚焦时显示,可说明适用场景、渠道差异和注意事项。" rules={[{ max: 500, message: "模型描述不能超过 500 字" }]}>
<ChannelModelTagsEditor />
<Form.Item name="description" label="模型描述" extra="在创作端二级渠道选项中常驻显示,可说明适用场景、渠道差异和注意事项。" rules={[{ max: 500, message: "模型描述不能超过 500 字" }]}>
<Input.TextArea rows={3} maxLength={500} showCount placeholder="填写此渠道模型的使用说明" />
</Form.Item>
</section>

View File

@@ -0,0 +1,34 @@
import { Button, Form, Input, Select } from "antd";
import { Plus, Trash2 } from "lucide-react";
import { ModelTags } from "@/components/model-tags";
import { modelTagColors, type ModelTag } from "@/lib/model-tags";
export function ChannelModelTagsEditor() {
const form = Form.useFormInstance();
const tags: ModelTag[] = Form.useWatch("tags", form) || [];
return <Form.Item label="展示标签" extra="最多 5 个,每个 12 字;展示在用户的二级渠道选项中,不改变实际计费。">
<Form.List name="tags" rules={[{ validator: async (_, value: ModelTag[] = []) => {
if (value.length > 5) throw new Error("最多添加 5 个标签");
const texts = value.map((tag) => tag?.text?.trim()).filter(Boolean);
if (new Set(texts).size !== texts.length) throw new Error("标签文字不能重复");
} }]}>
{(fields, { add, remove }, { errors }) => <div className="grid gap-2">
{fields.map(({ key, name, ...rest }) => <div key={key} className="flex flex-wrap items-start gap-2">
<Form.Item {...rest} name={[name, "text"]} className="mb-0 min-w-32 flex-1" rules={[{ validator: async (_, value: string) => {
const length = Array.from(value?.trim() || "").length;
if (length < 1 || length > 12) throw new Error("标签文字须为 1–12 字");
} }]}>
<Input aria-label={`标签 ${name + 1} 文字`} placeholder="例如:限时特价、官方1折" />
</Form.Item>
<Form.Item {...rest} name={[name, "color"]} className="mb-0 w-28" rules={[{ required: true, message: "请选择颜色" }]}>
<Select aria-label={`标签 ${name + 1} 颜色`} options={modelTagColors.map((color) => ({ value: color.value, label: <span className="model-tag" data-color={color.value}>{color.label}</span> }))} />
</Form.Item>
<Button type="text" aria-label={`删除标签 ${name + 1}`} icon={<Trash2 className="size-4" />} onClick={() => remove(name)} />
</div>)}
<Button type="dashed" icon={<Plus className="size-4" />} disabled={fields.length >= 5} onClick={() => add({ text: "", color: "purple" })}>添加标签</Button>
<Form.ErrorList errors={errors} />
{tags.some((tag) => tag?.text?.trim()) ? <div><span className="text-xs text-foreground/60">展示预览</span><ModelTags tags={tags.filter((tag) => tag?.text?.trim())} /></div> : null}
</div>}
</Form.List>
</Form.Item>;
}