fix(*): 模型目录 - 修复前台开关、系统模型价格与访问控制
This commit is contained in:
@@ -32,12 +32,14 @@ import { WORKSPACE_SIDEBAR_STORAGE_KEY } from "@/components/layout/workspace-sid
|
||||
import { getAdminAntThemeConfig } from "@/lib/app-theme";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
type AdminNavigationItem = {
|
||||
path: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
requireFeature?: "frontendModelsEnabled";
|
||||
};
|
||||
|
||||
const adminNavigation: Array<{ label: string; items: AdminNavigationItem[] }> = [
|
||||
@@ -50,7 +52,7 @@ const adminNavigation: Array<{ label: string; items: AdminNavigationItem[] }> =
|
||||
items: [
|
||||
{ path: "/admin/users", label: "用户管理", description: "账号、角色与状态", icon: <UsersRound className="size-4" /> },
|
||||
{ path: "/admin/channels", label: "系统渠道", description: "渠道、模型与售价", icon: <RadioTower className="size-4" /> },
|
||||
{ path: "/admin/models", label: "前台模型", description: "展示、线路与用户价格", icon: <Layers3 className="size-4" /> },
|
||||
{ path: "/admin/models", label: "前台模型", description: "展示、线路与用户价格", icon: <Layers3 className="size-4" />, requireFeature: "frontendModelsEnabled" },
|
||||
{ path: "/admin/prompt-templates", label: "提示词模板", description: "平台创作策略版本", icon: <MessageSquareText className="size-4" /> },
|
||||
],
|
||||
},
|
||||
@@ -160,62 +162,70 @@ export function AdminPageFrame({ title, actions, back, scroll = false, children
|
||||
}
|
||||
|
||||
function MobileAdminNavigation() {
|
||||
const features = useUserStore((state) => state.features);
|
||||
const visibleItems = adminNavigation.flatMap((group) => group.items).filter((item) => !item.requireFeature || features[item.requireFeature]);
|
||||
|
||||
return (
|
||||
<nav className="app-workspace-navigation hide-scrollbar flex shrink-0 gap-1 overflow-x-auto border-b border-border/70 px-3 py-2 lg:hidden" aria-label="管理后台分区">
|
||||
{adminNavigation
|
||||
.flatMap((group) => group.items)
|
||||
.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
end={item.path === "/admin"}
|
||||
className={({ isActive }) =>
|
||||
cn("app-workspace-nav-link flex h-8 shrink-0 items-center gap-1.5 rounded-md px-2.5 text-xs transition-colors", isActive ? "is-active font-medium" : "text-foreground/60 hover:bg-surface-hover hover:text-foreground")
|
||||
}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
{visibleItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
end={item.path === "/admin"}
|
||||
className={({ isActive }) =>
|
||||
cn("app-workspace-nav-link flex h-8 shrink-0 items-center gap-1.5 rounded-md px-2.5 text-xs transition-colors", isActive ? "is-active font-medium" : "text-foreground/60 hover:bg-surface-hover hover:text-foreground")
|
||||
}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
<AppChangelogButton className="grid size-8 shrink-0 place-items-center rounded-md text-foreground/55 transition-colors hover:bg-surface-hover hover:text-foreground [&_svg]:size-4" />
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminNavigation({ collapsed }: { collapsed: boolean }) {
|
||||
const features = useUserStore((state) => state.features);
|
||||
|
||||
return (
|
||||
<nav className="thin-scrollbar flex-1 overflow-y-auto px-2 py-2" aria-label="管理后台菜单">
|
||||
{adminNavigation.map((group) => (
|
||||
<div key={group.label} className="mb-3">
|
||||
{!collapsed ? (
|
||||
<div className="admin-nav-group-label mb-1 px-2.5 text-[var(--fs-tiny)] font-medium text-foreground/38">
|
||||
<span>{group.label}</span>
|
||||
{adminNavigation.map((group) => {
|
||||
const visibleItems = group.items.filter((item) => !item.requireFeature || features[item.requireFeature]);
|
||||
if (visibleItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={group.label} className="mb-3">
|
||||
{!collapsed ? (
|
||||
<div className="admin-nav-group-label mb-1 px-2.5 text-[var(--fs-tiny)] font-medium text-foreground/38">
|
||||
<span>{group.label}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-auto mb-1.5 h-px w-7 bg-border/80" />
|
||||
)}
|
||||
<div className="space-y-0.5">
|
||||
{visibleItems.map((item) => (
|
||||
<Tooltip key={item.path} mouseEnterDelay={0.1} title={collapsed ? item.label : undefined} placement="right">
|
||||
<NavLink
|
||||
to={item.path}
|
||||
end={item.path === "/admin"}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"app-workspace-nav-link flex h-8 items-center rounded-md text-[var(--fs-body)] transition-colors",
|
||||
collapsed ? "justify-center px-0" : "gap-2.5 px-2.5",
|
||||
isActive ? "is-active font-medium" : "text-foreground/62 hover:bg-surface-hover hover:text-foreground",
|
||||
)
|
||||
}
|
||||
>
|
||||
{item.icon}
|
||||
{!collapsed ? <span className="truncate">{item.label}</span> : null}
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-auto mb-1.5 h-px w-7 bg-border/80" />
|
||||
)}
|
||||
<div className="space-y-0.5">
|
||||
{group.items.map((item) => (
|
||||
<Tooltip key={item.path} mouseEnterDelay={0.1} title={collapsed ? item.label : undefined} placement="right">
|
||||
<NavLink
|
||||
to={item.path}
|
||||
end={item.path === "/admin"}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"app-workspace-nav-link flex h-8 items-center rounded-md text-[var(--fs-body)] transition-colors",
|
||||
collapsed ? "justify-center px-0" : "gap-2.5 px-2.5",
|
||||
isActive ? "is-active font-medium" : "text-foreground/62 hover:bg-surface-hover hover:text-foreground",
|
||||
)
|
||||
}
|
||||
>
|
||||
{item.icon}
|
||||
{!collapsed ? <span className="truncate">{item.label}</span> : null}
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { App, Switch } from "antd";
|
||||
import { Clapperboard, Coins, ListChecks, RadioTower, ToggleLeft } from "lucide-react";
|
||||
import { Clapperboard, Coins, ListChecks, RadioTower, ToggleLeft, Sparkles, Settings } from "lucide-react";
|
||||
|
||||
import { getAdminFeatureAvailability, updateAdminFeatureAvailability } from "@/services/api/auth";
|
||||
import { useUserStore, type FeatureAvailability } from "@/stores/use-user-store";
|
||||
import { SettingsSectionCard } from "./admin-ui";
|
||||
|
||||
type FeatureKey = "shortDramaEnabled" | "taskCenterEnabled" | "creditsEnabled" | "customChannelsEnabled";
|
||||
type FeatureKey = "shortDramaEnabled" | "taskCenterEnabled" | "creditsEnabled" | "customChannelsEnabled" | "frontendModelsEnabled";
|
||||
|
||||
const featureRows: Array<{ key: FeatureKey; title: string; menu: string; description: string; icon: ReactNode }> = [
|
||||
const userFeatureRows: Array<{ key: FeatureKey; title: string; menu: string; description: string; icon: ReactNode }> = [
|
||||
{ key: "shortDramaEnabled", title: "短剧创作", menu: "/projects", description: "关闭后隐藏短剧入口,并拦截项目列表、详情和项目 API。已有项目数据不会删除。", icon: <Clapperboard className="size-4" /> },
|
||||
{ key: "taskCenterEnabled", title: "任务", menu: "/tasks", description: "关闭后仅隐藏并拦截任务中心页面;生成任务仍会创建、执行、记录和恢复。", icon: <ListChecks className="size-4" /> },
|
||||
{ key: "creditsEnabled", title: "积分中心", menu: "/wallet", description: "关闭后隐藏用户积分入口,新创建的任务和系统渠道请求不再冻结或消费积分。", icon: <Coins className="size-4" /> },
|
||||
{ key: "customChannelsEnabled", title: "自定义渠道", menu: "/settings?section=channels", description: "关闭后隐藏用户自定义渠道入口,并拦截模型目录拉取、渠道中转和新的生成任务。已有渠道配置不会删除。", icon: <RadioTower className="size-4" /> },
|
||||
];
|
||||
|
||||
const adminFeatureRows: Array<{ key: FeatureKey; title: string; menu: string; description: string; icon: ReactNode }> = [
|
||||
{ key: "frontendModelsEnabled", title: "前台模型", menu: "/admin/models", description: "关闭后用户直接使用系统渠道中的模型;后台仍保留前台模型配置入口,方便重新开启。已有配置不会删除。", icon: <Sparkles className="size-4" /> },
|
||||
];
|
||||
|
||||
export default function FeatureAvailabilityPanel() {
|
||||
const { message, modal } = App.useApp();
|
||||
const setGlobalFeatures = useUserStore((state) => state.setFeatures);
|
||||
@@ -43,10 +47,11 @@ export default function FeatureAvailabilityPanel() {
|
||||
taskCenterEnabled: next.taskCenterEnabled,
|
||||
creditsEnabled: next.creditsEnabled,
|
||||
customChannelsEnabled: next.customChannelsEnabled,
|
||||
frontendModelsEnabled: next.frontendModelsEnabled,
|
||||
});
|
||||
setFeatures(result.features);
|
||||
setGlobalFeatures(result.features);
|
||||
message.success(`${featureRows.find((item) => item.key === key)?.title || "功能"}已${enabled ? "开放" : "关闭"}`);
|
||||
message.success(`${allFeatureRows.find((item) => item.key === key)?.title || "功能"}已${enabled ? "开放" : "关闭"}`);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "保存功能开放配置失败");
|
||||
} finally {
|
||||
@@ -55,30 +60,67 @@ export default function FeatureAvailabilityPanel() {
|
||||
};
|
||||
|
||||
const toggle = (key: FeatureKey, enabled: boolean) => {
|
||||
if (key !== "creditsEnabled" || enabled) {
|
||||
void save(key, enabled);
|
||||
// 积分关闭需要二次确认
|
||||
if (key === "creditsEnabled" && !enabled) {
|
||||
modal.confirm({
|
||||
title: "关闭用户积分功能?",
|
||||
content: "保存后新创建的任务和系统渠道请求将不再扣减积分;已经冻结的计费订单仍按原规则结算,已有余额和流水继续保留。",
|
||||
okText: "确认关闭",
|
||||
cancelText: "取消",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => save(key, false),
|
||||
});
|
||||
return;
|
||||
}
|
||||
modal.confirm({
|
||||
title: "关闭用户积分功能?",
|
||||
content: "保存后新创建的任务和系统渠道请求将不再扣减积分;已经冻结的计费订单仍按原规则结算,已有余额和流水继续保留。",
|
||||
okText: "确认关闭",
|
||||
cancelText: "取消",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => save(key, false),
|
||||
});
|
||||
// 前台模型关闭需要二次确认
|
||||
if (key === "frontendModelsEnabled" && !enabled) {
|
||||
modal.confirm({
|
||||
title: "关闭前台模型功能?",
|
||||
content: "关闭后用户将直接使用系统渠道中配置的模型;管理后台仍保留前台模型配置入口,重新开启后即可继续使用。",
|
||||
okText: "确认关闭",
|
||||
cancelText: "取消",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => save(key, false),
|
||||
});
|
||||
return;
|
||||
}
|
||||
void save(key, enabled);
|
||||
};
|
||||
|
||||
const enabledCount = features ? featureRows.filter((item) => features[item.key]).length : 0;
|
||||
const allFeatureRows = [...userFeatureRows, ...adminFeatureRows];
|
||||
const userEnabledCount = features ? userFeatureRows.filter((item) => features[item.key]).length : 0;
|
||||
const adminEnabledCount = features ? adminFeatureRows.filter((item) => features[item.key]).length : 0;
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<div className="space-y-6 pt-4">
|
||||
<SettingsSectionCard
|
||||
icon={<ToggleLeft className="size-4" />}
|
||||
title="用户功能开放"
|
||||
status={{ label: features ? `${enabledCount}/${featureRows.length} 已开放` : "读取中", color: enabledCount === featureRows.length ? "success" : "default" }}
|
||||
status={{ label: features ? `${userEnabledCount}/${userFeatureRows.length} 已开放` : "读取中", color: userEnabledCount === userFeatureRows.length ? "success" : "default" }}
|
||||
>
|
||||
<div className="divide-y divide-border/75">
|
||||
{featureRows.map((item) => (
|
||||
{userFeatureRows.map((item) => (
|
||||
<div key={item.key} className="flex min-h-16 items-center gap-3 px-4 py-3">
|
||||
<span className="grid size-8 shrink-0 place-items-center rounded-md bg-muted/35 text-foreground/65">{item.icon}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-sm font-medium">{item.title}</h3>
|
||||
<span className="rounded border border-border/70 px-1.5 py-0.5 text-[var(--fs-micro)] text-foreground/45">{item.menu}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={features?.[item.key] === true} loading={!features || saving === item.key} disabled={Boolean(saving && saving !== item.key)} onChange={(checked) => toggle(item.key, checked)} aria-label={`开放${item.title}`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SettingsSectionCard>
|
||||
|
||||
<SettingsSectionCard
|
||||
icon={<Settings className="size-4" />}
|
||||
title="管理后台功能"
|
||||
status={{ label: features ? `${adminEnabledCount}/${adminFeatureRows.length} 已开放` : "读取中", color: adminEnabledCount === adminFeatureRows.length ? "success" : "default" }}
|
||||
>
|
||||
<div className="divide-y divide-border/75">
|
||||
{adminFeatureRows.map((item) => (
|
||||
<div key={item.key} className="flex min-h-16 items-center gap-3 px-4 py-3">
|
||||
<span className="grid size-8 shrink-0 place-items-center rounded-md bg-muted/35 text-foreground/65">{item.icon}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
Reference in New Issue
Block a user