feat: customize style pro button colors
This commit is contained in:
parent
0869eefee3
commit
c433b714cd
@ -106,6 +106,47 @@ function modelLabel(model: AiModelOption) {
|
||||
return `${name}${id}${model.isHidden ? " · 前端隐藏" : ""}`;
|
||||
}
|
||||
|
||||
function ColorSettingField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const shortHex = value.match(/^#([0-9a-fA-F]{3})$/);
|
||||
const pickerValue = /^#[0-9a-fA-F]{6}$/.test(value)
|
||||
? value
|
||||
: shortHex
|
||||
? `#${shortHex[1]
|
||||
.split("")
|
||||
.map((char) => `${char}${char}`)
|
||||
.join("")}`
|
||||
: "#000000";
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label>{label}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
className="h-10 w-14 shrink-0 cursor-pointer p-1"
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
type="color"
|
||||
value={pickerValue}
|
||||
/>
|
||||
<Input
|
||||
className="font-mono"
|
||||
maxLength={7}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder="#2563eb"
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StyleProSettingsClient() {
|
||||
const { data, mutate } = useSWR<SettingsResponse>(
|
||||
`/api/admin/settings?key=${STYLE_PRO_CONFIG_KEY}`,
|
||||
@ -135,11 +176,6 @@ export function StyleProSettingsClient() {
|
||||
allImageModels.filter((model) => model.isHidden !== true),
|
||||
[allImageModels]
|
||||
);
|
||||
const hiddenImageModels = useMemo(
|
||||
() => allImageModels.filter((model) => model.isHidden === true),
|
||||
[allImageModels]
|
||||
);
|
||||
|
||||
const updateConfig = (updates: Partial<StyleProConfig>) => {
|
||||
setConfig((current) => ({ ...current, ...updates }));
|
||||
};
|
||||
@ -304,7 +340,7 @@ export function StyleProSettingsClient() {
|
||||
<CardHeader>
|
||||
<CardTitle>基础设置</CardTitle>
|
||||
<CardDescription>
|
||||
控制功能开关、风格化公开模型、实景化隐藏模型、默认提示词和生成规格。
|
||||
控制功能开关、风格化公开模型、实景化模型链路、默认提示词和生成规格。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-5 md:grid-cols-2">
|
||||
@ -356,9 +392,9 @@ export function StyleProSettingsClient() {
|
||||
|
||||
<div className="space-y-4 rounded-xl border p-4 md:col-span-2">
|
||||
<div>
|
||||
<Label>实景化隐藏模型链路</Label>
|
||||
<Label>实景化模型链路</Label>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
前台只显示“实景化”模式,不展示模型名称;这里仅可选择已在 AI 模型中设置为“前端隐藏”的图片模型。第一阶段失败时第二阶段不会继续执行。
|
||||
前台只显示“实景化”模式,不展示模型名称;这里可选择所有已启用的生图模型。第一阶段失败时第二阶段不会继续执行。
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
@ -379,16 +415,16 @@ export function StyleProSettingsClient() {
|
||||
<SelectContent>
|
||||
<SelectItem value={AUTO_MODEL_VALUE}>未配置</SelectItem>
|
||||
{config.realisticFirstModelId &&
|
||||
!hiddenImageModels.some(
|
||||
!allImageModels.some(
|
||||
(model) =>
|
||||
model.modelId === config.realisticFirstModelId ||
|
||||
model.id === config.realisticFirstModelId
|
||||
) ? (
|
||||
<SelectItem value={config.realisticFirstModelId}>
|
||||
当前保存值(未在隐藏模型列表中):{config.realisticFirstModelId}
|
||||
当前保存值(未在模型列表中):{config.realisticFirstModelId}
|
||||
</SelectItem>
|
||||
) : null}
|
||||
{hiddenImageModels.map((model) => {
|
||||
{allImageModels.map((model) => {
|
||||
const value = model.modelId || model.id;
|
||||
return (
|
||||
<SelectItem key={model.id} value={value}>
|
||||
@ -416,16 +452,16 @@ export function StyleProSettingsClient() {
|
||||
<SelectContent>
|
||||
<SelectItem value={AUTO_MODEL_VALUE}>未配置</SelectItem>
|
||||
{config.realisticSecondModelId &&
|
||||
!hiddenImageModels.some(
|
||||
!allImageModels.some(
|
||||
(model) =>
|
||||
model.modelId === config.realisticSecondModelId ||
|
||||
model.id === config.realisticSecondModelId
|
||||
) ? (
|
||||
<SelectItem value={config.realisticSecondModelId}>
|
||||
当前保存值(未在隐藏模型列表中):{config.realisticSecondModelId}
|
||||
当前保存值(未在模型列表中):{config.realisticSecondModelId}
|
||||
</SelectItem>
|
||||
) : null}
|
||||
{hiddenImageModels.map((model) => {
|
||||
{allImageModels.map((model) => {
|
||||
const value = model.modelId || model.id;
|
||||
return (
|
||||
<SelectItem key={model.id} value={value}>
|
||||
@ -523,6 +559,50 @@ export function StyleProSettingsClient() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 rounded-xl border p-4 md:col-span-2">
|
||||
<div>
|
||||
<Label>前台颜色</Label>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
控制风格化 PRO 页面里的分类标题、提示词按钮和选中按钮颜色,保存后前台立即按配置渲染。
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
<ColorSettingField
|
||||
label="标题文字颜色"
|
||||
onChange={(titleTextColor) => updateConfig({ titleTextColor })}
|
||||
value={config.titleTextColor}
|
||||
/>
|
||||
<ColorSettingField
|
||||
label="按钮颜色"
|
||||
onChange={(buttonBackgroundColor) =>
|
||||
updateConfig({ buttonBackgroundColor })
|
||||
}
|
||||
value={config.buttonBackgroundColor}
|
||||
/>
|
||||
<ColorSettingField
|
||||
label="按钮字体颜色"
|
||||
onChange={(buttonTextColor) =>
|
||||
updateConfig({ buttonTextColor })
|
||||
}
|
||||
value={config.buttonTextColor}
|
||||
/>
|
||||
<ColorSettingField
|
||||
label="选中按钮颜色"
|
||||
onChange={(selectedButtonBackgroundColor) =>
|
||||
updateConfig({ selectedButtonBackgroundColor })
|
||||
}
|
||||
value={config.selectedButtonBackgroundColor}
|
||||
/>
|
||||
<ColorSettingField
|
||||
label="选中按钮字体颜色"
|
||||
onChange={(selectedButtonTextColor) =>
|
||||
updateConfig({ selectedButtonTextColor })
|
||||
}
|
||||
value={config.selectedButtonTextColor}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label>默认系统提示词</Label>
|
||||
<Textarea
|
||||
|
||||
@ -18,11 +18,11 @@ export async function GET() {
|
||||
() => null
|
||||
);
|
||||
const config = normalizeStyleProConfig(setting?.value);
|
||||
const hiddenEntries = (
|
||||
const realisticModelEntries = (
|
||||
await getStyleProImageModelEntries({ includeHidden: true }).catch(
|
||||
() => []
|
||||
)
|
||||
).filter((entry) => entry.model.isHidden === true);
|
||||
);
|
||||
const publicConfig = toPublicStyleProConfig(config);
|
||||
|
||||
return NextResponse.json({
|
||||
@ -31,11 +31,11 @@ export async function GET() {
|
||||
...publicConfig,
|
||||
realisticModeAvailable: Boolean(
|
||||
findStyleProImageModelByRef(
|
||||
hiddenEntries,
|
||||
realisticModelEntries,
|
||||
config.realisticFirstModelId
|
||||
) &&
|
||||
findStyleProImageModelByRef(
|
||||
hiddenEntries,
|
||||
realisticModelEntries,
|
||||
config.realisticSecondModelId
|
||||
)
|
||||
),
|
||||
|
||||
@ -202,13 +202,9 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
const imageModelEntries = (
|
||||
await getStyleProImageModelEntries({
|
||||
includeHidden: generationMode === "realistic",
|
||||
})
|
||||
).filter((entry) =>
|
||||
generationMode === "realistic" ? entry.model.isHidden === true : true
|
||||
);
|
||||
const imageModelEntries = await getStyleProImageModelEntries({
|
||||
includeHidden: generationMode === "realistic",
|
||||
});
|
||||
|
||||
const selectedModels =
|
||||
generationMode === "realistic"
|
||||
|
||||
@ -24,14 +24,11 @@ export async function GET() {
|
||||
);
|
||||
}
|
||||
|
||||
const [entries, allHiddenCapableEntries, setting] = await Promise.all([
|
||||
const [entries, realisticModelEntries, setting] = await Promise.all([
|
||||
getStyleProImageModelEntries({ includeHidden: false }),
|
||||
getStyleProImageModelEntries({ includeHidden: true }),
|
||||
getSystemSetting({ key: STYLE_PRO_CONFIG_KEY }).catch(() => null),
|
||||
]);
|
||||
const hiddenEntries = allHiddenCapableEntries.filter(
|
||||
(entry) => entry.model.isHidden === true
|
||||
);
|
||||
const config = normalizeStyleProConfig(setting?.value);
|
||||
const configuredDefault = findStyleProImageModelByRef(
|
||||
entries,
|
||||
@ -43,11 +40,11 @@ export async function GET() {
|
||||
entries[0] ||
|
||||
null;
|
||||
const realisticFirstModel = findStyleProImageModelByRef(
|
||||
hiddenEntries,
|
||||
realisticModelEntries,
|
||||
config.realisticFirstModelId
|
||||
);
|
||||
const realisticSecondModel = findStyleProImageModelByRef(
|
||||
hiddenEntries,
|
||||
realisticModelEntries,
|
||||
config.realisticSecondModelId
|
||||
);
|
||||
const realisticUnitCost =
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { Clock3, Loader2, LogOut, Sparkles, Upload } from "lucide-react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import {
|
||||
type CSSProperties,
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
useCallback,
|
||||
@ -1014,6 +1015,15 @@ export function StyleProWorkspace({
|
||||
? `还差:${missingRequiredCategories[0].title}`
|
||||
: "";
|
||||
const promptDisplayMode = config?.promptDisplayMode || "stacked";
|
||||
const themeStyle = {
|
||||
"--style-pro-title-color": config?.titleTextColor || "#2563eb",
|
||||
"--style-pro-button-bg": config?.buttonBackgroundColor || "#ffffff",
|
||||
"--style-pro-button-text": config?.buttonTextColor || "#374151",
|
||||
"--style-pro-selected-button-bg":
|
||||
config?.selectedButtonBackgroundColor || "#eff6ff",
|
||||
"--style-pro-selected-button-text":
|
||||
config?.selectedButtonTextColor || "#2563eb",
|
||||
} as CSSProperties & Record<string, string>;
|
||||
const renderPromptCategory = (
|
||||
category: PublicStyleProCategory,
|
||||
layout: "stacked" | "carousel" = "stacked"
|
||||
@ -1057,7 +1067,10 @@ export function StyleProWorkspace({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="style-pro-index-shell flex h-dvh w-full overflow-hidden bg-[#f4f4f4] text-[#171717]">
|
||||
<div
|
||||
className="style-pro-index-shell flex h-dvh w-full overflow-hidden bg-[#f4f4f4] text-[#171717]"
|
||||
style={themeStyle}
|
||||
>
|
||||
<style>{`
|
||||
.style-pro-index-shell .section-card {
|
||||
background: #fff;
|
||||
@ -1066,27 +1079,27 @@ export function StyleProWorkspace({
|
||||
box-shadow: 0 12px 35px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
.style-pro-index-shell .subsection-title {
|
||||
color: #2563eb;
|
||||
color: var(--style-pro-title-color);
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.style-pro-index-shell .option-btn {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
background: #fff;
|
||||
color: rgba(17, 24, 39, 0.78);
|
||||
background: var(--style-pro-button-bg);
|
||||
color: var(--style-pro-button-text);
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
.style-pro-index-shell .option-btn:hover {
|
||||
background: #1c1c1e;
|
||||
border-color: #1c1c1e;
|
||||
color: #fff;
|
||||
background: var(--style-pro-selected-button-bg);
|
||||
border-color: var(--style-pro-selected-button-text);
|
||||
color: var(--style-pro-selected-button-text);
|
||||
}
|
||||
.style-pro-index-shell .option-btn.active {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(37, 99, 235, 0.05));
|
||||
color: #2563eb;
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||
background: var(--style-pro-selected-button-bg);
|
||||
color: var(--style-pro-selected-button-text);
|
||||
border-color: var(--style-pro-selected-button-text);
|
||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
.style-pro-index-shell .prompt-carousel-scroll {
|
||||
scrollbar-width: thin;
|
||||
|
||||
@ -32,6 +32,11 @@ export type StyleProConfig = {
|
||||
realisticSecondModelId: string;
|
||||
realisticFirstPrompt: string;
|
||||
realisticSecondPrompt: string;
|
||||
titleTextColor: string;
|
||||
buttonBackgroundColor: string;
|
||||
buttonTextColor: string;
|
||||
selectedButtonBackgroundColor: string;
|
||||
selectedButtonTextColor: string;
|
||||
defaultPrompt: string;
|
||||
negativePrompt: string;
|
||||
aspectRatio: string;
|
||||
@ -436,6 +441,11 @@ export const defaultStyleProConfig: StyleProConfig = {
|
||||
realisticSecondModelId: "",
|
||||
realisticFirstPrompt: DEFAULT_REALISTIC_FIRST_PROMPT,
|
||||
realisticSecondPrompt: DEFAULT_REALISTIC_SECOND_PROMPT,
|
||||
titleTextColor: "#2563eb",
|
||||
buttonBackgroundColor: "#ffffff",
|
||||
buttonTextColor: "#374151",
|
||||
selectedButtonBackgroundColor: "#eff6ff",
|
||||
selectedButtonTextColor: "#2563eb",
|
||||
defaultPrompt: DEFAULT_PROMPT,
|
||||
negativePrompt: "低清晰度,畸变,文字乱码,主体变形,产品结构错误,多余物体,水印",
|
||||
aspectRatio: "auto",
|
||||
@ -455,6 +465,11 @@ function cleanString(value: unknown, fallback = "", maxLength = 4000) {
|
||||
return (text || fallback).slice(0, maxLength);
|
||||
}
|
||||
|
||||
function cleanHexColor(value: unknown, fallback: string) {
|
||||
const text = cleanString(value, fallback, 32);
|
||||
return /^#(?:[0-9a-fA-F]{3}){1,2}$/.test(text) ? text : fallback;
|
||||
}
|
||||
|
||||
function cleanId(value: unknown, fallback: string) {
|
||||
const text = cleanString(value, fallback, 80)
|
||||
.toLowerCase()
|
||||
@ -563,6 +578,23 @@ export function normalizeStyleProConfig(value: unknown): StyleProConfig {
|
||||
fallback.realisticSecondPrompt,
|
||||
8000
|
||||
),
|
||||
titleTextColor: cleanHexColor(raw.titleTextColor, fallback.titleTextColor),
|
||||
buttonBackgroundColor: cleanHexColor(
|
||||
raw.buttonBackgroundColor,
|
||||
fallback.buttonBackgroundColor
|
||||
),
|
||||
buttonTextColor: cleanHexColor(
|
||||
raw.buttonTextColor,
|
||||
fallback.buttonTextColor
|
||||
),
|
||||
selectedButtonBackgroundColor: cleanHexColor(
|
||||
raw.selectedButtonBackgroundColor,
|
||||
fallback.selectedButtonBackgroundColor
|
||||
),
|
||||
selectedButtonTextColor: cleanHexColor(
|
||||
raw.selectedButtonTextColor,
|
||||
fallback.selectedButtonTextColor
|
||||
),
|
||||
defaultPrompt: cleanString(raw.defaultPrompt, fallback.defaultPrompt, 8000),
|
||||
negativePrompt: cleanString(raw.negativePrompt, fallback.negativePrompt, 4000),
|
||||
aspectRatio: cleanString(raw.aspectRatio, fallback.aspectRatio, 32),
|
||||
@ -637,6 +669,11 @@ export function toPublicStyleProConfig(
|
||||
aspectRatio: config.aspectRatio,
|
||||
imageSize: config.imageSize,
|
||||
promptDisplayMode: config.promptDisplayMode,
|
||||
titleTextColor: config.titleTextColor,
|
||||
buttonBackgroundColor: config.buttonBackgroundColor,
|
||||
buttonTextColor: config.buttonTextColor,
|
||||
selectedButtonBackgroundColor: config.selectedButtonBackgroundColor,
|
||||
selectedButtonTextColor: config.selectedButtonTextColor,
|
||||
realisticModeAvailable: Boolean(
|
||||
config.realisticFirstModelId && config.realisticSecondModelId
|
||||
),
|
||||
|
||||
@ -5,23 +5,8 @@ import {
|
||||
getImageCreditCostFromConfig,
|
||||
getImageModelCreditDiscountLabel,
|
||||
} from "@/lib/ai/image-model-pricing";
|
||||
import { isImageGenerationModelDescriptor } from "@/lib/ai/model-classification";
|
||||
import { getEnabledAiModelsWithProvider } from "@/lib/db/queries";
|
||||
|
||||
const STYLE_PRO_SUPPORTED_PROVIDER_TYPES = new Set([
|
||||
"gemini",
|
||||
"grsai-gpt-image",
|
||||
"alibaba-image",
|
||||
"bfl-image",
|
||||
"ruxa-image",
|
||||
"fal-image",
|
||||
"openai-image",
|
||||
"stepfun-image",
|
||||
"midjourney",
|
||||
"runway",
|
||||
"krea",
|
||||
]);
|
||||
|
||||
export type StyleProModelEntry = Awaited<
|
||||
ReturnType<typeof getEnabledAiModelsWithProvider>
|
||||
>[number];
|
||||
@ -39,15 +24,7 @@ export type PublicStyleProModelOption = {
|
||||
};
|
||||
|
||||
export function isStyleProImageModel(entry: StyleProModelEntry) {
|
||||
const { model, provider } = entry;
|
||||
if (!model.supportsImageGen) return false;
|
||||
if (!STYLE_PRO_SUPPORTED_PROVIDER_TYPES.has(provider.type)) return false;
|
||||
return isImageGenerationModelDescriptor({
|
||||
providerType: provider.type,
|
||||
modelId: model.modelId,
|
||||
capabilities: model.capabilities,
|
||||
supportsImageGen: model.supportsImageGen,
|
||||
});
|
||||
return Boolean(entry.model.supportsImageGen);
|
||||
}
|
||||
|
||||
export async function getStyleProImageModelEntries(options?: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user