refactor(admin): 模型定价 - 重排规格价格编辑并精简视频匹配条件
This commit is contained in:
@@ -90,9 +90,7 @@ export function validateChannelModelPrices(values: Pick<ChannelModelFormValues,
|
||||
if (tier.matchMode === "advanced") {
|
||||
if (tier.operation && tier.operation !== "*" && !operations[capability]?.includes(tier.operation)) fail("生成方式与模型能力不匹配");
|
||||
const specific = (value: string | undefined) => Boolean(value && value !== "*");
|
||||
if (capability === "video" && tier.videoGenerateAudio && !["*", "true", "false"].includes(tier.videoGenerateAudio)) fail("请选择任意音频、有声或无声");
|
||||
if (!(specific(tier.operation) || (capability === "image" && (specific(tier.quality) || specific(tier.size))) || (capability === "video" && (specific(tier.resolution) || tier.videoSeconds > 0 || tier.imageCount > 0 || specific(tier.videoGenerateAudio)))))
|
||||
fail("规格价格至少需要一个匹配条件;统一价格请选择默认价格");
|
||||
if (!(specific(tier.operation) || (capability === "image" && (specific(tier.quality) || specific(tier.size))) || (capability === "video" && specific(tier.resolution)))) fail("规格价格至少需要一个匹配条件;统一价格请选择默认价格");
|
||||
}
|
||||
const prices = tier.billingMode !== "token" ? [tier.unitPrice] : capability === "video" ? [tier.outputTokenPrice] : [tier.inputTokenPrice, tier.outputTokenPrice, tier.cachedTokenPrice];
|
||||
if (prices.some((price) => typeof price !== "number" || !Number.isFinite(price) || price < 0 || price > 1_000_000)) fail("积分价格必须是 0 到 1000000 之间的有效数值");
|
||||
|
||||
@@ -372,27 +372,31 @@ export function ChannelModelEditor({
|
||||
]}
|
||||
>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<div className="space-y-3">
|
||||
{fields.map((field, index) => (
|
||||
<PriceTierFields
|
||||
key={field.key}
|
||||
index={field.name}
|
||||
ordinal={index + 1}
|
||||
form={form}
|
||||
capability={modelCapability}
|
||||
protocol={modelProtocol}
|
||||
capabilityConfig={capabilityConfig}
|
||||
modelUpstream={modelUpstream}
|
||||
onDirty={() => {
|
||||
dirtyRef.current = true;
|
||||
}}
|
||||
onRemove={() => remove(field.name)}
|
||||
/>
|
||||
))}
|
||||
<Button className="admin-model-editor-add-tier" type="dashed" block icon={<Plus className="size-4" />} onClick={() => add(defaultPriceTier(hasDefaultPriceTier ? "advanced" : "default"))}>
|
||||
{hasDefaultPriceTier ? "新增规格价格" : "新增统一默认价格"}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors} />
|
||||
<div className="admin-price-tier-list-shell">
|
||||
<div className="admin-price-tier-list" aria-label="积分价格规则列表">
|
||||
{fields.map((field, index) => (
|
||||
<PriceTierFields
|
||||
key={field.key}
|
||||
index={field.name}
|
||||
ordinal={index + 1}
|
||||
form={form}
|
||||
capability={modelCapability}
|
||||
protocol={modelProtocol}
|
||||
capabilityConfig={capabilityConfig}
|
||||
modelUpstream={modelUpstream}
|
||||
onDirty={() => {
|
||||
dirtyRef.current = true;
|
||||
}}
|
||||
onRemove={() => remove(field.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="admin-price-tier-list-footer">
|
||||
<Button className="admin-model-editor-add-tier" type="dashed" block icon={<Plus className="size-4" />} onClick={() => add(defaultPriceTier(hasDefaultPriceTier ? "advanced" : "default"))}>
|
||||
{hasDefaultPriceTier ? "新增规格价格" : "新增统一默认价格"}
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
@@ -38,23 +38,26 @@ export function PriceTierFields({
|
||||
const staleTierUpstream = matchMode === "default" && tierUpstream && modelUpstream && tierUpstream !== modelUpstream ? tierUpstream : "";
|
||||
const video = capabilityConfig?.video;
|
||||
const resolutionOptions = video?.resolutions || [];
|
||||
const durationOptions = video?.duration.selection === "enum" ? video.duration.values || [] : [];
|
||||
const tokenEnabled = modelProtocolSupportsTokenBilling(capability, protocol);
|
||||
const isVideo = capability === "video";
|
||||
const isImage = capability === "image";
|
||||
return (
|
||||
<div className="admin-price-tier-card">
|
||||
<div className="admin-price-tier-card-header">
|
||||
<div>
|
||||
<div className="text-sm font-medium">{matchMode === "default" ? "默认价格" : `规格价格 ${ordinal}`}</div>
|
||||
<article className="admin-price-tier-card">
|
||||
<header className="admin-price-tier-card-header">
|
||||
<Form.Item name={[index, "priceConfigured"]} hidden valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={[index, "enabled"]} hidden valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<div className="admin-price-tier-card-heading">
|
||||
<span className="admin-price-tier-card-index">{String(ordinal).padStart(2, "0")}</span>
|
||||
<div className="admin-price-tier-card-heading-copy">
|
||||
<div className="admin-price-tier-card-title">{matchMode === "default" ? "默认价格" : `规格价格 ${ordinal}`}</div>
|
||||
<div className="admin-price-tier-card-summary">{matchMode === "default" ? "所有请求使用同一价格" : isVideo ? "按生成方式与分辨率匹配" : "按生成方式、质量或尺寸匹配"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-price-tier-controls">
|
||||
<Form.Item name={[index, "priceConfigured"]} hidden valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={[index, "enabled"]} hidden valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<div className="admin-price-tier-card-actions">
|
||||
<div className="admin-price-tier-toggle">
|
||||
<span title="关闭后保留配置,但不参与用户请求及计费匹配">可供用户使用</span>
|
||||
<Switch
|
||||
@@ -67,116 +70,125 @@ export function PriceTierFields({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button type="text" danger aria-label={`删除价格档 ${ordinal}`} icon={<Trash2 className="size-3.5" />} onClick={onRemove}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="text" danger aria-label={`删除价格档 ${ordinal}`} icon={<Trash2 className="size-3.5" />} onClick={onRemove}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="admin-price-tier-card-body">
|
||||
<div className="admin-price-tier-block admin-price-tier-match-block">
|
||||
<Form.Item className="mb-0" name={[index, "matchMode"]} label="定价方式" tooltip="默认价格适用于所有请求;按规格配置时,精确规则优先。" rules={[{ required: true }]}>
|
||||
<section className="admin-price-tier-mode-row">
|
||||
<div className="admin-price-tier-panel-heading">
|
||||
<div>
|
||||
<h3>适用范围</h3>
|
||||
<p>选择全部请求,或按生成规格精确匹配。</p>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item className="admin-price-tier-mode-control mb-0" name={[index, "matchMode"]} rules={[{ required: true }]}>
|
||||
<Segmented
|
||||
block
|
||||
aria-label="价格规则适用范围"
|
||||
options={[
|
||||
{ label: "统一价格", value: "default" },
|
||||
{ label: "按规格定价", value: "advanced" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="admin-price-tier-block admin-price-tier-billing-block">
|
||||
<div className="mb-2 text-sm font-medium">销售价格</div>
|
||||
<div className="admin-price-tier-billing-grid">
|
||||
<Form.Item className="admin-price-tier-billing-mode mb-0" name={[index, "billingMode"]} label="计费方式" rules={[{ required: true }]}>
|
||||
<Segmented
|
||||
className="w-full"
|
||||
options={[
|
||||
{ label: "按次", value: "fixed_request" },
|
||||
{ label: "按秒", value: "per_second", disabled: !isVideo },
|
||||
{ label: isVideo ? "视频 Token" : "Token", value: "token", disabled: !tokenEnabled },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
{billingMode === "token" ? (
|
||||
isVideo ? (
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "outputTokenPrice"]} label="积分 / 百万视频 Token" rules={[{ required: true, message: "请输入视频 Token 价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="admin-price-tier-token-grid">
|
||||
<Form.Item className="mb-0" name={[index, "inputTokenPrice"]} label="输入 / 百万 Token" rules={[{ required: true, message: "请输入输入价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item className="mb-0" name={[index, "outputTokenPrice"]} label="输出 / 百万 Token" rules={[{ required: true, message: "请输入输出价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item className="mb-0" name={[index, "cachedTokenPrice"]} label="缓存 / 百万 Token" rules={[{ required: true, message: "请输入缓存价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
</section>
|
||||
<div className={`admin-price-tier-workspace ${matchMode === "default" ? "is-default" : "is-advanced"}`}>
|
||||
{matchMode !== "default" && (
|
||||
<section className="admin-price-tier-panel admin-price-tier-conditions-panel">
|
||||
<div className="admin-price-tier-panel-heading">
|
||||
<div>
|
||||
<h3>匹配条件</h3>
|
||||
<p>符合以下条件时使用这条价格,精确规则优先。</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "unitPrice"]} label={billingMode === "per_second" ? "每秒消耗积分" : "每次消耗积分"} rules={[{ required: true, message: "请输入积分价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
{isVideo && billingMode === "token" ? (
|
||||
<p className="mb-0 mt-2 text-xs text-foreground/60">
|
||||
所有视频模型均支持 Token 计费,统一按火山引擎模式计算:宽 × 高 × 24 帧/秒 ×(输出时长 + 参考视频时长)÷ 1024。优先按成功任务返回的有效用量结算,未返回用量时按公式结算。平台额外预留 10%,不计入公式结算;最终可能补扣或退回差额。0 表示免费;有声、无声或参考视频可分别配置规格价格。
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<CreditCostFields index={index} form={form} billingMode={billingMode} isVideo={isVideo} />
|
||||
{matchMode !== "default" && (
|
||||
<div className="admin-price-tier-match-grid">
|
||||
<Form.Item className="mb-0" name={[index, "operation"]} label="生成方式" rules={[{ required: true, message: "请选择生成方式" }]}>
|
||||
<Select options={operationOptions(capability)} />
|
||||
</Form.Item>
|
||||
{isVideo ? (
|
||||
<Form.Item className="mb-0" name={[index, "resolution"]} label="分辨率" rules={[{ required: true, message: "请选择分辨率" }]}>
|
||||
<Select options={[{ label: "任意分辨率", value: "*" }, ...resolutionOptions.map((value) => ({ label: value.toUpperCase(), value }))]} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isVideo ? (
|
||||
<Form.Item className="mb-0" name={[index, "videoSeconds"]} label="时长" rules={[{ required: true, message: "请输入时长" }]}>
|
||||
{durationOptions.length ? <Select options={[{ label: "任意时长", value: 0 }, ...durationOptions.map((value) => ({ label: `${value} 秒`, value }))]} /> : <InputNumber className="w-full" min={0} precision={0} />}
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isVideo ? (
|
||||
<Form.Item className="mb-0" name={[index, "videoGenerateAudio"]} label="生成音频" rules={[{ required: true, message: "请选择音频条件" }]}>
|
||||
<Select options={[{ label: "任意音频", value: "*" }, { label: "有声", value: "true" }, { label: "无声", value: "false" }]} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isVideo ? (
|
||||
<Form.Item className="mb-0" name={[index, "imageCount"]} label="参考图数量" rules={[{ required: true, message: "请输入参考图数量" }]}>
|
||||
<InputNumber className="w-full" min={0} max={9} precision={0} placeholder="0 表示任意数量" />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isImage ? (
|
||||
<Form.Item className="mb-0" name={[index, "quality"]} label="质量/分辨率" rules={[{ required: true, message: "请选择质量或分辨率" }]}>
|
||||
<Select
|
||||
</div>
|
||||
<div className="admin-price-tier-match-grid">
|
||||
<Form.Item className="mb-0" name={[index, "operation"]} label="生成方式" rules={[{ required: true, message: "请选择生成方式" }]}>
|
||||
<Select options={operationOptions(capability)} />
|
||||
</Form.Item>
|
||||
{isVideo ? (
|
||||
<Form.Item className="mb-0" name={[index, "resolution"]} label="分辨率" rules={[{ required: true, message: "请选择分辨率" }]}>
|
||||
<Select options={[{ label: "任意分辨率", value: "*" }, ...resolutionOptions.map((value) => ({ label: value.toUpperCase(), value }))]} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isImage ? (
|
||||
<Form.Item className="mb-0" name={[index, "quality"]} label="质量/分辨率" rules={[{ required: true, message: "请选择质量或分辨率" }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "任意质量", value: "*" },
|
||||
{ label: "1K", value: "1k" },
|
||||
{ label: "2K", value: "2k" },
|
||||
{ label: "4K", value: "4k" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : null}
|
||||
{isImage ? (
|
||||
<Form.Item className="mb-0" name={[index, "size"]} label="画幅/尺寸">
|
||||
<Input placeholder="任意,或 1:1、16:9、1024x1024" />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item className="admin-price-tier-upstream mb-0" name={[index, "providerModelKey"]} label="命中后使用的上游模型 ID">
|
||||
<Input placeholder="留空则使用模型默认上游 ID" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<section className="admin-price-tier-panel admin-price-tier-selling-panel">
|
||||
<div className="admin-price-tier-panel-heading">
|
||||
<div>
|
||||
<h3>销售价格</h3>
|
||||
<p>设置用户完成一次生成实际消耗的积分。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-price-tier-billing-grid">
|
||||
<Form.Item className="admin-price-tier-billing-mode mb-0" name={[index, "billingMode"]} label="计费方式" rules={[{ required: true }]}>
|
||||
<Segmented
|
||||
className="w-full"
|
||||
options={[
|
||||
{ label: "任意质量", value: "*" },
|
||||
{ label: "1K", value: "1k" },
|
||||
{ label: "2K", value: "2k" },
|
||||
{ label: "4K", value: "4k" },
|
||||
{ label: "按次", value: "fixed_request" },
|
||||
{ label: "按秒", value: "per_second", disabled: !isVideo },
|
||||
{ label: isVideo ? "视频 Token" : "Token", value: "token", disabled: !tokenEnabled },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
{billingMode === "token" ? (
|
||||
isVideo ? (
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "outputTokenPrice"]} label="积分 / 百万视频 Token" rules={[{ required: true, message: "请输入视频 Token 价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="admin-price-tier-token-grid">
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "inputTokenPrice"]} label="输入 / 百万 Token" rules={[{ required: true, message: "请输入输入 Token 价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "outputTokenPrice"]} label="输出 / 百万 Token" rules={[{ required: true, message: "请输入输出 Token 价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "cachedTokenPrice"]} label="缓存 / 百万 Token" rules={[{ required: true, message: "请输入缓存 Token 价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Form.Item className="admin-price-tier-unit-price mb-0" name={[index, "unitPrice"]} label={billingMode === "per_second" ? "每秒消耗积分" : "每次消耗积分"} rules={[{ required: true, message: "请输入积分价格" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
{isVideo && billingMode === "token" ? (
|
||||
<div className="admin-price-tier-helper" role="note">
|
||||
<strong>视频 Token 计费</strong>
|
||||
<span>估算:宽 × 高 × 24 帧/秒 ×(输出时长 + 参考视频时长)÷ 1024;优先按上游有效用量结算,无用量时使用公式,授权预留 10% 会在结算后补扣或退回。</span>
|
||||
</div>
|
||||
) : null}
|
||||
{isImage ? (
|
||||
<Form.Item className="mb-0" name={[index, "size"]} label="画幅/尺寸">
|
||||
<Input placeholder="任意,或 1:1、16:9、1024x1024" />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item className="admin-price-tier-upstream mb-0" name={[index, "providerModelKey"]} label="命中后使用的上游模型 ID">
|
||||
<Input placeholder="留空则使用模型默认上游 ID" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
<CreditCostFields index={index} form={form} billingMode={billingMode} isVideo={isVideo} />
|
||||
</section>
|
||||
</div>
|
||||
{staleTierUpstream ? (
|
||||
<Alert
|
||||
className="admin-price-tier-alert"
|
||||
type="warning"
|
||||
showIcon
|
||||
title="该统一价格档仍指向独立上游模型 ID"
|
||||
@@ -195,7 +207,7 @@ export function PriceTierFields({
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ export type PriceTierMatchMode = "default" | "advanced";
|
||||
|
||||
// 与后端保存口径一致:上游键比较前剥掉一次 models/ 前缀,避免极旧存量行误报差异。
|
||||
export function normalizeUpstreamModelKey(value: unknown): string {
|
||||
return String(value || "").trim().replace(/^models\//, "");
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.replace(/^models\//, "");
|
||||
}
|
||||
|
||||
export type PriceTierFormValues = {
|
||||
@@ -60,7 +62,7 @@ export function defaultPriceTier(matchMode: PriceTierMatchMode = "default"): Pri
|
||||
|
||||
export function priceTierToForm(tier: ChannelModelPriceTier): PriceTierFormValues {
|
||||
const selector = tier.selector || {};
|
||||
const hasSpecificMatch = Object.values(selector).some((value) => value && value !== "*") || (tier.resolution && tier.resolution !== "*") || tier.videoSeconds > 0;
|
||||
const hasSpecificMatch = [selector.operation, selector.quality, selector.size].some((value) => value && value !== "*") || (tier.resolution && tier.resolution !== "*");
|
||||
return {
|
||||
matchMode: hasSpecificMatch ? "advanced" : "default",
|
||||
operation: selector.operation || "*",
|
||||
@@ -107,9 +109,6 @@ export function skuSelectorFromForm(capability: ModelCapabilityChoice, tier: Pri
|
||||
if (tier.operation && tier.operation !== "*") selector.operation = tier.operation;
|
||||
if (capability === "video") {
|
||||
if (tier.resolution && tier.resolution !== "*") selector.vquality = tier.resolution;
|
||||
if (Number(tier.videoSeconds) > 0) selector.videoSeconds = String(Number(tier.videoSeconds));
|
||||
if (Number(tier.imageCount) > 0) selector.imageCount = String(Number(tier.imageCount));
|
||||
if (tier.videoGenerateAudio && tier.videoGenerateAudio !== "*") selector.videoGenerateAudio = tier.videoGenerateAudio;
|
||||
}
|
||||
if (capability === "image") {
|
||||
if (tier.quality && tier.quality !== "*") selector.quality = tier.quality;
|
||||
@@ -123,7 +122,9 @@ export function priceTierResolutionFromForm(capability: ModelCapabilityChoice, t
|
||||
}
|
||||
|
||||
export function priceTierVideoSecondsFromForm(capability: ModelCapabilityChoice, tier: PriceTierFormValues) {
|
||||
return capability === "video" && tier.matchMode === "advanced" ? Number(tier.videoSeconds || 0) : 0;
|
||||
void capability;
|
||||
void tier;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function priceTierPayloadFromForm(capability: ModelCapabilityChoice, tier: PriceTierFormValues, upstreamModel: string) {
|
||||
|
||||
@@ -14,12 +14,21 @@ export function CreditCostFields({ index, form, billingMode, isVideo }: { index:
|
||||
]
|
||||
: [["costUnitPrice", billingMode === "per_second" ? "积分 / 秒" : "积分 / 次"]];
|
||||
return (
|
||||
<div className="admin-price-tier-block">
|
||||
<Form.Item name={[index, "costConfigured"]} label="积分成本价(仅管理员可见)" valuePropName="checked" extra="按当前价格档的计费方式核算成本,不影响用户售价;开启并填 0 表示零成本。">
|
||||
<Switch aria-label="配置积分成本价" />
|
||||
</Form.Item>
|
||||
<section className="admin-price-tier-cost-panel" aria-label="积分成本设置">
|
||||
<header className="admin-price-tier-cost-header">
|
||||
<div className="admin-price-tier-cost-heading">
|
||||
<div className="admin-price-tier-cost-title">
|
||||
<span>积分成本</span>
|
||||
<span className="admin-price-tier-cost-badge">仅管理员可见</span>
|
||||
</div>
|
||||
<p>按当前计费方式填写成本,不影响用户售价。</p>
|
||||
</div>
|
||||
<Form.Item className="admin-price-tier-cost-switch mb-0" name={[index, "costConfigured"]} valuePropName="checked">
|
||||
<Switch aria-label="配置积分成本价" />
|
||||
</Form.Item>
|
||||
</header>
|
||||
{configured ? (
|
||||
<div className={billingMode === "token" && !isVideo ? "admin-price-tier-token-grid" : "admin-price-tier-billing-grid"}>
|
||||
<div className={billingMode === "token" && !isVideo ? "admin-price-tier-cost-grid admin-price-tier-cost-token-grid" : "admin-price-tier-cost-grid"}>
|
||||
{fields.map(([field, label]) => (
|
||||
<Form.Item key={field} className="mb-0" name={[index, field]} label={label} rules={[{ required: true, message: "请输入积分成本价" }]}>
|
||||
<InputNumber className="w-full" min={0} max={1_000_000} precision={6} step={0.1} />
|
||||
@@ -27,6 +36,6 @@ export function CreditCostFields({ index, form, billingMode, isVideo }: { index:
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user