+
@@ -248,6 +249,7 @@ export default function BetaApplicationsPage({ session, onOpenLogin }: BetaAppli
{selectedApplication.selfStatement || "未填写"}
+
diff --git a/src/features/community-review/CommunityCaseAddPage.tsx b/src/features/community-review/CommunityCaseAddPage.tsx
index 8aff1a4..b17055e 100644
--- a/src/features/community-review/CommunityCaseAddPage.tsx
+++ b/src/features/community-review/CommunityCaseAddPage.tsx
@@ -10,6 +10,7 @@ import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { communityClient } from "../../api/communityClient";
import WorkspacePageShell from "../../components/WorkspacePageShell";
+import "../../styles/pages/compliance.css";
import type { WebCanvasWorkflow, WebUserSession } from "../../types";
import { getWorkflowCoverUrl, isCanvasWorkflow } from "../community/communityCaseUtils";
import { canManageCommunityCases } from "./communityPermissions";
diff --git a/src/features/community-review/CommunityReviewPage.tsx b/src/features/community-review/CommunityReviewPage.tsx
index 87c9fdd..d316472 100644
--- a/src/features/community-review/CommunityReviewPage.tsx
+++ b/src/features/community-review/CommunityReviewPage.tsx
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { communityClient, type ServerCommunityCase } from "../../api/communityClient";
import { reportClient, type AdminReportItem } from "../../api/reportClient";
import WorkspacePageShell from "../../components/WorkspacePageShell";
+import "../../styles/pages/compliance.css";
import type { WebUserSession } from "../../types";
import { canManageCommunityCases, canReviewCommunity } from "./communityPermissions";
diff --git a/src/features/report/ReportPage.tsx b/src/features/report/ReportPage.tsx
index 5a88fd3..f6886f7 100644
--- a/src/features/report/ReportPage.tsx
+++ b/src/features/report/ReportPage.tsx
@@ -2,6 +2,7 @@ import { CheckCircleOutlined, FlagOutlined, MailOutlined, PhoneOutlined } from "
import { useEffect, useState, type FormEvent } from "react";
import { publicConfigClient, type WebPublicConfig } from "../../api/publicConfigClient";
import { reportClient, type ReportInput } from "../../api/reportClient";
+import "../../styles/pages/compliance.css";
type SubmitState = "idle" | "loading" | "success" | "error";
diff --git a/src/features/workbench/WorkbenchPage.tsx b/src/features/workbench/WorkbenchPage.tsx
index af93be9..47b0e1c 100644
--- a/src/features/workbench/WorkbenchPage.tsx
+++ b/src/features/workbench/WorkbenchPage.tsx
@@ -67,7 +67,6 @@ import { downloadResultAsset } from "./workbenchDownload";
import { translateTaskError } from "../../utils/translateTaskError";
import {
buildLocalTimeoutMessage,
- formatTextTokenUsage,
getTaskTimeoutPolicy,
isTaskLocallyTimedOut,
} from "../../utils/taskLifecycle";
@@ -79,10 +78,12 @@ import {
import { isViduModel } from "../../utils/viduRouting";
import { isPixverseModel } from "../../utils/pixverseRouting";
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
-import { ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
+import { calculateEnterpriseVideoCredits, ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
import {
getImageQualityOptions,
+ getImageQualityOptionsForContext,
getDefaultImageQuality,
+ getDefaultImageQualityForContext,
getVideoQualityOptions,
getDefaultVideoQuality,
getVideoQualityLabel,
@@ -221,6 +222,12 @@ const MODE_ICONS: Record = {
video: ,
};
+function formatCreditValue(value: number): string {
+ if (!Number.isFinite(value)) return "-";
+ if (value >= 100) return Math.round(value).toLocaleString("zh-CN");
+ return Number(value.toFixed(2)).toString();
+}
+
function WorkbenchPage({
isAuthenticated,
session,
@@ -464,11 +471,72 @@ function WorkbenchPage({
setSidebarCollapsed(!hasSidebarRecords);
}, [hasSidebarRecords]);
- const imageQualityOptions = useMemo(() => getImageQualityOptions(imageModel), [imageModel]);
+ const hasImageReferences = activeMode === "image" && referenceItems.some((item) => item.kind === "image");
+ const isImageGridMode = activeMode === "image" && imageGridMode !== "single";
+ const imageQualityContext = useMemo(
+ () => ({
+ hasReferenceImages: hasImageReferences,
+ isGridMode: isImageGridMode,
+ }),
+ [hasImageReferences, isImageGridMode],
+ );
+ const imageQualityOptions = useMemo(
+ () => getImageQualityOptionsForContext(imageModel, imageQualityContext),
+ [imageModel, imageQualityContext],
+ );
+ const imageGridModeOptions = useMemo(
+ () =>
+ String(imageModel || "").toLowerCase().startsWith("wan2.7-")
+ ? GRID_MODE_OPTIONS.filter((option) => option.value !== "grid-25")
+ : GRID_MODE_OPTIONS,
+ [imageModel],
+ );
const videoQualityOptions = getVideoQualityOptions(videoModel);
const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality);
const imageSettingsSummary = `${imageRatio} / ${imageQuality}`;
+ const billingEstimate = useMemo(() => {
+ if (activeMode === "image") {
+ return {
+ label: "预计 20 积分",
+ title: `图片生成按任务计费:${activeModel},${imageSettingsSummary},预计 20 积分`,
+ };
+ }
+ if (activeMode === "video") {
+ try {
+ const durationSeconds = Math.max(1, Math.ceil(Number(videoDuration) || 1));
+ const credits = calculateEnterpriseVideoCredits({
+ model: activeModelValue,
+ resolution: videoQuality,
+ durationSeconds,
+ muted: false,
+ hasReferenceVideo: referenceItems.some((item) => item.kind === "video"),
+ });
+ return {
+ label: `预计 ${formatCreditValue(credits)} 积分`,
+ title: `${activeModel},${videoQualityLabel},${durationSeconds} 秒,预计 ${formatCreditValue(credits)} 积分`,
+ };
+ } catch {
+ return {
+ label: "计费以提交后为准",
+ title: "当前模型的预估计费暂不可用,实际扣费以服务端结算为准",
+ };
+ }
+ }
+ return {
+ label: "按 Token 结算",
+ title: "文本对话按输入、输出 Token 实际用量结算,完成后显示本次积分",
+ };
+ }, [
+ activeMode,
+ activeModel,
+ activeModelValue,
+ imageSettingsSummary,
+ referenceItems,
+ videoDuration,
+ videoQuality,
+ videoQualityLabel,
+ ]);
const composerPlaceholder =
referenceItems.length > 0 ? `${toolTheme.placeholder},可输入 @ 引用参考内容` : toolTheme.placeholder;
const dropdownDirection = hasActivatedWorkspace ? "up" : "down";
@@ -1158,9 +1226,15 @@ function WorkbenchPage({
useEffect(() => {
if (!imageQualityOptions.some((option) => option.value === imageQuality)) {
- setImageQuality(getDefaultImageQuality(imageModel));
+ setImageQuality(getDefaultImageQualityForContext(imageModel, imageQualityContext));
}
- }, [imageModel, imageQuality, imageQualityOptions]);
+ }, [imageModel, imageQuality, imageQualityContext, imageQualityOptions]);
+
+ useEffect(() => {
+ if (!imageGridModeOptions.some((option) => option.value === imageGridMode)) {
+ setImageGridMode("single");
+ }
+ }, [imageGridMode, imageGridModeOptions]);
useEffect(() => {
if (activeMode !== "video" || videoFrameMode !== "start-end" || referenceItems.length <= 2) return;
@@ -2585,7 +2659,15 @@ function WorkbenchPage({
}
};
- const sendDisabled = !inputValue.trim() || (activeMode !== "chat" && getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id)) >= 3);
+ const activeGenerationCount = getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id));
+ const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= 3;
+ const promptIsEmpty = !inputValue.trim();
+ const sendDisabled = promptIsEmpty || generationLimitReached;
+ const sendButtonTitle = promptIsEmpty
+ ? "输入内容后可发送"
+ : generationLimitReached
+ ? `当前已有 ${activeGenerationCount} 个任务进行中,请等待任一任务完成`
+ : billingEstimate.title;
const suggestedPrompts = [
{ text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode },
@@ -2850,7 +2932,7 @@ function WorkbenchPage({
toggleToolbarMenu("image-grid-mode")}
@@ -2922,10 +3004,15 @@ function WorkbenchPage({
)}
+
+ {billingEstimate.label}
+
)}
- {message.role === "assistant" && message.mode === "chat" && message.status === "completed" && (
-
- {formatTextTokenUsage(message.taskUsage)}
-
- )}
{(message.resultUrl || (message.result && message.status !== "thinking")) && (
((set) => ({
@@ -55,7 +55,7 @@ export const useSessionStore = create((set) => ({
showSessionReplaced: (message) => set({
sessionReplacedOpen: true,
- sessionReplacedMessage: message || '您的账号已在其他设备登录(最多同时 2 台设备),此设备的登录状态已失效。',
+ sessionReplacedMessage: message || '当前账号已在其他设备登录,此设备的登录状态已失效。',
}),
hideSessionReplaced: () => set({ sessionReplacedOpen: false }),
diff --git a/src/styles/pages/beta-applications.css b/src/styles/pages/beta-applications.css
index 47d9952..2cebadb 100644
--- a/src/styles/pages/beta-applications.css
+++ b/src/styles/pages/beta-applications.css
@@ -3,7 +3,11 @@
flex-direction: column;
gap: 18px;
width: min(1180px, calc(100vw - 48px));
+ margin: 0 auto;
+ height: 100%;
+ min-height: 0;
padding: 24px;
+ overflow: hidden;
}
.beta-admin-toolbar {
@@ -90,6 +94,8 @@
display: grid;
grid-template-columns: 300px minmax(0, 1fr);
gap: 16px;
+ flex: 1;
+ min-height: 0;
align-items: start;
}
@@ -97,7 +103,7 @@
display: flex;
flex-direction: column;
gap: 8px;
- max-height: calc(100vh - 220px);
+ max-height: 100%;
overflow: auto;
padding-right: 4px;
}
@@ -174,6 +180,10 @@
flex-direction: column;
gap: 14px;
min-width: 0;
+ max-height: 100%;
+ min-height: 0;
+ overflow: auto;
+ padding-right: 4px;
}
.beta-admin-detail__header,
@@ -376,6 +386,7 @@
.beta-admin-page__inner {
width: min(100%, calc(100vw - 24px));
padding: 16px 12px;
+ overflow: auto;
}
.beta-admin-toolbar,
@@ -385,11 +396,18 @@
.beta-admin-layout {
grid-template-columns: 1fr;
+ overflow: visible;
}
.beta-admin-list {
max-height: none;
}
+
+ .beta-admin-detail {
+ max-height: none;
+ overflow: visible;
+ padding-right: 0;
+ }
}
@media (max-width: 640px) {
diff --git a/src/styles/pages/legacy-pages.css b/src/styles/pages/legacy-pages.css
index 07c635e..75597b8 100644
--- a/src/styles/pages/legacy-pages.css
+++ b/src/styles/pages/legacy-pages.css
@@ -11915,6 +11915,21 @@
gap: 6px;
}
+.wb-composer__billing-estimate {
+ max-width: 138px;
+ padding: 6px 9px;
+ border: 2px solid #111;
+ background: #fffbe8;
+ color: #111;
+ font-size: 12px;
+ font-weight: 800;
+ line-height: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ box-shadow: 2px 2px 0 #111;
+}
+
.wb-composer__send-primary {
display: inline-flex;
align-items: center;
diff --git a/src/styles/themes/dark-green.css b/src/styles/themes/dark-green.css
index 7a39fc2..eb58736 100644
--- a/src/styles/themes/dark-green.css
+++ b/src/styles/themes/dark-green.css
@@ -1794,6 +1794,14 @@
box-shadow: none;
}
+.web-shell[data-ui-theme="dark-green"] .wb-composer__billing-estimate {
+ border: 1px solid var(--border-default);
+ border-radius: 999px;
+ background: var(--bg-elevated);
+ color: var(--fg-body);
+ box-shadow: none;
+}
+
.web-shell[data-ui-theme="dark-green"] .wb-composer__send-primary:hover:not(:disabled) {
background: var(--accent-hover);
color: var(--dg-button-text);
diff --git a/src/utils/enterpriseVideoPolicy.ts b/src/utils/enterpriseVideoPolicy.ts
index 2240ea2..359aa31 100644
--- a/src/utils/enterpriseVideoPolicy.ts
+++ b/src/utils/enterpriseVideoPolicy.ts
@@ -40,6 +40,7 @@ export const ENTERPRISE_VIDEO_RESOLUTION_OPTIONS = [
export const ENTERPRISE_DEFAULT_VIDEO_MODEL = HAPPY_HORSE_UI_MODEL;
export const ENTERPRISE_DEFAULT_VIDEO_RESOLUTION = "1080P";
+const CREDITS_PER_CNY = 100;
export interface EnterpriseVideoPricingInput {
model: string;
@@ -74,11 +75,11 @@ export function getEnterpriseVideoCreditRate(input: EnterpriseVideoPricingInput)
}
if (model.includes("vidu")) {
- return resolution === "720P" ? 0.4 : 0.8;
+ return resolution === "720P" ? 0.6 : 1.0;
}
if (model.includes("pixverse")) {
- return resolution === "720P" ? 0.4 : 0.8;
+ return resolution === "720P" ? 0.6 : 1.0;
}
if (model.includes("kling")) {
@@ -94,5 +95,5 @@ export function getEnterpriseVideoCreditRate(input: EnterpriseVideoPricingInput)
export function calculateEnterpriseVideoCredits(input: EnterpriseVideoPricingInput): number {
const duration = Math.max(1, Math.ceil(Number(input.durationSeconds) || 1));
- return Number((getEnterpriseVideoCreditRate(input) * duration).toFixed(2));
+ return Number((getEnterpriseVideoCreditRate(input) * duration * CREDITS_PER_CNY).toFixed(2));
}
diff --git a/src/utils/modelOptions.ts b/src/utils/modelOptions.ts
index 9b452d9..63d6371 100644
--- a/src/utils/modelOptions.ts
+++ b/src/utils/modelOptions.ts
@@ -25,11 +25,30 @@ export function getImageQualityOptions(model: string): CanvasOption[] {
: imageQualityOptions.filter((option) => option.value !== "4K");
}
+export function getImageQualityOptionsForContext(
+ model: string,
+ context?: { hasReferenceImages?: boolean; isGridMode?: boolean },
+): CanvasOption[] {
+ const options = getImageQualityOptions(model);
+ const shouldLimitTo2K =
+ String(model || "").toLowerCase() === "wan2.7-image-pro" &&
+ (context?.hasReferenceImages || context?.isGridMode);
+ return shouldLimitTo2K ? options.filter((option) => option.value !== "4K") : options;
+}
+
export function getDefaultImageQuality(model: string): string {
const options = getImageQualityOptions(model);
return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K";
}
+export function getDefaultImageQualityForContext(
+ model: string,
+ context?: { hasReferenceImages?: boolean; isGridMode?: boolean },
+): string {
+ const options = getImageQualityOptionsForContext(model, context);
+ return options.some((option) => option.value === "2K") ? "2K" : options[0]?.value || "1K";
+}
+
// ─── Video quality ────────────────────────────────────────────────────────────
function normalizeVideoModel(model: string): string {
diff --git a/src/utils/taskLifecycle.ts b/src/utils/taskLifecycle.ts
index ae3a850..757c4b2 100644
--- a/src/utils/taskLifecycle.ts
+++ b/src/utils/taskLifecycle.ts
@@ -32,8 +32,10 @@ export interface TextTokenUsage {
totalTokens?: number;
}
-export const TEXT_INPUT_CREDITS_PER_MILLION = 2;
-export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5;
+const CREDITS_PER_CNY = 100;
+
+export const TEXT_INPUT_CREDITS_PER_MILLION = 2 * CREDITS_PER_CNY;
+export const TEXT_OUTPUT_CREDITS_PER_MILLION = 5 * CREDITS_PER_CNY;
const IMAGE_TIMEOUT_POLICY: TaskTimeoutPolicy = {
submitTimeoutMs: 90_000,
@@ -151,7 +153,7 @@ export function estimateTextTokenCredits(usage: TextTokenUsage): number {
}
export function formatTextTokenUsage(usage?: TextTokenUsage | null): string {
- const rule = "文本计费规则:输入 Token 每百万 2 积分,输出 Token 每百万 5 积分,实际以服务端结算为准。";
+ const rule = "文本计费规则:输入 Token 每百万 200 积分,输出 Token 每百万 500 积分,实际以服务端结算为准。";
if (!usage) return rule;
const promptTokens = Math.max(0, Number(usage.promptTokens || 0));
const completionTokens = Math.max(0, Number(usage.completionTokens || 0));