Merge pull request 'Feat/commercial saas polish' (#12) from feat/commercial-saas-polish into master
Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
@@ -26,7 +26,6 @@
|
||||
VideoCameraOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import {
|
||||
Background,
|
||||
ReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type MouseEvent, type WheelEvent } from "react";
|
||||
@@ -3560,7 +3559,8 @@ function CanvasPage({
|
||||
onMouseMove={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasMouseMove}
|
||||
onWheel={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasWheel}
|
||||
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-y": `${canvasViewport.y}px`,
|
||||
cursor: canvasPanDrag ? "grabbing" : spacePanning ? "grab" : undefined,
|
||||
@@ -3748,9 +3748,7 @@ function CanvasPage({
|
||||
proOptions={{ hideAttribution: true }}
|
||||
onPaneClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handlePaneClick}
|
||||
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()}>
|
||||
<button type="button" title="缩小" onClick={zoomCanvasOut}>−</button>
|
||||
<button type="button" className="studio-canvas-zoom-controls__pct" title="重置缩放" onClick={resetCanvasZoom}>
|
||||
|
||||
@@ -5,10 +5,13 @@ import {
|
||||
CloseOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
FileImageOutlined,
|
||||
FolderOpenOutlined,
|
||||
LockOutlined,
|
||||
MailOutlined,
|
||||
MobileOutlined,
|
||||
PhoneOutlined,
|
||||
PlayCircleOutlined,
|
||||
PlusOutlined,
|
||||
SafetyOutlined,
|
||||
ShareAltOutlined,
|
||||
@@ -180,6 +183,19 @@ function formatAssetStatus(status: string | undefined): string {
|
||||
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({
|
||||
session,
|
||||
usage,
|
||||
@@ -608,22 +624,50 @@ function ProfilePage({
|
||||
</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 = () => {
|
||||
if (activePanel === "works") {
|
||||
return visibleWorks.length ? (
|
||||
<div className="profile-page__works-scroll">
|
||||
<div className="profile-page__list-grid motion-stagger">
|
||||
{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">
|
||||
<strong>{task.title}</strong>
|
||||
<span>{formatTaskType(task.type)}</span>
|
||||
</div>
|
||||
<p>{task.prompt}</p>
|
||||
<div className="profile-page__list-card-meta">
|
||||
<span>{formatTaskStatus(task.status)}</span>
|
||||
<span>{formatProfileDate(task.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
@@ -637,10 +681,11 @@ function ProfilePage({
|
||||
return projects.length ? (
|
||||
<div className="profile-page__list-grid motion-stagger">
|
||||
{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">
|
||||
<strong>{project.name}</strong>
|
||||
<span>{formatProfileDate(project.updatedAt)}</span>
|
||||
{onDeleteProject ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -655,7 +700,8 @@ function ProfilePage({
|
||||
<p>{project.description || "最近更新的项目"}</p>
|
||||
<div className="profile-page__list-card-meta">
|
||||
<span>{project.storyboardCount} 节点</span>
|
||||
<span>{project.imageCount} 图 / {project.videoCount} 视频</span>
|
||||
<span>{formatProfileDate(project.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
@@ -669,16 +715,19 @@ function ProfilePage({
|
||||
return savedAssets.length ? (
|
||||
<div className="profile-page__list-grid">
|
||||
{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">
|
||||
<strong>{asset.name}</strong>
|
||||
<span>{formatAssetStatus(asset.status)}</span>
|
||||
</div>
|
||||
<p>{asset.description}</p>
|
||||
<div className="profile-page__list-card-meta">
|
||||
<span>{asset.type}</span>
|
||||
<span>{formatAssetType(asset.type)}</span>
|
||||
<span>{formatProfileDate(asset.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
@@ -791,6 +840,50 @@ function ProfilePage({
|
||||
</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">
|
||||
<ShareAltOutlined />
|
||||
{packageLabel}
|
||||
@@ -838,52 +931,6 @@ function ProfilePage({
|
||||
</span>
|
||||
{renderActivePanel()}
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -405,6 +405,7 @@ function ScriptTokensPage() {
|
||||
<div className="script-eval-v5-page">
|
||||
{/* Left Panel */}
|
||||
<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-label">上传剧本</div>
|
||||
<div
|
||||
@@ -511,6 +512,7 @@ function ScriptTokensPage() {
|
||||
<span>导出评测报告</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Right Area */}
|
||||
|
||||
@@ -271,9 +271,8 @@ function TokenUsagePage({
|
||||
) : null}
|
||||
|
||||
<section className="management-metric-cards" aria-label="关键指标">
|
||||
{metricCards.map((card, index) => (
|
||||
{metricCards.map((card) => (
|
||||
<article key={card.key} className={`management-metric-card is-${card.tone}`}>
|
||||
<span className="management-metric-card__index">{String(index + 1).padStart(2, "0")}</span>
|
||||
<span className="management-metric-card__label">{card.label}</span>
|
||||
<strong className="management-metric-card__value">{card.value}</strong>
|
||||
<span className="management-metric-card__hint">{card.hint}</span>
|
||||
|
||||
@@ -8598,9 +8598,8 @@
|
||||
}
|
||||
|
||||
.product-clone-page[data-tool="clone"] .clone-ai-logo {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
position: static;
|
||||
z-index: auto;
|
||||
margin: -18px -18px 2px;
|
||||
padding: 16px 18px 14px;
|
||||
border-bottom-color: var(--ecm-line);
|
||||
@@ -9103,7 +9102,7 @@
|
||||
}
|
||||
|
||||
.product-clone-page[data-tool="clone"] .clone-ai-logo {
|
||||
margin: -14px -14px 0;
|
||||
margin: 0;
|
||||
padding: 14px 54px 12px 14px;
|
||||
}
|
||||
|
||||
@@ -9382,3 +9381,42 @@
|
||||
padding-top: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile clone header alignment: keep the tool title in normal flow, but attach it to the top nav rhythm. */
|
||||
@media (max-width: 900px) {
|
||||
.product-clone-page[data-tool="clone"] {
|
||||
padding-top: 59px;
|
||||
}
|
||||
|
||||
.product-clone-page[data-tool="clone"] > .product-clone-shell {
|
||||
min-height: calc(100% - 59px);
|
||||
}
|
||||
|
||||
.product-clone-page[data-tool="clone"] .clone-ai-panel {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.product-clone-page[data-tool="clone"] .clone-ai-logo {
|
||||
margin: 0 -18px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.product-clone-page[data-tool="clone"] .clone-ai-panel {
|
||||
padding: 0 14px 14px;
|
||||
}
|
||||
|
||||
.product-clone-page[data-tool="clone"] .clone-ai-logo {
|
||||
margin: 0 -14px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.product-clone-page[data-tool="clone"] {
|
||||
padding-top: 59px;
|
||||
}
|
||||
|
||||
.product-clone-page[data-tool="clone"] > .product-clone-shell {
|
||||
min-height: calc(100% - 59px);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user