Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b65206b84 | |||
| fb4011bf1f | |||
| b08a7918da | |||
| 7be4e65e1e | |||
| 73a6043310 | |||
| 9282e6c0d8 |
@@ -88,7 +88,7 @@ function AppShell({
|
|||||||
"avatarConsole",
|
"avatarConsole",
|
||||||
"characterMix",
|
"characterMix",
|
||||||
] as WebViewKey[];
|
] as WebViewKey[];
|
||||||
const showPageScrollActions = showFloatingNav && !toolSurfaceViews.includes(activeView);
|
const showPageScrollActions = false;
|
||||||
|
|
||||||
const visibleNavItems = useMemo(
|
const visibleNavItems = useMemo(
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
Background,
|
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type MouseEvent, type WheelEvent } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type MouseEvent, type WheelEvent } from "react";
|
||||||
@@ -3542,7 +3541,8 @@ function CanvasPage({
|
|||||||
onMouseMove={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasMouseMove}
|
onMouseMove={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasMouseMove}
|
||||||
onWheel={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasWheel}
|
onWheel={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasWheel}
|
||||||
style={{
|
style={{
|
||||||
"--canvas-bg-size": `${24 * canvasViewport.zoom}px`,
|
"--canvas-bg-size": `${34 * canvasViewport.zoom}px`,
|
||||||
|
"--canvas-bg-dot": `${1.35 * canvasViewport.zoom}px`,
|
||||||
"--canvas-bg-x": `${canvasViewport.x}px`,
|
"--canvas-bg-x": `${canvasViewport.x}px`,
|
||||||
"--canvas-bg-y": `${canvasViewport.y}px`,
|
"--canvas-bg-y": `${canvasViewport.y}px`,
|
||||||
cursor: canvasPanDrag ? "grabbing" : spacePanning ? "grab" : undefined,
|
cursor: canvasPanDrag ? "grabbing" : spacePanning ? "grab" : undefined,
|
||||||
@@ -3727,9 +3727,7 @@ function CanvasPage({
|
|||||||
proOptions={{ hideAttribution: true }}
|
proOptions={{ hideAttribution: true }}
|
||||||
onPaneClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handlePaneClick}
|
onPaneClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handlePaneClick}
|
||||||
onPaneContextMenu={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handlePaneContextMenu}
|
onPaneContextMenu={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handlePaneContextMenu}
|
||||||
>
|
/>
|
||||||
<Background gap={24} color="transparent" className="studio-canvas__background" />
|
|
||||||
</ReactFlow>
|
|
||||||
<div className="studio-canvas-zoom-controls" onMouseDown={(e) => e.stopPropagation()}>
|
<div className="studio-canvas-zoom-controls" onMouseDown={(e) => e.stopPropagation()}>
|
||||||
<button type="button" title="缩小" onClick={zoomCanvasOut}>−</button>
|
<button type="button" title="缩小" onClick={zoomCanvasOut}>−</button>
|
||||||
<button type="button" className="studio-canvas-zoom-controls__pct" title="重置缩放" onClick={resetCanvasZoom}>
|
<button type="button" className="studio-canvas-zoom-controls__pct" title="重置缩放" onClick={resetCanvasZoom}>
|
||||||
|
|||||||
@@ -523,6 +523,12 @@ const formatUploadedImageRatio = (image?: CloneImageItem) => {
|
|||||||
if (!image.width || !image.height) return `上传图片\u00a0\u00a0\u00a0原图比例${format}`;
|
if (!image.width || !image.height) return `上传图片\u00a0\u00a0\u00a0原图比例${format}`;
|
||||||
return `上传图片 ${image.width}×${image.height}px\u00a0\u00a0\u00a0${formatAspectRatio(image.width, image.height)}${format}`;
|
return `上传图片 ${image.width}×${image.height}px\u00a0\u00a0\u00a0${formatAspectRatio(image.width, image.height)}${format}`;
|
||||||
};
|
};
|
||||||
|
const formatProductImageSpec = (image?: CloneImageItem | null) => {
|
||||||
|
if (!image) return "等待上传";
|
||||||
|
const format = image.format ? ` · ${image.format}` : "";
|
||||||
|
if (!image.width || !image.height) return `正在识别尺寸${format}`;
|
||||||
|
return `${image.width}×${image.height}px · ${formatAspectRatio(image.width, image.height)}${format}`;
|
||||||
|
};
|
||||||
const defaultMarketLanguageOption = marketLanguageOptions[0]!;
|
const defaultMarketLanguageOption = marketLanguageOptions[0]!;
|
||||||
const normalizeMarket = (value: string) =>
|
const normalizeMarket = (value: string) =>
|
||||||
marketLanguageOptions.some((option) => option.country === value) ? value : defaultMarketLanguageOption.country;
|
marketLanguageOptions.some((option) => option.country === value) ? value : defaultMarketLanguageOption.country;
|
||||||
@@ -778,6 +784,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<{ src: string; label: string } | null>(null);
|
const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<{ src: string; label: string } | null>(null);
|
||||||
const [showHostingModal, setShowHostingModal] = useState(false);
|
const [showHostingModal, setShowHostingModal] = useState(false);
|
||||||
const [productImages, setProductImages] = useState<CloneImageItem[]>([]);
|
const [productImages, setProductImages] = useState<CloneImageItem[]>([]);
|
||||||
|
const [selectedProductImageId, setSelectedProductImageId] = useState<string | null>(null);
|
||||||
const [isProductUploadDragging, setIsProductUploadDragging] = useState(false);
|
const [isProductUploadDragging, setIsProductUploadDragging] = useState(false);
|
||||||
const [cloneOutput, setCloneOutput] = useState<CloneOutputKey>("detail");
|
const [cloneOutput, setCloneOutput] = useState<CloneOutputKey>("detail");
|
||||||
const [openCloneBasicSelect, setOpenCloneBasicSelect] = useState<CloneBasicSelectKey | null>(null);
|
const [openCloneBasicSelect, setOpenCloneBasicSelect] = useState<CloneBasicSelectKey | null>(null);
|
||||||
@@ -862,6 +869,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const selectedProductSetOutput =
|
const selectedProductSetOutput =
|
||||||
productSetOutputOptions.find((option) => option.key === productSetOutput) ?? productSetOutputOptions[0]!;
|
productSetOutputOptions.find((option) => option.key === productSetOutput) ?? productSetOutputOptions[0]!;
|
||||||
const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!;
|
const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!;
|
||||||
|
const selectedProductImage = productImages.find((image) => image.id === selectedProductImageId) ?? productImages[0] ?? null;
|
||||||
|
const selectedProductImageIndex = selectedProductImage
|
||||||
|
? productImages.findIndex((image) => image.id === selectedProductImage.id)
|
||||||
|
: -1;
|
||||||
|
const selectedProductImageLabel = selectedProductImageIndex >= 0 ? `商品图 ${selectedProductImageIndex + 1}` : "商品图";
|
||||||
|
const selectedProductImageSpec = formatProductImageSpec(selectedProductImage);
|
||||||
|
const isProductImageLimitReached = productImages.length >= maxCloneProductImages;
|
||||||
const productSetPreviewReady = productSetStatus === "done";
|
const productSetPreviewReady = productSetStatus === "done";
|
||||||
const cloneSetTotal = Object.values(cloneSetCounts).reduce((sum, value) => sum + value, 0);
|
const cloneSetTotal = Object.values(cloneSetCounts).reduce((sum, value) => sum + value, 0);
|
||||||
const canGenerateSet = setImages.length > 0 && productSetStatus !== "generating";
|
const canGenerateSet = setImages.length > 0 && productSetStatus !== "generating";
|
||||||
@@ -890,6 +904,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!productImages.length) {
|
||||||
|
if (selectedProductImageId !== null) setSelectedProductImageId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectedProductImageId || !productImages.some((image) => image.id === selectedProductImageId)) {
|
||||||
|
setSelectedProductImageId(productImages[0].id);
|
||||||
|
}
|
||||||
|
}, [productImages, selectedProductImageId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
productImages
|
||||||
|
.filter((item) => !item.width || !item.height)
|
||||||
|
.forEach((item) => {
|
||||||
|
readImageDimensions(item.src)
|
||||||
|
.then(({ width, height }) => {
|
||||||
|
setProductImages((current) =>
|
||||||
|
current.map((currentItem) => (currentItem.id === item.id ? { ...currentItem, width, height } : currentItem)),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
});
|
||||||
|
}, [productImages]);
|
||||||
|
|
||||||
const addSetImages = (files: File[]) => {
|
const addSetImages = (files: File[]) => {
|
||||||
if (setImages.length >= 3) return;
|
if (setImages.length >= 3) return;
|
||||||
const imageFiles = files.filter((file) => file.type.startsWith("image/"));
|
const imageFiles = files.filter((file) => file.type.startsWith("image/"));
|
||||||
@@ -945,6 +983,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const handleProductDrop = (event: DragEvent<HTMLDivElement>) => {
|
const handleProductDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setIsProductUploadDragging(false);
|
setIsProductUploadDragging(false);
|
||||||
|
if (isProductImageLimitReached) return;
|
||||||
const files = Array.from(event.dataTransfer.files);
|
const files = Array.from(event.dataTransfer.files);
|
||||||
if (files.length) addProductImages(files);
|
if (files.length) addProductImages(files);
|
||||||
};
|
};
|
||||||
@@ -1815,6 +1854,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
setSelectedProductSetPreview(null);
|
setSelectedProductSetPreview(null);
|
||||||
setShowHostingModal(false);
|
setShowHostingModal(false);
|
||||||
setProductImages([]);
|
setProductImages([]);
|
||||||
|
setSelectedProductImageId(null);
|
||||||
setIsProductUploadDragging(false);
|
setIsProductUploadDragging(false);
|
||||||
setCloneOutput("detail");
|
setCloneOutput("detail");
|
||||||
setRatio((current) => normalizeRatioForPlatform(platform, current, "detail"));
|
setRatio((current) => normalizeRatioForPlatform(platform, current, "detail"));
|
||||||
@@ -2061,18 +2101,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
</h2>
|
</h2>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={isProductImageLimitReached ? -1 : 0}
|
||||||
className={`clone-ai-upload-zone${isProductUploadDragging ? " is-dragging" : ""}`}
|
aria-disabled={isProductImageLimitReached}
|
||||||
onClick={() => productInputRef.current?.click()}
|
className={`clone-ai-upload-zone${isProductUploadDragging ? " is-dragging" : ""}${isProductImageLimitReached ? " is-full" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (isProductImageLimitReached) return;
|
||||||
|
productInputRef.current?.click();
|
||||||
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.target !== event.currentTarget) return;
|
if (event.target !== event.currentTarget) return;
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (isProductImageLimitReached) return;
|
||||||
productInputRef.current?.click();
|
productInputRef.current?.click();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDragEnter={(event) => {
|
onDragEnter={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (isProductImageLimitReached) return;
|
||||||
setIsProductUploadDragging(true);
|
setIsProductUploadDragging(true);
|
||||||
}}
|
}}
|
||||||
onDragOver={(event) => event.preventDefault()}
|
onDragOver={(event) => event.preventDefault()}
|
||||||
@@ -2085,35 +2131,68 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
</span>
|
</span>
|
||||||
<span className="clone-ai-upload-title">拖拽或点击上传</span>
|
<span className="clone-ai-upload-title">拖拽或点击上传</span>
|
||||||
<strong>
|
<strong>
|
||||||
|
{isProductImageLimitReached ? (
|
||||||
|
"已达上限"
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<span aria-hidden="true">+</span>
|
<span aria-hidden="true">+</span>
|
||||||
上传图片
|
上传图片
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</strong>
|
</strong>
|
||||||
<span className="clone-ai-upload-hint">同一产品,最多 7 张</span>
|
<span className="clone-ai-upload-hint">同一产品,最多 7 张</span>
|
||||||
</div>
|
</div>
|
||||||
{productImages.length ? (
|
{productImages.length ? (
|
||||||
<div className="clone-ai-uploaded-files" aria-label="已上传商品原图">
|
<div className="clone-ai-upload-preview-wrap" onClick={(event) => event.stopPropagation()} aria-live="polite">
|
||||||
{productImages.map((item) => (
|
<div className="clone-ai-upload-preview">
|
||||||
<figure key={item.id} className="clone-ai-uploaded-file">
|
<img src={selectedProductImage?.src ?? productImages[0].src} alt={`当前预览:${selectedProductImageLabel}`} />
|
||||||
<img src={item.src} alt={item.name} />
|
</div>
|
||||||
<span className="uploaded-image-zoom" aria-hidden="true">
|
<div className="clone-ai-upload-preview__meta">
|
||||||
<img src={item.src} alt="" />
|
<span>
|
||||||
|
<b>{selectedProductImageLabel}</b>
|
||||||
|
<em title={selectedProductImage?.name ?? productImages[0].name}>{selectedProductImageSpec}</em>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{productImages.length ? (
|
||||||
|
<div className="clone-ai-uploaded-stack">
|
||||||
|
<div className="clone-ai-uploaded-head">
|
||||||
|
<span>已上传素材</span>
|
||||||
|
<b>{productImages.length}/{maxCloneProductImages}</b>
|
||||||
|
</div>
|
||||||
|
<div className="clone-ai-uploaded-files" aria-label="已上传商品原图">
|
||||||
|
{productImages.map((item, index) => (
|
||||||
|
<figure key={item.id} className={`clone-ai-uploaded-file${item.id === selectedProductImage?.id ? " is-active" : ""}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="clone-ai-uploaded-file__thumb"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setSelectedProductImageId(item.id);
|
||||||
|
}}
|
||||||
|
aria-label={`预览商品图 ${index + 1}`}
|
||||||
|
>
|
||||||
|
<img src={item.src} alt={`商品图 ${index + 1}`} />
|
||||||
|
<span>{index + 1}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
removeProductImage(item.id);
|
removeProductImage(item.id);
|
||||||
}}
|
}}
|
||||||
aria-label={`删除${item.name}`}
|
aria-label={`删除商品图 ${index + 1}`}
|
||||||
>
|
>
|
||||||
<CloseOutlined />
|
<CloseOutlined />
|
||||||
</button>
|
</button>
|
||||||
</figure>
|
</figure>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<input ref={productInputRef} type="file" accept="image/*" multiple onChange={handleProductUpload} />
|
<input ref={productInputRef} type="file" accept="image/*" multiple disabled={isProductImageLimitReached} onChange={handleProductUpload} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="clone-ai-card">
|
<section className="clone-ai-card">
|
||||||
@@ -2873,6 +2952,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
<span>
|
<span>
|
||||||
上传商品图,AI 即刻生成 <b>符合多电商平台规范</b> 的高转化率商品素材。
|
上传商品图,AI 即刻生成 <b>符合多电商平台规范</b> 的高转化率商品素材。
|
||||||
</span>
|
</span>
|
||||||
|
<div className="clone-ai-preview-summary" aria-label="当前生成配置">
|
||||||
|
<span>{selectedCloneOutput.label}</span>
|
||||||
|
<span>{platform}</span>
|
||||||
|
<span>{market}</span>
|
||||||
|
<span>{language}</span>
|
||||||
|
<span>{formatRatioDisplayValue(ratio)}</span>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{status === "done" ? (
|
{status === "done" ? (
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import {
|
|||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
FileImageOutlined,
|
||||||
|
FolderOpenOutlined,
|
||||||
LockOutlined,
|
LockOutlined,
|
||||||
MailOutlined,
|
MailOutlined,
|
||||||
MobileOutlined,
|
MobileOutlined,
|
||||||
PhoneOutlined,
|
PhoneOutlined,
|
||||||
|
PlayCircleOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
SafetyOutlined,
|
SafetyOutlined,
|
||||||
ShareAltOutlined,
|
ShareAltOutlined,
|
||||||
@@ -179,6 +182,19 @@ function formatAssetStatus(status: string | undefined): string {
|
|||||||
return status || "资产";
|
return status || "资产";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAssetType(type: SavedAssetItem["type"]): string {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
character: "角色",
|
||||||
|
scene: "场景",
|
||||||
|
prop: "道具",
|
||||||
|
video: "视频",
|
||||||
|
image: "图像",
|
||||||
|
asset: "资产",
|
||||||
|
other: "素材",
|
||||||
|
};
|
||||||
|
return labels[type] || "素材";
|
||||||
|
}
|
||||||
|
|
||||||
function ProfilePage({
|
function ProfilePage({
|
||||||
session,
|
session,
|
||||||
usage,
|
usage,
|
||||||
@@ -527,21 +543,49 @@ function ProfilePage({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderCardPreview = (
|
||||||
|
url: string | null | undefined,
|
||||||
|
type: "image" | "video" | "project" | "asset",
|
||||||
|
label: string,
|
||||||
|
) => {
|
||||||
|
const mediaUrl = typeof url === "string" ? url.trim() : "";
|
||||||
|
const isVideoPreview = type === "video" || /\.(mp4|webm|mov)(\?|#|$)/i.test(mediaUrl);
|
||||||
|
const placeholderIcon =
|
||||||
|
type === "video" ? <PlayCircleOutlined /> : type === "project" ? <FolderOpenOutlined /> : <FileImageOutlined />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`profile-page__list-card-preview${mediaUrl ? " has-media" : ""}`} aria-hidden="true">
|
||||||
|
{mediaUrl ? (
|
||||||
|
isVideoPreview ? (
|
||||||
|
<video src={mediaUrl} muted playsInline preload="metadata" />
|
||||||
|
) : (
|
||||||
|
<img src={mediaUrl} alt="" loading="lazy" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span className="profile-page__list-card-placeholder">{placeholderIcon}</span>
|
||||||
|
)}
|
||||||
|
<span className="profile-page__media-badge">{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderActivePanel = () => {
|
const renderActivePanel = () => {
|
||||||
if (activePanel === "works") {
|
if (activePanel === "works") {
|
||||||
return visibleWorks.length ? (
|
return visibleWorks.length ? (
|
||||||
<div className="profile-page__list-grid motion-stagger">
|
<div className="profile-page__list-grid motion-stagger">
|
||||||
{visibleWorks.map((task) => (
|
{visibleWorks.map((task) => (
|
||||||
<article key={task.id} className="profile-page__list-card">
|
<article key={task.id} className="profile-page__list-card profile-page__media-card">
|
||||||
|
{renderCardPreview(task.outputUrl, task.type === "video" ? "video" : "image", formatTaskType(task.type))}
|
||||||
|
<div className="profile-page__list-card-body">
|
||||||
<div className="profile-page__list-card-head">
|
<div className="profile-page__list-card-head">
|
||||||
<strong>{task.title}</strong>
|
<strong>{task.title}</strong>
|
||||||
<span>{formatTaskType(task.type)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p>{task.prompt}</p>
|
<p>{task.prompt}</p>
|
||||||
<div className="profile-page__list-card-meta">
|
<div className="profile-page__list-card-meta">
|
||||||
<span>{formatTaskStatus(task.status)}</span>
|
<span>{formatTaskStatus(task.status)}</span>
|
||||||
<span>{formatProfileDate(task.createdAt)}</span>
|
<span>{formatProfileDate(task.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -554,10 +598,11 @@ function ProfilePage({
|
|||||||
return projects.length ? (
|
return projects.length ? (
|
||||||
<div className="profile-page__list-grid motion-stagger">
|
<div className="profile-page__list-grid motion-stagger">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<article key={project.id} className="profile-page__list-card">
|
<article key={project.id} className="profile-page__list-card profile-page__media-card">
|
||||||
|
{renderCardPreview(project.thumbnailUrl, "project", "项目")}
|
||||||
|
<div className="profile-page__list-card-body">
|
||||||
<div className="profile-page__list-card-head">
|
<div className="profile-page__list-card-head">
|
||||||
<strong>{project.name}</strong>
|
<strong>{project.name}</strong>
|
||||||
<span>{formatProfileDate(project.updatedAt)}</span>
|
|
||||||
{onDeleteProject ? (
|
{onDeleteProject ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -572,7 +617,8 @@ function ProfilePage({
|
|||||||
<p>{project.description || "最近更新的项目"}</p>
|
<p>{project.description || "最近更新的项目"}</p>
|
||||||
<div className="profile-page__list-card-meta">
|
<div className="profile-page__list-card-meta">
|
||||||
<span>{project.storyboardCount} 节点</span>
|
<span>{project.storyboardCount} 节点</span>
|
||||||
<span>{project.imageCount} 图 / {project.videoCount} 视频</span>
|
<span>{formatProfileDate(project.updatedAt)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
@@ -586,16 +632,19 @@ function ProfilePage({
|
|||||||
return savedAssets.length ? (
|
return savedAssets.length ? (
|
||||||
<div className="profile-page__list-grid">
|
<div className="profile-page__list-grid">
|
||||||
{savedAssets.map((asset) => (
|
{savedAssets.map((asset) => (
|
||||||
<article key={asset.id} className="profile-page__list-card">
|
<article key={asset.id} className="profile-page__list-card profile-page__media-card">
|
||||||
|
{renderCardPreview(asset.imageUrl || asset.url, asset.type === "video" ? "video" : "asset", formatAssetType(asset.type))}
|
||||||
|
<div className="profile-page__list-card-body">
|
||||||
<div className="profile-page__list-card-head">
|
<div className="profile-page__list-card-head">
|
||||||
<strong>{asset.name}</strong>
|
<strong>{asset.name}</strong>
|
||||||
<span>{formatAssetStatus(asset.status)}</span>
|
<span>{formatAssetStatus(asset.status)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{asset.description}</p>
|
<p>{asset.description}</p>
|
||||||
<div className="profile-page__list-card-meta">
|
<div className="profile-page__list-card-meta">
|
||||||
<span>{asset.type}</span>
|
<span>{formatAssetType(asset.type)}</span>
|
||||||
<span>{formatProfileDate(asset.updatedAt)}</span>
|
<span>{formatProfileDate(asset.updatedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -708,6 +757,50 @@ function ProfilePage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="profile-page__account-card">
|
||||||
|
<div className="profile-page__list-tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={accountPanel === "credits" ? "is-active" : ""}
|
||||||
|
onClick={() => setAccountPanel("credits")}
|
||||||
|
>
|
||||||
|
积分 {(totalBalance / 100).toFixed(2)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={accountPanel === "tasks" ? "is-active" : ""}
|
||||||
|
onClick={() => setAccountPanel("tasks")}
|
||||||
|
>
|
||||||
|
任务 {tasks.length}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="profile-page__upload-card profile-page__upload-card--meta">
|
||||||
|
{accountPanel === "credits" ? (
|
||||||
|
<>
|
||||||
|
<span className="profile-page__meta-item">
|
||||||
|
<small>当前账号</small>
|
||||||
|
<strong>{displayName}</strong>
|
||||||
|
</span>
|
||||||
|
<span className="profile-page__meta-item">
|
||||||
|
<small>积分剩余</small>
|
||||||
|
<strong>{(usage.balanceCents / 100).toFixed(2)}</strong>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="profile-page__meta-item">
|
||||||
|
<small>任务总数</small>
|
||||||
|
<strong>{tasks.length}</strong>
|
||||||
|
</span>
|
||||||
|
<span className="profile-page__meta-item">
|
||||||
|
<small>已完成</small>
|
||||||
|
<strong>{completedTasks.length}</strong>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" className="profile-page__share-btn profile-page__share-btn--plan">
|
<button type="button" className="profile-page__share-btn profile-page__share-btn--plan">
|
||||||
<ShareAltOutlined />
|
<ShareAltOutlined />
|
||||||
{packageLabel}
|
{packageLabel}
|
||||||
@@ -755,52 +848,6 @@ function ProfilePage({
|
|||||||
</span>
|
</span>
|
||||||
{renderActivePanel()}
|
{renderActivePanel()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="profile-page__section">
|
|
||||||
<div className="profile-page__list-bar">
|
|
||||||
<div className="profile-page__list-tabs">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={accountPanel === "credits" ? "is-active" : ""}
|
|
||||||
onClick={() => setAccountPanel("credits")}
|
|
||||||
>
|
|
||||||
积分 {(totalBalance / 100).toFixed(2)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={accountPanel === "tasks" ? "is-active" : ""}
|
|
||||||
onClick={() => setAccountPanel("tasks")}
|
|
||||||
>
|
|
||||||
任务 {tasks.length}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="profile-page__upload-card profile-page__upload-card--meta">
|
|
||||||
{accountPanel === "credits" ? (
|
|
||||||
<>
|
|
||||||
<span className="profile-page__meta-item">
|
|
||||||
<small>当前账号</small>
|
|
||||||
<strong>{displayName}</strong>
|
|
||||||
</span>
|
|
||||||
<span className="profile-page__meta-item">
|
|
||||||
<small>积分剩余</small>
|
|
||||||
<strong>{(usage.balanceCents / 100).toFixed(2)}</strong>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="profile-page__meta-item">
|
|
||||||
<small>任务总数</small>
|
|
||||||
<strong>{tasks.length}</strong>
|
|
||||||
</span>
|
|
||||||
<span className="profile-page__meta-item">
|
|
||||||
<small>已完成</small>
|
|
||||||
<strong>{completedTasks.length}</strong>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -850,7 +897,6 @@ function ProfilePage({
|
|||||||
<span className="auth-page__logo">
|
<span className="auth-page__logo">
|
||||||
<img src={AUTH_LOGO_URL} alt="OmniAI" />
|
<img src={AUTH_LOGO_URL} alt="OmniAI" />
|
||||||
</span>
|
</span>
|
||||||
<span className="auth-page__form-kicker">{mode === "login" ? "账户登录" : "新用户注册"}</span>
|
|
||||||
<h2 className="auth-page__title">{mode === "login" ? "欢迎回来" : "创建账号"}</h2>
|
<h2 className="auth-page__title">{mode === "login" ? "欢迎回来" : "创建账号"}</h2>
|
||||||
<p className="auth-page__subtitle">
|
<p className="auth-page__subtitle">
|
||||||
{mode === "login" ? "登录后继续你的 AI 创作之旅" : "注册即可免费体验全部功能"}
|
{mode === "login" ? "登录后继续你的 AI 创作之旅" : "注册即可免费体验全部功能"}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
|
BarChartOutlined,
|
||||||
CheckCircleFilled,
|
CheckCircleFilled,
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
|
ThunderboltOutlined,
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
|
import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
|
||||||
@@ -168,6 +171,12 @@ function normalizeUploadedText(raw: string, ext: string): string {
|
|||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatFileSize(size: number): string {
|
||||||
|
if (size < 1024) return `${size} B`;
|
||||||
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
const SCORE_DIMENSIONS: ScoreDimension[] = [
|
const SCORE_DIMENSIONS: ScoreDimension[] = [
|
||||||
{ key: "hook", label: "钩子设计", maxScore: 20, hint: "开篇吸引力·悬念设置·黄金三秒", detail: "开篇即抛出高概念钩子,悬念设置紧凑有力。" },
|
{ key: "hook", label: "钩子设计", maxScore: 20, hint: "开篇吸引力·悬念设置·黄金三秒", detail: "开篇即抛出高概念钩子,悬念设置紧凑有力。" },
|
||||||
{ key: "character", label: "角色塑造", maxScore: 15, hint: "人物立体度·动机合理性·弧光设计", detail: "主角动机有铺垫,配角功能性较强,人物弧光尚可进一步深化。" },
|
{ key: "character", label: "角色塑造", maxScore: 15, hint: "人物立体度·动机合理性·弧光设计", detail: "主角动机有铺垫,配角功能性较强,人物弧光尚可进一步深化。" },
|
||||||
@@ -346,12 +355,14 @@ function ScriptTokensPage() {
|
|||||||
const compactTitle = uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? "剧本评测";
|
const compactTitle = uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? "剧本评测";
|
||||||
const scriptMinutes = Math.max(8, Math.round(script.length / 460));
|
const scriptMinutes = Math.max(8, Math.round(script.length / 460));
|
||||||
const reportDate = new Date().toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit" });
|
const reportDate = new Date().toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit" });
|
||||||
|
const statusClass = loading ? "is-loading" : result ? "is-complete" : hasContent ? "is-ready" : "is-idle";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="script-eval-v5 page-motion">
|
<section className={`script-eval-v5 page-motion ${statusClass}`}>
|
||||||
<div className="script-eval-v5-page">
|
<div className="script-eval-v5-page">
|
||||||
{/* Left Panel */}
|
{/* Left Panel */}
|
||||||
<aside className="script-eval-v5-left">
|
<aside className="script-eval-v5-left">
|
||||||
|
<div className="script-eval-v5-left-main">
|
||||||
<div className="script-eval-v5-lp-section">
|
<div className="script-eval-v5-lp-section">
|
||||||
<div className="script-eval-v5-lp-label">上传剧本</div>
|
<div className="script-eval-v5-lp-label">上传剧本</div>
|
||||||
<div
|
<div
|
||||||
@@ -364,7 +375,10 @@ function ScriptTokensPage() {
|
|||||||
{uploadedFile ? (
|
{uploadedFile ? (
|
||||||
<div className="script-eval-v5-upload-done is-show">
|
<div className="script-eval-v5-upload-done is-show">
|
||||||
<CheckCircleFilled />
|
<CheckCircleFilled />
|
||||||
|
<span className="script-eval-v5-uf-meta">
|
||||||
<span className="script-eval-v5-uf-name">{uploadedFile.name}</span>
|
<span className="script-eval-v5-uf-name">{uploadedFile.name}</span>
|
||||||
|
<span className="script-eval-v5-uf-size">{formatFileSize(uploadedFile.size)}</span>
|
||||||
|
</span>
|
||||||
<span className="script-eval-v5-uf-re" onClick={(e) => { e.stopPropagation(); handleReset(); }}>
|
<span className="script-eval-v5-uf-re" onClick={(e) => { e.stopPropagation(); handleReset(); }}>
|
||||||
重新上传
|
重新上传
|
||||||
</span>
|
</span>
|
||||||
@@ -374,7 +388,7 @@ function ScriptTokensPage() {
|
|||||||
<div className="script-eval-v5-upload-icon"><UploadOutlined /></div>
|
<div className="script-eval-v5-upload-icon"><UploadOutlined /></div>
|
||||||
<div className="script-eval-v5-upload-text">拖拽或点击上传</div>
|
<div className="script-eval-v5-upload-text">拖拽或点击上传</div>
|
||||||
<button type="button" className="script-eval-v5-upload-btn" onClick={(e) => { e.stopPropagation(); fileInputRef.current?.click(); }}>
|
<button type="button" className="script-eval-v5-upload-btn" onClick={(e) => { e.stopPropagation(); fileInputRef.current?.click(); }}>
|
||||||
+ 上传剧本
|
<UploadOutlined /> 选择剧本
|
||||||
</button>
|
</button>
|
||||||
<div className="script-eval-v5-upload-hint">{TEXT_FILE_HINT}</div>
|
<div className="script-eval-v5-upload-hint">{TEXT_FILE_HINT}</div>
|
||||||
</>
|
</>
|
||||||
@@ -445,12 +459,15 @@ function ScriptTokensPage() {
|
|||||||
disabled={loading || !hasContent}
|
disabled={loading || !hasContent}
|
||||||
onClick={() => void handleEvaluate()}
|
onClick={() => void handleEvaluate()}
|
||||||
>
|
>
|
||||||
{loading ? "◆ 评测中..." : "◆ 开始评测"}
|
{loading ? <LoadingOutlined /> : <ThunderboltOutlined />}
|
||||||
|
<span>{loading ? "评测中..." : "开始评测"}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="script-eval-v5-export-btn" disabled={!result} onClick={handleExportMarkdown}>
|
<button type="button" className="script-eval-v5-export-btn" disabled={!result} onClick={handleExportMarkdown}>
|
||||||
导出评测报告
|
<DownloadOutlined />
|
||||||
|
<span>导出评测报告</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Right Area */}
|
{/* Right Area */}
|
||||||
@@ -482,6 +499,11 @@ function ScriptTokensPage() {
|
|||||||
<div className="page-loading-spinner" />
|
<div className="page-loading-spinner" />
|
||||||
<strong>AI 正在分析剧本...</strong>
|
<strong>AI 正在分析剧本...</strong>
|
||||||
<p>正在调用模型进行六维评分,预计需要 15-30 秒</p>
|
<p>正在调用模型进行六维评分,预计需要 15-30 秒</p>
|
||||||
|
<div className="script-eval-v5-loading-steps" aria-hidden="true">
|
||||||
|
<span>结构识别</span>
|
||||||
|
<span>冲突评估</span>
|
||||||
|
<span>商业潜力</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -568,13 +590,23 @@ function ScriptTokensPage() {
|
|||||||
<span>0%</span>
|
<span>0%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="script-eval-report__chart-grid">
|
<div className="script-eval-report__chart-grid">
|
||||||
{SCORE_DIMENSIONS.map((dim) => {
|
{SCORE_DIMENSIONS.map((dim, dimIndex) => {
|
||||||
const score = result.dimensionScores[dim.key] ?? 0;
|
const score = result.dimensionScores[dim.key] ?? 0;
|
||||||
const pct = Math.max(0, Math.min(1, score / dim.maxScore));
|
const pct = Math.max(0, Math.min(1, score / dim.maxScore));
|
||||||
const lossPct = 1 - pct;
|
const lossPct = 1 - pct;
|
||||||
const isPerfect = score === dim.maxScore;
|
const isPerfect = score === dim.maxScore;
|
||||||
|
const isActive = activeDim === null || activeDim === dimIndex;
|
||||||
return (
|
return (
|
||||||
<button key={dim.key} type="button" className="script-eval-report__bar-col">
|
<button
|
||||||
|
key={dim.key}
|
||||||
|
type="button"
|
||||||
|
className={`script-eval-report__bar-col${isActive ? "" : " is-dimmed"}`}
|
||||||
|
onMouseEnter={() => setActiveDim(dimIndex)}
|
||||||
|
onFocus={() => setActiveDim(dimIndex)}
|
||||||
|
onMouseLeave={() => setActiveDim(null)}
|
||||||
|
onBlur={() => setActiveDim(null)}
|
||||||
|
aria-label={`${dim.label} ${score}/${dim.maxScore},${dim.hint}`}
|
||||||
|
>
|
||||||
<div className="script-eval-report__bar-score">
|
<div className="script-eval-report__bar-score">
|
||||||
<b>{score}</b><small>/{dim.maxScore}</small>{isPerfect ? <em>*</em> : null}
|
<b>{score}</b><small>/{dim.maxScore}</small>{isPerfect ? <em>*</em> : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -589,6 +621,14 @@ function ScriptTokensPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="script-eval-report__chart-note">
|
||||||
|
<BarChartOutlined />
|
||||||
|
<span>
|
||||||
|
{activeDim === null
|
||||||
|
? "悬停维度可查看当前分项表现,优先从低分项制定改稿计划。"
|
||||||
|
: `${SCORE_DIMENSIONS[activeDim].label}:${SCORE_DIMENSIONS[activeDim].detail}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="script-eval-report__findings">
|
<div className="script-eval-report__findings">
|
||||||
|
|||||||
@@ -230,25 +230,35 @@ function TokenUsagePage({
|
|||||||
{ label: "账户类型", value: isEnterpriseAccount ? "企业账户" : "个人账户", tone: "good" },
|
{ label: "账户类型", value: isEnterpriseAccount ? "企业账户" : "个人账户", tone: "good" },
|
||||||
{ label: "企业空间", value: enterpriseUsage?.enterpriseName || session?.user.enterpriseName || "-" },
|
{ label: "企业空间", value: enterpriseUsage?.enterpriseName || session?.user.enterpriseName || "-" },
|
||||||
];
|
];
|
||||||
|
const pageStatusClass = enterpriseUsageLoading
|
||||||
|
? "is-syncing"
|
||||||
|
: enterpriseUsageError
|
||||||
|
? "has-sync-error"
|
||||||
|
: isLowBalance
|
||||||
|
? "has-low-balance"
|
||||||
|
: "is-healthy";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="script-token-page token-usage-page management-center-page" aria-label="管理中心">
|
<section className={`script-token-page token-usage-page management-center-page ${pageStatusClass}`} aria-label="管理中心">
|
||||||
<main className="management-center-shell">
|
<main className="management-center-shell">
|
||||||
<header className="management-center-toolbar" aria-label="管理中心操作">
|
<header className="management-center-toolbar" aria-label="管理中心操作">
|
||||||
<div className="management-center-toolbar__title">
|
<div className="management-center-toolbar__title">
|
||||||
<button type="button" className="management-center-toolbar__back" aria-label="返回工具盒" onClick={onOpenMore}>
|
<button type="button" className="management-center-toolbar__back" aria-label="返回工具盒" onClick={onOpenMore}>
|
||||||
<ArrowLeftOutlined />
|
<ArrowLeftOutlined />
|
||||||
</button>
|
</button>
|
||||||
|
<span>
|
||||||
<strong>管理中心</strong>
|
<strong>管理中心</strong>
|
||||||
|
<small>用量、成员与模型调用监控</small>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="management-center-status-pill">
|
<span className={`management-center-status-pill ${enterpriseUsageError ? "is-error" : enterpriseUsageLoading ? "is-loading" : "is-online"}`}>
|
||||||
{enterpriseUsageLoading ? "正在同步企业用量" : enterpriseUsageError || "服务器已连接"}
|
{enterpriseUsageLoading ? "正在同步企业用量" : enterpriseUsageError || "服务器已连接"}
|
||||||
</span>
|
</span>
|
||||||
<button type="button" onClick={refreshEnterpriseUsage}>
|
<button type="button" onClick={refreshEnterpriseUsage} disabled={enterpriseUsageLoading}>
|
||||||
<ReloadOutlined />
|
<ReloadOutlined />
|
||||||
刷新数据
|
刷新数据
|
||||||
</button>
|
</button>
|
||||||
<button type="button">
|
<button type="button" className="is-muted-action">
|
||||||
<UserOutlined />
|
<UserOutlined />
|
||||||
成员管理
|
成员管理
|
||||||
</button>
|
</button>
|
||||||
@@ -283,7 +293,7 @@ function TokenUsagePage({
|
|||||||
<BarChartOutlined />
|
<BarChartOutlined />
|
||||||
模型消耗分布
|
模型消耗分布
|
||||||
</h2>
|
</h2>
|
||||||
<span>{modelBreakdown.length ? `${modelBreakdown.length} 个模型` : "LIVE"}</span>
|
<span>{enterpriseUsageLoading ? "SYNC" : modelBreakdown.length ? `${modelBreakdown.length} 个模型` : "LIVE"}</span>
|
||||||
</div>
|
</div>
|
||||||
{modelBreakdown.length ? (
|
{modelBreakdown.length ? (
|
||||||
<div className="management-model-list">
|
<div className="management-model-list">
|
||||||
@@ -310,7 +320,10 @@ function TokenUsagePage({
|
|||||||
|
|
||||||
<article className="management-card management-status-card">
|
<article className="management-card management-status-card">
|
||||||
<div className="management-card__head">
|
<div className="management-card__head">
|
||||||
<h2>系统状态</h2>
|
<h2>
|
||||||
|
<LineChartOutlined />
|
||||||
|
系统状态
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<dl>
|
<dl>
|
||||||
{systemStatus.map((item) => (
|
{systemStatus.map((item) => (
|
||||||
@@ -364,7 +377,10 @@ function TokenUsagePage({
|
|||||||
|
|
||||||
<section className="management-card management-records">
|
<section className="management-card management-records">
|
||||||
<div className="management-card__head">
|
<div className="management-card__head">
|
||||||
<h2>调用记录</h2>
|
<h2>
|
||||||
|
<BarChartOutlined />
|
||||||
|
调用记录
|
||||||
|
</h2>
|
||||||
<span>{records.length} 条记录</span>
|
<span>{records.length} 条记录</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="management-record-table" role="table" aria-label="调用记录">
|
<div className="management-record-table" role="table" aria-label="调用记录">
|
||||||
|
|||||||
@@ -3,6 +3,24 @@ import type { ReactNode } from "react";
|
|||||||
import type { WorkbenchOption, WorkbenchFieldGroup } from "./workbenchConstants";
|
import type { WorkbenchOption, WorkbenchFieldGroup } from "./workbenchConstants";
|
||||||
import { getRatioOptionClassName, getSettingsGridColumnsClassName } from "./workbenchReferenceUtils";
|
import { getRatioOptionClassName, getSettingsGridColumnsClassName } from "./workbenchReferenceUtils";
|
||||||
|
|
||||||
|
const VIDEO_MODEL_ICON_URLS = {
|
||||||
|
happyHorse: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/HappyHorse.svg",
|
||||||
|
pixverse: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/Pixverse.svg",
|
||||||
|
vidu: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/viduQ3.svg",
|
||||||
|
wanxiang: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/wan.svg",
|
||||||
|
kling: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/kling.svg",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function getVideoModelIconUrl(option: WorkbenchOption): string | null {
|
||||||
|
const text = `${option.value} ${option.label}`.toLowerCase();
|
||||||
|
if (text.includes("happyhorse")) return VIDEO_MODEL_ICON_URLS.happyHorse;
|
||||||
|
if (text.includes("pixverse")) return VIDEO_MODEL_ICON_URLS.pixverse;
|
||||||
|
if (text.includes("vidu")) return VIDEO_MODEL_ICON_URLS.vidu;
|
||||||
|
if (text.includes("wan") || text.includes("万相")) return VIDEO_MODEL_ICON_URLS.wanxiang;
|
||||||
|
if (text.includes("kling") || text.includes("可灵")) return VIDEO_MODEL_ICON_URLS.kling;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function SelectChip({
|
export function SelectChip({
|
||||||
chipId,
|
chipId,
|
||||||
value,
|
value,
|
||||||
@@ -56,6 +74,7 @@ export function SelectChip({
|
|||||||
>
|
>
|
||||||
{options.map((option, index) => {
|
{options.map((option, index) => {
|
||||||
const active = option.value === value;
|
const active = option.value === value;
|
||||||
|
const iconUrl = chipId === "video-model" ? getVideoModelIconUrl(option) : null;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
@@ -71,6 +90,11 @@ export function SelectChip({
|
|||||||
>
|
>
|
||||||
<span className="ai-workbench-select-chip__option-label">
|
<span className="ai-workbench-select-chip__option-label">
|
||||||
<span className="ai-workbench-select-chip__option-dot" aria-hidden="true" />
|
<span className="ai-workbench-select-chip__option-dot" aria-hidden="true" />
|
||||||
|
{iconUrl ? (
|
||||||
|
<span className="ai-workbench-select-chip__option-icon" aria-hidden="true">
|
||||||
|
<img src={iconUrl} alt="" loading="lazy" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
<span className="ai-workbench-select-chip__option-copy">
|
<span className="ai-workbench-select-chip__option-copy">
|
||||||
<span className="ai-workbench-select-chip__option-title">
|
<span className="ai-workbench-select-chip__option-title">
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
|
|||||||
+1184
-1
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5377,3 +5377,559 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Token usage commercial SaaS polish ===== */
|
||||||
|
.token-usage-page.management-center-page {
|
||||||
|
--usage-panel: rgba(17, 21, 21, 0.96);
|
||||||
|
--usage-panel-strong: rgba(21, 26, 25, 0.98);
|
||||||
|
--usage-inset: rgba(255, 255, 255, 0.035);
|
||||||
|
--usage-inset-strong: rgba(255, 255, 255, 0.055);
|
||||||
|
--usage-line: rgba(255, 255, 255, 0.08);
|
||||||
|
--usage-line-strong: rgba(var(--accent-rgb), 0.28);
|
||||||
|
--usage-muted: rgba(232, 240, 235, 0.66);
|
||||||
|
--usage-soft: rgba(232, 240, 235, 0.44);
|
||||||
|
--usage-card-shadow: 0 18px 46px rgba(0, 0, 0, 0.22);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% 0%, rgba(var(--accent-rgb), 0.06), transparent 34%),
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.022), transparent 220px),
|
||||||
|
var(--bg-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-shell {
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 30px 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 8;
|
||||||
|
min-height: 64px;
|
||||||
|
border-bottom-color: var(--usage-line);
|
||||||
|
border-bottom-left-radius: 18px;
|
||||||
|
background: rgba(14, 17, 17, 0.88);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar__title {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar__title > span {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar__title strong {
|
||||||
|
color: #f2f8f5;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar__title small {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--usage-soft);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar button,
|
||||||
|
.token-usage-page .management-card__head button,
|
||||||
|
.token-usage-page .management-center-status-pill {
|
||||||
|
border-color: var(--usage-line);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--usage-inset);
|
||||||
|
color: var(--usage-muted);
|
||||||
|
transition: border-color 160ms ease, background 160ms ease, color 160ms ease, transform 160ms ease, opacity 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar button:hover:not(:disabled),
|
||||||
|
.token-usage-page .management-card__head button:hover {
|
||||||
|
border-color: var(--usage-line-strong);
|
||||||
|
background: rgba(var(--accent-rgb), 0.08);
|
||||||
|
color: var(--fg-body);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar button:disabled {
|
||||||
|
opacity: 0.52;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar__back {
|
||||||
|
border-radius: 999px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar button.is-muted-action {
|
||||||
|
color: var(--usage-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar button.is-primary {
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.72);
|
||||||
|
background: linear-gradient(180deg, #2fffa5, var(--accent));
|
||||||
|
color: rgb(5, 15, 11);
|
||||||
|
box-shadow: 0 12px 26px rgba(var(--accent-rgb), 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-status-pill {
|
||||||
|
position: relative;
|
||||||
|
gap: 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-status-pill::before {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 14px rgba(var(--accent-rgb), 0.45);
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-status-pill.is-loading::before {
|
||||||
|
animation: token-usage-pulse 1.2s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-status-pill.is-error {
|
||||||
|
border-color: rgba(245, 158, 11, 0.42);
|
||||||
|
background: rgba(245, 158, 11, 0.1);
|
||||||
|
color: #f7ca73;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-status-pill.is-error::before {
|
||||||
|
background: #f59e0b;
|
||||||
|
box-shadow: 0 0 14px rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes token-usage-pulse {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.42; transform: scale(0.76); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-balance-alert {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
border-color: rgba(245, 158, 11, 0.34);
|
||||||
|
border-radius: 14px;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(245, 158, 11, 0.14), rgba(245, 158, 11, 0.045)),
|
||||||
|
var(--usage-panel);
|
||||||
|
box-shadow: var(--usage-card-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-balance-alert button {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-cards {
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card {
|
||||||
|
position: relative;
|
||||||
|
min-height: 132px;
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 18px;
|
||||||
|
border-color: var(--usage-line);
|
||||||
|
border-radius: 18px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.048), transparent 70%),
|
||||||
|
var(--usage-panel-strong);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
width: 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card.is-accent {
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.32);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 88% 16%, rgba(var(--accent-rgb), 0.18), transparent 38%),
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.052), transparent 72%),
|
||||||
|
var(--usage-panel-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card.is-accent::before {
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 18px rgba(var(--accent-rgb), 0.44);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card.is-warn::before {
|
||||||
|
background: #f59e0b;
|
||||||
|
box-shadow: 0 0 18px rgba(245, 158, 11, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card__index {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
right: 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.14);
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 950;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card__label {
|
||||||
|
color: var(--usage-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card__value {
|
||||||
|
color: #f6fbf8;
|
||||||
|
font-size: clamp(24px, 2.5vw, 34px);
|
||||||
|
font-weight: 920;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card__hint {
|
||||||
|
color: var(--usage-soft);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-overview {
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-card {
|
||||||
|
border-color: var(--usage-line);
|
||||||
|
border-radius: 18px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 64%),
|
||||||
|
var(--usage-panel);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.026);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-card__head {
|
||||||
|
min-height: 50px;
|
||||||
|
border-bottom-color: var(--usage-line);
|
||||||
|
padding-inline: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-card__head h2 {
|
||||||
|
color: #ecf5f0;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-card__head h2 .anticon {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-card__head > span,
|
||||||
|
.token-usage-page .management-card__head button {
|
||||||
|
border-color: var(--usage-line);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--usage-inset);
|
||||||
|
color: var(--usage-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-card--chart {
|
||||||
|
height: clamp(390px, 50vh, 580px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-empty-chart,
|
||||||
|
.token-usage-page .management-record-empty,
|
||||||
|
.token-usage-page .management-status-trend__empty {
|
||||||
|
border: 1px dashed var(--usage-line);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.024);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-model-list {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-model-bar {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.045);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--usage-inset);
|
||||||
|
transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-model-bar:hover {
|
||||||
|
border-color: var(--usage-line-strong);
|
||||||
|
background: rgba(var(--accent-rgb), 0.052);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-model-bar__top strong {
|
||||||
|
color: #eef6f2;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-model-bar__track {
|
||||||
|
height: 7px;
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-status-card dl {
|
||||||
|
padding: 14px 18px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-status-card div {
|
||||||
|
min-height: 38px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.045);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-status-card div:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-status-card dt {
|
||||||
|
color: var(--usage-soft);
|
||||||
|
font-weight: 780;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-status-trend {
|
||||||
|
padding: 14px 18px 18px;
|
||||||
|
border-top-color: var(--usage-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-status-trend__title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--usage-muted);
|
||||||
|
font-weight: 820;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .usage-trend__svg {
|
||||||
|
min-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .usage-trend__line {
|
||||||
|
filter: drop-shadow(0 0 8px rgba(var(--accent-rgb), 0.22));
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .usage-trend__dot {
|
||||||
|
transition: r 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .usage-trend__meta {
|
||||||
|
border-top-color: var(--usage-line);
|
||||||
|
color: var(--usage-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-members,
|
||||||
|
.token-usage-page .management-records {
|
||||||
|
padding-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-member-list {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 18px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-member-row {
|
||||||
|
min-height: 64px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.045);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--usage-inset);
|
||||||
|
transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-member-row:hover {
|
||||||
|
border-color: var(--usage-line-strong);
|
||||||
|
background: rgba(var(--accent-rgb), 0.052);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-member-avatar {
|
||||||
|
color: rgb(5, 15, 11);
|
||||||
|
box-shadow: 0 8px 20px rgba(var(--accent-rgb), 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-member-role {
|
||||||
|
border-color: var(--usage-line);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-table {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 14px 18px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-table__head,
|
||||||
|
.token-usage-page .management-record-table__row {
|
||||||
|
min-width: 880px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-table__head {
|
||||||
|
min-height: 38px;
|
||||||
|
background: rgba(255, 255, 255, 0.045);
|
||||||
|
color: var(--usage-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-table__row {
|
||||||
|
min-height: 46px;
|
||||||
|
background: var(--usage-inset);
|
||||||
|
transition: border-color 160ms ease, background 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-table__row:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.065);
|
||||||
|
background: rgba(255, 255, 255, 0.052);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-table__row span.is-good,
|
||||||
|
.token-usage-page .management-record-table__row span.is-error {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 24px;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0 9px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-table__row span.is-good {
|
||||||
|
background: rgba(var(--accent-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-table__row span.is-error {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-pagination {
|
||||||
|
border-top-color: var(--usage-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-pagination button {
|
||||||
|
border-color: var(--usage-line);
|
||||||
|
border-radius: 9px;
|
||||||
|
background: var(--usage-inset);
|
||||||
|
color: var(--usage-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-record-pagination button:hover:not(:disabled) {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: rgb(5, 15, 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.token-usage-page.management-center-page {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-shell {
|
||||||
|
padding-inline: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 901px) and (max-width: 1180px) {
|
||||||
|
.token-usage-page.management-center-page {
|
||||||
|
padding-left: 82px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.token-usage-page.management-center-page {
|
||||||
|
padding-top: 74px;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-shell {
|
||||||
|
padding: 0 16px 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar {
|
||||||
|
top: 0;
|
||||||
|
align-items: stretch;
|
||||||
|
margin: 0 -16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 0 0 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-toolbar__title {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-status-pill {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-cards {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-overview {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-card--chart {
|
||||||
|
height: auto;
|
||||||
|
min-height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-member-row {
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: 10px 12px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-member-role,
|
||||||
|
.token-usage-page .management-member-row > span:not(.management-member-avatar):not(.management-member-role):not(.management-member-meter),
|
||||||
|
.token-usage-page .management-member-meter,
|
||||||
|
.token-usage-page .management-member-row > .anticon {
|
||||||
|
grid-column: 2;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.token-usage-page .management-center-toolbar button:not(.management-center-toolbar__back) {
|
||||||
|
flex: 1 1 calc(50% - 6px);
|
||||||
|
padding-inline: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-center-status-pill {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card {
|
||||||
|
min-height: 118px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-metric-card__value {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-card__head {
|
||||||
|
min-height: 46px;
|
||||||
|
padding-inline: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-usage-page .management-model-list,
|
||||||
|
.token-usage-page .management-member-list,
|
||||||
|
.token-usage-page .management-record-table {
|
||||||
|
padding-inline: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user