fix(web): 管理价格与创作空态 - 合并 PR #373 并保留 create 路由
This commit is contained in:
@@ -77,6 +77,7 @@ export default function AnalyticsPanel({ users, channels }: Props) {
|
||||
const [pricingWorkspaceOpen, setPricingWorkspaceOpen] = useState(false);
|
||||
const [pendingPricing, setPendingPricing] = useState<ModelPricing | null | undefined>(undefined);
|
||||
const [form] = Form.useForm<PricingFormValues>();
|
||||
const pricingChannelId = Form.useWatch("channelId", form);
|
||||
const analyticsPageSize = 20;
|
||||
|
||||
const filters = useMemo<AnalyticsFilters>(
|
||||
@@ -148,11 +149,13 @@ export default function AnalyticsPanel({ users, channels }: Props) {
|
||||
|
||||
const pricingModelOptions = useMemo(() => {
|
||||
const names = new Set<string>();
|
||||
const sourceChannels = channels.filter((channel) => channel.enabled !== false);
|
||||
const sourceChannels = channels.filter((channel) => channel.enabled !== false && (!pricingChannelId || channel.id === pricingChannelId));
|
||||
sourceChannels.forEach((channel) => channel.models?.forEach((name) => names.add(name)));
|
||||
if (editingPricing?.model) names.add(editingPricing.model);
|
||||
if (editingPricing?.model && (!pricingChannelId || editingPricing.channelId === pricingChannelId)) {
|
||||
names.add(editingPricing.model);
|
||||
}
|
||||
return [...names].sort().map((name) => ({ label: name, value: name }));
|
||||
}, [channels, editingPricing?.model]);
|
||||
}, [channels, editingPricing?.channelId, editingPricing?.model, pricingChannelId]);
|
||||
|
||||
const preparePricingForm = (pricing: ModelPricing | null) => {
|
||||
setEditingPricing(pricing);
|
||||
@@ -188,9 +191,7 @@ export default function AnalyticsPanel({ users, channels }: Props) {
|
||||
|
||||
const handlePricingValuesChange = (changedValues: Partial<PricingFormValues>, values: PricingFormValues) => {
|
||||
if (Object.prototype.hasOwnProperty.call(changedValues, "channelId")) {
|
||||
const nextModels = channels
|
||||
.filter((channel) => channel.enabled !== false && (!values.channelId || channel.id === values.channelId))
|
||||
.flatMap((channel) => channel.models || []);
|
||||
const nextModels = channels.filter((channel) => channel.enabled !== false && (!values.channelId || channel.id === values.channelId)).flatMap((channel) => channel.models || []);
|
||||
if (values.model && !nextModels.includes(values.model)) form.setFieldValue("model", undefined);
|
||||
return;
|
||||
}
|
||||
@@ -642,11 +643,30 @@ export default function AnalyticsPanel({ users, channels }: Props) {
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
<Modal rootClassName="admin-modal-root admin-analytics-pricing-modal" title={editingPricing ? "编辑模型价格" : "新增模型价格"} open={pricingModalOpen} onCancel={() => setPricingModalOpen(false)} onOk={() => void savePricing()} confirmLoading={savingPricing} okText="保存" cancelText="取消" width={760} zIndex={1200} destroyOnHidden>
|
||||
<Modal
|
||||
rootClassName="admin-modal-root admin-analytics-pricing-modal"
|
||||
title={editingPricing ? "编辑模型价格" : "新增模型价格"}
|
||||
open={pricingModalOpen}
|
||||
onCancel={() => setPricingModalOpen(false)}
|
||||
onOk={() => void savePricing()}
|
||||
confirmLoading={savingPricing}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
width={760}
|
||||
zIndex={1200}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={form} layout="vertical" requiredMark={false} onValuesChange={handlePricingValuesChange}>
|
||||
<div className="grid grid-cols-1 gap-x-4 sm:grid-cols-2">
|
||||
<Form.Item name="model" label="模型" rules={[{ required: true, message: "请选择模型" }]}>
|
||||
<Select showSearch optionFilterProp="label" placeholder={pricingModelOptions.length ? "选择已启用模型" : "暂无已启用模型"} options={pricingModelOptions} disabled={!pricingModelOptions.length} onChange={handlePricingModelChange} />
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder={pricingModelOptions.length ? "选择已启用模型" : "暂无已启用模型"}
|
||||
options={pricingModelOptions}
|
||||
disabled={!pricingModelOptions.length}
|
||||
onChange={handlePricingModelChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="channelId" label="渠道范围">
|
||||
<Select allowClear placeholder="全部渠道" options={channels.filter((channel) => channel.enabled !== false).map((channel) => ({ label: channel.name, value: channel.id }))} />
|
||||
|
||||
@@ -96,6 +96,7 @@ export const router = createBrowserRouter([
|
||||
errorElement: <RouteErrorPage />,
|
||||
children: [
|
||||
{ path: "/", element: <RequireAuth>{deferred(<CreatePage />)}</RequireAuth> },
|
||||
{ path: "/create", element: <RequireAuth>{deferred(<CreatePage />)}</RequireAuth> },
|
||||
{
|
||||
path: "/tasks",
|
||||
element: (
|
||||
|
||||
@@ -15598,28 +15598,38 @@ html:not(.dark) .creation-home .storyboard-workbench-bar-action:hover { backgrou
|
||||
.creation-home .creation-empty-suggest { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* 空态布局收敛:banner 与下方对话列同宽,整组垂直居中;用首/末子项 auto margin 居中,
|
||||
内容超高时自动 margin 归零、回到顶部对齐,避免 justify-content:center 在滚动容器里的裁切问题。 */
|
||||
/* 空态布局收敛:保持图片区、文案和输入区的文档流顺序,内容超高时由主区滚动承载。 */
|
||||
.creation-home .creation-empty-workspace {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.creation-home .creation-empty-workspace > .creation-empty-art,
|
||||
.creation-home .creation-empty-workspace > .creation-chat-intro,
|
||||
.creation-home .creation-empty-workspace > .creation-empty-composer,
|
||||
.creation-home .creation-empty-workspace > .creation-empty-suggest {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.creation-empty-workspace > .creation-empty-art {
|
||||
width: min(820px, calc(100% - 28px));
|
||||
height: 200px;
|
||||
margin: auto auto 20px;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.creation-empty-workspace .creation-empty-art-frame.is-back { top: 18px; height: 146px; }
|
||||
.creation-empty-workspace .creation-empty-art-frame.is-main { top: 0; height: 182px; }
|
||||
.creation-empty-workspace .creation-empty-art-frame.is-front { top: 24px; height: 146px; }
|
||||
|
||||
.creation-empty-workspace .creation-chat-intro { margin-top: 24px; margin-bottom: 18px; }
|
||||
.creation-empty-workspace .creation-chat-intro { margin: 0 auto 18px; }
|
||||
|
||||
.creation-empty-workspace > .creation-empty-suggest { margin: 26px auto auto; }
|
||||
.creation-empty-workspace > .creation-empty-suggest { margin: 26px auto 0; }
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.creation-empty-workspace > .creation-empty-art { height: 168px; margin-bottom: 16px; }
|
||||
.creation-empty-workspace .creation-empty-art-frame.is-back { top: 14px; height: 122px; }
|
||||
.creation-empty-workspace .creation-empty-art-frame.is-main { top: 0; height: 152px; }
|
||||
.creation-empty-workspace .creation-empty-art-frame.is-front { top: 18px; height: 122px; }
|
||||
.creation-empty-workspace .creation-chat-intro { margin-top: 18px; margin-bottom: 14px; }
|
||||
.creation-empty-workspace .creation-chat-intro { margin-top: 0; margin-bottom: 14px; }
|
||||
.creation-empty-workspace > .creation-empty-suggest { margin-top: 20px; }
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,10 @@ test("analytics keeps fixed range presets distinct and uses enabled channel mode
|
||||
expect(source).toContain("onChange={handlePricingModelChange}");
|
||||
expect(source).toContain('hasOwnProperty.call(changedValues, "model")');
|
||||
expect(source).toContain('if (matchingChannels.length) form.setFieldValue("channelId", matchingChannels[0].id)');
|
||||
expect(source).toContain("const sourceChannels = channels.filter((channel) => channel.enabled !== false)");
|
||||
expect(source).toContain("const sourceChannels = channels.filter(");
|
||||
expect(source).toContain('Form.useWatch("channelId", form)');
|
||||
expect(source).toContain("pricingChannelId");
|
||||
expect(source).toContain("channel.id === pricingChannelId");
|
||||
expect(source).toContain('inputMode="decimal"');
|
||||
expect(source).toContain('className="admin-analytics-price-input"');
|
||||
expect(source).toContain('className="admin-analytics-price-field"');
|
||||
|
||||
18
web/test/create-empty-layout.test.ts
Normal file
18
web/test/create-empty-layout.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
test("create empty state keeps the banner and composer in document flow", () => {
|
||||
const styles = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8");
|
||||
const layoutStart = styles.indexOf("/* 空态布局收敛:保持图片区、文案和输入区的文档流顺序");
|
||||
const layoutEnd = styles.indexOf("/* ===== 素材库", layoutStart);
|
||||
const layout = styles.slice(layoutStart, layoutEnd);
|
||||
|
||||
expect(layoutStart).toBeGreaterThanOrEqual(0);
|
||||
expect(layoutEnd).toBeGreaterThan(layoutStart);
|
||||
expect(layout).toContain("justify-content: flex-start;");
|
||||
expect(layout).toContain("flex: 0 0 auto;");
|
||||
expect(layout).toContain("margin: 0 auto 20px;");
|
||||
expect(layout).not.toContain("margin: auto auto 20px;");
|
||||
expect(layout).not.toContain("margin: 26px auto auto;");
|
||||
});
|
||||
@@ -29,12 +29,12 @@ describe("workspace route loading", () => {
|
||||
expect(navigation).toContain("onFocus={() => preloadWorkspaceRoute(linkTo)}");
|
||||
});
|
||||
|
||||
test("uses the creation page as the root entry without legacy home or create routes", () => {
|
||||
test("keeps the creation page at root and preserves the create compatibility route", () => {
|
||||
const router = source("../src/router.tsx");
|
||||
const navigation = source("../src/components/layout/workspace-sidebar-nav.tsx");
|
||||
|
||||
expect(router).toContain('{ path: "/", element: <RequireAuth>{deferred(<CreatePage />)}</RequireAuth> }');
|
||||
expect(router).not.toContain('path: "/create"');
|
||||
expect(router).toContain('{ path: "/create", element: <RequireAuth>{deferred(<CreatePage />)}</RequireAuth> }');
|
||||
expect(router).not.toContain('path: "/home"');
|
||||
expect(router).not.toContain("HomePage");
|
||||
expect(navigation).toContain('{ id: "home", title: "首页", icon: Home, to: "/" }');
|
||||
|
||||
Reference in New Issue
Block a user