大量优化修改扣积分规则
This commit is contained in:
@@ -11,7 +11,6 @@ const SIDEBAR_COLLAPSED_KEY = "airshelf:sidebar-collapsed";
|
||||
// 全局命令面板(Ctrl K / 点搜索框)—— 忠实搬设计稿 SHELL_COMMANDS,href 改成真路由导航
|
||||
type Command = { id: string; group: string; label: string; sub: string; page: Page; icon: string; key?: string };
|
||||
const SHELL_COMMANDS: Command[] = [
|
||||
{ id: "dashboard", group: "导航", label: "工作台", sub: "任务队列、今日消耗、项目进度", page: "dashboard", icon: "dashboard", key: "D" },
|
||||
{ id: "omni-create", group: "导航", label: "全能创作", sub: "预设工作流 + 对话式 Agent", page: "omniCreate", icon: "sparkles", key: "O" },
|
||||
{ id: "omni-history", group: "导航", label: "创作历史", sub: "查看与继续独立会话", page: "omniHistory", icon: "history", key: "H" },
|
||||
{ id: "products", group: "导航", label: "商品库", sub: "管理 SKU、商品图册、卖点信息", page: "products", icon: "package", key: "P" },
|
||||
@@ -225,7 +224,6 @@ export function AccountMenu({ open, anchorRect, onClose, navigate, logout, user,
|
||||
type NavDef = { id: string; page: Page; label: string; icon: string; badge?: number };
|
||||
|
||||
const NAV: NavDef[] = [
|
||||
{ id: "dashboard", page: "dashboard", label: "工作台", icon: "dashboard" },
|
||||
{ id: "products", page: "products", label: "商品库", icon: "package" },
|
||||
{ id: "models", page: "models", label: "模特库", icon: "model" },
|
||||
{ id: "projects", page: "projects", label: "视频创作", icon: "clapperboard" },
|
||||
@@ -281,7 +279,6 @@ export function topModuleForPage(page: Page): TopModule | null {
|
||||
}
|
||||
|
||||
const MODE_TABS: { id: TopModule; label: string; page: Page }[] = [
|
||||
{ id: "workbench", label: "工作台", page: "dashboard" },
|
||||
{ id: "omni", label: "全能创作", page: "omniCreate" },
|
||||
{ id: "image", label: "图片创作", page: "assetFactory" },
|
||||
{ id: "video", label: "视频创作", page: "projects" },
|
||||
@@ -365,10 +362,9 @@ export function ModeTabs({ active, navigate }: { active: TopModule | null; navig
|
||||
<svg className="mode-clip-defs" width="0" height="0" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<clipPath id="modeButtonMask" clipPathUnits="userSpaceOnUse">
|
||||
<rect rx="22" ry="22" x="0" y="0" width="104" height="44" />
|
||||
<rect rx="22" ry="22" x="120" y="0" width="110" height="44" />
|
||||
<rect rx="22" ry="22" x="246" y="0" width="110" height="44" />
|
||||
<rect rx="22" ry="22" x="372" y="0" width="110" height="44" />
|
||||
<rect rx="22" ry="22" x="0" y="0" width="110" height="44" />
|
||||
<rect rx="22" ry="22" x="126" y="0" width="110" height="44" />
|
||||
<rect rx="22" ry="22" x="252" y="0" width="110" height="44" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
@@ -567,7 +563,7 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p
|
||||
{mobileNavOpen && <div className="mobile-nav-backdrop" onClick={() => setMobileNavOpen(false)} />}
|
||||
<aside className={`sidebar${mobileNavOpen ? " mobile-open" : ""}`}>
|
||||
<div className="sidebar-head">
|
||||
<a className="brand" href="/dashboard" aria-label="影擎工作台" onClick={(event) => { event.preventDefault(); navigate("dashboard"); }}>
|
||||
<a className="brand" href="/omni-create" aria-label="影擎全能创作" onClick={(event) => { event.preventDefault(); navigate("omniCreate"); }}>
|
||||
<span className="brand-clip"><img className="brand-logo" src="/assets/yz/logo-horizontal.png" alt="影擎" /></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,37 @@ export function modelDurations(config: ModelConfig | undefined): number[] {
|
||||
return list?.length ? list : [...FC_DURATIONS];
|
||||
}
|
||||
|
||||
/** 老板挂牌: metadata.points_pricing 每秒积分(视频)。无挂牌返回 null。 */
|
||||
export function pointsPerSecondFromCatalog(
|
||||
config: ModelConfig | undefined,
|
||||
resolution: string,
|
||||
hasVideoRef = false,
|
||||
): number | null {
|
||||
const pricing = (config?.metadata?.points_pricing || {}) as Record<string, unknown>;
|
||||
const tiers = Array.isArray(pricing.tiers) ? pricing.tiers : [];
|
||||
const res = (resolution || "").toLowerCase();
|
||||
const hit = tiers.find((row) => row && typeof row === "object" && String((row as { resolution?: string }).resolution || "").toLowerCase() === res) as
|
||||
| { points_per_second?: number; points_per_second_with_ref?: number }
|
||||
| undefined;
|
||||
if (!hit) return null;
|
||||
if (hasVideoRef && hit.points_per_second_with_ref != null) {
|
||||
const n = Number(hit.points_per_second_with_ref);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
const n = Number(hit.points_per_second);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
export function pointsPerImageFromCatalog(config: ModelConfig | undefined): number | null {
|
||||
const pricing = (config?.metadata?.points_pricing || {}) as Record<string, unknown>;
|
||||
if (pricing.points_per_image != null) {
|
||||
const n = Number(pricing.points_per_image);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
const unit = Number(config?.unit_price);
|
||||
return Number.isFinite(unit) && unit > 0 ? unit : null;
|
||||
}
|
||||
|
||||
export const MODE_LABELS: Record<FreeMode, string> = { universal: "全能参考", keyframe: "首尾帧" };
|
||||
|
||||
// 上传素材限制(与后端 FreeVideoUploadView / jimeng inputBar 对齐)
|
||||
@@ -117,28 +148,27 @@ export function tokenPrice(config: ModelConfig | undefined, resolution: string,
|
||||
export type BillingRates = { margin: number; rate: number; multiplier: number };
|
||||
export const DEFAULT_BILLING_RATES: BillingRates = { margin: 1.5, rate: 10, multiplier: 1 };
|
||||
|
||||
export function hasVideoPointsPricing(config: ModelConfig | undefined): boolean {
|
||||
const pricing = (config?.metadata?.points_pricing || {}) as Record<string, unknown>;
|
||||
const tiers = Array.isArray(pricing.tiers) ? pricing.tiers : [];
|
||||
return tiers.some((row) => row && typeof row === "object" && Number((row as { points_per_second?: number }).points_per_second) >= 0);
|
||||
}
|
||||
|
||||
/** 视频预估积分。只认后台 metadata.points_pricing 挂牌;没填挂牌返回 0(不拿旧 token×毛利糊弄用户)。 */
|
||||
export function estimateCost(
|
||||
config: ModelConfig | undefined,
|
||||
params: { ratio: string; resolution: string; duration: number; refs: { type: string; duration?: number }[] },
|
||||
billing: BillingRates = DEFAULT_BILLING_RATES
|
||||
): { tokens: number; points: number } {
|
||||
const inputVideoSeconds = params.refs
|
||||
.filter((r) => r.type === "video")
|
||||
.reduce((sum, r) => sum + (r.duration || 0), 0);
|
||||
const tokens = estimateTokens(params.ratio, params.resolution, params.duration, inputVideoSeconds);
|
||||
): { tokens: number; points: number; listed: boolean } {
|
||||
const hasVideoRef = params.refs.some((r) => r.type === "video");
|
||||
const price = tokenPrice(config, params.resolution, hasVideoRef);
|
||||
const costYuan = Math.round(((tokens * price) / 1e6) * 100) / 100;
|
||||
if (costYuan <= 0) return { tokens, points: 0 };
|
||||
// 浮点乘积在 .5 边界可能落成 27.499999…,先吸附 6 位小数再 round,
|
||||
// 与后端 Decimal ROUND_HALF_UP 逐字对齐。只吸浮点噪声(1e-9 级),
|
||||
// 不能用 toFixed(2):非默认 margin 下 x.495 会被先四舍五入成 x.50 多显 1 积分(review 确认)
|
||||
const raw = Number((costYuan * billing.margin * billing.rate).toFixed(6));
|
||||
const listPoints = Math.max(1, Math.round(raw));
|
||||
// 团队价格系数(差异化调价):两步取整与后端 apply_team_price 逐字对齐(先挂牌取整,再乘系数取整)
|
||||
const multiplier = billing.multiplier || 1;
|
||||
const points = multiplier === 1 ? listPoints : Math.max(1, Math.round(Number((listPoints * multiplier).toFixed(6))));
|
||||
return { tokens, points };
|
||||
const pps = pointsPerSecondFromCatalog(config, params.resolution, hasVideoRef);
|
||||
if (pps != null && params.duration > 0) {
|
||||
const listPoints = Math.max(1, Math.round(pps * params.duration));
|
||||
const multiplier = billing.multiplier || 1;
|
||||
const points = multiplier === 1 ? listPoints : Math.max(1, Math.round(Number((listPoints * multiplier).toFixed(6))));
|
||||
return { tokens: 0, points, listed: true };
|
||||
}
|
||||
return { tokens: 0, points: 0, listed: false };
|
||||
}
|
||||
|
||||
export function modelLabel(name: string): string {
|
||||
|
||||
@@ -59,7 +59,7 @@ function KeyframeSlot({ role, item, onPickLibrary, onRemove }: {
|
||||
);
|
||||
}
|
||||
|
||||
export function FreeInputBar({ mode, model, ratio, resolution, duration, seed, refs, videoConfigs, billingRates, submitting, promptRef, onFiles, onRemoveRef, onModeChange, onModelChange, onRatioChange, onResolutionChange, onDurationChange, onSeedChange, onOpenLibrary, onOpenPlatformLibrary, onClear, onSend }: {
|
||||
export function FreeInputBar({ mode, model, ratio, resolution, duration, seed, refs, videoConfigs, billingRates, submitting, promptRef, onFiles, onRemoveRef, onModeChange, onModelChange, onRatioChange, onResolutionChange, onDurationChange, onSeedChange, onOpenLibrary, onOpenPlatformLibrary, onSend }: {
|
||||
mode: FreeMode;
|
||||
model: string;
|
||||
ratio: string;
|
||||
@@ -82,7 +82,6 @@ export function FreeInputBar({ mode, model, ratio, resolution, duration, seed, r
|
||||
onSeedChange: (seed: number) => void;
|
||||
onOpenLibrary: (role?: "first_frame" | "last_frame") => void;
|
||||
onOpenPlatformLibrary: (role?: "first_frame" | "last_frame") => void;
|
||||
onClear: () => void;
|
||||
onSend: () => void;
|
||||
}) {
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
@@ -175,7 +174,6 @@ export function FreeInputBar({ mode, model, ratio, resolution, duration, seed, r
|
||||
onResolutionChange={onResolutionChange}
|
||||
onDurationChange={onDurationChange}
|
||||
onSeedChange={onSeedChange}
|
||||
onClear={onClear}
|
||||
onSend={onSend}
|
||||
/>
|
||||
{dragOver && <div className="fc-drop-hint">松开上传到「{MODE_LABELS[mode]}」</div>}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
// 自由创作·输入条工具栏:模型/模式/比例/分辨率/时长/种子 下拉 + 预估消耗 + 清空 + 生成。
|
||||
// 自由创作·输入条工具栏:模型/模式/比例/分辨率/时长/种子 下拉 + 预估积分 + 生成。
|
||||
// 模型下拉列后端返回的全部视频模型(FC_MODELS 只提供好看的标签)。
|
||||
// 约束联动(与后端校验一致):分辨率与时长档位取自所选模型的 metadata.capabilities,
|
||||
// 换档时把超出新模型能力的选择夹回去。
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ArrowRight, ChevronDown } from "lucide-react";
|
||||
import { ArrowRight, ChevronDown, Coins } from "lucide-react";
|
||||
import type { ModelConfig } from "../../types";
|
||||
import {
|
||||
DEFAULT_BILLING_RATES,
|
||||
FC_MODELS,
|
||||
FC_RATIOS,
|
||||
FC_RESOLUTIONS,
|
||||
MODE_LABELS,
|
||||
estimateCost,
|
||||
modelDurations,
|
||||
@@ -77,7 +76,7 @@ function FcDropdown({ label, display, items, onSelect, disabled }: {
|
||||
);
|
||||
}
|
||||
|
||||
export function FreeToolbar({ mode, model, ratio, resolution, duration, seed, refs, videoConfigs, billingRates, hasPrompt, submitting, onModeChange, onModelChange, onRatioChange, onResolutionChange, onDurationChange, onSeedChange, onClear, onSend }: {
|
||||
export function FreeToolbar({ mode, model, ratio, resolution, duration, seed, refs, videoConfigs, billingRates, hasPrompt, submitting, onModeChange, onModelChange, onRatioChange, onResolutionChange, onDurationChange, onSeedChange, onSend }: {
|
||||
mode: FreeMode;
|
||||
model: string;
|
||||
ratio: string;
|
||||
@@ -95,14 +94,13 @@ export function FreeToolbar({ mode, model, ratio, resolution, duration, seed, re
|
||||
onResolutionChange: (resolution: string) => void;
|
||||
onDurationChange: (duration: number) => void;
|
||||
onSeedChange: (seed: number) => void;
|
||||
onClear: () => void;
|
||||
onSend: () => void;
|
||||
}) {
|
||||
const config = videoConfigs.find((c) => c.name === model);
|
||||
// 可选的分辨率/时长按所选模型的能力来,不再写死 —— Seedance 2.5 能出 30 秒,2.0 只有 15。
|
||||
const allowedResolutions = modelResolutions(config);
|
||||
const allowedDurations = modelDurations(config);
|
||||
const { tokens, points } = estimateCost(config, { ratio, resolution, duration, refs }, billingRates || DEFAULT_BILLING_RATES);
|
||||
const { points } = estimateCost(config, { ratio, resolution, duration, refs }, billingRates || DEFAULT_BILLING_RATES);
|
||||
const [seedOpen, setSeedOpen] = useState(false);
|
||||
const seedRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
@@ -152,11 +150,9 @@ export function FreeToolbar({ mode, model, ratio, resolution, duration, seed, re
|
||||
<FcDropdown
|
||||
label="分辨率"
|
||||
display={resolution.toUpperCase()}
|
||||
items={FC_RESOLUTIONS.map((r) => ({
|
||||
items={allowedResolutions.map((r) => ({
|
||||
value: r,
|
||||
label: r.toUpperCase(),
|
||||
disabled: !allowedResolutions.includes(r),
|
||||
hint: "当前模型不支持这一档"
|
||||
}))}
|
||||
onSelect={onResolutionChange}
|
||||
/>
|
||||
@@ -191,10 +187,12 @@ export function FreeToolbar({ mode, model, ratio, resolution, duration, seed, re
|
||||
</div>
|
||||
</div>
|
||||
<div className="fc-toolbar-r">
|
||||
<span className="fc-estimate" title="预估消耗(实际按真实用量结算,多退少不补超)">
|
||||
≈ {tokens.toLocaleString()} tokens · {points.toLocaleString()} 积分
|
||||
</span>
|
||||
<button type="button" className="fc-clear" onClick={onClear}>清空</button>
|
||||
{points > 0 ? (
|
||||
<span className="fc-estimate" title="预估积分(实际按真实用量结算)">
|
||||
<Coins aria-hidden="true" />
|
||||
≈ {points.toLocaleString()} 积分
|
||||
</span>
|
||||
) : null}
|
||||
<button type="button" className="fc-gen" disabled={!canSend} onClick={onSend} title="Ctrl/Cmd + Enter">
|
||||
{submitting ? "提交中…" : uploading ? "素材上传中…" : "生成"}
|
||||
<ArrowRight />
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ChevronDown, SlidersHorizontal, Sparkles } from "lucide-react";
|
||||
import { CustomSelect } from "./custom-select";
|
||||
import { modelDurations, modelResolutions } from "./free-create/constants";
|
||||
import type { ModelConfig } from "../types";
|
||||
|
||||
/** 无目录时的回落清单(展示名,与会话 params.model 历史值兼容)。 */
|
||||
export const OMNI_VIDEO_MODELS = ["Seedance 2.5", "Seedance 2.0", "Seedance 2.0 Fast", "Seedance 2.0 Mini"];
|
||||
export const OMNI_IMAGE_MODELS = ["Seedream5.0", "YQ image2"];
|
||||
export const OMNI_RESOLUTIONS = ["1080p", "720p", "480p"];
|
||||
export const OMNI_RATIOS = ["9:16", "16:9", "1:1", "4:3", "3:4"];
|
||||
export const OMNI_VIDEO_DURATIONS = [
|
||||
"4 秒", "5 秒", "6 秒", "7 秒", "8 秒", "9 秒", "10 秒",
|
||||
"智能时长", "4 秒", "5 秒", "6 秒", "7 秒", "8 秒", "9 秒", "10 秒",
|
||||
"11 秒", "12 秒", "13 秒", "14 秒", "15 秒", "30 秒",
|
||||
];
|
||||
export const OMNI_IMAGE_COUNTS = ["1 张", "2 张", "4 张", "8 张"];
|
||||
@@ -20,6 +23,59 @@ function withCurrent(values: string[], current: string) {
|
||||
return current && !values.includes(current) ? [current, ...values] : values;
|
||||
}
|
||||
|
||||
function modelOptionLabel(config: ModelConfig): string {
|
||||
return (config.display_name || config.name || "").trim();
|
||||
}
|
||||
|
||||
/** 忽略空格/横杠/大小写,兼容旧会话「Seedance 2.0 Mini」对上后台「Seedance-2.0-Mini」。 */
|
||||
function normalizeModelKey(value: string): string {
|
||||
return String(value || "").toLowerCase().replace(/[\s_\-·.]+/g, "");
|
||||
}
|
||||
|
||||
/** 会话里存的是展示名;用 display_name / name 都能对上目录项。 */
|
||||
export function findCatalogModel(
|
||||
configs: ModelConfig[] | undefined,
|
||||
label: string,
|
||||
capability: "video" | "image",
|
||||
): ModelConfig | undefined {
|
||||
const list = (configs || []).filter((c) => c.capability === capability && c.status !== "disabled");
|
||||
const key = (label || "").trim();
|
||||
if (!key) return list[0];
|
||||
const norm = normalizeModelKey(key);
|
||||
return (
|
||||
list.find((c) => modelOptionLabel(c) === key)
|
||||
|| list.find((c) => c.name === key)
|
||||
|| list.find((c) => normalizeModelKey(modelOptionLabel(c)) === norm)
|
||||
|| list.find((c) => normalizeModelKey(c.name) === norm)
|
||||
|| list.find((c) => {
|
||||
const dn = normalizeModelKey(c.display_name || "");
|
||||
const nm = normalizeModelKey(c.name || "");
|
||||
return (dn && (dn.includes(norm) || norm.includes(dn))) || (nm && (nm.includes(norm) || norm.includes(nm)));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function catalogModelLabels(
|
||||
configs: ModelConfig[] | undefined,
|
||||
capability: "video" | "image",
|
||||
fallback: string[],
|
||||
): string[] {
|
||||
const labels = (configs || [])
|
||||
.filter((c) => c.capability === capability)
|
||||
.map(modelOptionLabel)
|
||||
.filter(Boolean);
|
||||
const seen = new Set<string>();
|
||||
const unique = labels.filter((x) => (seen.has(x) ? false : (seen.add(x), true)));
|
||||
return unique.length ? unique : fallback;
|
||||
}
|
||||
|
||||
function durationLabelsForModel(config: ModelConfig | undefined, isVideo: boolean): string[] {
|
||||
if (!isVideo) return [...OMNI_IMAGE_COUNTS];
|
||||
const seconds = modelDurations(config);
|
||||
if (!seconds.length) return [...OMNI_VIDEO_DURATIONS];
|
||||
return ["智能时长", ...seconds.map((n) => `${n} 秒`)];
|
||||
}
|
||||
|
||||
export function OmniParamBar({
|
||||
isVideo,
|
||||
disabled,
|
||||
@@ -27,6 +83,7 @@ export function OmniParamBar({
|
||||
resolution,
|
||||
ratio,
|
||||
duration,
|
||||
catalogModels,
|
||||
onModel,
|
||||
onResolution,
|
||||
onRatio,
|
||||
@@ -38,6 +95,7 @@ export function OmniParamBar({
|
||||
resolution: string;
|
||||
ratio: string;
|
||||
duration: string;
|
||||
catalogModels?: ModelConfig[];
|
||||
onModel: (value: string) => void;
|
||||
onResolution: (value: string) => void;
|
||||
onRatio: (value: string) => void;
|
||||
@@ -47,6 +105,20 @@ export function OmniParamBar({
|
||||
const [customOn, setCustomOn] = useState(isVideo ? duration !== "智能时长" : true);
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const capability = isVideo ? "video" as const : "image" as const;
|
||||
const selected = useMemo(
|
||||
() => findCatalogModel(catalogModels, model, capability),
|
||||
[catalogModels, model, capability],
|
||||
);
|
||||
|
||||
const models = withCurrent(
|
||||
catalogModelLabels(catalogModels, capability, isVideo ? OMNI_VIDEO_MODELS : OMNI_IMAGE_MODELS),
|
||||
model,
|
||||
);
|
||||
const allowedRes = isVideo ? modelResolutions(selected) : [];
|
||||
const resolutions = withCurrent(allowedRes.length ? allowedRes : OMNI_RESOLUTIONS, resolution);
|
||||
const durations = withCurrent(durationLabelsForModel(selected, isVideo), duration);
|
||||
|
||||
useEffect(() => {
|
||||
setCustomOn(isVideo ? duration !== "智能时长" : true);
|
||||
}, [isVideo, duration]);
|
||||
@@ -60,8 +132,18 @@ export function OmniParamBar({
|
||||
return () => document.removeEventListener("mousedown", onDown);
|
||||
}, [menuOpen]);
|
||||
|
||||
const models = withCurrent(isVideo ? OMNI_VIDEO_MODELS : OMNI_IMAGE_MODELS, model);
|
||||
const durations = isVideo ? OMNI_VIDEO_DURATIONS : OMNI_IMAGE_COUNTS;
|
||||
useEffect(() => {
|
||||
if (!selected || !isVideo) return;
|
||||
const nextRes = modelResolutions(selected);
|
||||
if (nextRes.length && resolution && !nextRes.includes(resolution)) {
|
||||
onResolution(nextRes.includes("720p") ? "720p" : nextRes[0]);
|
||||
}
|
||||
const nextDur = durationLabelsForModel(selected, true);
|
||||
if (duration && duration !== "智能时长" && !nextDur.includes(duration)) {
|
||||
onDuration(nextDur.includes("8 秒") ? "8 秒" : (nextDur.find((x) => x !== "智能时长") || "智能时长"));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selected?.id, isVideo]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -84,7 +166,7 @@ export function OmniParamBar({
|
||||
disabled={disabled}
|
||||
value={resolution}
|
||||
onChange={onResolution}
|
||||
options={toOptions(withCurrent(OMNI_RESOLUTIONS, resolution))}
|
||||
options={toOptions(resolutions)}
|
||||
/>
|
||||
</label>
|
||||
<label className="omni-parameter">
|
||||
@@ -136,7 +218,7 @@ export function OmniParamBar({
|
||||
</div>
|
||||
) : null}
|
||||
<div className="omni-duration-values" hidden={isVideo && !customOn}>
|
||||
{durations.map((value) => (
|
||||
{(isVideo ? durations.filter((value) => value !== "智能时长") : durations).map((value) => (
|
||||
<button
|
||||
type="button"
|
||||
key={value}
|
||||
|
||||
Reference in New Issue
Block a user