feat(home): add contact config modal and prompt tag toggles
This commit is contained in:
parent
e4315282a2
commit
30e943d787
@ -198,6 +198,8 @@ type SiteSettings = {
|
||||
canvasAutoModeDefaultModelId: string | null;
|
||||
lovartComposerPrefix: string;
|
||||
lovartComposerPreviewTexts: string[];
|
||||
lovartPromptTagVisibility: LovartPromptTagVisibility;
|
||||
contactConfig: ContactConfig;
|
||||
pointsSystemConfig: PointsSystemConfig;
|
||||
pointsPackages: PointsPackage[];
|
||||
dulupayConfig: DulupayConfig;
|
||||
@ -211,6 +213,46 @@ type SiteSettings = {
|
||||
membershipUserOverrides: ReturnType<typeof normalizeMembershipUserOverrides>;
|
||||
};
|
||||
|
||||
type LovartPromptTagVisibility = {
|
||||
nanoBananaPro: boolean;
|
||||
design: boolean;
|
||||
branding: boolean;
|
||||
illustration: boolean;
|
||||
ecommerce: boolean;
|
||||
video: boolean;
|
||||
};
|
||||
|
||||
type ContactConfig = {
|
||||
phone: string;
|
||||
wechatQrUrl: string;
|
||||
};
|
||||
|
||||
const defaultLovartPromptTagVisibility: LovartPromptTagVisibility = {
|
||||
nanoBananaPro: true,
|
||||
design: true,
|
||||
branding: true,
|
||||
illustration: true,
|
||||
ecommerce: true,
|
||||
video: true,
|
||||
};
|
||||
|
||||
const defaultContactConfig: ContactConfig = {
|
||||
phone: "",
|
||||
wechatQrUrl: "",
|
||||
};
|
||||
|
||||
const lovartPromptTagOptions: Array<{
|
||||
key: keyof LovartPromptTagVisibility;
|
||||
label: string;
|
||||
}> = [
|
||||
{ key: "nanoBananaPro", label: "Nano Banana Pro" },
|
||||
{ key: "design", label: "Design" },
|
||||
{ key: "branding", label: "Branding" },
|
||||
{ key: "illustration", label: "Illustration" },
|
||||
{ key: "ecommerce", label: "E-Commerce" },
|
||||
{ key: "video", label: "Video" },
|
||||
];
|
||||
|
||||
type NavLink = {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -442,6 +484,8 @@ export function AdminSettingsClient() {
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
"基于参考图延展 3 套同风格方案",
|
||||
],
|
||||
lovartPromptTagVisibility: defaultLovartPromptTagVisibility,
|
||||
contactConfig: defaultContactConfig,
|
||||
pointsSystemConfig: defaultPointsSystemConfig,
|
||||
pointsPackages: defaultPointsPackages,
|
||||
dulupayConfig: defaultDulupayConfig,
|
||||
@ -513,6 +557,20 @@ export function AdminSettingsClient() {
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
"基于参考图延展 3 套同风格方案",
|
||||
],
|
||||
lovartPromptTagVisibility: {
|
||||
nanoBananaPro:
|
||||
(siteSettingsData as any).lovartPromptTagVisibility?.nanoBananaPro !== false,
|
||||
design: (siteSettingsData as any).lovartPromptTagVisibility?.design !== false,
|
||||
branding: (siteSettingsData as any).lovartPromptTagVisibility?.branding !== false,
|
||||
illustration:
|
||||
(siteSettingsData as any).lovartPromptTagVisibility?.illustration !== false,
|
||||
ecommerce: (siteSettingsData as any).lovartPromptTagVisibility?.ecommerce !== false,
|
||||
video: (siteSettingsData as any).lovartPromptTagVisibility?.video !== false,
|
||||
},
|
||||
contactConfig: {
|
||||
phone: String((siteSettingsData as any).contactConfig?.phone || "").trim(),
|
||||
wechatQrUrl: String((siteSettingsData as any).contactConfig?.wechatQrUrl || "").trim(),
|
||||
},
|
||||
pointsSystemConfig: normalizedPointsSystemConfig,
|
||||
pointsPackages: normalizedPointsPackages,
|
||||
dulupayConfig: normalizedDulupayConfig,
|
||||
@ -932,6 +990,7 @@ export function AdminSettingsClient() {
|
||||
const [showModelDialog, setShowModelDialog] = useState(false);
|
||||
const [editingModel, setEditingModel] = useState<string | null>(null);
|
||||
const [modelAvatarUploading, setModelAvatarUploading] = useState(false);
|
||||
const [contactQrUploading, setContactQrUploading] = useState(false);
|
||||
|
||||
// 🆕 火山引擎视频多模型表单(动态从API加载)
|
||||
const [volcanoVideoModels, setVolcanoVideoModels] = useState<Array<{
|
||||
@ -1862,6 +1921,42 @@ export function AdminSettingsClient() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleContactQrUpload = async (file: File) => {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
toast.error("请上传图片文件");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setContactQrUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await fetch("/api/files/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result?.url) {
|
||||
throw new Error(result?.error || "上传失败");
|
||||
}
|
||||
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
contactConfig: {
|
||||
...prev.contactConfig,
|
||||
wechatQrUrl: result.url,
|
||||
},
|
||||
}));
|
||||
toast.success("微信二维码上传成功");
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "微信二维码上传失败");
|
||||
} finally {
|
||||
setContactQrUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveModel = async () => {
|
||||
try {
|
||||
// 🆕 检查是否是火山引擎视频Provider
|
||||
@ -3168,6 +3263,113 @@ export function AdminSettingsClient() {
|
||||
仅配置动态尾文案;前端会固定显示前缀后再进行打字与删字切换。支持最多 20 条。
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 rounded-lg border p-3">
|
||||
<div className="text-sm font-medium">对话框下方标签按钮显示</div>
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
{lovartPromptTagOptions.map((item) => (
|
||||
<div key={item.key} className="flex items-center justify-between rounded-md border px-3 py-2">
|
||||
<Label htmlFor={`lovart-tag-${item.key}`}>{item.label}</Label>
|
||||
<Switch
|
||||
id={`lovart-tag-${item.key}`}
|
||||
checked={siteSettings.lovartPromptTagVisibility[item.key] !== false}
|
||||
onCheckedChange={(checked) =>
|
||||
setSiteSettings({
|
||||
...siteSettings,
|
||||
lovartPromptTagVisibility: {
|
||||
...siteSettings.lovartPromptTagVisibility,
|
||||
[item.key]: checked,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
关闭后对应标签按钮会在新版首页对话框下方隐藏。
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 rounded-lg border p-3">
|
||||
<div className="text-sm font-medium">联系我们配置</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contactPhone">联系电话</Label>
|
||||
<Input
|
||||
id="contactPhone"
|
||||
value={siteSettings.contactConfig.phone}
|
||||
onChange={(e) =>
|
||||
setSiteSettings({
|
||||
...siteSettings,
|
||||
contactConfig: {
|
||||
...siteSettings.contactConfig,
|
||||
phone: e.target.value.slice(0, 64),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="例如:400-888-8888"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contactWechatQrUrl">微信二维码 URL</Label>
|
||||
<Input
|
||||
id="contactWechatQrUrl"
|
||||
value={siteSettings.contactConfig.wechatQrUrl}
|
||||
onChange={(e) =>
|
||||
setSiteSettings({
|
||||
...siteSettings,
|
||||
contactConfig: {
|
||||
...siteSettings.contactConfig,
|
||||
wechatQrUrl: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="https://example.com/wechat-qr.png"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8"
|
||||
disabled={contactQrUploading}
|
||||
onClick={() => document.getElementById("contact-qr-upload")?.click()}
|
||||
>
|
||||
{contactQrUploading ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
上传中
|
||||
</>
|
||||
) : (
|
||||
"上传二维码"
|
||||
)}
|
||||
</Button>
|
||||
<input
|
||||
id="contact-qr-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={async (event) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
await handleContactQrUpload(file);
|
||||
}
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{siteSettings.contactConfig.wechatQrUrl ? (
|
||||
<div className="rounded-lg border p-2">
|
||||
<img
|
||||
src={siteSettings.contactConfig.wechatQrUrl}
|
||||
alt="微信二维码预览"
|
||||
className="h-28 w-28 rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
新版首页用户菜单点击“联系我们”后会弹出该二维码和联系电话。
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lovartPromoText">顶部升级提示文案</Label>
|
||||
<Input
|
||||
|
||||
@ -31,6 +31,8 @@ const HOME_SURPRISE_CATEGORIES_KEY = "home_surprise_categories";
|
||||
const AI_DETAIL_GENERATOR_ENABLED_KEY = "ai_detail_generator_enabled";
|
||||
const LOVART_COMPOSER_PREVIEW_TEXTS_KEY = "lovart_composer_preview_texts";
|
||||
const LOVART_COMPOSER_PREFIX_KEY = "lovart_composer_prefix";
|
||||
const LOVART_PROMPT_TAG_VISIBILITY_KEY = "lovart_prompt_tag_visibility";
|
||||
const CONTACT_CONFIG_KEY = "contact_config";
|
||||
const MEMBERSHIP_CONFIG_KEY = "membership_config";
|
||||
const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
|
||||
|
||||
@ -89,6 +91,47 @@ function normalizeLovartComposerPrefix(value: unknown): string {
|
||||
return normalized.slice(0, 40);
|
||||
}
|
||||
|
||||
function normalizeLovartPromptTagVisibility(value: unknown): {
|
||||
nanoBananaPro: boolean;
|
||||
design: boolean;
|
||||
branding: boolean;
|
||||
illustration: boolean;
|
||||
ecommerce: boolean;
|
||||
video: boolean;
|
||||
} {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
nanoBananaPro: raw.nanoBananaPro !== false,
|
||||
design: raw.design !== false,
|
||||
branding: raw.branding !== false,
|
||||
illustration: raw.illustration !== false,
|
||||
ecommerce: raw.ecommerce !== false,
|
||||
video: raw.video !== false,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeContactConfig(value: unknown): {
|
||||
phone: string;
|
||||
wechatQrUrl: string;
|
||||
} {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const phone = String(raw.phone || "").trim().slice(0, 64);
|
||||
const wechatQrUrl = String(raw.wechatQrUrl || "").trim().slice(0, 1000);
|
||||
|
||||
return {
|
||||
phone,
|
||||
wechatQrUrl,
|
||||
};
|
||||
}
|
||||
|
||||
// GET /api/site-settings - 获取网站设置 (公开)
|
||||
export async function GET() {
|
||||
try {
|
||||
@ -102,6 +145,8 @@ export async function GET() {
|
||||
aiDetailGeneratorEnabledSetting,
|
||||
lovartComposerPreviewTextsSetting,
|
||||
lovartComposerPrefixSetting,
|
||||
lovartPromptTagVisibilitySetting,
|
||||
contactConfigSetting,
|
||||
membershipConfigSetting,
|
||||
membershipOverridesSetting,
|
||||
pointsSystemConfigSetting,
|
||||
@ -145,6 +190,18 @@ export async function GET() {
|
||||
.where(eq(systemSettings.key, LOVART_COMPOSER_PREFIX_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, LOVART_PROMPT_TAG_VISIBILITY_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, CONTACT_CONFIG_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
@ -187,6 +244,10 @@ export async function GET() {
|
||||
const lovartComposerPrefix = normalizeLovartComposerPrefix(
|
||||
lovartComposerPrefixSetting?.value
|
||||
);
|
||||
const lovartPromptTagVisibility = normalizeLovartPromptTagVisibility(
|
||||
lovartPromptTagVisibilitySetting?.value
|
||||
);
|
||||
const contactConfig = normalizeContactConfig(contactConfigSetting?.value);
|
||||
const membershipConfig = normalizeMembershipConfig(membershipConfigSetting?.value);
|
||||
const membershipUserOverrides = normalizeMembershipUserOverrides(
|
||||
membershipOverridesSetting?.value
|
||||
@ -233,6 +294,8 @@ export async function GET() {
|
||||
aiDetailGenerationEnabled,
|
||||
lovartComposerPrefix,
|
||||
lovartComposerPreviewTexts,
|
||||
lovartPromptTagVisibility,
|
||||
contactConfig,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
pointsSystemConfig,
|
||||
@ -254,6 +317,8 @@ export async function GET() {
|
||||
aiDetailGenerationEnabled,
|
||||
lovartComposerPrefix,
|
||||
lovartComposerPreviewTexts,
|
||||
lovartPromptTagVisibility,
|
||||
contactConfig,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
pointsSystemConfig,
|
||||
@ -309,6 +374,22 @@ const updateSchema = z.object({
|
||||
surpriseCategories: z.array(z.string().trim().min(1).max(20)).max(12).optional(),
|
||||
lovartComposerPrefix: z.string().trim().min(1).max(40).optional(),
|
||||
lovartComposerPreviewTexts: z.array(z.string().trim().min(1).max(120)).max(20).optional(),
|
||||
lovartPromptTagVisibility: z
|
||||
.object({
|
||||
nanoBananaPro: z.boolean().optional(),
|
||||
design: z.boolean().optional(),
|
||||
branding: z.boolean().optional(),
|
||||
illustration: z.boolean().optional(),
|
||||
ecommerce: z.boolean().optional(),
|
||||
video: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
contactConfig: z
|
||||
.object({
|
||||
phone: z.string().max(64).optional(),
|
||||
wechatQrUrl: z.string().max(1000).optional(),
|
||||
})
|
||||
.optional(),
|
||||
membershipConfig: z.unknown().optional(),
|
||||
membershipUserOverrides: z.unknown().optional(),
|
||||
pointsSystemConfig: z.unknown().optional(),
|
||||
@ -337,6 +418,8 @@ export async function PUT(request: NextRequest) {
|
||||
aiDetailGenerationEnabled,
|
||||
lovartComposerPrefix,
|
||||
lovartComposerPreviewTexts,
|
||||
lovartPromptTagVisibility,
|
||||
contactConfig,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
pointsSystemConfig,
|
||||
@ -458,6 +541,46 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (lovartPromptTagVisibility !== undefined) {
|
||||
const normalized = normalizeLovartPromptTagVisibility(lovartPromptTagVisibility);
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: LOVART_PROMPT_TAG_VISIBILITY_KEY,
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (contactConfig !== undefined) {
|
||||
const normalized = normalizeContactConfig(contactConfig);
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: CONTACT_CONFIG_KEY,
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (membershipConfig !== undefined) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -574,6 +697,14 @@ export async function PUT(request: NextRequest) {
|
||||
lovartComposerPreviewTexts !== undefined
|
||||
? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts)
|
||||
: normalizeLovartComposerPreviewTexts(undefined),
|
||||
lovartPromptTagVisibility:
|
||||
lovartPromptTagVisibility !== undefined
|
||||
? normalizeLovartPromptTagVisibility(lovartPromptTagVisibility)
|
||||
: normalizeLovartPromptTagVisibility(undefined),
|
||||
contactConfig:
|
||||
contactConfig !== undefined
|
||||
? normalizeContactConfig(contactConfig)
|
||||
: normalizeContactConfig(undefined),
|
||||
membershipConfig:
|
||||
membershipConfig !== undefined
|
||||
? normalizeMembershipConfig(membershipConfig)
|
||||
@ -731,6 +862,46 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (lovartPromptTagVisibility !== undefined) {
|
||||
const normalized = normalizeLovartPromptTagVisibility(lovartPromptTagVisibility);
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: LOVART_PROMPT_TAG_VISIBILITY_KEY,
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (contactConfig !== undefined) {
|
||||
const normalized = normalizeContactConfig(contactConfig);
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: CONTACT_CONFIG_KEY,
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (membershipConfig !== undefined) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -847,6 +1018,14 @@ export async function PUT(request: NextRequest) {
|
||||
lovartComposerPreviewTexts !== undefined
|
||||
? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts)
|
||||
: normalizeLovartComposerPreviewTexts(undefined),
|
||||
lovartPromptTagVisibility:
|
||||
lovartPromptTagVisibility !== undefined
|
||||
? normalizeLovartPromptTagVisibility(lovartPromptTagVisibility)
|
||||
: normalizeLovartPromptTagVisibility(undefined),
|
||||
contactConfig:
|
||||
contactConfig !== undefined
|
||||
? normalizeContactConfig(contactConfig)
|
||||
: normalizeContactConfig(undefined),
|
||||
membershipConfig:
|
||||
membershipConfig !== undefined
|
||||
? normalizeMembershipConfig(membershipConfig)
|
||||
|
||||
@ -123,6 +123,11 @@ type PublicAuthSettings = {
|
||||
isGoogleConfigured?: boolean;
|
||||
};
|
||||
|
||||
type ContactConfig = {
|
||||
phone?: string;
|
||||
wechatQrUrl?: string;
|
||||
};
|
||||
|
||||
type MembershipSummaryResponse = {
|
||||
config: MembershipConfig;
|
||||
currentPlanId: string;
|
||||
@ -213,13 +218,16 @@ const DISCOVERY_DEFAULT_TABS = [
|
||||
];
|
||||
|
||||
const PROMPT_TAGS = [
|
||||
{ label: "Nano Banana Pro", icon: Sparkles, highlighted: true },
|
||||
{ label: "Design", icon: ImageIcon, highlighted: false },
|
||||
{ label: "Branding", icon: Star, highlighted: false },
|
||||
{ label: "Illustration", icon: Palette, highlighted: false },
|
||||
{ label: "E-Commerce", icon: Monitor, highlighted: false },
|
||||
{ label: "Video", icon: Clapperboard, highlighted: false },
|
||||
];
|
||||
{ key: "nanoBananaPro", label: "Nano Banana Pro", icon: Sparkles, highlighted: true },
|
||||
{ key: "design", label: "Design", icon: ImageIcon, highlighted: false },
|
||||
{ key: "branding", label: "Branding", icon: Star, highlighted: false },
|
||||
{ key: "illustration", label: "Illustration", icon: Palette, highlighted: false },
|
||||
{ key: "ecommerce", label: "E-Commerce", icon: Monitor, highlighted: false },
|
||||
{ key: "video", label: "Video", icon: Clapperboard, highlighted: false },
|
||||
] as const;
|
||||
|
||||
type LovartPromptTagKey = (typeof PROMPT_TAGS)[number]["key"];
|
||||
type LovartPromptTagVisibility = Record<LovartPromptTagKey, boolean>;
|
||||
|
||||
const MASONRY_HEIGHTS = [420, 290, 340, 470, 300, 360, 430, 320];
|
||||
const FEATURED_PAGE_SIZE = 24;
|
||||
@ -338,6 +346,7 @@ export function LovartHome({
|
||||
const [editorImageUrl, setEditorImageUrl] = useState("");
|
||||
const [tutorialDialogOpen, setTutorialDialogOpen] = useState(false);
|
||||
const [currentTutorialVideo, setCurrentTutorialVideo] = useState<TutorialVideo | null>(null);
|
||||
const [contactDialogOpen, setContactDialogOpen] = useState(false);
|
||||
const [typedPreviewText, setTypedPreviewText] = useState("");
|
||||
const [previewCursorVisible, setPreviewCursorVisible] = useState(true);
|
||||
const projectSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -380,6 +389,12 @@ export function LovartHome({
|
||||
|
||||
const { data: modelsData } = useSWR<{
|
||||
models: ModelOption[];
|
||||
agentModel?: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
avatarUrl?: string | null;
|
||||
providerType: string;
|
||||
} | null;
|
||||
}>(isAuthenticated ? "/api/models" : null, fetcher);
|
||||
|
||||
const { data: navLinksData } = useSWR<{ links: NavLink[] }>(
|
||||
@ -391,6 +406,8 @@ export function LovartHome({
|
||||
canvasDefaultModelId?: string | null;
|
||||
lovartComposerPrefix?: string | null;
|
||||
lovartComposerPreviewTexts?: string[];
|
||||
lovartPromptTagVisibility?: Partial<LovartPromptTagVisibility>;
|
||||
contactConfig?: ContactConfig;
|
||||
membershipConfig?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
@ -437,18 +454,21 @@ export function LovartHome({
|
||||
[modelsData?.models]
|
||||
);
|
||||
|
||||
const autoComposerModel = useMemo<ModelOption>(
|
||||
() => ({
|
||||
id: "auto",
|
||||
modelId: "auto",
|
||||
displayName: "Agent模式",
|
||||
providerType: "auto",
|
||||
supportsImageGen: true,
|
||||
avatarUrl: modelsData?.agentModel?.avatarUrl || null,
|
||||
}),
|
||||
[modelsData?.agentModel?.avatarUrl]
|
||||
);
|
||||
|
||||
const allComposerModels = useMemo<ModelOption[]>(
|
||||
() => [
|
||||
{
|
||||
id: "auto",
|
||||
modelId: "auto",
|
||||
displayName: "Agent模式",
|
||||
providerType: "auto",
|
||||
supportsImageGen: true,
|
||||
},
|
||||
...imageModels,
|
||||
],
|
||||
[imageModels]
|
||||
() => [autoComposerModel, ...imageModels],
|
||||
[autoComposerModel, imageModels]
|
||||
);
|
||||
|
||||
const composerModelValue = useMemo(() => {
|
||||
@ -516,6 +536,31 @@ export function LovartHome({
|
||||
.filter(Boolean);
|
||||
}, [composerFixedPrefix, composerPreviewTexts]);
|
||||
|
||||
const lovartPromptTagVisibility = useMemo<LovartPromptTagVisibility>(() => {
|
||||
const raw = siteSettingsData?.lovartPromptTagVisibility || {};
|
||||
return {
|
||||
nanoBananaPro: raw.nanoBananaPro !== false,
|
||||
design: raw.design !== false,
|
||||
branding: raw.branding !== false,
|
||||
illustration: raw.illustration !== false,
|
||||
ecommerce: raw.ecommerce !== false,
|
||||
video: raw.video !== false,
|
||||
};
|
||||
}, [siteSettingsData?.lovartPromptTagVisibility]);
|
||||
|
||||
const visiblePromptTags = useMemo(
|
||||
() => PROMPT_TAGS.filter((tag) => lovartPromptTagVisibility[tag.key]),
|
||||
[lovartPromptTagVisibility]
|
||||
);
|
||||
|
||||
const contactConfig = useMemo(
|
||||
() => ({
|
||||
phone: String(siteSettingsData?.contactConfig?.phone || "").trim(),
|
||||
wechatQrUrl: String(siteSettingsData?.contactConfig?.wechatQrUrl || "").trim(),
|
||||
}),
|
||||
[siteSettingsData?.contactConfig?.phone, siteSettingsData?.contactConfig?.wechatQrUrl]
|
||||
);
|
||||
|
||||
const loadedFeaturedImages = useMemo(() => {
|
||||
return (featuredPages || []).flatMap((page) => page.images || []);
|
||||
}, [featuredPages]);
|
||||
@ -1276,7 +1321,7 @@ export function LovartHome({
|
||||
<AccountMenuItem
|
||||
icon={Mail}
|
||||
label="联系我们"
|
||||
onClick={() => toast.info("联系我们后续开放")}
|
||||
onClick={() => setContactDialogOpen(true)}
|
||||
/>
|
||||
<AccountMenuItem
|
||||
icon={LogOut}
|
||||
@ -1615,7 +1660,7 @@ export function LovartHome({
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-[238px] max-h-[340px] overflow-hidden rounded-[18px] border border-[#cfd4dc] bg-[#f3f4f6] p-1.5 shadow-[0_8px_20px_rgba(15,23,42,0.13)]">
|
||||
<div className="max-h-[312px] space-y-0.5 overflow-y-auto pr-1">
|
||||
{[{ id: "auto", displayName: "Agent模式", avatarUrl: null }, ...imageModels].map((model) => (
|
||||
{allComposerModels.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
@ -1651,6 +1696,7 @@ export function LovartHome({
|
||||
onParamsChange={handleImageParamsChange}
|
||||
inline
|
||||
providerType={triggerComposerModel?.providerType}
|
||||
hideImageCount
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -1672,7 +1718,7 @@ export function LovartHome({
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center justify-center gap-1.5">
|
||||
{PROMPT_TAGS.map((tag) => (
|
||||
{visiblePromptTags.map((tag) => (
|
||||
<button
|
||||
key={tag.label}
|
||||
type="button"
|
||||
@ -1876,6 +1922,37 @@ export function LovartHome({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={contactDialogOpen} onOpenChange={setContactDialogOpen}>
|
||||
<DialogContent className="w-[92vw] max-w-[420px] rounded-[20px] border-black/10 bg-white p-0">
|
||||
<DialogHeader className="border-b border-black/10 px-5 py-4">
|
||||
<DialogTitle className="text-[16px] font-semibold text-black">联系我们</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 px-5 py-4">
|
||||
{contactConfig.wechatQrUrl ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<img
|
||||
src={contactConfig.wechatQrUrl}
|
||||
alt="微信二维码"
|
||||
className="h-52 w-52 rounded-xl border border-black/10 object-cover"
|
||||
/>
|
||||
<p className="text-[12px] text-black/50">微信扫码添加客服</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-dashed border-black/20 bg-black/[0.02] px-3 py-5 text-center text-[13px] text-black/45">
|
||||
暂未配置微信二维码
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-xl border border-black/10 bg-black/[0.02] px-3.5 py-3">
|
||||
<div className="text-[12px] text-black/50">联系电话</div>
|
||||
<div className="mt-1 text-[15px] font-medium text-black">
|
||||
{contactConfig.phone || "暂未配置联系电话"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{loginDialogOpen ? (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/85 p-4 backdrop-blur-[1px]"
|
||||
|
||||
@ -38,6 +38,7 @@ interface ImageParamsBarProps {
|
||||
onParamsChange?: (params: ImageGenerationParams) => void;
|
||||
inline?: boolean; // 内联模式:只显示参数按钮,不显示外层容器
|
||||
providerType?: string; // 模型类型:gemini, volcano-vision, alibaba-image, ruxa-image 等
|
||||
hideImageCount?: boolean; // 隐藏图片数量按钮(仅个别页面需要)
|
||||
// 🆕 图片生成模型特性配置(从模型配置中读取)
|
||||
imageGenConfig?: {
|
||||
maxImages?: number;
|
||||
@ -77,6 +78,7 @@ export function ImageParamsBar({
|
||||
onParamsChange,
|
||||
inline = false,
|
||||
providerType = "gemini", // 默认 gemini
|
||||
hideImageCount = false,
|
||||
imageGenConfig,
|
||||
}: ImageParamsBarProps) {
|
||||
const getImageCountOptions = () => {
|
||||
@ -222,27 +224,28 @@ export function ImageParamsBar({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* 图片数量 */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0">
|
||||
<Images size={14} />
|
||||
<span className="font-medium text-xs">{params.imageCount || 1}张</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" className="min-w-[80px]">
|
||||
{imageCountOptions.map((count) => (
|
||||
<DropdownMenuItem
|
||||
key={count}
|
||||
onClick={() => updateParam("imageCount", count)}
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
|
||||
>
|
||||
<Check className={cn("mr-2 h-4 w-4", (params.imageCount || 1) === count ? "opacity-100" : "opacity-0")} />
|
||||
{count}张
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{!hideImageCount && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0">
|
||||
<Images size={14} />
|
||||
<span className="font-medium text-xs">{params.imageCount || 1}张</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" className="min-w-[80px]">
|
||||
{imageCountOptions.map((count) => (
|
||||
<DropdownMenuItem
|
||||
key={count}
|
||||
onClick={() => updateParam("imageCount", count)}
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0"
|
||||
>
|
||||
<Check className={cn("mr-2 h-4 w-4", (params.imageCount || 1) === count ? "opacity-100" : "opacity-0")} />
|
||||
{count}张
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* 🆕 阿里生图专用:提示词优化 & 反向提示词 */}
|
||||
{(showPromptExtend || showNegativePrompt) && (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user