Dark模式 oss存储
Deploy dev / deploy (push) Successful in 1m16s

This commit is contained in:
Azmat@qq.com
2026-09-24 18:06:00 +08:00
parent ceb5163881
commit 5dbb240b59
46 changed files with 3491 additions and 350 deletions
+4 -4
View File
@@ -15,10 +15,10 @@ REDIS_CACHE_URL=redis://:redis_WFSzWJ@yingqing-redis:6379/0
CELERY_BROKER_URL=redis://:redis_WFSzWJ@yingqing-redis:6379/1 CELERY_BROKER_URL=redis://:redis_WFSzWJ@yingqing-redis:6379/1
CELERY_RESULT_BACKEND=redis://:redis_WFSzWJ@yingqing-redis:6379/2 CELERY_RESULT_BACKEND=redis://:redis_WFSzWJ@yingqing-redis:6379/2
REDIS_LOCK_URL=redis://:redis_WFSzWJ@yingqing-redis:6379/3 REDIS_LOCK_URL=redis://:redis_WFSzWJ@yingqing-redis:6379/3
TOS_ENDPOINT=https://tos-s3-cn-shanghai.volces.com TOS_ENDPOINT=https://oss-cn-beijing.aliyuncs.com
TOS_BUCKET=airshelf TOS_BUCKET=yingqing-media
TOS_ACCESS_KEY_ID=AKLTODVhY2U1NzY1MTU3NDA4NThiYzk2ZDMyZDNjYmZhZGY TOS_ACCESS_KEY_ID=LTAI5t62RF5Swgs2uwopvuZz
TOS_SECRET_ACCESS_KEY=TWpjNVpqVm1NbVkzTWprNE5ESXlZMkUyT1dNNFlqVmtaRGRoTVdNME5qRQ== TOS_SECRET_ACCESS_KEY=CMmQNGiG2ieESRrhD1ORwuB6FYBmxE
VOLCANO_ARK_API_KEY=ark-a0efcea2-fbf4-47bb-a43e-e3eed4138abc-0ad26 VOLCANO_ARK_API_KEY=ark-a0efcea2-fbf4-47bb-a43e-e3eed4138abc-0ad26
VOLCANO_ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 VOLCANO_ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
# 2026-07-09 视频(Seedance)ARK key 换成新账号,与新账号素材库审核同账号 → 真人 asset:// 可解析。EP=ep-20260707204223-wkcn9(备用,代码按模型名调不用它)。 # 2026-07-09 视频(Seedance)ARK key 换成新账号,与新账号素材库审核同账号 → 真人 asset:// 可解析。EP=ep-20260707204223-wkcn9(备用,代码按模型名调不用它)。
+15
View File
@@ -238,11 +238,26 @@ CREATION_AGENT_THINKING_MODE = (env("CREATION_AGENT_THINKING_MODE", "disabled")
REDIS_LOCK_URL = env("REDIS_LOCK_URL", "redis://127.0.0.1:6379/3") REDIS_LOCK_URL = env("REDIS_LOCK_URL", "redis://127.0.0.1:6379/3")
def _tos_region() -> str:
"""阿里云 OSS 的 S3 签名区域是 oss-cn-beijing 这种主机名前缀。火山 TOS 仍用 cn-shanghai。"""
explicit = (env("TOS_REGION") or "").strip()
if explicit:
return explicit
host = (env("TOS_ENDPOINT") or "").split("://", 1)[-1].split("/", 1)[0]
if host.startswith("oss-") and host.endswith(".aliyuncs.com"):
return host.split(".", 1)[0]
return "cn-shanghai"
TOS = { TOS = {
"endpoint": env("TOS_ENDPOINT"), "endpoint": env("TOS_ENDPOINT"),
"bucket": env("TOS_BUCKET"), "bucket": env("TOS_BUCKET"),
"region": _tos_region(),
"access_key_id": env("TOS_ACCESS_KEY_ID"), "access_key_id": env("TOS_ACCESS_KEY_ID"),
"secret_access_key": env("TOS_SECRET_ACCESS_KEY"), "secret_access_key": env("TOS_SECRET_ACCESS_KEY"),
# 换到阿里云之后,历史文件仍在火山引擎旧桶。按桶名把旧链接指回原地址。
"legacy_endpoint": env("TOS_LEGACY_ENDPOINT", "https://tos-s3-cn-shanghai.volces.com"),
"legacy_bucket": env("TOS_LEGACY_BUCKET", "airshelf"),
} }
VOLCANO = { VOLCANO = {
@@ -3,5 +3,16 @@ from .base import * # noqa: F403
DEBUG = True DEBUG = True
# 本机 runserver 不在 K8s 的 service DNS 中;若直接沿用集群 Redis 地址,任何依赖
# cache.get/cache.add 的普通读取接口都会变成 500。开发环境默认使用进程内缓存,确需
# 联调分布式锁或 Celery 时再显式设 AIRSHELF_DEV_USE_REDIS_CACHE=true。
if not env_bool("AIRSHELF_DEV_USE_REDIS_CACHE", False): # noqa: F405
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "airshelf-development-cache",
}
}
# MySQL 连接复用(CONN_MAX_AGE / 健康检查 / connect_timeout)已上移到 base.py, # MySQL 连接复用(CONN_MAX_AGE / 健康检查 / connect_timeout)已上移到 base.py,
# development 与 production 共用,此处不再重复设置。 # development 与 production 共用,此处不再重复设置。
+17
View File
@@ -1,6 +1,8 @@
from decimal import Decimal from decimal import Decimal
from unittest.mock import patch
from django.conf import settings from django.conf import settings
from django.db import IntegrityError
from django.test import TestCase from django.test import TestCase
from rest_framework.test import APIClient from rest_framework.test import APIClient
@@ -103,6 +105,21 @@ class SingleDeviceLoginTests(TestCase):
self.assertEqual(active.count(), 1) self.assertEqual(active.count(), 1)
self.assertEqual(active.get().user_agent, "Device B") self.assertEqual(active.get().user_agent, "Device B")
def test_login_succeeds_when_device_audit_write_fails(self):
"""设备审计表异常不能回滚已经签发的登录 Token。"""
with patch(
"apps.accounts.views.LoginSession.objects.create",
side_effect=IntegrityError("device audit unavailable"),
):
response = APIClient().post(
"/api/auth/login/",
{"username": self.user.username, "password": "strong-password"},
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertIn("token", response.data)
class InvitationFlowTests(TestCase): class InvitationFlowTests(TestCase):
def _register(self, client, username, **extra): def _register(self, client, username, **extra):
+4 -1
View File
@@ -99,7 +99,10 @@ def issue_single_device_token(request, user):
LoginSession.objects.filter(user=locked_user, revoked_at__isnull=True).update(revoked_at=timezone.now()) LoginSession.objects.filter(user=locked_user, revoked_at__isnull=True).update(revoked_at=timezone.now())
Token.objects.filter(user=locked_user).delete() Token.objects.filter(user=locked_user).delete()
token = Token.objects.create(user=locked_user) token = Token.objects.create(user=locked_user)
record_login_session(request, locked_user) # 设备记录只用于设置页展示和审计,不属于 Token 签发的关键路径。此前它在
# 同一事务里即使被 record_login_session 捕获,数据库事务仍可能已被标记为
# rollback,离开 atomic 后登录照样变成 500。
record_login_session(request, locked_user)
return token return token
+5 -9
View File
@@ -2991,7 +2991,7 @@ def notify_generation_failure(
def _asset_preview_url(asset) -> str: def _asset_preview_url(asset) -> str:
"""资产主文件的可公开访问 URL(已写绝对 URL 优先,否则实时签 TOS GET)""" """资产主文件的公读直链。写进会话的地址不能带签名,否则 1 小时后图片会消失"""
if asset is None: if asset is None:
return "" return ""
primary = asset.files.filter(is_primary=True).first() or asset.files.first() primary = asset.files.filter(is_primary=True).first() or asset.files.first()
@@ -2999,16 +2999,16 @@ def _asset_preview_url(asset) -> str:
return "" return ""
if primary.preview_url: if primary.preview_url:
return primary.preview_url return primary.preview_url
if not primary.object_key:
return ""
try: try:
return TosStorage().presigned_get_url(object_key=primary.object_key) return TosStorage().public_url(object_key=primary.object_key, bucket=primary.bucket or None)
except Exception: except Exception:
return "" return ""
def asset_stable_url(asset) -> tuple[str, str]: def asset_stable_url(asset) -> tuple[str, str]:
"""资产的长期可打开」直链 (主文件, 封面图)。历史记录这类隔天还要点开的场景必须用这个: """资产的长期直链 (主文件, 封面图)。公读地址不过期,结果卡隔天还能打开。"""
预签名链只活 1 小时,存进 payload 快照的话第二天就播不了 —— 公读直链不过期还能被缓存。
顺序:落库 preview_url → TOS 公读直链 → 兜底预签名。"""
if asset is None: if asset is None:
return "", "" return "", ""
files = list(asset.files.all()) files = list(asset.files.all())
@@ -3031,10 +3031,6 @@ def asset_stable_url(asset) -> tuple[str, str]:
return "" return ""
try: try:
return TosStorage().public_url(object_key=f.object_key, bucket=f.bucket or None) return TosStorage().public_url(object_key=f.object_key, bucket=f.bucket or None)
except Exception: # noqa: BLE001
pass
try:
return TosStorage().presigned_get_url(object_key=f.object_key)
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
return "" return ""
+10 -2
View File
@@ -1780,12 +1780,20 @@ class ModelConfigViewSet(ReadOnlyModelViewSet):
) )
cache_key = MODEL_CATALOG_CACHE_KEY if page_size in {"", "200"} else f"{MODEL_CATALOG_CACHE_KEY}:{page_size}" cache_key = MODEL_CATALOG_CACHE_KEY if page_size in {"", "200"} else f"{MODEL_CATALOG_CACHE_KEY}:{page_size}"
if is_plain: if is_plain:
cached = cache.get(cache_key) # 模型目录缓存只是加速层;开发机或 Redis 短暂不可用时仍应正常返回目录。
try:
cached = cache.get(cache_key)
except Exception:
logger.warning("模型目录缓存不可用,改为直接读取", exc_info=True)
cached = None
if cached is not None: if cached is not None:
return Response(cached) return Response(cached)
response = super().list(request, *args, **kwargs) response = super().list(request, *args, **kwargs)
if is_plain and response.status_code == 200: if is_plain and response.status_code == 200:
cache.set(cache_key, response.data, MODEL_CATALOG_CACHE_TTL) try:
cache.set(cache_key, response.data, MODEL_CATALOG_CACHE_TTL)
except Exception:
logger.warning("模型目录缓存写入失败,已返回直读结果", exc_info=True)
return response return response
+2 -3
View File
@@ -45,9 +45,8 @@ class AssetFileSerializer(serializers.ModelSerializer):
] ]
def get_preview_url(self, obj): def get_preview_url(self, obj):
# 存储字段优先(如外部已写入绝对 URL);否则用 object_key 拼 TOS 公读直链。 # 存储字段优先(如外部已写入绝对 URL);否则用 object_key 拼公读直链。
# TOS 桶公读,直链(虚拟主机式)免签名、且稳定可缓存——不再逐图签发预签名 URL # 桶公读,直链不过期、可缓存。不再签发 1 小时就失效的预签名 URL
# (签名是纯本地计算但每次都变、1h 过期会打不到浏览器/CDN 缓存;直链一劳永逸)。
if obj.preview_url: if obj.preview_url:
return obj.preview_url return obj.preview_url
if not obj.object_key or not settings.TOS.get("endpoint"): if not obj.object_key or not settings.TOS.get("endpoint"):
+24 -5
View File
@@ -19,13 +19,25 @@ class TosStorage:
def __init__(self) -> None: def __init__(self) -> None:
tos = settings.TOS tos = settings.TOS
self.bucket = tos["bucket"] self.bucket = tos["bucket"]
config_kwargs = {
"signature_version": "s3v4",
"s3": {"addressing_style": "virtual"},
}
try:
client_config = Config(
**config_kwargs,
request_checksum_calculation="when_required",
response_checksum_validation="when_required",
)
except TypeError:
client_config = Config(**config_kwargs)
self.client = boto3.client( self.client = boto3.client(
"s3", "s3",
endpoint_url=tos["endpoint"], endpoint_url=tos["endpoint"],
aws_access_key_id=tos["access_key_id"], aws_access_key_id=tos["access_key_id"],
aws_secret_access_key=tos["secret_access_key"], aws_secret_access_key=tos["secret_access_key"],
region_name="cn-shanghai", region_name=tos.get("region") or "cn-shanghai",
config=Config(s3={"addressing_style": "virtual"}), config=client_config,
) )
def upload_fileobj(self, *, fileobj: BinaryIO, object_key: str, content_type: str) -> StoredObject: def upload_fileobj(self, *, fileobj: BinaryIO, object_key: str, content_type: str) -> StoredObject:
@@ -57,6 +69,13 @@ class TosStorage:
def public_url(self, *, object_key: str, bucket: str | None = None) -> str: def public_url(self, *, object_key: str, bucket: str | None = None) -> str:
"""桶公读时的虚拟主机式直链:https://{bucket}.{host}/{key} """桶公读时的虚拟主机式直链:https://{bucket}.{host}/{key}
相比预签名 URL:不签名( boto3 客户端/签名开销) URL 稳定可被浏览器/CDN 缓存 相比预签名 URL:不签名( boto3 客户端/签名开销) URL 稳定可被浏览器/CDN 缓存
(签名链每次都变1h 过期,反而打不到缓存)仅用于公开可读的预览图""" (签名链每次都变1h 过期,反而打不到缓存)仅用于公开可读的预览图
host = urlparse(settings.TOS["endpoint"]).netloc 历史文件的 bucket 仍是火山引擎旧桶必须用旧域名不能拼到当前阿里云地址上"""
return f"https://{bucket or self.bucket}.{host}/{quote(object_key, safe='/')}" bucket_name = bucket or self.bucket
endpoint = settings.TOS.get("endpoint") or ""
legacy_bucket = settings.TOS.get("legacy_bucket") or ""
legacy_endpoint = settings.TOS.get("legacy_endpoint") or ""
if legacy_bucket and legacy_endpoint and bucket_name == legacy_bucket:
endpoint = legacy_endpoint
host = urlparse(endpoint).netloc
return f"https://{bucket_name}.{host}/{quote(object_key, safe='/')}"
+11 -1
View File
@@ -1,3 +1,4 @@
import logging
from datetime import timedelta from datetime import timedelta
from django.core.cache import cache from django.core.cache import cache
@@ -25,6 +26,9 @@ from .models import Notification
from .serializers import NotificationSerializer from .serializers import NotificationSerializer
logger = logging.getLogger(__name__)
def project_stage_label(project): def project_stage_label(project):
return { return {
"script": "Stage 1 · 脚本", "script": "Stage 1 · 脚本",
@@ -288,7 +292,13 @@ class NotificationViewSet(TeamScopedViewSetMixin, ModelViewSet):
if not Notification.objects.filter(team=team).exists(): if not Notification.objects.filter(team=team).exists():
ensure_team_notifications(team, request.user) ensure_team_notifications(team, request.user)
return return
if cache.add(f"ops:notif-refresh:{team.id}", 1, timeout=60): # 通知列表是普通读接口;Redis 不可用时跳过后台刷新,不能因此返回 500。
try:
should_refresh = cache.add(f"ops:notif-refresh:{team.id}", 1, timeout=60)
except Exception:
logger.warning("通知刷新缓存不可用,跳过本次后台刷新", exc_info=True)
should_refresh = False
if should_refresh:
from .tasks import ensure_team_notifications_task from .tasks import ensure_team_notifications_task
user_id = str(request.user.id) if getattr(request.user, "id", None) else None user_id = str(request.user.id) if getattr(request.user, "id", None) else None
+14
View File
@@ -6,6 +6,20 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="icon" href="/assets/yz/logo.png" /> <link rel="icon" href="/assets/yz/logo.png" />
<title>影擎</title> <title>影擎</title>
<script>
(function () {
try {
var key = "yingqing-appearance";
var raw = localStorage.getItem(key) || "system";
var theme = raw;
if (raw !== "light" && raw !== "dark") {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
document.documentElement.setAttribute("data-theme", theme);
document.documentElement.style.colorScheme = theme;
} catch (e) {}
})();
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1
View File
@@ -5,6 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",
"be": "npm --prefix ../.. run be",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"preview": "vite preview --host 0.0.0.0" "preview": "vite preview --host 0.0.0.0"
}, },
+54 -17
View File
@@ -21,6 +21,7 @@ import type {
UserPreference UserPreference
} from "./types"; } from "./types";
import { publicModelDisplayName } from "./model-display"; import { publicModelDisplayName } from "./model-display";
import { applyTheme, normalizeAppearance, readStoredAppearance } from "./theme";
import { generationErrorText } from "./generation-error"; import { generationErrorText } from "./generation-error";
import { isQuickCreateBusy, lockedQuickCreateProject, rememberQuickCreateJob, withQuickCreateStatus } from "./quick-create-lock"; import { isQuickCreateBusy, lockedQuickCreateProject, rememberQuickCreateJob, withQuickCreateStatus } from "./quick-create-lock";
import { AccountMenu, CornerMarks, Decorations, ModeTabs, openCommandPalette, Sidebar, ToastLike, topModuleForPage } from "./components/app-shell"; import { AccountMenu, CornerMarks, Decorations, ModeTabs, openCommandPalette, Sidebar, ToastLike, topModuleForPage } from "./components/app-shell";
@@ -143,6 +144,22 @@ export function App() {
// 合成成片单飞:合成不占全局 loading,自己守一把锁,防止连点起两轮轮询 // 合成成片单飞:合成不占全局 loading,自己守一把锁,防止连点起两轮轮询
const exportingRef = useRef(false); const exportingRef = useRef(false);
const [preferences, setPreferences] = useState<UserPreference | null>(null); const [preferences, setPreferences] = useState<UserPreference | null>(null);
// 首屏先用本地外观,后端 preferences 到达后再对齐;跟随系统时监听系统切换
useEffect(() => {
applyTheme(readStoredAppearance());
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = () => {
if (readStoredAppearance() === "system") applyTheme("system");
};
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, []);
useEffect(() => {
const appearance = preferences?.display?.appearance;
if (appearance) applyTheme(normalizeAppearance(appearance));
}, [preferences?.display?.appearance]);
const [sessions, setSessions] = useState<LoginSession[]>([]); const [sessions, setSessions] = useState<LoginSession[]>([]);
const [activeProductId, setActiveProductId] = useState(route.productId || ""); const [activeProductId, setActiveProductId] = useState(route.productId || "");
@@ -211,19 +228,25 @@ export function App() {
} }
}, [loadData]); }, [loadData]);
// 设置页数据:偏好 + 登录会话(进入设置页时按需加载) // 设置页数据:偏好 + 登录会话。settingsLoadSeq 挡住过期 GET 覆盖更新的 PUT 结果。
const settingsLoadSeq = useRef(0);
const loadSettingsData = useCallback(async () => { const loadSettingsData = useCallback(async () => {
const seq = ++settingsLoadSeq.current;
const [pref, sess] = await Promise.all([ const [pref, sess] = await Promise.all([
api.preferences().catch(() => null), api.preferences().catch(() => null),
api.loginSessions().catch(() => []) api.loginSessions().catch(() => [])
]); ]);
if (seq !== settingsLoadSeq.current) return;
if (pref) setPreferences(pref); if (pref) setPreferences(pref);
setSessions(sess); setSessions(sess);
}, []); }, []);
async function savePreferences(payload: Partial<UserPreference>) { async function savePreferences(payload: Partial<UserPreference>) {
const next = await api.updatePreferences(payload).catch(() => null); // 失败必须抛出,不能吞成 null —— 否则 settings 会误以为已落盘并推进 baseline
if (next) setPreferences(next); const next = await api.updatePreferences(payload);
// 使在途的 preferences GET 失效,避免刚保存的外观被旧响应打回 system
settingsLoadSeq.current += 1;
setPreferences(next);
return next; return next;
} }
@@ -399,16 +422,30 @@ export function App() {
// Load preferences + sessions when entering settings. // Load preferences + sessions when entering settings.
// 进创作相关页时重拉模型目录,避免后台刚改能力/积分,前端还捧着首屏缓存 // 进创作相关页时重拉模型目录,避免后台刚改能力/积分,前端还捧着首屏缓存
useEffect(() => { useEffect(() => {
if (!authed || !dataLoaded) return; // 模型目录是创作页的必要数据,不能被商品/项目等全局数据的加载状态卡住。
// 否则任一非关键列表短暂失败时,模型按钮仍显示默认名却展开空菜单。
if (!authed) return;
if (!["freeCreate", "omniCreate", "omniSession", "omniHistory", "quickCreate", "videoReplace", "pipeline"].includes(page)) return; if (!["freeCreate", "omniCreate", "omniSession", "omniHistory", "quickCreate", "videoReplace", "pipeline"].includes(page)) return;
refreshModelConfigs(); refreshModelConfigs();
}, [authed, page, dataLoaded, refreshModelConfigs]); }, [authed, page, refreshModelConfigs]);
useEffect(() => { useEffect(() => {
if (!authed || (page !== "settings" && page !== "settingsNotify")) return; if (!authed || (page !== "settings" && page !== "settingsNotify")) return;
loadSettingsData(); loadSettingsData();
}, [authed, page, loadSettingsData]); }, [authed, page, loadSettingsData]);
// 登录后拉一次偏好(外观等),不必等进设置页才生效;仍走 settingsLoadSeq,与设置页重载互斥
useEffect(() => {
if (!authed || !user) return;
const seq = ++settingsLoadSeq.current;
void api.preferences()
.then((pref) => {
if (seq !== settingsLoadSeq.current) return;
if (pref) setPreferences(pref);
})
.catch(() => undefined);
}, [authed, user]);
// YYX#row22:登录后拉一次未读生成任务,并每 30s 静默轮询(生成是慢任务,出图后角标自动亮) // YYX#row22:登录后拉一次未读生成任务,并每 30s 静默轮询(生成是慢任务,出图后角标自动亮)
useEffect(() => { useEffect(() => {
if (!authed || !user) return; if (!authed || !user) return;
@@ -901,16 +938,15 @@ export function App() {
return; return;
} }
navigate("omniCreate", { replace: true }); navigate("omniCreate", { replace: true });
// 先把工作台必需数据拉好,这期间「登录页」保持「登录成功,正在进入工作台…」提示(authed 仍 false → AuthScreen 不卸载), // 登录凭证已验证即可进入工作台。此前等待商品/项目等整套数据(包含重试)才
// 数据就绪后才揭开外壳直接进有数据的工作台 —— 避免中间闪一个空白「加载中…」页(PMC#7)。 // setAuthed,任一慢接口都会让登录页长时间卡在「正在进入工作台」;而刷新后的
try { // boot 路径本来就是先展示外壳、后台补数据。首次登录也保持同一体验。
await loadDataWithRetry(); setDataLoaded(false);
} catch (error) { setAuthed(true);
void loadDataWithRetry().catch((error) => {
console.error("[login] data hydrate failed:", error); console.error("[login] data hydrate failed:", error);
setNotice({ type: "error", text: "数据加载失败,请刷新页面重试" }); setNotice({ type: "error", text: "部分数据加载失败,页面会自动重试" });
} finally { });
setAuthed(true);
}
} }
async function logout() { async function logout() {
@@ -1216,9 +1252,9 @@ export function App() {
case "modelPhotoDemoB": case "modelPhotoDemoB":
return <ModelPhotoDemoPage variant="B" products={products} onBack={() => goBack("modelPhoto")} navigate={navigate} />; return <ModelPhotoDemoPage variant="B" products={products} onBack={() => goBack("modelPhoto")} navigate={navigate} />;
case "settings": case "settings":
return <SettingsPage user={currentUser} team={currentTeam} preferences={preferences} sessions={sessions} onSavePreferences={savePreferences} onRevokeSession={revokeSession} onRevokeOthers={revokeOtherSessions} onSaveProfile={saveProfile} onChangePassword={changeOwnPassword} onUploadAvatar={uploadOwnAvatar} onResetAvatar={resetOwnAvatar} onNotify={(text) => setNotice({ type: "success", text })} onLogout={logout} />; return <SettingsPage user={currentUser} team={currentTeam} preferences={preferences} sessions={sessions} onSavePreferences={savePreferences} onRevokeSession={revokeSession} onRevokeOthers={revokeOtherSessions} onSaveProfile={saveProfile} onChangePassword={changeOwnPassword} onUploadAvatar={uploadOwnAvatar} onResetAvatar={resetOwnAvatar} onNotify={(type, text) => setNotice({ type, text })} onLogout={logout} />;
case "settingsNotify": case "settingsNotify":
return <SettingsPage user={currentUser} team={currentTeam} initialSection="notify" preferences={preferences} sessions={sessions} onSavePreferences={savePreferences} onRevokeSession={revokeSession} onRevokeOthers={revokeOtherSessions} onSaveProfile={saveProfile} onChangePassword={changeOwnPassword} onUploadAvatar={uploadOwnAvatar} onResetAvatar={resetOwnAvatar} onNotify={(text) => setNotice({ type: "success", text })} onLogout={logout} />; return <SettingsPage user={currentUser} team={currentTeam} initialSection="notify" preferences={preferences} sessions={sessions} onSavePreferences={savePreferences} onRevokeSession={revokeSession} onRevokeOthers={revokeOtherSessions} onSaveProfile={saveProfile} onChangePassword={changeOwnPassword} onUploadAvatar={uploadOwnAvatar} onResetAvatar={resetOwnAvatar} onNotify={(type, text) => setNotice({ type, text })} onLogout={logout} />;
default: default:
return <OmniCreatePage modelConfigs={modelConfigs} navigate={navigate} onNotify={(type, text) => setNotice({ type, text })} />; return <OmniCreatePage modelConfigs={modelConfigs} navigate={navigate} onNotify={(type, text) => setNotice({ type, text })} />;
} }
@@ -1379,7 +1415,8 @@ export function App() {
)} )}
<span className="balance-chip" onClick={() => navigate("account")}> <span className="balance-chip" onClick={() => navigate("account")}>
<IconKitSvg name="creditCard" /> <IconKitSvg name="creditCard" />
<strong>{money(billing?.account.balance)}</strong> <span className="balance-chip-label"></span>
<strong>{money(billing?.account.balance)}</strong>
</span> </span>
<button className="icon-btn" type="button" onClick={() => navigate("messages")} title="消息中心"> <button className="icon-btn" type="button" onClick={() => navigate("messages")} title="消息中心">
<IconKitSvg name="bell" /> <IconKitSvg name="bell" />
+5 -5
View File
@@ -347,7 +347,7 @@
border: 0; border: 0;
border-radius: 9px; border-radius: 9px;
color: #0b1d4c; color: #0b1d4c;
background: #fff; background: var(--surface);
font-weight: 650; font-weight: 650;
cursor: pointer; cursor: pointer;
transition: color .18s ease, background .18s ease, transform .18s ease; transition: color .18s ease, background .18s ease, transform .18s ease;
@@ -716,7 +716,7 @@
padding: 0 8px; padding: 0 8px;
border: 1px solid rgba(34, 42, 54, 0.12); border: 1px solid rgba(34, 42, 54, 0.12);
border-radius: 8px; border-radius: 8px;
background: #fff; background: var(--surface);
color: var(--text); color: var(--text);
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
@@ -755,7 +755,7 @@
padding: 0 12px; padding: 0 12px;
border: 1px solid rgba(34, 42, 54, 0.12); border: 1px solid rgba(34, 42, 54, 0.12);
border-radius: 8px; border-radius: 8px;
background: #fff; background: var(--surface);
cursor: pointer; cursor: pointer;
} }
.bill-jump button:disabled { .bill-jump button:disabled {
@@ -807,7 +807,7 @@
padding: 24px; padding: 24px;
border: 1px solid rgba(34, 42, 54, 0.11); border: 1px solid rgba(34, 42, 54, 0.11);
border-radius: 16px; border-radius: 16px;
background: #fff; background: var(--surface);
box-shadow: 0 24px 70px rgba(18, 25, 38, 0.18); box-shadow: 0 24px 70px rgba(18, 25, 38, 0.18);
transform: translateY(10px) scale(.985); transform: translateY(10px) scale(.985);
transition: transform .2s ease; transition: transform .2s ease;
@@ -840,7 +840,7 @@
border: 1px solid rgba(34, 42, 54, 0.1); border: 1px solid rgba(34, 42, 54, 0.1);
border-radius: 9px; border-radius: 9px;
color: #20242c; color: #20242c;
background: #fff; background: var(--surface);
cursor: pointer; cursor: pointer;
} }
.billing-contact-close:hover { .billing-contact-close:hover {
+4 -4
View File
@@ -146,7 +146,7 @@
height: var(--topbar-height); height: var(--topbar-height);
min-height: var(--topbar-height); min-height: var(--topbar-height);
padding: 0 28px; padding: 0 28px;
background: #fff; background: var(--surface);
border-bottom: 1px solid rgba(27, 32, 40, 0.08); border-bottom: 1px solid rgba(27, 32, 40, 0.08);
} }
.admin-crumb { .admin-crumb {
@@ -370,7 +370,7 @@
.admin-table-wrap { overflow-x: auto; } .admin-table-wrap { overflow-x: auto; }
.admin-page table.t.admin-table { .admin-page table.t.admin-table {
background: #fff; background: var(--surface);
border-color: var(--st-line); border-color: var(--st-line);
border-radius: 8px; border-radius: 8px;
} }
@@ -626,7 +626,7 @@
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
padding: 16px 18px; padding: 16px 18px;
background: #fff; background: var(--surface);
border: 1px solid var(--st-line); border: 1px solid var(--st-line);
border-radius: 8px; border-radius: 8px;
} }
@@ -679,7 +679,7 @@
.pt-foot .spacer { flex: 1; } .pt-foot .spacer { flex: 1; }
.admin-page .card-hard { .admin-page .card-hard {
background: #fff; background: var(--surface);
border-color: var(--st-line); border-color: var(--st-line);
} }
.admin-page .empty-state { .admin-page .empty-state {
+8 -8
View File
@@ -187,7 +187,7 @@
padding: 0 13px; padding: 0 13px;
border: 1px solid var(--af-line); border: 1px solid var(--af-line);
border-radius: 10px; border-radius: 10px;
background: #fff; background: var(--surface);
} }
.asset-factory .af-search svg { .asset-factory .af-search svg {
width: 16px; width: 16px;
@@ -229,7 +229,7 @@
border: 1px solid rgba(34, 42, 54, 0.12); border: 1px solid rgba(34, 42, 54, 0.12);
border-radius: 10px; border-radius: 10px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
text-align: left; text-align: left;
@@ -261,7 +261,7 @@
padding: 6px; padding: 6px;
border: 1px solid var(--af-line); border: 1px solid var(--af-line);
border-radius: 11px; border-radius: 11px;
background: #fff; background: var(--surface);
box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14); box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
@@ -280,7 +280,7 @@
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
text-align: left; text-align: left;
@@ -2746,7 +2746,7 @@
place-items: center; place-items: center;
border: 1px solid rgba(34, 42, 54, 0.18); border: 1px solid rgba(34, 42, 54, 0.18);
border-radius: 50%; border-radius: 50%;
background: #fff; background: var(--surface);
} }
.yz-image .product-choice.active .choice-check, .yz-image .product-choice.active .choice-check,
.yz-image .model-choice.active .choice-check, .yz-image .model-choice.active .choice-check,
@@ -2761,7 +2761,7 @@
width: 5px; width: 5px;
height: 5px; height: 5px;
border-radius: 50%; border-radius: 50%;
background: #fff; background: var(--surface);
} }
.yz-image .product-library-choice, .yz-image .product-library-choice,
.yz-image .upload-choice { .yz-image .upload-choice {
@@ -2909,7 +2909,7 @@
} }
.yz-image .image-prompt:focus { .yz-image .image-prompt:focus {
border-color: rgba(0, 47, 167, 0.48); border-color: rgba(0, 47, 167, 0.48);
background: #fff; background: var(--surface);
box-shadow: 0 0 0 3px rgba(0, 47, 167, 0.07); box-shadow: 0 0 0 3px rgba(0, 47, 167, 0.07);
} }
.yz-image .model-line { .yz-image .model-line {
@@ -3310,7 +3310,7 @@
overflow: hidden; overflow: hidden;
color: #fff; color: #fff;
} }
.yz-image .platform-choice .p-logo.has-img { background: #fff; } .yz-image .platform-choice .p-logo.has-img { background: var(--surface); }
.yz-image .model-pill { .yz-image .model-pill {
display: inline-flex; display: inline-flex;
+2 -2
View File
@@ -584,8 +584,8 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p
aria-hidden="true" aria-hidden="true"
style={{ transform: `translateY(${liquid.y}px)` }} style={{ transform: `translateY(${liquid.y}px)` }}
> >
<svg className="liquid-shape" viewBox="0 0 132 76" preserveAspectRatio="none"> <svg className="liquid-shape" viewBox="0 0 152 76" preserveAspectRatio="none">
<path fill="#101012" d="M0 11H103C120 11 130 22 130 38C130 54 120 65 103 65H0Z" /> <path fill="currentColor" d="M0 11H123C140 11 150 22 150 38C150 54 140 65 123 65H0Z" />
</svg> </svg>
{activeMajor && ( {activeMajor && (
<div className="indicator-content"> <div className="indicator-content">
@@ -97,8 +97,6 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
return () => window.clearInterval(timer); return () => window.clearInterval(timer);
}, [open, activeGroup, assets]); }, [open, activeGroup, assets]);
if (!open) return null;
const createGroup = async () => { const createGroup = async () => {
const name = newName.trim(); const name = newName.trim();
if (!name || creatingGroup) return; if (!name || creatingGroup) return;
@@ -160,6 +158,9 @@ export function AssetLibraryModal({ open, onClose, onPick, notify }: {
} }
}; };
// 所有 Hook 必须在关闭态也保持同一调用顺序;不能在此之前提前 return。
if (!open) return null;
const startRenameAsset = (item: FreeAssetItem) => { const startRenameAsset = (item: FreeAssetItem) => {
setConfirmDeleteAssetId(null); setConfirmDeleteAssetId(null);
setConfirmDeleteGroupId(null); setConfirmDeleteGroupId(null);
+4 -3
View File
@@ -4,12 +4,13 @@
100% { background-position: 480px 0; } 100% { background-position: 480px 0; }
} }
.skeleton-block { .skeleton-block {
background: var(--background-base, #f1f1f1); background: var(--black-alpha-4);
background-image: linear-gradient(90deg, rgba(0,0,0,0.03) 0%, rgba(0,0,0,0.07) 40%, rgba(0,0,0,0.03) 80%); background-image: linear-gradient(90deg, transparent 0%, var(--black-alpha-8) 40%, transparent 80%);
background-size: 480px 100%; background-size: 480px 100%;
background-repeat: no-repeat; background-repeat: no-repeat;
animation: sk-shimmer 1.2s ease-in-out infinite; animation: sk-shimmer 1.2s ease-in-out infinite;
border-radius: 8px; box-shadow: inset 0 0 0 1px var(--border-faint);
border-radius: var(--r-md);
} }
.skeleton-grid { .skeleton-grid {
display: grid; display: grid;
File diff suppressed because it is too large Load Diff
+284 -23
View File
@@ -153,8 +153,228 @@
--shadow-cta-active: --shadow-cta-active:
0 1px 2px rgba(0, 47, 167, .18); 0 1px 2px rgba(0, 47, 167, .18);
--shadow-floating: 0 8px 28px rgba(17, 19, 24, .06); --shadow-floating: 0 8px 28px rgba(17, 19, 24, .06);
/* 容器四角准星:用 mask 着色,让它跟随当前主题的边框层级。 */
--corner-mark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21'%3E%3Cpath fill='%23000' d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E");
} }
/* ============================================================
Dark theme · 同一套语义 token 反色
html[data-theme="dark"] 驱动( theme.ts / settings 外观)
============================================================ */
html[data-theme="dark"] {
color-scheme: dark;
--background-base: #0f1115;
--background-lighter: #14171c;
--surface: #181b21;
--surface-raised: #1e2229;
--border-faint: #2a2f38;
--border-muted: #323843;
--border-loud: #3c4450;
/* 主文字走近白;accent-white 保持真白(按钮/反色字) */
--accent-black: #e8eaed;
--accent-white: #ffffff;
--nav-active: #e8eaed;
--forest-bg: rgba(66, 195, 102, .14);
--forest-bd: rgba(66, 195, 102, .32);
--crimson-bg: rgba(235, 52, 36, .14);
--crimson-bd: rgba(235, 52, 36, .32);
--honey-bg: rgba(236, 183, 48, .14);
--honey-bd: rgba(236, 183, 48, .32);
/* 克莱因在深底上略提亮,避免发灰 */
--klein: #3d6bff;
--klein-hover: #5a82ff;
--heat: var(--klein);
--heat-hover: var(--klein-hover);
--black: var(--accent-black);
--text: var(--accent-black);
--heat-90: rgba(61, 107, 255, .90);
--heat-40: rgba(61, 107, 255, .40);
--heat-20: rgba(61, 107, 255, .20);
--heat-16: rgba(61, 107, 255, .16);
--heat-12: rgba(61, 107, 255, .14);
--heat-8: rgba(61, 107, 255, .10);
--heat-4: rgba(61, 107, 255, .06);
/* black-alpha → 白透明阶梯(同语义:轻微叠色) */
--black-alpha-1: rgba(255, 255, 255, .015);
--black-alpha-2: rgba(255, 255, 255, .025);
--black-alpha-3: rgba(255, 255, 255, .035);
--black-alpha-4: rgba(255, 255, 255, .05);
--black-alpha-5: rgba(255, 255, 255, .06);
--black-alpha-6: rgba(255, 255, 255, .07);
--black-alpha-7: rgba(255, 255, 255, .08);
--black-alpha-8: rgba(255, 255, 255, .09);
--black-alpha-10: rgba(255, 255, 255, .11);
--black-alpha-12: rgba(255, 255, 255, .13);
--black-alpha-16: rgba(255, 255, 255, .16);
--black-alpha-20: rgba(255, 255, 255, .20);
--black-alpha-24: rgba(255, 255, 255, .24);
--black-alpha-32: rgba(255, 255, 255, .32);
--black-alpha-40: rgba(255, 255, 255, .40);
--black-alpha-48: rgba(255, 255, 255, .48);
--black-alpha-56: rgba(255, 255, 255, .56);
--black-alpha-64: rgba(255, 255, 255, .64);
--black-alpha-72: rgba(255, 255, 255, .72);
--black-alpha-88: rgba(255, 255, 255, .88);
--shadow-cta:
0 1px 2px rgba(61, 107, 255, .22),
0 6px 16px rgba(61, 107, 255, .28);
--shadow-cta-hover:
0 2px 4px rgba(61, 107, 255, .26),
0 10px 22px rgba(61, 107, 255, .34);
--shadow-cta-active:
0 1px 2px rgba(61, 107, 255, .22);
--shadow-floating: 0 8px 28px rgba(0, 0, 0, .45);
/* styles.css legacy aliases */
--bg: var(--background-base);
--bg-soft: var(--background-lighter);
--card: var(--surface);
--border: var(--border-faint);
--border-soft: var(--border-muted);
--ink: var(--accent-black);
--ink-2: rgba(232, 234, 237, .72);
--ink-3: rgba(232, 234, 237, .52);
--ink-4: rgba(232, 234, 237, .32);
--orange: var(--klein);
--orange-soft: var(--heat-40);
--orange-tint: var(--heat-8);
--orange-hover: var(--klein-hover);
--green: #6dd88a;
--green-bg: var(--forest-bg);
--green-bd: var(--forest-bd);
--red: #ff6b5c;
--red-bg: var(--crimson-bg);
--red-bd: var(--crimson-bd);
}
/* 壳层硬编码浅色 → dark 对齐 */
html[data-theme="dark"] aside.sidebar {
/* 盖过 aside.sidebar 本地 --klein:#002fa7,否则子菜单 active 仍是深蓝 */
--klein: #3d6bff;
--klein-hover: #5a82ff;
--st-black: #e8eaed;
border-right-color: rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
}
html[data-theme="dark"] aside.sidebar .nav-submenu {
border-left-color: rgba(61, 107, 255, 0.32);
}
html[data-theme="dark"] aside.sidebar .nav-subitem {
color: rgba(232, 234, 237, 0.78);
}
html[data-theme="dark"] aside.sidebar .nav-subitem:hover {
color: #a8bbff;
background: rgba(61, 107, 255, 0.14);
}
html[data-theme="dark"] aside.sidebar .nav-subitem.active {
color: #c5d2ff;
background: rgba(61, 107, 255, 0.2);
}
html[data-theme="dark"] aside.sidebar .nav-subitem.active::before {
box-shadow: 0 0 0 4px rgba(61, 107, 255, 0.22);
}
html[data-theme="dark"] aside.sidebar .nav-subitem .pill-mini {
background: rgba(255, 255, 255, 0.1);
color: #a8bbff;
}
html[data-theme="dark"] .sidebar-head {
border-bottom-color: rgba(255, 255, 255, 0.07);
}
html[data-theme="dark"] .brand-logo {
mix-blend-mode: normal;
filter: brightness(0) invert(1);
opacity: 0.92;
}
html[data-theme="dark"] aside.sidebar .nav-item {
color: rgba(232, 234, 237, 0.78);
}
html[data-theme="dark"] aside.sidebar .nav-item .nav-desc,
html[data-theme="dark"] aside.sidebar .nav-item .nav-sub {
color: rgba(232, 234, 237, 0.42);
}
html[data-theme="dark"] aside.sidebar .nav-item.active,
html[data-theme="dark"] aside.sidebar .nav-item[aria-current="page"] {
color: #e8eaed;
}
html[data-theme="dark"] aside.sidebar .org-switcher,
html[data-theme="dark"] aside.sidebar .org-switcher span {
color: rgba(232, 234, 237, 0.78);
}
html[data-theme="dark"] aside.sidebar .org-switcher svg {
color: rgba(232, 234, 237, 0.48);
}
html[data-theme="dark"] .user .av {
background: #3a4050;
color: #fff;
}
html[data-theme="dark"] .grid-bg {
background-image:
linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
}
html[data-theme="dark"] .topbar {
border-bottom-color: rgba(255, 255, 255, 0.08);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}
html[data-theme="dark"] .top-search {
border-color: #3a4050;
color: #e8eaed;
background: rgba(255, 255, 255, 0.04);
}
html[data-theme="dark"] .top-search:hover {
border-color: #4a5160;
background: rgba(255, 255, 255, 0.07);
}
html[data-theme="dark"] .queue-chip,
html[data-theme="dark"] .balance-chip {
border-color: var(--border-faint);
background: var(--surface);
}
html[data-theme="dark"] aside.sidebar .nav-item .nav-chevron {
color: rgba(232, 234, 237, 0.42);
}
html[data-theme="dark"] aside.sidebar .nav-item:hover {
color: #e8eaed;
}
html[data-theme="dark"] aside.sidebar .liquid-shape {
/* 深色下不用纯黑胶囊,改用品牌蓝选中态 */
color: #3d6bff;
filter: drop-shadow(0 5px 10px rgba(61, 107, 255, 0.28));
}
html[data-theme="dark"] aside.sidebar .nav-liquid,
html[data-theme="dark"] aside.sidebar .nav-capsule {
background: rgba(61, 107, 255, 0.12);
box-shadow: 0 0 0 4px rgba(61, 107, 255, 0.12);
}
html[data-theme="dark"] .main,
html[data-theme="dark"] .content,
html[data-theme="dark"] .page {
background: var(--background-base);
color: var(--accent-black);
}
html[data-theme="dark"] .icon-btn {
background: var(--surface);
border-color: var(--border-faint);
color: var(--black-alpha-64);
}
html[data-theme="dark"] .icon-btn:hover {
background: var(--background-lighter);
color: var(--accent-black);
border-color: var(--border-loud);
}
::selection { background: var(--heat-20); color: var(--heat); } ::selection { background: var(--heat-20); color: var(--heat); }
html, body { html, body {
@@ -268,7 +488,7 @@ aside.sidebar {
min-height: var(--topbar-height); min-height: var(--topbar-height);
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
background: #fff; background: var(--surface);
border-bottom: 1px solid rgba(27, 32, 40, 0.07); border-bottom: 1px solid rgba(27, 32, 40, 0.07);
} }
.brand { display: flex; align-items: center; justify-content: center; min-width: 0; color: var(--accent-black); } .brand { display: flex; align-items: center; justify-content: center; min-width: 0; color: var(--accent-black); }
@@ -607,7 +827,7 @@ aside.sidebar .nav-subitem .pill-mini {
padding: 0 5px; padding: 0 5px;
border: 0; border: 0;
border-radius: 999px; border-radius: 999px;
background: #fff; background: var(--surface);
color: var(--klein); color: var(--klein);
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
@@ -619,7 +839,7 @@ aside.sidebar .liquid-indicator {
top: 0; top: 0;
left: 0; left: 0;
z-index: 2; z-index: 2;
width: 132px; width: 152px;
height: 76px; height: 76px;
pointer-events: none; pointer-events: none;
transition: transform 520ms cubic-bezier(0.22, 1, 0.36, 1); transition: transform 520ms cubic-bezier(0.22, 1, 0.36, 1);
@@ -635,6 +855,7 @@ aside.sidebar .liquid-shape {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: visible; overflow: visible;
color: #101012;
filter: drop-shadow(0 5px 7px rgba(0, 0, 0, 0.14)); filter: drop-shadow(0 5px 7px rgba(0, 0, 0, 0.14));
transform-origin: left center; transform-origin: left center;
} }
@@ -651,7 +872,7 @@ aside.sidebar .indicator-content {
top: 0; top: 0;
left: 33px; left: 33px;
height: 76px; height: 76px;
width: 94px; width: 112px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 9px; gap: 9px;
@@ -776,7 +997,7 @@ body.sidebar-collapsed .user::after { display: none; }
grid-column: 2; grid-column: 2;
grid-row: 2; grid-row: 2;
position: relative; position: relative;
background: #fff; background: var(--surface);
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
overflow-x: clip; overflow-x: clip;
@@ -827,7 +1048,7 @@ body.sidebar-collapsed .user::after { display: none; }
display: flex; align-items: center; gap: 16px; display: flex; align-items: center; gap: 16px;
padding: 0 clamp(36px, 3.2vw, 68px); padding: 0 clamp(36px, 3.2vw, 68px);
border-bottom: 1px solid rgba(27, 32, 40, 0.08); border-bottom: 1px solid rgba(27, 32, 40, 0.08);
background: #fff; background: var(--surface);
box-shadow: 0 4px 16px rgba(20, 27, 38, 0.035); box-shadow: 0 4px 16px rgba(20, 27, 38, 0.035);
position: sticky; top: 0; z-index: var(--z-topbar); position: sticky; top: 0; z-index: var(--z-topbar);
align-self: start; align-self: start;
@@ -1168,8 +1389,11 @@ body.sidebar-collapsed .user::after { display: none; }
.with-corners .corner-tr, .with-corners .corner-bl { .with-corners .corner-tr, .with-corners .corner-bl {
content: ''; position: absolute; content: ''; position: absolute;
width: 14px; height: 14px; width: 14px; height: 14px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-color: var(--border-muted);
background-size: contain; -webkit-mask: var(--corner-mark) center / contain no-repeat;
mask: var(--corner-mark) center / contain no-repeat;
color: transparent;
font-size: 0;
pointer-events: none; pointer-events: none;
} }
.card-hard.with-corners::before { top: -7px; left: -7px; } .card-hard.with-corners::before { top: -7px; left: -7px; }
@@ -2269,6 +2493,11 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
color: var(--black-alpha-56); color: var(--black-alpha-56);
font-weight: 500; font-weight: 500;
} }
html[data-theme="dark"] .placeholder .ph-frame {
background: var(--surface-raised);
border-color: var(--border-faint);
color: var(--black-alpha-56);
}
.placeholder.has-mock-media { .placeholder.has-mock-media {
background-color: var(--background-lighter); background-color: var(--background-lighter);
background-image: var(--mock-media-url); background-image: var(--mock-media-url);
@@ -2599,6 +2828,7 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
margin: 6px 4px; margin: 6px 4px;
} }
/* ─── Modal ─── */ /* ─── Modal ─── */
.modal-bg { .modal-bg {
position: fixed; inset: 0; position: fixed; inset: 0;
@@ -2632,24 +2862,27 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
content: ''; content: '';
position: absolute; position: absolute;
width: 14px; height: 14px; width: 14px; height: 14px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-color: var(--border-muted);
background-size: contain; -webkit-mask: var(--corner-mark) center / contain no-repeat;
mask: var(--corner-mark) center / contain no-repeat;
pointer-events: none; pointer-events: none;
color: var(--black-alpha-48); z-index: 1;
font-family: var(--font-mono);
font-size: 13px; line-height: 1;
} }
.modal::before { top: -7px; left: -7px; } .modal::before { top: 0; left: 0; }
.modal::after { bottom: -7px; right: -7px; } .modal::after { bottom: 0; right: 0; }
.modal .corner-tr, .modal .corner-bl { .modal .corner-tr, .modal .corner-bl {
position: absolute; position: absolute;
width: 14px; height: 14px; width: 14px; height: 14px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-color: var(--border-muted);
background-size: contain; -webkit-mask: var(--corner-mark) center / contain no-repeat;
mask: var(--corner-mark) center / contain no-repeat;
color: transparent;
font-size: 0;
pointer-events: none; pointer-events: none;
z-index: 1;
} }
.modal .corner-tr { top: -7px; right: -7px; } .modal .corner-tr { top: 0; right: 0; }
.modal .corner-bl { bottom: -7px; left: -7px; } .modal .corner-bl { bottom: 0; left: 0; }
.modal-h { .modal-h {
padding: 22px 24px 16px; padding: 22px 24px 16px;
border-bottom: 1px solid var(--border-faint); border-bottom: 1px solid var(--border-faint);
@@ -2716,6 +2949,12 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
font-family: inherit; font-family: inherit;
} }
.card-del-btn svg { width: 14px; height: 14px; } .card-del-btn svg { width: 14px; height: 14px; }
html[data-theme="dark"] .card-del-btn {
background: var(--surface-raised);
border-color: var(--border-faint);
color: var(--black-alpha-56);
box-shadow: none;
}
.product-card:hover .card-del-btn, .product-card:hover .card-del-btn,
.asset-card:hover .card-del-btn, .asset-card:hover .card-del-btn,
.project-card:hover .card-del-btn, .project-card:hover .card-del-btn,
@@ -2804,10 +3043,18 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
position: fixed; top: 0; left: 0; bottom: 0; position: fixed; top: 0; left: 0; bottom: 0;
width: var(--sidebar-width); width: var(--sidebar-width);
z-index: 1200; z-index: 1200;
/* 手机上的侧栏是抽屉,必须遮住页面内容,不能沿用桌面端的半透明背景。 */
background: var(--surface-raised);
border-right-color: var(--border-faint);
transform: translateX(-100%); transform: translateX(-100%);
transition: transform .28s cubic-bezier(.32, .72, 0, 1); transition: transform .28s cubic-bezier(.32, .72, 0, 1);
box-shadow: 0 0 40px rgba(0, 0, 0, .18); box-shadow: 0 0 40px rgba(0, 0, 0, .18);
} }
/* 暗色桌面侧栏可保留轻透层;窄屏抽屉必须是实底,避免透出页面内容。 */
html[data-theme="dark"] aside.sidebar {
background: var(--surface-raised);
border-right-color: var(--border-faint);
}
/* 抽屉里收窄态不适用,强制展开宽度,避免 body.sidebar-collapsed 把抽屉压成 96px */ /* 抽屉里收窄态不适用,强制展开宽度,避免 body.sidebar-collapsed 把抽屉压成 96px */
body.sidebar-collapsed aside.sidebar { width: var(--sidebar-width); } body.sidebar-collapsed aside.sidebar { width: var(--sidebar-width); }
aside.sidebar.mobile-open { transform: translateX(0); } aside.sidebar.mobile-open { transform: translateX(0); }
@@ -2822,8 +3069,22 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
} }
.mobile-nav-btn span { display: block; width: 16px; height: 1.6px; background: var(--accent-black); border-radius: 2px; } .mobile-nav-btn span { display: block; width: 16px; height: 1.6px; background: var(--accent-black); border-radius: 2px; }
.mobile-nav-backdrop { position: fixed; inset: 0; z-index: 1150; background: rgba(21, 20, 15, .42); } .mobile-nav-backdrop { position: fixed; inset: 0; z-index: 1150; background: rgba(21, 20, 15, .42); }
/* 顶栏给汉堡键让出左侧空间 */ /* 顶栏给汉堡键让出左侧空间;搜索与余额压缩为单行工具,避免中文在窄窗逐字折行。 */
.topbar { padding-left: 64px; } .topbar { padding: 0 16px 0 64px; gap: 8px; }
.topbar .right { gap: 8px; flex: 0 0 auto; }
.top-search {
width: 40px;
min-width: 40px;
padding: 0;
justify-content: center;
}
.top-search span { display: none; }
.balance-chip {
flex: 0 0 auto;
padding: 0 12px;
white-space: nowrap;
}
.balance-chip-label { display: none; }
.stats { grid-template-columns: repeat(2, 1fr); } .stats { grid-template-columns: repeat(2, 1fr); }
.stat:nth-child(2) { border-right: 0; } .stat:nth-child(2) { border-right: 0; }
.stat:nth-child(1), .stat:nth-child(2) { border-bottom: 1px solid var(--border-faint); } .stat:nth-child(1), .stat:nth-child(2) { border-bottom: 1px solid var(--border-faint); }
@@ -2899,7 +3160,7 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
left: 0; left: 0;
width: 252px; width: 252px;
padding: 11px 13px 14px; padding: 11px 13px 14px;
background: #fff; background: var(--surface);
color: var(--accent-black); color: var(--accent-black);
border: 1px solid rgba(34, 42, 54, 0.10); border: 1px solid rgba(34, 42, 54, 0.10);
border-radius: var(--r-md); border-radius: var(--r-md);
@@ -2921,7 +3182,7 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
position: absolute; position: absolute;
top: -5px; left: 14px; top: -5px; left: 14px;
width: 10px; height: 10px; width: 10px; height: 10px;
background: #fff; background: var(--surface);
border-left: 1px solid rgba(34, 42, 54, 0.10); border-left: 1px solid rgba(34, 42, 54, 0.10);
border-top: 1px solid rgba(34, 42, 54, 0.10); border-top: 1px solid rgba(34, 42, 54, 0.10);
transform: rotate(45deg); transform: rotate(45deg);
+3 -3
View File
@@ -334,7 +334,7 @@
border: 1px solid rgba(28, 34, 43, 0.12); border: 1px solid rgba(28, 34, 43, 0.12);
border-radius: 8px; border-radius: 8px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
@@ -598,7 +598,7 @@
max-width: 280px; max-width: 280px;
max-height: 260px; max-height: 260px;
overflow-y: auto; overflow-y: auto;
background: #fff; background: var(--surface);
border: 1px solid rgba(34, 42, 54, 0.10); border: 1px solid rgba(34, 42, 54, 0.10);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 12px 30px rgba(20, 27, 38, 0.12); box-shadow: 0 12px 30px rgba(20, 27, 38, 0.12);
@@ -708,7 +708,7 @@
left: 0; left: 0;
z-index: 60; z-index: 60;
min-width: 180px; min-width: 180px;
background: #fff; background: var(--surface);
border: 1px solid rgba(34, 42, 54, 0.10); border: 1px solid rgba(34, 42, 54, 0.10);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 12px 30px rgba(20, 27, 38, 0.12); box-shadow: 0 12px 30px rgba(20, 27, 38, 0.12);
+4 -4
View File
@@ -124,7 +124,7 @@
padding: 0 13px; padding: 0 13px;
border: 1px solid var(--lib-line); border: 1px solid var(--lib-line);
border-radius: 10px; border-radius: 10px;
background: #fff; background: var(--surface);
} }
.library-page .lib-search svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--lib-muted); } .library-page .lib-search svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--lib-muted); }
.library-page .lib-search input { .library-page .lib-search input {
@@ -152,7 +152,7 @@
border: 1px solid rgba(34, 42, 54, 0.12); border: 1px solid rgba(34, 42, 54, 0.12);
border-radius: 10px; border-radius: 10px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
text-align: left; text-align: left;
@@ -175,7 +175,7 @@
padding: 6px; padding: 6px;
border: 1px solid var(--lib-line); border: 1px solid var(--lib-line);
border-radius: 11px; border-radius: 11px;
background: #fff; background: var(--surface);
box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14); box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
@@ -197,7 +197,7 @@
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
text-align: left; text-align: left;
+4
View File
@@ -1,3 +1,4 @@
import { applyTheme, readStoredAppearance } from "./theme";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { App } from "./App"; import { App } from "./App";
import "./styles.css"; import "./styles.css";
@@ -22,5 +23,8 @@ import "./quick-create-page.css";
import "./admin-page.css"; import "./admin-page.css";
import "./omni-create-page.css"; import "./omni-create-page.css";
import "./omni-session-page.css"; import "./omni-session-page.css";
import "./dark-mode-pages.css";
applyTheme(readStoredAppearance());
createRoot(document.getElementById("root")!).render(<App />); createRoot(document.getElementById("root")!).render(<App />);
+5 -2
View File
@@ -50,7 +50,7 @@
padding: 0; padding: 0;
border-radius: 8px; border-radius: 8px;
border: 1px solid rgba(28, 34, 43, 0.12); border: 1px solid rgba(28, 34, 43, 0.12);
background: #fff; background: var(--surface);
color: var(--st-text); color: var(--st-text);
cursor: pointer; cursor: pointer;
} }
@@ -178,7 +178,7 @@
padding: 0 13px; padding: 0 13px;
border: 1px solid rgba(34, 42, 54, 0.10); border: 1px solid rgba(34, 42, 54, 0.10);
border-radius: 10px; border-radius: 10px;
background: #fff; background: var(--surface);
color: var(--st-muted); color: var(--st-muted);
} }
.msg-search input { .msg-search input {
@@ -196,6 +196,8 @@
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
display: flex;
flex-direction: column;
} }
.msg-load-more { .msg-load-more {
padding: 14px 16px 18px; padding: 14px 16px 18px;
@@ -274,6 +276,7 @@
} }
.msg-empty { .msg-empty {
flex: 1;
min-height: 320px; min-height: 320px;
display: grid; display: grid;
place-items: center; place-items: center;
+2 -1
View File
@@ -204,6 +204,7 @@
pointer-events: none; pointer-events: none;
} }
.models-page .ml-check svg { width: 13px; height: 13px; } .models-page .ml-check svg { width: 13px; height: 13px; }
.models-page.manage-mode .ml-card:not(.selectable) { cursor: not-allowed; }
.models-page.manage-mode .ml-card.selectable .ml-check { display: grid; } .models-page.manage-mode .ml-card.selectable .ml-check { display: grid; }
.models-page.manage-mode .ml-card.selected .ml-check { .models-page.manage-mode .ml-card.selected .ml-check {
color: #fff; color: #fff;
@@ -383,7 +384,7 @@
padding: 0 12px; padding: 0 12px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 999px; border-radius: 999px;
background: #fff; background: var(--surface);
color: #414750; color: #414750;
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
+75 -45
View File
@@ -36,24 +36,31 @@
position: absolute; position: absolute;
top: 20px; top: 20px;
right: 0; right: 0;
height: 40px; height: 36px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 6px;
padding: 0 14px; padding: 0 14px;
border: 1.5px solid rgba(16, 16, 18, .22); border: 1px solid var(--border-faint);
border-radius: 12px; border-radius: var(--r-md);
color: var(--black); color: var(--black-alpha-56);
background: #fff; background: transparent;
font-size: 13px; font-size: 13px;
font-weight: 650; font-weight: 500;
box-shadow: 0 4px 12px rgba(16, 16, 18, .08); box-shadow: none;
cursor: pointer; cursor: pointer;
transition: background-color var(--t-base), border-color var(--t-base), color var(--t-base);
} }
.omni-home-history:hover { .omni-home-history:hover {
border-color: var(--black); border-color: var(--black-alpha-24);
background: #f7f7f8; color: var(--accent-black);
background: var(--black-alpha-4);
}
.omni-home-history:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--background-base), 0 0 0 4px var(--heat-40);
} }
.omni-home-history svg { .omni-home-history svg {
@@ -378,7 +385,7 @@
padding: 8px; padding: 8px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 13px; border-radius: 13px;
background: #fff; background: var(--surface);
box-shadow: 0 18px 38px rgba(20, 27, 38, .15); box-shadow: 0 18px 38px rgba(20, 27, 38, .15);
} }
@@ -447,7 +454,7 @@
padding: 8px; padding: 8px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 13px; border-radius: 13px;
background: #fff; background: var(--surface);
box-shadow: 0 18px 38px rgba(20, 27, 38, .15); box-shadow: 0 18px 38px rgba(20, 27, 38, .15);
} }
@@ -582,7 +589,7 @@
.omni-parameter .custom-select-trigger:hover, .omni-parameter .custom-select-trigger:hover,
.omni-parameter .custom-select-shell.open .custom-select-trigger { .omni-parameter .custom-select-shell.open .custom-select-trigger {
border-color: rgba(0, 47, 167, .36); border-color: rgba(0, 47, 167, .36);
background: #fff; background: var(--surface);
} }
.omni-parameter .custom-select-trigger svg { .omni-parameter .custom-select-trigger svg {
@@ -640,7 +647,7 @@
.omni-duration-trigger:hover, .omni-duration-trigger:hover,
.omni-duration-trigger[aria-expanded="true"] { .omni-duration-trigger[aria-expanded="true"] {
border-color: rgba(0, 47, 167, .36); border-color: rgba(0, 47, 167, .36);
background: #fff; background: var(--surface);
box-shadow: 0 0 0 3px rgba(0, 47, 167, .07); box-shadow: 0 0 0 3px rgba(0, 47, 167, .07);
} }
@@ -664,7 +671,7 @@
padding: 16px; padding: 16px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 16px; border-radius: 16px;
background: #fff; background: var(--surface);
box-shadow: 0 18px 38px rgba(20, 27, 38, .15); box-shadow: 0 18px 38px rgba(20, 27, 38, .15);
} }
@@ -738,7 +745,7 @@
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
color: #575e69; color: #575e69;
background: #fff; background: var(--surface);
font-size: 12px; font-size: 12px;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
@@ -995,15 +1002,15 @@
.omni-preset-dialog { .omni-preset-dialog {
position: relative; position: relative;
z-index: 1; z-index: 1;
width: min(1040px, 100%); width: min(1180px, 100%);
display: grid; display: grid;
grid-template-columns: 440px minmax(0, 1fr); grid-template-columns: 500px minmax(0, 1fr);
gap: 42px; gap: 32px;
padding: 30px; padding: 28px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-top: 3px solid var(--klein); border-top: 3px solid var(--klein);
border-radius: 20px; border-radius: 20px;
background: #fff; background: var(--surface);
box-shadow: 0 28px 70px rgba(10, 17, 30, .22); box-shadow: 0 28px 70px rgba(10, 17, 30, .22);
animation: omniMessageIn 180ms ease both; animation: omniMessageIn 180ms ease both;
} }
@@ -1139,7 +1146,7 @@
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 10px; border-radius: 10px;
color: #616873; color: #616873;
background: #fff; background: var(--surface);
font-size: 13px; font-size: 13px;
cursor: pointer; cursor: pointer;
} }
@@ -1231,7 +1238,7 @@
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 10px; border-radius: 10px;
color: #414750; color: #414750;
background: #fff; background: var(--surface);
cursor: pointer; cursor: pointer;
} }
@@ -1307,35 +1314,34 @@
.omni-history-list { .omni-history-list {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px; gap: 14px;
} }
.omni-history-item { .omni-history-item {
min-width: 0; min-width: 0;
display: grid; display: grid;
grid-template-columns: 150px minmax(0, 1fr) 70px; grid-template-columns: 150px minmax(0, 1fr) 36px;
align-items: center; align-items: center;
gap: 20px; gap: 16px;
padding: 17px; padding: 16px;
border: 1px solid rgba(34, 42, 54, .09); border: 1px solid rgba(34, 42, 54, .09);
border-radius: 19px; border-radius: var(--r-md);
background: rgba(255, 255, 255, .96); background: rgba(255, 255, 255, .96);
cursor: pointer; cursor: pointer;
box-shadow: 0 8px 22px rgba(20, 27, 38, .05); box-shadow: none;
transition: transform 170ms ease, border-color 170ms ease, box-shadow 170ms ease; transition: border-color 170ms ease, background-color 170ms ease;
} }
.omni-history-item:hover { .omni-history-item:hover {
transform: translateY(-2px);
border-color: rgba(0, 47, 167, .30); border-color: rgba(0, 47, 167, .30);
box-shadow: 0 13px 26px rgba(20, 27, 38, .09); background: var(--background-lighter);
} }
.omni-history-item > img { .omni-history-item > img {
width: 150px; width: 150px;
height: 112px; height: 112px;
display: block; display: block;
border-radius: 13px; border-radius: var(--r-md);
object-fit: cover; object-fit: cover;
background: #edf0f4; background: #edf0f4;
} }
@@ -1354,21 +1360,24 @@
} }
.omni-history-item h2 { .omni-history-item h2 {
margin: 8px 0 7px; margin: 6px 0 5px;
font-size: 17px; font-size: 14px;
line-height: 1.35; font-weight: 500;
line-height: 1.4;
} }
.omni-history-item p, .omni-history-item p,
.omni-history-item small { .omni-history-item small {
color: #858b94; color: #858b94;
font-size: 14px; font-size: 13px;
line-height: 1.45; line-height: 1.55;
} }
.omni-history-item small { .omni-history-item small {
display: block; display: block;
margin-top: 9px; margin-top: 7px;
font-family: var(--font-mono);
font-size: 11px;
} }
.omni-history-actions { .omni-history-actions {
@@ -1389,14 +1398,16 @@
} }
.omni-history-actions button { .omni-history-actions button {
width: 36px; width: 32px;
height: 36px; height: 32px;
display: grid; display: grid;
place-items: center; place-items: center;
border: 1px solid transparent;
border-radius: 8px; border-radius: 8px;
color: #858b94; color: #858b94;
background: #f4f5f7; background: transparent;
cursor: pointer; cursor: pointer;
transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease, opacity 160ms ease;
} }
.omni-history-actions button:hover { .omni-history-actions button:hover {
@@ -1404,6 +1415,25 @@
background: #fff0f0; background: #fff0f0;
} }
@media (hover: hover) {
.omni-history-actions button {
opacity: 0;
pointer-events: none;
}
.omni-history-item:hover .omni-history-actions button,
.omni-history-item:focus-within .omni-history-actions button,
.omni-history-actions button:focus-visible {
opacity: 1;
pointer-events: auto;
}
.omni-history-item:hover .omni-history-actions button {
border-color: rgba(34, 42, 54, .09);
background: #f4f5f7;
}
}
.omni-delete-confirm { .omni-delete-confirm {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -1432,7 +1462,7 @@
padding: 26px; padding: 26px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 17px; border-radius: 17px;
background: #fff; background: var(--surface);
text-align: center; text-align: center;
box-shadow: 0 26px 64px rgba(10, 17, 30, .22); box-shadow: 0 26px 64px rgba(10, 17, 30, .22);
animation: omniMessageIn 180ms ease both; animation: omniMessageIn 180ms ease both;
@@ -1476,7 +1506,7 @@
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 9px; border-radius: 9px;
color: #5e6570; color: #5e6570;
background: #fff; background: var(--surface);
font-size: 12px; font-size: 12px;
font-weight: 650; font-weight: 650;
cursor: pointer; cursor: pointer;
@@ -1547,7 +1577,7 @@
.omni-parameter .rs-select-btn:hover, .omni-parameter .rs-select-btn:hover,
.omni-parameter .rs-select.open .rs-select-btn { .omni-parameter .rs-select.open .rs-select-btn {
border-color: rgba(0, 47, 167, .36); border-color: rgba(0, 47, 167, .36);
background: #fff; background: var(--surface);
} }
.omni-parameter .rs-select-btn svg { .omni-parameter .rs-select-btn svg {
+35 -22
View File
@@ -71,12 +71,25 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 7px; gap: 7px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid var(--border-faint);
border-radius: 10px; border-radius: var(--r-md);
color: #414750; color: var(--black-alpha-56);
background: #fff; background: transparent;
font-size: 12px; font-size: 12px;
font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background-color var(--t-base), border-color var(--t-base), color var(--t-base);
}
.omni-session-back:hover {
border-color: var(--black-alpha-24);
color: var(--accent-black);
background: var(--black-alpha-4);
}
.omni-session-back:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--background-base), 0 0 0 4px var(--heat-40);
} }
.omni-session-back svg { .omni-session-back svg {
@@ -375,7 +388,7 @@
margin: 0 0 24px 44px; margin: 0 0 24px 44px;
border: 1px solid rgba(34, 42, 54, .09); border: 1px solid rgba(34, 42, 54, .09);
border-radius: 15px; border-radius: 15px;
background: #fff; background: var(--surface);
box-shadow: 0 10px 26px rgba(20, 27, 38, .06); box-shadow: 0 10px 26px rgba(20, 27, 38, .06);
animation: omniMessageIn 220ms ease both; animation: omniMessageIn 220ms ease both;
overflow: hidden; overflow: hidden;
@@ -407,7 +420,7 @@
margin: 0 0 24px 44px; margin: 0 0 24px 44px;
border: 1px solid rgba(34, 42, 54, .09); border: 1px solid rgba(34, 42, 54, .09);
border-radius: 16px; border-radius: 16px;
background: #fff; background: var(--surface);
box-shadow: 0 12px 30px rgba(20, 27, 38, .065); box-shadow: 0 12px 30px rgba(20, 27, 38, .065);
animation: omniMessageIn 220ms ease both; animation: omniMessageIn 220ms ease both;
} }
@@ -594,7 +607,7 @@
border: 1px solid rgba(34, 42, 54, .08); border: 1px solid rgba(34, 42, 54, .08);
border-radius: 10px; border-radius: 10px;
color: #5b626d; color: #5b626d;
background: #fff; background: var(--surface);
font-size: 13px; font-size: 13px;
line-height: 1.65; line-height: 1.65;
} }
@@ -689,7 +702,7 @@
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 9px; border-radius: 9px;
color: #333944; color: #333944;
background: #fff; background: var(--surface);
font-size: 12px; font-size: 12px;
font-weight: 650; font-weight: 650;
cursor: pointer; cursor: pointer;
@@ -712,7 +725,7 @@
border: 1px solid rgba(0, 47, 167, .20); border: 1px solid rgba(0, 47, 167, .20);
border-radius: 9px; border-radius: 9px;
color: var(--klein); color: var(--klein);
background: #fff; background: var(--surface);
font-size: 12px; font-size: 12px;
font-weight: 650; font-weight: 650;
cursor: pointer; cursor: pointer;
@@ -814,7 +827,7 @@
.omni-session-preset-trigger[aria-expanded="true"] { .omni-session-preset-trigger[aria-expanded="true"] {
border-color: rgba(0, 47, 167, .36); border-color: rgba(0, 47, 167, .36);
color: var(--klein); color: var(--klein);
background: #fff; background: var(--surface);
} }
.omni-session-preset-trigger span { .omni-session-preset-trigger span {
@@ -837,7 +850,7 @@
overflow: hidden; overflow: hidden;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 17px; border-radius: 17px;
background: #fff; background: var(--surface);
box-shadow: 0 24px 60px rgba(20, 27, 38, .18); box-shadow: 0 24px 60px rgba(20, 27, 38, .18);
transform: translateX(-52%); transform: translateX(-52%);
} }
@@ -1035,7 +1048,7 @@
overflow: hidden; overflow: hidden;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 12px; border-radius: 12px;
background: #fff; background: var(--surface);
box-shadow: 0 16px 36px rgba(20, 27, 38, .15); box-shadow: 0 16px 36px rgba(20, 27, 38, .15);
} }
@@ -1707,7 +1720,7 @@
padding: 7px 13px; padding: 7px 13px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 8px; border-radius: 8px;
background: #fff; background: var(--surface);
color: #575d66; color: #575d66;
font-size: 13px; font-size: 13px;
cursor: pointer; cursor: pointer;
@@ -1730,7 +1743,7 @@
padding: 9px 12px; padding: 9px 12px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 8px; border-radius: 8px;
background: #fff; background: var(--surface);
color: var(--black); color: var(--black);
font-size: 13px; font-size: 13px;
font-family: inherit; font-family: inherit;
@@ -1754,7 +1767,7 @@
padding: 6px; padding: 6px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 8px; border-radius: 8px;
background: #fff; background: var(--surface);
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
} }
@@ -1844,7 +1857,7 @@
padding: 14px 16px; padding: 14px 16px;
border: 1px solid rgba(34, 42, 54, 0.09); border: 1px solid rgba(34, 42, 54, 0.09);
border-radius: 16px; border-radius: 16px;
background: #fff; background: var(--surface);
box-shadow: 0 12px 30px rgba(20, 27, 38, .065); box-shadow: 0 12px 30px rgba(20, 27, 38, .065);
/* 不走入场 opacity 动画:poll 重渲染时避免「闪一下」 */ /* 不走入场 opacity 动画:poll 重渲染时避免「闪一下」 */
animation: none; animation: none;
@@ -1905,7 +1918,7 @@
} }
.omni-step-confirm-actions button.is-secondary { .omni-step-confirm-actions button.is-secondary {
background: #fff; background: var(--surface);
border: 1px solid rgba(34, 42, 54, 0.14); border: 1px solid rgba(34, 42, 54, 0.14);
color: #222a36; color: #222a36;
} }
@@ -1946,7 +1959,7 @@
.omni-step-confirm-revise-input:focus { .omni-step-confirm-revise-input:focus {
border-color: rgba(0, 47, 167, .42); border-color: rgba(0, 47, 167, .42);
background: #fff; background: var(--surface);
box-shadow: 0 0 0 3px rgba(0, 47, 167, .14); box-shadow: 0 0 0 3px rgba(0, 47, 167, .14);
} }
@@ -1973,7 +1986,7 @@
padding: 14px 16px; padding: 14px 16px;
border: 1px solid rgba(34, 42, 54, .10); border: 1px solid rgba(34, 42, 54, .10);
border-radius: 14px; border-radius: 14px;
background: #fff; background: var(--surface);
/* 时长/参数下拉要冒出卡片,不能被裁 */ /* 时长/参数下拉要冒出卡片,不能被裁 */
overflow: visible; overflow: visible;
} }
@@ -2604,7 +2617,7 @@
gap: 10px; gap: 10px;
padding: 14px 14px 12px; padding: 14px 14px 12px;
border-bottom: 1px solid rgba(34, 42, 54, .08); border-bottom: 1px solid rgba(34, 42, 54, .08);
background: #fff; background: var(--surface);
} }
.omni-prompt-drawer-file { .omni-prompt-drawer-file {
@@ -2684,7 +2697,7 @@
padding: 14px 15px; padding: 14px 15px;
border: 1px solid rgba(34, 42, 54, .06); border: 1px solid rgba(34, 42, 54, .06);
border-radius: 14px; border-radius: 14px;
background: #fff; background: var(--surface);
box-shadow: 0 6px 16px rgba(20, 27, 38, .04); box-shadow: 0 6px 16px rgba(20, 27, 38, .04);
} }
@@ -2756,7 +2769,7 @@
overflow-x: auto; overflow-x: auto;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(34, 42, 54, .08); border: 1px solid rgba(34, 42, 54, .08);
background: #fff; background: var(--surface);
} }
.omni-prompt-table { .omni-prompt-table {
+22 -22
View File
@@ -110,7 +110,7 @@
padding: 12px; padding: 12px;
border: 1px solid rgba(34, 42, 54, 0.08); border: 1px solid rgba(34, 42, 54, 0.08);
border-radius: 12px; border-radius: 12px;
background: #fff; background: var(--surface);
} }
.pipeline-page .pl-rail-summary > span { color: var(--pl-muted); font-size: 10px; } .pipeline-page .pl-rail-summary > span { color: var(--pl-muted); font-size: 10px; }
.pipeline-page .pl-rail-track { .pipeline-page .pl-rail-track {
@@ -139,7 +139,7 @@
border: 1px solid rgba(34, 42, 54, 0.085); border: 1px solid rgba(34, 42, 54, 0.085);
border-radius: 12px; border-radius: 12px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
box-sizing: border-box; box-sizing: border-box;
@@ -156,7 +156,7 @@
} }
.pipeline-page .pl-step.static:hover { .pipeline-page .pl-step.static:hover {
border-color: rgba(34, 42, 54, 0.085); border-color: rgba(34, 42, 54, 0.085);
background: #fff; background: var(--surface);
} }
.pipeline-page .pl-step.static:hover .pl-step-chev { color: var(--pl-muted); } .pipeline-page .pl-step.static:hover .pl-step-chev { color: var(--pl-muted); }
.pipeline-page .pl-step.current { .pipeline-page .pl-step.current {
@@ -250,7 +250,7 @@
.pipeline-page .pl-ghost { .pipeline-page .pl-ghost {
border: 1px solid rgba(34, 42, 54, 0.11); border: 1px solid rgba(34, 42, 54, 0.11);
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
} }
.pipeline-page .pl-next { .pipeline-page .pl-next {
min-width: 210px; min-width: 210px;
@@ -328,7 +328,7 @@
border-radius: 8px; border-radius: 8px;
} }
.chat-msg.ai .chat-bubble { .chat-msg.ai .chat-bubble {
background: #fff; background: var(--surface);
border: 1px solid rgba(34, 42, 54, 0.08); border: 1px solid rgba(34, 42, 54, 0.08);
border-radius: 8px 8px 8px 2px; border-radius: 8px 8px 8px 2px;
} }
@@ -465,7 +465,7 @@
overflow: hidden; overflow: hidden;
border: 1px solid rgba(0, 47, 167, 0.13); border: 1px solid rgba(0, 47, 167, 0.13);
border-radius: 14px; border-radius: 14px;
background: #fff; background: var(--surface);
box-shadow: 0 11px 26px rgba(20, 27, 38, 0.07); box-shadow: 0 11px 26px rgba(20, 27, 38, 0.07);
} }
.shot-head { .shot-head {
@@ -504,7 +504,7 @@
border: 1px solid rgba(34, 42, 54, 0.09); border: 1px solid rgba(34, 42, 54, 0.09);
border-radius: 8px; border-radius: 8px;
color: var(--pl-muted); color: var(--pl-muted);
background: #fff; background: var(--surface);
cursor: pointer; cursor: pointer;
} }
.shot-icon-btn svg { width: 13px; height: 13px; } .shot-icon-btn svg { width: 13px; height: 13px; }
@@ -542,7 +542,7 @@
.shot-area:focus { .shot-area:focus {
outline: none; outline: none;
border-color: rgba(0, 47, 167, 0.38); border-color: rgba(0, 47, 167, 0.38);
background: #fff; background: var(--surface);
box-shadow: 0 0 0 3px rgba(0, 47, 167, 0.07); box-shadow: 0 0 0 3px rgba(0, 47, 167, 0.07);
} }
.shot-dialogue { min-height: 42px; } .shot-dialogue { min-height: 42px; }
@@ -564,7 +564,7 @@
.icon-mini-btn.armed { width: auto; padding: 0 8px; font-size: 12px; color: var(--accent-crimson); border-color: var(--accent-crimson); background: var(--surface); } .icon-mini-btn.armed { width: auto; padding: 0 8px; font-size: 12px; color: var(--accent-crimson); border-color: var(--accent-crimson); background: var(--surface); }
.shot-insert-gap { flex: 0 0 auto; height: 10px; position: relative; display: flex; align-items: center; justify-content: center; padding: 0; transition: height .24s cubic-bezier(.18,.72,.28,1), padding .24s cubic-bezier(.18,.72,.28,1); } .shot-insert-gap { flex: 0 0 auto; height: 10px; position: relative; display: flex; align-items: center; justify-content: center; padding: 0; transition: height .24s cubic-bezier(.18,.72,.28,1), padding .24s cubic-bezier(.18,.72,.28,1); }
.shot-insert-gap:hover { height: 72px; padding: 14px 0; } .shot-insert-gap:hover { height: 72px; padding: 14px 0; }
.shot-insert-gap .add-shot-btn { opacity: 0; transform: translateY(4px) scale(.96); height: 28px; padding: 0 14px; background: #fff; color: var(--klein); border: 1px dashed rgba(0, 47, 167, 0.35); border-radius: 8px; font-size: 13px; font-family: inherit; font-weight: 500; cursor: pointer; transition: opacity .2s ease .04s, transform .24s cubic-bezier(.18,.72,.28,1) .04s, background var(--t-base), border-color var(--t-base), color var(--t-base); display: inline-flex; align-items: center; gap: 6px; pointer-events: none; white-space: nowrap; } .shot-insert-gap .add-shot-btn { opacity: 0; transform: translateY(4px) scale(.96); height: 28px; padding: 0 14px; background: var(--surface); color: var(--klein); border: 1px dashed rgba(0, 47, 167, 0.35); border-radius: 8px; font-size: 13px; font-family: inherit; font-weight: 500; cursor: pointer; transition: opacity .2s ease .04s, transform .24s cubic-bezier(.18,.72,.28,1) .04s, background var(--t-base), border-color var(--t-base), color var(--t-base); display: inline-flex; align-items: center; gap: 6px; pointer-events: none; white-space: nowrap; }
.shot-insert-gap .add-shot-btn svg { width: 12px; height: 12px; } .shot-insert-gap .add-shot-btn svg { width: 12px; height: 12px; }
.shot-insert-gap:hover .add-shot-btn { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; } .shot-insert-gap:hover .add-shot-btn { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
.shot-insert-gap .add-shot-btn:hover { background: rgba(0, 47, 167, 0.06); border-style: solid; border-color: var(--klein); } .shot-insert-gap .add-shot-btn:hover { background: rgba(0, 47, 167, 0.06); border-style: solid; border-color: var(--klein); }
@@ -613,7 +613,7 @@
width: 100%; width: 100%;
min-height: 48px; min-height: 48px;
padding: 0 14px; padding: 0 14px;
background: #fff; background: var(--surface);
border: 1px solid rgba(34, 42, 54, 0.11); border: 1px solid rgba(34, 42, 54, 0.11);
border-radius: 12px; border-radius: 12px;
font-size: 14px; font-size: 14px;
@@ -638,7 +638,7 @@
display: grid; display: grid;
grid-template-rows: 72px minmax(0, 1fr) 74px; grid-template-rows: 72px minmax(0, 1fr) 74px;
overflow: hidden; overflow: hidden;
background: #fff; background: var(--surface);
} }
.as-head { .as-head {
display: flex; display: flex;
@@ -677,7 +677,7 @@
padding: 18px; padding: 18px;
border: 1px solid rgba(0, 47, 167, 0.13); border: 1px solid rgba(0, 47, 167, 0.13);
border-radius: 16px; border-radius: 16px;
background: #fff; background: var(--surface);
box-shadow: 0 12px 30px rgba(20, 27, 38, 0.075); box-shadow: 0 12px 30px rgba(20, 27, 38, 0.075);
} }
.as-row::before { .as-row::before {
@@ -755,7 +755,7 @@
overflow: hidden; overflow: hidden;
border: 1px solid rgba(0, 47, 167, 0.15); border: 1px solid rgba(0, 47, 167, 0.15);
border-radius: 12px; border-radius: 12px;
background: #fff; background: var(--surface);
box-shadow: 0 7px 18px rgba(20, 27, 38, 0.06); box-shadow: 0 7px 18px rgba(20, 27, 38, 0.06);
} }
.as-preview.ready { .as-preview.ready {
@@ -782,7 +782,7 @@
position: absolute; position: absolute;
inset: 0; inset: 0;
pointer-events: none; pointer-events: none;
background-color: #fff; background-color: var(--surface);
background-image: background-image:
radial-gradient(ellipse 4px 1px at center, rgba(0, 47, 167, 0.28) 98%, transparent), radial-gradient(ellipse 4px 1px at center, rgba(0, 47, 167, 0.28) 98%, transparent),
radial-gradient(ellipse 1px 4px at center, rgba(0, 47, 167, 0.28) 98%, transparent); radial-gradient(ellipse 1px 4px at center, rgba(0, 47, 167, 0.28) 98%, transparent);
@@ -807,7 +807,7 @@
border: 1px solid rgba(0, 47, 167, 0.26); border: 1px solid rgba(0, 47, 167, 0.26);
border-radius: 8px; border-radius: 8px;
color: var(--klein); color: var(--klein);
background: #fff; background: var(--surface);
box-shadow: 0 8px 22px rgba(0, 47, 167, 0.11); box-shadow: 0 8px 22px rgba(0, 47, 167, 0.11);
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
@@ -911,7 +911,7 @@
overflow: hidden; overflow: hidden;
border: 1px solid rgba(0, 47, 167, 0.15); border: 1px solid rgba(0, 47, 167, 0.15);
border-radius: 12px; border-radius: 12px;
background: #fff; background: var(--surface);
box-shadow: 0 12px 28px rgba(20, 27, 38, 0.085); box-shadow: 0 12px 28px rgba(20, 27, 38, 0.085);
} }
.as-gen-preview { .as-gen-preview {
@@ -923,7 +923,7 @@
overflow: hidden; overflow: hidden;
border: 0; border: 0;
border-bottom: 1px solid rgba(34, 42, 54, 0.08); border-bottom: 1px solid rgba(34, 42, 54, 0.08);
background: #fff; background: var(--surface);
} }
.as-gen-preview.ready { .as-gen-preview.ready {
background-image: var(--mock-media-url); background-image: var(--mock-media-url);
@@ -1001,7 +1001,7 @@
} }
.as-gen-prompt:focus { .as-gen-prompt:focus {
border-color: rgba(0, 47, 167, 0.38); border-color: rgba(0, 47, 167, 0.38);
background: #fff; background: var(--surface);
box-shadow: 0 0 0 3px rgba(0, 47, 167, 0.06); box-shadow: 0 0 0 3px rgba(0, 47, 167, 0.06);
} }
.as-gen-actions { .as-gen-actions {
@@ -1088,7 +1088,7 @@
.as-ai-btn svg, .as-ghost-btn svg { width: 14px; height: 14px; } .as-ai-btn svg, .as-ghost-btn svg { width: 14px; height: 14px; }
.as-ai-btn:disabled, .as-ghost-btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; } .as-ai-btn:disabled, .as-ghost-btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }
.as-ai-btn-lg { min-height: 44px; margin-top: 18px; padding: 0 18px; font-size: 14px; } .as-ai-btn-lg { min-height: 44px; margin-top: 18px; padding: 0 18px; font-size: 14px; }
.as-ghost-btn { border: 1px solid rgba(0, 47, 167, 0.19); color: var(--klein); background: #fff; } .as-ghost-btn { border: 1px solid rgba(0, 47, 167, 0.19); color: var(--klein); background: var(--surface); }
.as-ghost-btn:hover { transform: translateY(-1px); border-color: rgba(0, 47, 167, 0.38); background: #eef5ff; } .as-ghost-btn:hover { transform: translateY(-1px); border-color: rgba(0, 47, 167, 0.38); background: #eef5ff; }
.as-ghost-btn-sm { min-height: 30px; font-size: 12px; } .as-ghost-btn-sm { min-height: 30px; font-size: 12px; }
@media (max-width: 1180px) { @media (max-width: 1180px) {
@@ -1264,14 +1264,14 @@
aspect-ratio: 9 / 16; aspect-ratio: 9 / 16;
max-height: 280px; max-height: 280px;
overflow: hidden; overflow: hidden;
background: #fff; background: var(--surface);
} }
.video-thumb.pending::before { .video-thumb.pending::before {
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
pointer-events: none; pointer-events: none;
background-color: #fff; background-color: var(--surface);
background-image: background-image:
radial-gradient(ellipse 4px 1px at center, rgba(0, 47, 167, 0.28) 98%, transparent), radial-gradient(ellipse 4px 1px at center, rgba(0, 47, 167, 0.28) 98%, transparent),
radial-gradient(ellipse 1px 4px at center, rgba(0, 47, 167, 0.28) 98%, transparent); radial-gradient(ellipse 1px 4px at center, rgba(0, 47, 167, 0.28) 98%, transparent);
@@ -1296,7 +1296,7 @@
border: 1px solid rgba(0, 47, 167, 0.26); border: 1px solid rgba(0, 47, 167, 0.26);
border-radius: 8px; border-radius: 8px;
color: var(--klein); color: var(--klein);
background: #fff; background: var(--surface);
box-shadow: 0 8px 22px rgba(0, 47, 167, 0.11); box-shadow: 0 8px 22px rgba(0, 47, 167, 0.11);
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
+7 -7
View File
@@ -310,7 +310,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
box-shadow: -28px 0 70px rgba(10, 15, 24, 0.22); box-shadow: -28px 0 70px rgba(10, 15, 24, 0.22);
transform: translateX(100%); transform: translateX(100%);
transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1); transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
@@ -422,7 +422,7 @@
overflow-y: auto; overflow-y: auto;
border: 1px solid rgba(34, 42, 54, 0.10); border: 1px solid rgba(34, 42, 54, 0.10);
border-radius: 11px; border-radius: 11px;
background: #fff; background: var(--surface);
box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14); box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
@@ -445,7 +445,7 @@
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
text-align: left; text-align: left;
@@ -635,7 +635,7 @@
padding: 0 8px; padding: 0 8px;
border: 0; border: 0;
border-radius: 999px; border-radius: 999px;
background: #fff; background: var(--surface);
color: var(--accent-black); color: var(--accent-black);
font: inherit; font: inherit;
font-size: 10px; font-size: 10px;
@@ -766,7 +766,7 @@
padding: 10px 14px; padding: 10px 14px;
border: 1px solid rgba(34, 42, 54, 0.10); border: 1px solid rgba(34, 42, 54, 0.10);
border-radius: 11px; border-radius: 11px;
background: #fff; background: var(--surface);
box-shadow: 0 12px 28px rgba(20, 27, 38, 0.12); box-shadow: 0 12px 28px rgba(20, 27, 38, 0.12);
} }
.pcd-toast-ic { .pcd-toast-ic {
@@ -816,7 +816,7 @@
border: 1px solid rgba(34, 42, 54, 0.11); border: 1px solid rgba(34, 42, 54, 0.11);
border-radius: 11px; border-radius: 11px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
@@ -845,7 +845,7 @@
background: rgba(255, 255, 255, 0.90); background: rgba(255, 255, 255, 0.90);
box-shadow: 0 10px 28px rgba(20, 27, 38, 0.07); box-shadow: 0 10px 28px rgba(20, 27, 38, 0.07);
} }
.pcd-gallery-slot img { width: 100%; height: 100%; display: block; object-fit: contain; background: #fff; } .pcd-gallery-slot img { width: 100%; height: 100%; display: block; object-fit: contain; background: var(--surface); }
.pcd-gallery-slot.empty { .pcd-gallery-slot.empty {
display: grid; display: grid;
place-items: center; place-items: center;
+42 -5
View File
@@ -108,7 +108,7 @@
padding: 0 13px; padding: 0 13px;
border: 1px solid var(--pl-line); border: 1px solid var(--pl-line);
border-radius: 10px; border-radius: 10px;
background: #fff; background: var(--surface);
} }
.pl-search svg { .pl-search svg {
width: 16px; width: 16px;
@@ -148,7 +148,7 @@
border: 1px solid rgba(34, 42, 54, 0.12); border: 1px solid rgba(34, 42, 54, 0.12);
border-radius: 10px; border-radius: 10px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
text-align: left; text-align: left;
@@ -181,7 +181,7 @@
padding: 6px; padding: 6px;
border: 1px solid var(--pl-line); border: 1px solid var(--pl-line);
border-radius: 11px; border-radius: 11px;
background: #fff; background: var(--surface);
box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14); box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
@@ -203,7 +203,7 @@
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
text-align: left; text-align: left;
@@ -316,6 +316,35 @@
border-color: var(--klein); border-color: var(--klein);
box-shadow: 0 0 0 2px rgba(0, 47, 167, 0.16), var(--pl-shadow); box-shadow: 0 0 0 2px rgba(0, 47, 167, 0.16), var(--pl-shadow);
} }
.pl-skeleton-card {
pointer-events: none;
cursor: default;
}
.pl-skeleton-card:hover { transform: none; }
.pl-skeleton-thumb {
aspect-ratio: 4 / 3;
margin: 8px 8px 0;
border-radius: var(--r-md);
}
.pl-skeleton-meta {
display: grid;
gap: 8px;
}
.pl-skeleton-line { display: block; height: 12px; }
.pl-skeleton-line--title { width: 52%; }
.pl-skeleton-line--copy { width: 76%; height: 10px; }
.pl-skeleton-tags {
display: flex;
gap: 8px;
margin-top: 2px;
}
.pl-skeleton-pill {
display: block;
width: 58px;
height: 22px;
border-radius: var(--r-sm);
}
.pl-skeleton-pill--short { width: 46px; }
.pl-thumb { .pl-thumb {
position: relative; position: relative;
@@ -492,7 +521,15 @@
border-color: var(--klein, #002fa7); border-color: var(--klein, #002fa7);
background: var(--klein, #002fa7); background: var(--klein, #002fa7);
} }
.products-page.manage-mode .pl-card .card-del-btn { opacity: 0 !important; pointer-events: none !important; } /* 删除是管理动作:普通浏览态不因 hover 露出,进入管理态才显示。 */
.products-page:not(.manage-mode) .product-card:hover .card-del-btn {
opacity: 0;
pointer-events: none;
}
.products-page.manage-mode .pl-card .card-del-btn {
opacity: 1;
pointer-events: auto;
}
.products-page.manage-mode .pl-card button.pl-tag { pointer-events: none; } .products-page.manage-mode .pl-card button.pl-tag { pointer-events: none; }
/* 影擎吸底选择条 · 管理模式即出现 */ /* 影擎吸底选择条 · 管理模式即出现 */
+8 -8
View File
@@ -116,7 +116,7 @@
padding: 12px; padding: 12px;
border: 1px solid rgba(34, 42, 54, 0.08); border: 1px solid rgba(34, 42, 54, 0.08);
border-radius: 12px; border-radius: 12px;
background: #fff; background: var(--surface);
} }
.project-wizard-page .nw-rail-summary > span { color: var(--nw-muted); font-size: 10px; } .project-wizard-page .nw-rail-summary > span { color: var(--nw-muted); font-size: 10px; }
.project-wizard-page .nw-rail-track { .project-wizard-page .nw-rail-track {
@@ -148,7 +148,7 @@
padding: 10px; padding: 10px;
border: 1px solid rgba(34, 42, 54, 0.085); border: 1px solid rgba(34, 42, 54, 0.085);
border-radius: 12px; border-radius: 12px;
background: #fff; background: var(--surface);
} }
.project-wizard-page .nw-step[hidden] { .project-wizard-page .nw-step[hidden] {
display: none; display: none;
@@ -270,7 +270,7 @@
padding: 0 13px; padding: 0 13px;
border: 1px solid var(--nw-line); border: 1px solid var(--nw-line);
border-radius: 10px; border-radius: 10px;
background: #fff; background: var(--surface);
} }
.project-wizard-page .nw-search svg { width: 16px; height: 16px; color: var(--nw-muted); } .project-wizard-page .nw-search svg { width: 16px; height: 16px; color: var(--nw-muted); }
.project-wizard-page .nw-search input { .project-wizard-page .nw-search input {
@@ -297,7 +297,7 @@
border: 1px solid rgba(0, 47, 167, 0.15); border: 1px solid rgba(0, 47, 167, 0.15);
border-radius: 10px; border-radius: 10px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
text-align: left; text-align: left;
@@ -317,7 +317,7 @@
padding: 6px; padding: 6px;
border: 1px solid var(--nw-line); border: 1px solid var(--nw-line);
border-radius: 11px; border-radius: 11px;
background: #fff; background: var(--surface);
box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14); box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
@@ -334,7 +334,7 @@
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
text-align: left; text-align: left;
@@ -513,7 +513,7 @@
padding: 0 13px; padding: 0 13px;
border: 1px solid var(--nw-line); border: 1px solid var(--nw-line);
border-radius: 10px; border-radius: 10px;
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 14px; font-size: 14px;
color: var(--accent-black); color: var(--accent-black);
@@ -549,7 +549,7 @@
border: 1px solid var(--nw-line); border: 1px solid var(--nw-line);
border-radius: 999px; border-radius: 999px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
cursor: pointer; cursor: pointer;
+4 -4
View File
@@ -192,7 +192,7 @@
padding: 0 13px; padding: 0 13px;
border: 1px solid var(--vc-line); border: 1px solid var(--vc-line);
border-radius: 10px; border-radius: 10px;
background: #fff; background: var(--surface);
} }
.projects-page .vc-search svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--vc-muted); } .projects-page .vc-search svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--vc-muted); }
.projects-page .vc-search input { .projects-page .vc-search input {
@@ -220,7 +220,7 @@
border: 1px solid rgba(34, 42, 54, 0.12); border: 1px solid rgba(34, 42, 54, 0.12);
border-radius: 10px; border-radius: 10px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
text-align: left; text-align: left;
@@ -243,7 +243,7 @@
padding: 6px; padding: 6px;
border: 1px solid var(--vc-line); border: 1px solid var(--vc-line);
border-radius: 11px; border-radius: 11px;
background: #fff; background: var(--surface);
box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14); box-shadow: 0 14px 34px rgba(20, 27, 38, 0.14);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
@@ -262,7 +262,7 @@
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
color: var(--accent-black); color: var(--accent-black);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
text-align: left; text-align: left;
+7 -7
View File
@@ -37,7 +37,7 @@
.quick-create-page .quick-field-label { display: flex; align-items: center; justify-content: space-between; gap: 14px; font-size: 14px; font-weight: 700; } .quick-create-page .quick-field-label { display: flex; align-items: center; justify-content: space-between; gap: 14px; font-size: 14px; font-weight: 700; }
.quick-create-page .quick-field-label small { color: var(--quick-muted); font-size: 11px; font-weight: 500; } .quick-create-page .quick-field-label small { color: var(--quick-muted); font-size: 11px; font-weight: 500; }
.quick-create-page .quick-name-input { height: 50px; padding: 0 15px; border: 1px solid rgba(34,42,54,.13); border-radius: 12px; outline: none; background: rgba(248,249,252,.88); transition: border-color 180ms ease, box-shadow 180ms ease, background 180ms ease; } .quick-create-page .quick-name-input { height: 50px; padding: 0 15px; border: 1px solid rgba(34,42,54,.13); border-radius: 12px; outline: none; background: rgba(248,249,252,.88); transition: border-color 180ms ease, box-shadow 180ms ease, background 180ms ease; }
.quick-create-page .quick-name-input:focus { border-color: rgba(0,47,167,.55); background: #fff; box-shadow: 0 0 0 4px rgba(0,47,167,.08); } .quick-create-page .quick-name-input:focus { border-color: rgba(0,47,167,.55); background: var(--surface); box-shadow: 0 0 0 4px rgba(0,47,167,.08); }
.quick-create-page .quick-upload { position: relative; min-height: 190px; display: grid; place-items: center; overflow: hidden; border: 1px dashed rgba(0,47,167,.28); border-radius: 14px; background: radial-gradient(circle at center,rgba(0,47,167,.07),transparent 47%),rgba(248,249,252,.86); } .quick-create-page .quick-upload { position: relative; min-height: 190px; display: grid; place-items: center; overflow: hidden; border: 1px dashed rgba(0,47,167,.28); border-radius: 14px; background: radial-gradient(circle at center,rgba(0,47,167,.07),transparent 47%),rgba(248,249,252,.86); }
.quick-create-page .quick-upload input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; } .quick-create-page .quick-upload input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.quick-create-page .quick-upload-copy { display: grid; justify-items: center; gap: 8px; padding: 24px; border: 0; color: inherit; background: transparent; font: inherit; text-align: center; cursor: pointer; } .quick-create-page .quick-upload-copy { display: grid; justify-items: center; gap: 8px; padding: 24px; border: 0; color: inherit; background: transparent; font: inherit; text-align: center; cursor: pointer; }
@@ -52,7 +52,7 @@
.quick-create-page .quick-image-tile button { position: absolute; top: 3px; right: 3px; width: 19px; height: 19px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 50%; color: #fff; background: rgba(25,28,34,.72); cursor: pointer; }.quick-create-page .quick-image-tile button svg { width: 12px; height: 12px; } .quick-create-page .quick-image-tile button { position: absolute; top: 3px; right: 3px; width: 19px; height: 19px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 50%; color: #fff; background: rgba(25,28,34,.72); cursor: pointer; }.quick-create-page .quick-image-tile button svg { width: 12px; height: 12px; }
.quick-create-page .quick-upload-more { min-height: 240px; display: grid; place-items: center; align-content: center; gap: 8px; border: 1px dashed rgba(0,47,167,.35); border-radius: 13px; background: rgba(243,247,255,.78); } .quick-create-page .quick-upload-more { min-height: 240px; display: grid; place-items: center; align-content: center; gap: 8px; border: 1px dashed rgba(0,47,167,.35); border-radius: 13px; background: rgba(243,247,255,.78); }
.quick-create-page .quick-upload-trigger { display: grid; justify-items: center; gap: 8px; padding: 0; border: 0; color: #303746; background: transparent; font: inherit; cursor: pointer; }.quick-create-page .quick-upload-trigger strong { font-size: 14px; font-weight: 700; }.quick-create-page .quick-upload-trigger small { color: var(--quick-muted); font-size: 12px; } .quick-create-page .quick-upload-trigger { display: grid; justify-items: center; gap: 8px; padding: 0; border: 0; color: #303746; background: transparent; font: inherit; cursor: pointer; }.quick-create-page .quick-upload-trigger strong { font-size: 14px; font-weight: 700; }.quick-create-page .quick-upload-trigger small { color: var(--quick-muted); font-size: 12px; }
.quick-create-page .quick-upload-more-icon { width: 40px; height: 40px; display: grid; place-items: center; border-radius: 12px; color: var(--quick-blue); background: #fff; box-shadow: 0 5px 16px rgba(0,47,167,.08); }.quick-create-page .quick-upload-more-icon svg { width: 24px; height: 24px; stroke-width: 1.75; } .quick-create-page .quick-upload-more-icon { width: 40px; height: 40px; display: grid; place-items: center; border-radius: 12px; color: var(--quick-blue); background: var(--surface); box-shadow: 0 5px 16px rgba(0,47,167,.08); }.quick-create-page .quick-upload-more-icon svg { width: 24px; height: 24px; stroke-width: 1.75; }
.quick-create-page .quick-clear-all { padding: 5px 9px; border: 0; border-radius: 7px; color: var(--quick-muted); background: rgba(255,255,255,.72); font: inherit; font-size: 11px; cursor: pointer; } .quick-create-page .quick-clear-all { padding: 5px 9px; border: 0; border-radius: 7px; color: var(--quick-muted); background: rgba(255,255,255,.72); font: inherit; font-size: 11px; cursor: pointer; }
.quick-create-page .quick-parameter-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 12px; margin-top: 20px; } .quick-create-page .quick-parameter-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 12px; margin-top: 20px; }
.quick-create-page .quick-parameter-field { min-width: 0; display: grid; gap: 9px; padding: 14px; border: 1px solid rgba(34,42,54,.10); border-radius: 12px; background: rgba(248,249,252,.72); } .quick-create-page .quick-parameter-field { min-width: 0; display: grid; gap: 9px; padding: 14px; border: 1px solid rgba(34,42,54,.10); border-radius: 12px; background: rgba(248,249,252,.72); }
@@ -61,7 +61,7 @@
.quick-create-page .quick-parameter-field .rs-select-btn { height: 38px; font-size: 12px; } .quick-create-page .quick-parameter-field .rs-select-btn { height: 38px; font-size: 12px; }
.quick-create-page .quick-form-footer { display: flex; margin-top: 26px; } .quick-create-page .quick-form-footer { display: flex; margin-top: 26px; }
.quick-create-page .quick-generate-button { width: 100%; height: 48px; display: inline-flex; align-items: center; justify-content: center; gap: 9px; border: 0; border-radius: 12px; color: #fff; background: var(--quick-blue); font: inherit; font-weight: 700; cursor: pointer; box-shadow: 0 12px 24px rgba(0,47,167,.22); }.quick-create-page .quick-generate-button:disabled { color: #99a1ae; background: #e5e8ee; box-shadow: none; cursor: not-allowed; }.quick-create-page .quick-generate-button svg { width: 20px; height: 20px; } .quick-create-page .quick-generate-button { width: 100%; height: 48px; display: inline-flex; align-items: center; justify-content: center; gap: 9px; border: 0; border-radius: 12px; color: #fff; background: var(--quick-blue); font: inherit; font-weight: 700; cursor: pointer; box-shadow: 0 12px 24px rgba(0,47,167,.22); }.quick-create-page .quick-generate-button:disabled { color: #99a1ae; background: #e5e8ee; box-shadow: none; cursor: not-allowed; }.quick-create-page .quick-generate-button svg { width: 20px; height: 20px; }
.quick-create-page .quick-cancel-button { width: 100%; height: 48px; display: inline-flex; align-items: center; justify-content: center; gap: 9px; border: 1px solid rgba(34,42,54,.16); border-radius: 12px; color: #272a30; background: #fff; font: inherit; font-weight: 700; cursor: pointer; }.quick-create-page .quick-cancel-button:hover:not(:disabled) { border-color: rgba(200,61,77,.35); color: #c83d4d; background: rgba(200,61,77,.06); }.quick-create-page .quick-cancel-button:disabled { opacity: .55; cursor: not-allowed; }.quick-create-page .quick-cancel-button svg { width: 18px; height: 18px; } .quick-create-page .quick-cancel-button { width: 100%; height: 48px; display: inline-flex; align-items: center; justify-content: center; gap: 9px; border: 1px solid rgba(34,42,54,.16); border-radius: 12px; color: #272a30; background: var(--surface); font: inherit; font-weight: 700; cursor: pointer; }.quick-create-page .quick-cancel-button:hover:not(:disabled) { border-color: rgba(200,61,77,.35); color: #c83d4d; background: rgba(200,61,77,.06); }.quick-create-page .quick-cancel-button:disabled { opacity: .55; cursor: not-allowed; }.quick-create-page .quick-cancel-button svg { width: 18px; height: 18px; }
.quick-create-page .quick-create-shell.is-generating .quick-form-panel { opacity: .82; } .quick-create-page .quick-create-shell.is-generating .quick-form-panel { opacity: .82; }
.quick-create-page .quick-create-shell.is-generating .quick-form-footer { pointer-events: auto; opacity: 1; } .quick-create-page .quick-create-shell.is-generating .quick-form-footer { pointer-events: auto; opacity: 1; }
.quick-create-page .quick-status-panel { position: relative; display: grid; place-items: center; min-height: 0; padding: 34px; background: radial-gradient(circle at 50% 45%,rgba(0,47,167,.075),transparent 34%),rgba(250,251,253,.88); }.quick-create-page .quick-state { width: min(560px,100%); display: none; }.quick-create-page .quick-state-ready { display: grid; justify-items: center; text-align: center; } .quick-create-page .quick-status-panel { position: relative; display: grid; place-items: center; min-height: 0; padding: 34px; background: radial-gradient(circle at 50% 45%,rgba(0,47,167,.075),transparent 34%),rgba(250,251,253,.88); }.quick-create-page .quick-state { width: min(560px,100%); display: none; }.quick-create-page .quick-state-ready { display: grid; justify-items: center; text-align: center; }
@@ -85,7 +85,7 @@
.quick-create-page .quick-state-ready { position: relative; top: -10px; } .quick-create-page .quick-state-ready { position: relative; top: -10px; }
.quick-create-page .quick-state-ready h2 { margin: 0 0 9px; font-size: 23px; }.quick-create-page .quick-state-ready > p { max-width: 430px; margin: 0; color: var(--quick-muted); font-size: 13px; line-height: 1.7; }.quick-create-page .quick-ready-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 20px; }.quick-create-page .quick-ready-tags span { padding: 7px 10px; border: 1px solid rgba(34,42,54,.08); border-radius: 999px; color: #505762; background: rgba(255,255,255,.72); font-size: 11px; } .quick-create-page .quick-state-ready h2 { margin: 0 0 9px; font-size: 23px; }.quick-create-page .quick-state-ready > p { max-width: 430px; margin: 0; color: var(--quick-muted); font-size: 13px; line-height: 1.7; }.quick-create-page .quick-ready-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 20px; }.quick-create-page .quick-ready-tags span { padding: 7px 10px; border: 1px solid rgba(34,42,54,.08); border-radius: 999px; color: #505762; background: rgba(255,255,255,.72); font-size: 11px; }
.quick-create-page .quick-state-generating { width: min(460px,100%); justify-items: center; text-align: center; } .quick-create-page .quick-state-generating { width: min(460px,100%); justify-items: center; text-align: center; }
.quick-create-page .quick-generating-preview { position: relative; width: 460px; max-width: 100%; height: 258px; display: grid; place-items: center; margin-bottom: 27px; border: 1px solid rgba(0,47,167,.16); border-radius: 15px; background: #edf4ff; box-shadow: 0 18px 34px rgba(0,47,167,.08); }.quick-create-page .quick-generating-preview::before { content: ""; position: absolute; inset: 18px; border-radius: 10px; background: #fff; } .quick-create-page .quick-generating-preview { position: relative; width: 460px; max-width: 100%; height: 258px; display: grid; place-items: center; margin-bottom: 27px; border: 1px solid rgba(0,47,167,.16); border-radius: 15px; background: #edf4ff; box-shadow: 0 18px 34px rgba(0,47,167,.08); }.quick-create-page .quick-generating-preview::before { content: ""; position: absolute; inset: 18px; border-radius: 10px; background: var(--surface); }
.quick-create-page .quick-preview-spinner { position: relative; z-index: 1; width: 62px; height: 62px; border-radius: 50%; background: conic-gradient(from 160deg,transparent 0deg,transparent 136deg,var(--quick-blue) 300deg,rgba(0,47,167,.12) 360deg); -webkit-mask: radial-gradient(circle,transparent 0 66%,#000 69%); mask: radial-gradient(circle,transparent 0 66%,#000 69%); animation: quick-spinner-rotate 1.1s linear infinite; } .quick-create-page .quick-preview-spinner { position: relative; z-index: 1; width: 62px; height: 62px; border-radius: 50%; background: conic-gradient(from 160deg,transparent 0deg,transparent 136deg,var(--quick-blue) 300deg,rgba(0,47,167,.12) 360deg); -webkit-mask: radial-gradient(circle,transparent 0 66%,#000 69%); mask: radial-gradient(circle,transparent 0 66%,#000 69%); animation: quick-spinner-rotate 1.1s linear infinite; }
.quick-create-page .quick-generating-copy { width: 100%; text-align: center; }.quick-create-page .quick-generating-copy h2 { margin: 0 0 8px; font-size: 23px; }.quick-create-page .quick-generating-copy p { margin: 0; color: var(--quick-muted); font-size: 13px; line-height: 1.7; } .quick-create-page .quick-generating-copy { width: 100%; text-align: center; }.quick-create-page .quick-generating-copy h2 { margin: 0 0 8px; font-size: 23px; }.quick-create-page .quick-generating-copy p { margin: 0; color: var(--quick-muted); font-size: 13px; line-height: 1.7; }
.quick-create-page .quick-progress-track { width: 100%; display: flex; align-items: flex-start; margin-top: 28px; } .quick-create-page .quick-progress-track { width: 100%; display: flex; align-items: flex-start; margin-top: 28px; }
@@ -104,7 +104,7 @@
.quick-create-page .quick-state-complete { width: min(560px,100%); gap: 17px; } .quick-create-page .quick-state-complete { width: min(560px,100%); gap: 17px; }
.quick-create-page .quick-video-result-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 12px; } .quick-create-page .quick-video-result-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 12px; }
.quick-create-page .quick-video-result-grid.is-single { grid-template-columns: minmax(0, 1fr); width: min(320px,100%); justify-self: center; } .quick-create-page .quick-video-result-grid.is-single { grid-template-columns: minmax(0, 1fr); width: min(320px,100%); justify-self: center; }
.quick-create-page .quick-video-result-card { min-width: 0; padding: 8px 8px 9px; border: 1px solid rgba(34,42,54,.10); border-radius: 8px; color: #25282d; background: #fff; font: inherit; text-align: left; cursor: pointer; } .quick-create-page .quick-video-result-card { min-width: 0; padding: 8px 8px 9px; border: 1px solid rgba(34,42,54,.10); border-radius: 8px; color: #25282d; background: var(--surface); font: inherit; text-align: left; cursor: pointer; }
.quick-create-page .quick-video-result-thumb { position: relative; display: block; aspect-ratio: 16/9; overflow: hidden; border-radius: 8px; background: #eef1f5; } .quick-create-page .quick-video-result-thumb { position: relative; display: block; aspect-ratio: 16/9; overflow: hidden; border-radius: 8px; background: #eef1f5; }
.quick-create-page .quick-video-result-thumb img,.quick-create-page .quick-video-result-thumb video { width: 100%; height: 100%; display: block; object-fit: cover; pointer-events: none; } .quick-create-page .quick-video-result-thumb img,.quick-create-page .quick-video-result-thumb video { width: 100%; height: 100%; display: block; object-fit: cover; pointer-events: none; }
.quick-create-page .quick-video-play { position: absolute; inset: 0; display: grid; place-items: center; color: #fff; background: rgba(0,0,0,.22); } .quick-create-page .quick-video-play { position: absolute; inset: 0; display: grid; place-items: center; color: #fff; background: rgba(0,0,0,.22); }
@@ -130,7 +130,7 @@
.quick-create-page .quick-history-head h2 { margin: 0; font-size: 20px; font-weight: 600; } .quick-create-page .quick-history-head h2 { margin: 0; font-size: 20px; font-weight: 600; }
.quick-create-page .quick-history-head span { color: var(--quick-muted); font-size: 13px; } .quick-create-page .quick-history-head span { color: var(--quick-muted); font-size: 13px; }
.quick-create-page .quick-history-list { display: grid; gap: 10px; } .quick-create-page .quick-history-list { display: grid; gap: 10px; }
.quick-create-page .quick-history-card { display: grid; grid-template-columns: 112px minmax(0,1fr) auto; align-items: center; gap: 16px; padding: 12px 14px; border: 1px solid rgba(34,42,54,.10); border-radius: 8px; background: #fff; box-shadow: inset 3px 0 0 var(--quick-blue); } .quick-create-page .quick-history-card { display: grid; grid-template-columns: 112px minmax(0,1fr) auto; align-items: center; gap: 16px; padding: 12px 14px; border: 1px solid rgba(34,42,54,.10); border-radius: 8px; background: var(--surface); box-shadow: inset 3px 0 0 var(--quick-blue); }
.quick-create-page .quick-history-thumb { position: relative; width: 112px; aspect-ratio: 16/9; overflow: hidden; padding: 0; border: 0; border-radius: 8px; background: #eef1f5; cursor: pointer; } .quick-create-page .quick-history-thumb { position: relative; width: 112px; aspect-ratio: 16/9; overflow: hidden; padding: 0; border: 0; border-radius: 8px; background: #eef1f5; cursor: pointer; }
.quick-create-page .quick-history-thumb:disabled { cursor: default; } .quick-create-page .quick-history-thumb:disabled { cursor: default; }
.quick-create-page .quick-history-thumb img { width: 100%; height: 100%; display: block; object-fit: cover; } .quick-create-page .quick-history-thumb img { width: 100%; height: 100%; display: block; object-fit: cover; }
@@ -144,7 +144,7 @@
.quick-create-page .quick-history-badge.is-wait { color: var(--quick-muted); background: rgba(34,42,54,.06); } .quick-create-page .quick-history-badge.is-wait { color: var(--quick-muted); background: rgba(34,42,54,.06); }
.quick-create-page .quick-history-copy h3 { margin: 6px 0 4px; font-size: 16px; font-weight: 600; } .quick-create-page .quick-history-copy h3 { margin: 6px 0 4px; font-size: 16px; font-weight: 600; }
.quick-create-page .quick-history-copy p { margin: 0; color: var(--quick-muted); font-size: 12px; } .quick-create-page .quick-history-copy p { margin: 0; color: var(--quick-muted); font-size: 12px; }
.quick-create-page .quick-history-open { min-height: 36px; display: inline-flex; align-items: center; gap: 6px; padding: 0 12px; border: 1px solid rgba(34,42,54,.12); border-radius: 8px; color: var(--quick-blue); background: #fff; font: inherit; font-size: 13px; cursor: pointer; } .quick-create-page .quick-history-open { min-height: 36px; display: inline-flex; align-items: center; gap: 6px; padding: 0 12px; border: 1px solid rgba(34,42,54,.12); border-radius: 8px; color: var(--quick-blue); background: var(--surface); font: inherit; font-size: 13px; cursor: pointer; }
.quick-create-page .quick-history-open:hover { background: rgba(0,47,167,.05); } .quick-create-page .quick-history-open:hover { background: rgba(0,47,167,.05); }
.quick-create-page .quick-history-open svg { width: 15px; height: 15px; } .quick-create-page .quick-history-open svg { width: 15px; height: 15px; }
.quick-create-page .quick-history-empty { margin: 0; padding: 28px 8px; color: var(--quick-muted); font-size: 13px; } .quick-create-page .quick-history-empty { margin: 0; padding: 28px 8px; color: var(--quick-muted); font-size: 13px; }
+12 -3
View File
@@ -20,7 +20,7 @@ const TABS: { k: Tab; label: string; title: string; note: string }[] = [
{ k: "mine", label: "我的模特", title: "我的模特", note: "维护可复用的品牌模特与人物参考资产" } { k: "mine", label: "我的模特", title: "我的模特", note: "维护可复用的品牌模特与人物参考资产" }
]; ];
const PAGE_SIZE = 20; const PAGE_SIZE = 24;
type Preview = { src: string; kind: "image"; name: string }; type Preview = { src: string; kind: "image"; name: string };
@@ -294,7 +294,7 @@ function modelTags(m: ModelEntity): string[] {
} }
export function ModelsPage({ onNotify, onBillingChanged }: { export function ModelsPage({ onNotify, onBillingChanged }: {
onNotify?: (type: "success" | "error", text: string) => void; onNotify?: (type: "success" | "error" | "info", text: string) => void;
onBillingChanged?: () => void; onBillingChanged?: () => void;
}) { }) {
const [tab, setTab] = useState<Tab>("all"); const [tab, setTab] = useState<Tab>("all");
@@ -313,6 +313,14 @@ export function ModelsPage({ onNotify, onBillingChanged }: {
const fileRef = useRef<HTMLInputElement | null>(null); const fileRef = useRef<HTMLInputElement | null>(null);
const tabCopy = TABS.find((item) => item.k === tab) || TABS[0]; const tabCopy = TABS.find((item) => item.k === tab) || TABS[0];
const exitEdit = () => { setEditMode(false); setSelected(new Set()); }; const exitEdit = () => { setEditMode(false); setSelected(new Set()); };
const enterEdit = () => {
setEditMode(true);
// 全部列表把官方模板排在前面,第一页往往一张都删不了,底栏会一直停在 0。
if (items.length > 0 && items.every((m) => m.is_official) && tab !== "mine") {
setTab("mine");
onNotify?.("info", "官方模特不能删除,已切换到我的模特");
}
};
const toggleSelect = (id: string) => setSelected((prev) => { const toggleSelect = (id: string) => setSelected((prev) => {
const next = new Set(prev); const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id); if (next.has(id)) next.delete(id); else next.add(id);
@@ -415,7 +423,7 @@ export function ModelsPage({ onNotify, onBillingChanged }: {
<p></p> <p></p>
</div> </div>
<div className="ml-actions"> <div className="ml-actions">
<button className={`ml-ghost${editMode ? " is-on" : ""}`} type="button" onClick={() => (editMode ? exitEdit() : setEditMode(true))}> <button className={`ml-ghost${editMode ? " is-on" : ""}`} type="button" onClick={() => (editMode ? exitEdit() : enterEdit())}>
<Settings2 /> <Settings2 />
<span>{editMode ? "完成" : "管理模特"}</span> <span>{editMode ? "完成" : "管理模特"}</span>
</button> </button>
@@ -483,6 +491,7 @@ export function ModelsPage({ onNotify, onBillingChanged }: {
const onCardClick = () => { const onCardClick = () => {
if (editMode) { if (editMode) {
if (selectable) toggleSelect(m.id); if (selectable) toggleSelect(m.id);
else onNotify?.("info", "官方模特不能删除");
return; return;
} }
setDetail(m); setDetail(m);
+1 -1
View File
@@ -941,7 +941,7 @@ export function OmniHistoryPage({
<div className="omni-history-actions"> <div className="omni-history-actions">
<button <button
type="button" type="button"
aria-label="删除项目" aria-label="删除会话"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
setDeleteTarget(item); setDeleteTarget(item);
+21 -1
View File
@@ -297,7 +297,7 @@ export function ProductsPage({ products, projects = [], loading = false, navigat
</div> </div>
{loading && products.length === 0 ? ( {loading && products.length === 0 ? (
<SkeletonGrid count={8} /> <ProductGridSkeleton />
) : pageItems.length === 0 ? ( ) : pageItems.length === 0 ? (
<div className="pl-empty" role="status"> <div className="pl-empty" role="status">
<PackageX /> <PackageX />
@@ -381,6 +381,26 @@ export function ProductsPage({ products, projects = [], loading = false, navigat
); );
} }
function ProductGridSkeleton() {
return (
<div className="pl-grid pl-skeleton-grid" aria-busy="true" aria-label="正在加载商品">
{Array.from({ length: 6 }).map((_, index) => (
<article className="pl-card pl-skeleton-card" key={index} aria-hidden="true">
<div className="pl-skeleton-thumb skeleton-block" />
<div className="pl-meta pl-skeleton-meta">
<span className="pl-skeleton-line pl-skeleton-line--title skeleton-block" />
<span className="pl-skeleton-line pl-skeleton-line--copy skeleton-block" />
<div className="pl-skeleton-tags">
<span className="pl-skeleton-pill skeleton-block" />
<span className="pl-skeleton-pill pl-skeleton-pill--short skeleton-block" />
</div>
</div>
</article>
))}
</div>
);
}
export function ProductCard({ product, coverUrl = "", videoCount = 0, onOpen, onOpenVideos, editMode = false, selected = false, onDelete }: { product: Product; coverUrl?: string; videoCount?: number; onOpen: () => void; onOpenVideos?: () => void; editMode?: boolean; selected?: boolean; onDelete?: () => void }) { export function ProductCard({ product, coverUrl = "", videoCount = 0, onOpen, onOpenVideos, editMode = false, selected = false, onDelete }: { product: Product; coverUrl?: string; videoCount?: number; onOpen: () => void; onOpenVideos?: () => void; editMode?: boolean; selected?: boolean; onDelete?: () => void }) {
const mock = productCover(product.title); const mock = productCover(product.title);
const assetCount = product.images?.length || 0; const assetCount = product.images?.length || 0;
+131 -97
View File
@@ -4,6 +4,7 @@ import {
Bell, Bell,
KeyRound, KeyRound,
LogOut, LogOut,
Monitor,
Shield, Shield,
Upload, Upload,
User as UserIcon, User as UserIcon,
@@ -12,16 +13,18 @@ import type { LoginSession, Team, User, UserPreference } from "../types";
import { TeamModal } from "../components/overlays"; import { TeamModal } from "../components/overlays";
import { CustomSelect } from "../components/custom-select"; import { CustomSelect } from "../components/custom-select";
import { useFileDrop } from "../components/use-file-drop"; import { useFileDrop } from "../components/use-file-drop";
import { applyTheme, normalizeAppearance, readStoredAppearance } from "../theme";
type SectionKey = "profile" | "security" | "notify" | "pref" | "display"; type SectionKey = "profile" | "security" | "notify" | "pref" | "display";
// 可导航(UI 可见)的分区。pref(创作默认)/ display(显示)已隐藏入口,故不在此列 // 可导航分区。pref(创作默认)仍隐藏;display(显示)开放外观切换
const SECTION_KEYS: SectionKey[] = ["profile", "security", "notify"]; const SECTION_KEYS: SectionKey[] = ["profile", "security", "notify", "display"];
const NAV: Array<{ key: SectionKey; label: string; icon: ReactNode }> = [ const NAV: Array<{ key: SectionKey; label: string; icon: ReactNode }> = [
{ key: "profile", label: "个人信息", icon: <UserIcon /> }, { key: "profile", label: "个人信息", icon: <UserIcon /> },
{ key: "security", label: "安全", icon: <Shield /> }, { key: "security", label: "安全", icon: <Shield /> },
{ key: "notify", label: "通知", icon: <Bell /> }, { key: "notify", label: "通知", icon: <Bell /> },
{ key: "display", label: "显示", icon: <Monitor /> },
]; ];
function SettingRow({ title, hint, stack, children }: { title: string; hint?: string; stack?: boolean; children: ReactNode }) { function SettingRow({ title, hint, stack, children }: { title: string; hint?: string; stack?: boolean; children: ReactNode }) {
@@ -174,7 +177,7 @@ export function SettingsPage({
onChangePassword: (payload: { old_password: string; new_password: string }) => void | Promise<unknown>; onChangePassword: (payload: { old_password: string; new_password: string }) => void | Promise<unknown>;
onUploadAvatar: (formData: FormData) => void | Promise<unknown>; onUploadAvatar: (formData: FormData) => void | Promise<unknown>;
onResetAvatar?: () => void | Promise<unknown>; onResetAvatar?: () => void | Promise<unknown>;
onNotify?: (text: string) => void; onNotify?: (type: "success" | "error" | "info", text: string) => void;
onLogout?: () => void | Promise<void>; onLogout?: () => void | Promise<void>;
}) { }) {
const normalizedInitial = (SECTION_KEYS as readonly string[]).includes(initialSection) const normalizedInitial = (SECTION_KEYS as readonly string[]).includes(initialSection)
@@ -199,7 +202,10 @@ export function SettingsPage({
transition: cd?.transition ?? DEFAULT_PREFS.transition, transition: cd?.transition ?? DEFAULT_PREFS.transition,
twoFactor: !!preferences?.two_factor_enabled, twoFactor: !!preferences?.two_factor_enabled,
notify: { ...DEFAULT_PREFS.notify, ...(preferences?.notify || {}) }, notify: { ...DEFAULT_PREFS.notify, ...(preferences?.notify || {}) },
appearance: dp?.appearance ?? DEFAULT_PREFS.appearance, // preferences 尚未加载时用本地已选外观,避免一进设置就被默认 system 盖掉
appearance: !preferences
? readStoredAppearance()
: (dp?.appearance ?? DEFAULT_PREFS.appearance),
language: dp?.language ?? DEFAULT_PREFS.language, language: dp?.language ?? DEFAULT_PREFS.language,
density: dp?.density ?? DEFAULT_PREFS.density, density: dp?.density ?? DEFAULT_PREFS.density,
}; };
@@ -210,18 +216,19 @@ export function SettingsPage({
const [draft, setDraft] = useState<TrackedState>(baselineFromProps); const [draft, setDraft] = useState<TrackedState>(baselineFromProps);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// 后端 preferences / user 到达或刷新时,把新值同时灌进 baseline 与 draft(用户未改时跟随后端) // 外观即时生效(含后端 preferences 到达 / 放弃修改回滚)
useEffect(() => { useEffect(() => {
setBaseline(baselineFromProps); applyTheme(normalizeAppearance(draft.appearance));
setDraft(baselineFromProps); }, [draft.appearance]);
}, [baselineFromProps]);
// 单字段 draft 更新器 // 已保存基线的 ref,自动保存时避免连点用到过期 baseline
const patchDraft = useCallback(<K extends keyof TrackedState>(key: K, value: TrackedState[K]) => { const baselineRef = useRef(baseline);
setDraft((prev) => ({ ...prev, [key]: value })); baselineRef.current = baseline;
}, []); const draftRef = useRef(draft);
draftRef.current = draft;
const saveSeq = useRef(0);
// ─── 聚合脏字段集 + 涉及分区集(顶栏计数 / nav dirty-dot / beforeunload 全由此驱动) ─── // ─── 聚合脏字段集(文本框编辑中仍可能短暂脏;选项类会立即落盘) ───
const dirtyFields = useMemo<Array<keyof TrackedState>>(() => { const dirtyFields = useMemo<Array<keyof TrackedState>>(() => {
const keys = Object.keys(FIELD_SECTION) as Array<keyof TrackedState>; const keys = Object.keys(FIELD_SECTION) as Array<keyof TrackedState>;
return keys.filter((key) => { return keys.filter((key) => {
@@ -239,6 +246,97 @@ export function SettingsPage({
const dirtyCount = dirtyFields.length; const dirtyCount = dirtyFields.length;
const isDirty = dirtyCount > 0; const isDirty = dirtyCount > 0;
// 后端 preferences / user 到达或刷新时同步;保存中或本地有未落盘编辑时不覆盖 draft
useEffect(() => {
if (saving || isDirty) return;
setBaseline(baselineFromProps);
setDraft(baselineFromProps);
}, [baselineFromProps, saving, isDirty]);
// 把 next 相对 baseline 的变更拆成 profile / preferences 提交;成功后升基线
const persist = useCallback(async (next: TrackedState) => {
const base = baselineRef.current;
const profilePayload: { name?: string; email?: string; phone?: string } = {};
if (next.name !== base.name) profilePayload.name = next.name.trim();
if (next.email !== base.email) profilePayload.email = next.email.trim();
if (next.phone !== base.phone) profilePayload.phone = next.phone.trim();
const prefPayload: Partial<UserPreference> = {};
const creationKeys: Array<keyof TrackedState> = ["template", "duration", "subtitle", "bgm", "transition"];
if (creationKeys.some((key) => next[key] !== base[key])) {
prefPayload.creation_defaults = {
template: next.template,
duration: next.duration,
subtitle: next.subtitle,
bgm: next.bgm,
transition: next.transition,
};
}
const displayKeys: Array<keyof TrackedState> = ["appearance", "language", "density"];
if (displayKeys.some((key) => next[key] !== base[key])) {
prefPayload.display = { appearance: next.appearance, language: next.language, density: next.density };
}
if (!notifyEqual(next.notify, base.notify)) prefPayload.notify = { ...next.notify };
if (next.twoFactor !== base.twoFactor) prefPayload.two_factor_enabled = next.twoFactor;
const hasProfile = Object.keys(profilePayload).length > 0;
const hasPref = Object.keys(prefPayload).length > 0;
if (!hasProfile && !hasPref) {
setBaseline(next);
return;
}
const seq = ++saveSeq.current;
setSaving(true);
try {
const tasks: Array<Promise<unknown>> = [];
if (hasProfile) tasks.push(Promise.resolve(onSaveProfile(profilePayload)));
if (hasPref && onSavePreferences) {
tasks.push(
Promise.resolve(onSavePreferences(prefPayload)).then((result) => {
if (result == null) throw new Error("偏好设置保存失败,请重试");
return result;
}),
);
}
await Promise.all(tasks);
if (seq !== saveSeq.current) return;
baselineRef.current = next;
setBaseline(next);
} catch (error) {
if (seq !== saveSeq.current) return;
// 失败不推进 baseline,回滚 draft 并恢复主题
const restored = baselineRef.current;
draftRef.current = restored;
setDraft(restored);
applyTheme(normalizeAppearance(restored.appearance));
onNotify?.(
"error",
error instanceof Error ? error.message : "设置保存失败,请重试",
);
} finally {
if (seq === saveSeq.current) setSaving(false);
}
}, [onSaveProfile, onSavePreferences, onNotify]);
// 选项类:改完立即存
const commitField = useCallback(<K extends keyof TrackedState>(key: K, value: TrackedState[K]) => {
const next = { ...draftRef.current, [key]: value };
draftRef.current = next;
setDraft(next);
if (key === "appearance") applyTheme(normalizeAppearance(value as string));
void persist(next);
}, [persist]);
// 文本框:只改 draft,失焦再存
const patchDraft = useCallback(<K extends keyof TrackedState>(key: K, value: TrackedState[K]) => {
setDraft((prev) => ({ ...prev, [key]: value }));
}, []);
const saveDraftNow = useCallback(() => {
void persist(draftRef.current);
}, [persist]);
// 个人信息 · 受控输入(draft 派生) // 个人信息 · 受控输入(draft 派生)
const name = draft.name; const name = draft.name;
const email = draft.email; const email = draft.email;
@@ -271,55 +369,6 @@ export function SettingsPage({
[notifyOnCount, sessions.length], [notifyOnCount, sessions.length],
); );
// ─── 取消:draft 回退到 baseline(放弃所有未保存改动) ───
function discardChanges() {
if (!isDirty) return;
setDraft(baseline);
onNotify?.("已放弃未保存的改动");
}
// ─── 统一保存:把所有脏字段拆成 profile / preferences 两个 payload 提交,成功后把 draft 升为新基线 ───
async function handleSaveAll() {
if (!isDirty || saving) return;
setSaving(true);
try {
const profilePayload: { name?: string; email?: string; phone?: string } = {};
if (draft.name !== baseline.name) profilePayload.name = draft.name.trim();
if (draft.email !== baseline.email) profilePayload.email = draft.email.trim();
if (draft.phone !== baseline.phone) profilePayload.phone = draft.phone.trim();
const prefPayload: Partial<UserPreference> = {};
const creationKeys: Array<keyof TrackedState> = ["template", "duration", "subtitle", "bgm", "transition"];
if (creationKeys.some((key) => draft[key] !== baseline[key])) {
prefPayload.creation_defaults = {
template: draft.template,
duration: draft.duration,
subtitle: draft.subtitle,
bgm: draft.bgm,
transition: draft.transition,
};
}
const displayKeys: Array<keyof TrackedState> = ["appearance", "language", "density"];
if (displayKeys.some((key) => draft[key] !== baseline[key])) {
prefPayload.display = { appearance: draft.appearance, language: draft.language, density: draft.density };
}
if (!notifyEqual(draft.notify, baseline.notify)) prefPayload.notify = { ...draft.notify };
if (draft.twoFactor !== baseline.twoFactor) prefPayload.two_factor_enabled = draft.twoFactor;
const tasks: Array<Promise<unknown>> = [];
if (Object.keys(profilePayload).length > 0) tasks.push(Promise.resolve(onSaveProfile(profilePayload)));
if (Object.keys(prefPayload).length > 0 && onSavePreferences) tasks.push(Promise.resolve(onSavePreferences(prefPayload)));
await Promise.all(tasks);
// 提交成功 → draft 升为新基线,脏集清空
const savedCount = dirtyCount;
setBaseline(draft);
onNotify?.(`${savedCount} 项已保存`);
} finally {
setSaving(false);
}
}
function openPasswordModal() { function openPasswordModal() {
setOldPassword(""); setOldPassword("");
setNewPassword(""); setNewPassword("");
@@ -381,32 +430,13 @@ export function SettingsPage({
return () => URL.revokeObjectURL(avatarPreview); return () => URL.revokeObjectURL(avatarPreview);
}, [avatarPreview]); }, [avatarPreview]);
// ─── 离开页面前提醒:有未保存改动时弹浏览器原生确认 ───
useEffect(() => {
if (!isDirty) return;
const handler = (event: BeforeUnloadEvent) => {
event.preventDefault();
event.returnValue = "";
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, [isDirty]);
return ( return (
<section className="settings-page"> <section className="settings-page">
<div className="settings-inner"> <div className="settings-inner">
<div className="page-head"> <div className="page-head">
<div> <div>
<h1></h1> <h1></h1>
<div className="sub"></div> <div className="sub"> · </div>
</div>
<div className="actions">
<button className="btn" type="button" onClick={discardChanges} disabled={!isDirty || saving}></button>
<button className="btn btn-primary" type="button" onClick={handleSaveAll} disabled={!isDirty || saving}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12l5 5L20 6" /></svg>
{isDirty ? <span className="save-count">· {dirtyCount} </span> : null}
</button>
</div> </div>
</div> </div>
@@ -449,7 +479,7 @@ export function SettingsPage({
</div> </div>
</SettingRow> </SettingRow>
<SettingRow title="显示名称" hint="用于团队和项目协作中展示"> <SettingRow title="显示名称" hint="用于团队和项目协作中展示">
<input className="st-input" value={name} onChange={(event) => patchDraft("name", event.target.value)} /> <input className="st-input" value={name} onChange={(event) => patchDraft("name", event.target.value)} onBlur={saveDraftNow} />
</SettingRow> </SettingRow>
<SettingRow title="登录邮箱" hint="仅做记录用,不用于登录"> <SettingRow title="登录邮箱" hint="仅做记录用,不用于登录">
<input <input
@@ -458,13 +488,15 @@ export function SettingsPage({
value={email} value={email}
placeholder="邮箱验证未启用" placeholder="邮箱验证未启用"
onChange={(event) => patchDraft("email", event.target.value)} onChange={(event) => patchDraft("email", event.target.value)}
onBlur={saveDraftNow}
/> />
</SettingRow> </SettingRow>
<SettingRow title="手机号" hint="保存所有更改时一并提交"> <SettingRow title="手机号" hint="修改后自动保存">
<input <input
className="st-input" className="st-input"
value={phone} value={phone}
onChange={(event) => patchDraft("phone", event.target.value)} onChange={(event) => patchDraft("phone", event.target.value)}
onBlur={saveDraftNow}
placeholder="138****8000" placeholder="138****8000"
/> />
</SettingRow> </SettingRow>
@@ -499,7 +531,7 @@ export function SettingsPage({
</div> </div>
</SettingRow> </SettingRow>
<SettingRow title="两步验证" hint="推荐开启短信 + Authenticator"> <SettingRow title="两步验证" hint="推荐开启短信 + Authenticator">
<Switch checked={draft.twoFactor} onChange={(v) => patchDraft("twoFactor", v)} /> <Switch checked={draft.twoFactor} onChange={(v) => commitField("twoFactor", v)} />
</SettingRow> </SettingRow>
<SettingRow stack title="在用设备" hint="真实登录会话 · 每次登录记录设备 UA / IP"> <SettingRow stack title="在用设备" hint="真实登录会话 · 每次登录记录设备 UA / IP">
<div className="device-list"> <div className="device-list">
@@ -541,7 +573,7 @@ export function SettingsPage({
<section className="pane" aria-label="通知"> <section className="pane" aria-label="通知">
{NOTIFY_ROWS.map((row) => ( {NOTIFY_ROWS.map((row) => (
<SettingRow key={row.key} title={row.title} hint={row.sub?.replace(/^\/\/\s*/, "")}> <SettingRow key={row.key} title={row.title} hint={row.sub?.replace(/^\/\/\s*/, "")}>
<Switch checked={!!draft.notify[row.key]} onChange={(next) => patchDraft("notify", { ...draft.notify, [row.key]: next })} /> <Switch checked={!!draft.notify[row.key]} onChange={(next) => commitField("notify", { ...draft.notify, [row.key]: next })} />
</SettingRow> </SettingRow>
))} ))}
<div className="pane-desc" style={{ margin: "18px 0 4px" }}></div> <div className="pane-desc" style={{ margin: "18px 0 4px" }}></div>
@@ -549,7 +581,7 @@ export function SettingsPage({
<SettingRow key={row.key} title={row.title} hint={row.hint}> <SettingRow key={row.key} title={row.title} hint={row.hint}>
<Switch <Switch
checked={!draft.notify[`mute-${row.key}`]} checked={!draft.notify[`mute-${row.key}`]}
onChange={(show) => patchDraft("notify", { ...draft.notify, [`mute-${row.key}`]: !show })} onChange={(show) => commitField("notify", { ...draft.notify, [`mute-${row.key}`]: !show })}
/> />
</SettingRow> </SettingRow>
))} ))}
@@ -571,7 +603,7 @@ export function SettingsPage({
className={`pref-choice ${draft.template === choice.v ? "selected" : ""}`} className={`pref-choice ${draft.template === choice.v ? "selected" : ""}`}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => patchDraft("template", choice.v)} onClick={() => commitField("template", choice.v)}
> >
<div className="t">{choice.t}</div> <div className="t">{choice.t}</div>
<div className="d">{choice.d}</div> <div className="d">{choice.d}</div>
@@ -590,7 +622,7 @@ export function SettingsPage({
className={`dur-chip ${draft.duration === d ? "selected" : ""}`} className={`dur-chip ${draft.duration === d ? "selected" : ""}`}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => patchDraft("duration", d)} onClick={() => commitField("duration", d)}
> >
{d}s {d}s
</span> </span>
@@ -609,7 +641,7 @@ export function SettingsPage({
className={`pref-choice ${draft.subtitle === choice.v ? "selected" : ""}`} className={`pref-choice ${draft.subtitle === choice.v ? "selected" : ""}`}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => patchDraft("subtitle", choice.v)} onClick={() => commitField("subtitle", choice.v)}
> >
<div className="t">{choice.t}</div> <div className="t">{choice.t}</div>
<div className="d">{choice.d}</div> <div className="d">{choice.d}</div>
@@ -621,7 +653,7 @@ export function SettingsPage({
<div className="form-row"> <div className="form-row">
<div className="lbl"> BGM </div> <div className="lbl"> BGM </div>
<div className="val"> <div className="val">
<CustomSelect fill value={draft.bgm} onChange={(next) => patchDraft("bgm", next)} options={[ <CustomSelect fill value={draft.bgm} onChange={(next) => commitField("bgm", next)} options={[
{ value: "kapian", label: "抖音 Top10 卡点曲库" }, { value: "kapian", label: "抖音 Top10 卡点曲库" },
{ value: "emotion", label: "情绪向 · 治愈/悬念" }, { value: "emotion", label: "情绪向 · 治愈/悬念" },
{ value: "urban", label: "都市电子 · 通勤场景" }, { value: "urban", label: "都市电子 · 通勤场景" },
@@ -632,7 +664,7 @@ export function SettingsPage({
<div className="form-row"> <div className="form-row">
<div className="lbl"></div> <div className="lbl"></div>
<div className="val"> <div className="val">
<CustomSelect fill value={draft.transition} onChange={(next) => patchDraft("transition", next)} options={[ <CustomSelect fill value={draft.transition} onChange={(next) => commitField("transition", next)} options={[
{ value: "none", label: "无转场" }, { value: "none", label: "无转场" },
{ value: "fade", label: "淡入淡出 · 0.3s" }, { value: "fade", label: "淡入淡出 · 0.3s" },
{ value: "slide", label: "滑动 · 0.3s" }, { value: "slide", label: "滑动 · 0.3s" },
@@ -651,6 +683,8 @@ export function SettingsPage({
</section> </section>
)} )}
{section === "display" && ( {section === "display" && (
<section className="pane" aria-label="显示"> <section className="pane" aria-label="显示">
<h3></h3> <h3></h3>
@@ -659,17 +693,17 @@ export function SettingsPage({
<div className="form-row"> <div className="form-row">
<div className="lbl"></div> <div className="lbl"></div>
<div className="val"> <div className="val">
<CustomSelect fill value={draft.appearance} onChange={(next) => patchDraft("appearance", next)} options={[ <CustomSelect fill value={draft.appearance} onChange={(next) => commitField("appearance", next)} options={[
{ value: "system", label: "跟随系统" }, { value: "system", label: "跟随系统" },
{ value: "light", label: "浅色" }, { value: "light", label: "浅色" },
{ value: "dark", label: "深色(V2)", disabled: true }, { value: "dark", label: "深色" },
]} /> ]} />
</div> </div>
</div> </div>
<div className="form-row"> <div className="form-row">
<div className="lbl"></div> <div className="lbl"></div>
<div className="val"> <div className="val">
<CustomSelect fill value={draft.language} onChange={(next) => patchDraft("language", next)} options={[ <CustomSelect fill value={draft.language} onChange={(next) => commitField("language", next)} options={[
{ value: "zh", label: "简体中文" }, { value: "zh", label: "简体中文" },
{ value: "en", label: "English(V2)", disabled: true }, { value: "en", label: "English(V2)", disabled: true },
]} /> ]} />
@@ -678,7 +712,7 @@ export function SettingsPage({
<div className="form-row"> <div className="form-row">
<div className="lbl"></div> <div className="lbl"></div>
<div className="val"> <div className="val">
<CustomSelect fill value={draft.density} onChange={(next) => patchDraft("density", next)} options={[ <CustomSelect fill value={draft.density} onChange={(next) => commitField("density", next)} options={[
{ value: "compact", label: "紧凑" }, { value: "compact", label: "紧凑" },
{ value: "standard", label: "标准" }, { value: "standard", label: "标准" },
{ value: "loose", label: "宽松" }, { value: "loose", label: "宽松" },
@@ -813,7 +847,7 @@ export function SettingsPage({
<div className="li"></div> <div className="li"></div>
<div className="li">,线</div> <div className="li">,线</div>
</div> </div>
{isDirty ? <div className="logout-unsaved-note"> {dirtyCount} ,退</div> : null} {isDirty ? <div className="logout-unsaved-note"> {dirtyCount} ,退</div> : null}
</TeamModal> </TeamModal>
</section> </section>
); );
+21 -11
View File
@@ -28,7 +28,7 @@
.settings-grid { .settings-grid {
display: grid; display: grid;
grid-template-columns: 210px minmax(0, 1fr); grid-template-columns: 210px minmax(0, 1fr);
gap: 22px; gap: 18px;
align-items: start; align-items: start;
background: transparent; background: transparent;
border: 0; border: 0;
@@ -42,9 +42,9 @@
display: grid; display: grid;
gap: 7px; gap: 7px;
align-content: start; align-content: start;
padding: 10px; padding: 12px;
border: 1px solid var(--st-line); border: 1px solid var(--st-line);
border-radius: 13px; border-radius: 20px;
background: var(--st-wash); background: var(--st-wash);
} }
.settings-nav .nav-h { .settings-nav .nav-h {
@@ -104,7 +104,7 @@
flex: 0 0 auto; flex: 0 0 auto;
} }
.st-tab.has-changes .nav-dot { display: block; } .st-tab.has-changes .nav-dot { display: block; }
.st-tab.active .nav-dot { background: #fff; } .st-tab.active .nav-dot { background: var(--surface); }
.st-tab:not(:has(.nav-badge)) .nav-dot { margin-left: auto; } .st-tab:not(:has(.nav-badge)) .nav-dot { margin-left: auto; }
.logout-pill { .logout-pill {
@@ -132,24 +132,34 @@
} }
.settings-content { .settings-content {
padding: 8px 22px; padding: 22px 28px 24px;
border: 1px solid var(--st-line); border: 1px solid var(--st-line);
border-radius: 13px; border-radius: 20px;
background: rgba(34, 42, 54, 0.04); background: rgba(34, 42, 54, 0.035);
box-shadow: var(--st-shadow); box-shadow: var(--st-shadow);
min-width: 0; min-width: 0;
overflow: hidden;
} }
.pane { background: transparent; border: 0; border-radius: 0; padding: 0; margin: 0; } /* 内容区本身是卡,pane 不再叠第二层底/阴影 */
.pane {
background: transparent;
border: 0;
border-radius: 0;
padding: 0;
margin: 0;
box-shadow: none;
}
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; } .pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.pane .pane-desc { font-size: 12px; color: var(--st-muted); margin-bottom: 18px; } .pane .pane-desc { font-size: 12px; color: var(--st-muted); margin-bottom: 18px; }
.st-row { .st-row {
min-height: 82px; min-height: 76px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 24px; gap: 24px;
padding: 4px 0;
border-bottom: 1px solid rgba(34, 42, 54, 0.075); border-bottom: 1px solid rgba(34, 42, 54, 0.075);
} }
.st-row:last-child { border-bottom: 0; } .st-row:last-child { border-bottom: 0; }
@@ -196,7 +206,7 @@
border: 1px solid rgba(34, 42, 54, 0.11); border: 1px solid rgba(34, 42, 54, 0.11);
border-radius: 10px; border-radius: 10px;
outline: none; outline: none;
background: #fff; background: var(--surface);
color: var(--st-text); color: var(--st-text);
font: inherit; font: inherit;
font-size: 14px; font-size: 14px;
@@ -283,7 +293,7 @@
.switch { position: relative; width: 46px; height: 26px; flex: 0 0 46px; display: inline-block; } .switch { position: relative; width: 46px; height: 26px; flex: 0 0 46px; display: inline-block; }
.switch input { opacity: 0; width: 0; height: 0; } .switch input { opacity: 0; width: 0; height: 0; }
.switch .slider { position: absolute; inset: 0; background: #cfd3da; border-radius: 999px; cursor: pointer; transition: background 170ms ease; } .switch .slider { position: absolute; inset: 0; background: #cfd3da; border-radius: 999px; cursor: pointer; transition: background 170ms ease; }
.switch .slider::before { content: ''; position: absolute; left: 4px; top: 4px; width: 18px; height: 18px; background: #fff; border-radius: 50%; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18); transition: transform 170ms ease; } .switch .slider::before { content: ''; position: absolute; left: 4px; top: 4px; width: 18px; height: 18px; background: var(--surface); border-radius: 50%; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18); transition: transform 170ms ease; }
.switch input:checked + .slider { background: var(--klein); } .switch input:checked + .slider { background: var(--klein); }
.switch input:checked + .slider::before { transform: translateX(20px); } .switch input:checked + .slider::before { transform: translateX(20px); }
.switch input:disabled + .slider { cursor: not-allowed; opacity: .55; } .switch input:disabled + .slider { cursor: not-allowed; opacity: .55; }
+28 -3
View File
@@ -68,6 +68,31 @@
--red-bd: #F2D6CE; --red-bd: #F2D6CE;
} }
html[data-theme="dark"] {
color-scheme: dark;
--bg: #0f1115;
--bg-soft: #14171c;
--card: #181b21;
--border: #2a2f38;
--border-soft: #323843;
--ink: #e8eaed;
--ink-2: rgba(232, 234, 237, .72);
--ink-3: rgba(232, 234, 237, .52);
--ink-4: rgba(232, 234, 237, .32);
--orange: #3d6bff;
--orange-soft: rgba(61, 107, 255, .28);
--orange-tint: rgba(61, 107, 255, .10);
--orange-hover: #5a82ff;
--green: #6dd88a;
--green-bg: rgba(66, 195, 102, .14);
--green-bd: rgba(66, 195, 102, .32);
--red: #ff6b5c;
--red-bg: rgba(235, 52, 36, .14);
--red-bd: rgba(235, 52, 36, .32);
}
html, body { html, body {
background: var(--bg); background: var(--bg);
color: var(--ink); color: var(--ink);
@@ -177,7 +202,7 @@ nav a.disabled:hover { background: transparent; color: var(--ink-4); }
.user .em { font-size: 13px; } .user .em { font-size: 13px; }
/* ─── Main + grid background ─── */ /* ─── Main + grid background ─── */
main { position: relative; background: #fff; } main { position: relative; background: var(--surface); }
.grid-bg { .grid-bg {
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -1529,7 +1554,7 @@ table.t tbody tr:hover { background: var(--bg-soft); }
.topup-modal .topup-amt { text-align: center; font-size: 34px; font-weight: 600; color: var(--ink); margin: 4px 0; } .topup-modal .topup-amt { text-align: center; font-size: 34px; font-weight: 600; color: var(--ink); margin: 4px 0; }
.topup-note { text-align: center; color: var(--orange); font-family: 'JetBrains Mono', monospace; font-size: 12px; } .topup-note { text-align: center; color: var(--orange); font-family: 'JetBrains Mono', monospace; font-size: 12px; }
.topup-qr { width: 180px; height: 180px; margin: 18px auto; background: repeating-linear-gradient(45deg, var(--ink) 0 4px, #fff 4px 8px); border: 8px solid #fff; display: grid; place-items: center; } .topup-qr { width: 180px; height: 180px; margin: 18px auto; background: repeating-linear-gradient(45deg, var(--ink) 0 4px, #fff 4px 8px); border: 8px solid #fff; display: grid; place-items: center; }
.topup-qr .center { background: #fff; padding: 12px; text-align: center; font-weight: 600; } .topup-qr .center { background: var(--surface); padding: 12px; text-align: center; font-weight: 600; }
.topup-qr .center span { color: var(--ink-3); font-family: 'JetBrains Mono', monospace; font-size: 12px; } .topup-qr .center span { color: var(--ink-3); font-family: 'JetBrains Mono', monospace; font-size: 12px; }
/* 团队页旧全局块已移除 · 详见 src/team-page.css 的 .team-page 作用域。保留通用 .av(app-shell/account/dashboard 在用)。 */ /* 团队页旧全局块已移除 · 详见 src/team-page.css 的 .team-page 作用域。保留通用 .av(app-shell/account/dashboard 在用)。 */
@@ -1597,7 +1622,7 @@ table.t tbody tr:hover { background: var(--bg-soft); }
.switch { position: relative; display: inline-flex; width: 42px; height: 24px; } .switch { position: relative; display: inline-flex; width: 42px; height: 24px; }
.switch input { opacity: 0; width: 0; height: 0; } .switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; inset: 0; border-radius: 999px; background: var(--border); transition: .15s; } .slider { position: absolute; inset: 0; border-radius: 999px; background: var(--border); transition: .15s; }
.slider::before { content: ''; position: absolute; width: 18px; height: 18px; left: 3px; top: 3px; border-radius: 50%; background: #fff; transition: .15s; } .slider::before { content: ''; position: absolute; width: 18px; height: 18px; left: 3px; top: 3px; border-radius: 50%; background: var(--surface); transition: .15s; }
.switch input:checked + .slider { background: var(--orange); } .switch input:checked + .slider { background: var(--orange); }
.switch input:checked + .slider::before { transform: translateX(18px); } .switch input:checked + .slider::before { transform: translateX(18px); }
+2 -2
View File
@@ -162,7 +162,7 @@
padding: 0 13px; padding: 0 13px;
border: 1px solid rgba(34, 42, 54, 0.10); border: 1px solid rgba(34, 42, 54, 0.10);
border-radius: 10px; border-radius: 10px;
background: #fff; background: var(--surface);
color: var(--st-muted); color: var(--st-muted);
} }
.tc-search input { .tc-search input {
@@ -239,7 +239,7 @@
line-height: 0; line-height: 0;
border: 1px solid var(--st-line); border: 1px solid var(--st-line);
border-radius: 8px; border-radius: 8px;
background: #fff; background: var(--surface);
cursor: pointer; cursor: pointer;
color: var(--st-muted); color: var(--st-muted);
padding: 0; padding: 0;
+40
View File
@@ -0,0 +1,40 @@
/** 外观主题:浅色 / 深色 / 跟随系统。写到 html[data-theme] + localStorage,避免首屏闪白。 */
export type Appearance = "light" | "dark" | "system";
export const THEME_STORAGE_KEY = "yingqing-appearance";
export function normalizeAppearance(value: unknown): Appearance {
if (value === "light" || value === "dark" || value === "system") return value;
return "system";
}
export function resolveTheme(appearance: Appearance): "light" | "dark" {
if (appearance === "light" || appearance === "dark") return appearance;
if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
}
export function applyTheme(appearance: Appearance | string | null | undefined) {
if (typeof document === "undefined") return;
const normalized = normalizeAppearance(appearance);
const theme = resolveTheme(normalized);
const root = document.documentElement;
root.setAttribute("data-theme", theme);
root.style.colorScheme = theme;
try {
localStorage.setItem(THEME_STORAGE_KEY, normalized);
} catch {
/* ignore quota / private mode */
}
}
export function readStoredAppearance(): Appearance {
try {
return normalizeAppearance(localStorage.getItem(THEME_STORAGE_KEY));
} catch {
return "system";
}
}
+6 -6
View File
@@ -810,7 +810,7 @@
border: 1px solid rgba(0, 47, 167, 0.16); border: 1px solid rgba(0, 47, 167, 0.16);
border-radius: 50%; border-radius: 50%;
color: #78849a; color: #78849a;
background: #fff; background: var(--surface);
font-size: 10px; font-size: 10px;
line-height: 1; line-height: 1;
transition: color 180ms ease, border-color 180ms ease, background-color 180ms ease, box-shadow 180ms ease; transition: color 180ms ease, border-color 180ms ease, background-color 180ms ease, box-shadow 180ms ease;
@@ -903,7 +903,7 @@
box-sizing: content-box; box-sizing: content-box;
border: 1px solid rgba(0, 47, 167, 0.12); border: 1px solid rgba(0, 47, 167, 0.12);
border-radius: 14px; border-radius: 14px;
background: #fff; background: var(--surface);
box-shadow: 0 8px 20px rgba(0, 47, 167, 0.08); box-shadow: 0 8px 20px rgba(0, 47, 167, 0.08);
} }
@@ -1015,7 +1015,7 @@
.vr-page .remix-page .remix-prompt-panel .analysis-prompt { .vr-page .remix-page .remix-prompt-panel .analysis-prompt {
border-left: 3px solid var(--klein); border-left: 3px solid var(--klein);
background: #fff; background: var(--surface);
box-shadow: inset 0 1px 3px rgba(27, 45, 83, 0.03), 0 8px 22px rgba(22, 45, 92, 0.045); box-shadow: inset 0 1px 3px rgba(27, 45, 83, 0.03), 0 8px 22px rgba(22, 45, 92, 0.045);
} }
@@ -1245,7 +1245,7 @@
border: 1px solid rgba(0, 47, 167, 0.24); border: 1px solid rgba(0, 47, 167, 0.24);
border-radius: 9px; border-radius: 9px;
color: var(--klein); color: var(--klein);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
@@ -1312,7 +1312,7 @@
border-radius: 9px; border-radius: 9px;
outline: none; outline: none;
color: #27334a; color: #27334a;
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 12px; font-size: 12px;
line-height: 1.7; line-height: 1.7;
@@ -1327,7 +1327,7 @@
border: 1px solid rgba(0, 47, 167, 0.2); border: 1px solid rgba(0, 47, 167, 0.2);
border-radius: 8px; border-radius: 8px;
color: var(--klein); color: var(--klein);
background: #fff; background: var(--surface);
font: inherit; font: inherit;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
+7 -7
View File
@@ -168,7 +168,7 @@
.vrep-page .replace-mode-button.active { .vrep-page .replace-mode-button.active {
color: var(--klein); color: var(--klein);
background: #fff; background: var(--surface);
box-shadow: 0 7px 18px rgba(22, 45, 92, 0.09), inset 0 0 0 1px rgba(0, 47, 167, 0.11); box-shadow: 0 7px 18px rgba(22, 45, 92, 0.09), inset 0 0 0 1px rgba(0, 47, 167, 0.11);
} }
@@ -553,7 +553,7 @@
border: 1px solid rgba(0, 47, 167, 0.12); border: 1px solid rgba(0, 47, 167, 0.12);
border-radius: 13px; border-radius: 13px;
color: var(--klein); color: var(--klein);
background: #fff; background: var(--surface);
box-shadow: 0 8px 18px rgba(0, 47, 167, 0.07); box-shadow: 0 8px 18px rgba(0, 47, 167, 0.07);
} }
@@ -623,7 +623,7 @@
padding: 6px; padding: 6px;
border: 1px solid rgba(34, 42, 54, 0.09); border: 1px solid rgba(34, 42, 54, 0.09);
border-radius: 11px; border-radius: 11px;
background: #fff; background: var(--surface);
} }
.vrep-page .replace-temporary-cell { .vrep-page .replace-temporary-cell {
@@ -686,7 +686,7 @@
place-items: center; place-items: center;
border-radius: 11px; border-radius: 11px;
color: var(--klein); color: var(--klein);
background: #fff; background: var(--surface);
} }
.vrep-page .replace-temporary-more svg { .vrep-page .replace-temporary-more svg {
@@ -1130,7 +1130,7 @@
border: 1px solid rgba(0, 47, 167, 0.18); border: 1px solid rgba(0, 47, 167, 0.18);
border-radius: 8px; border-radius: 8px;
color: var(--klein); color: var(--klein);
background: #fff; background: var(--surface);
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
white-space: nowrap; white-space: nowrap;
@@ -1232,7 +1232,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
background: #fff; background: var(--surface);
box-shadow: -22px 0 48px rgba(7, 12, 22, 0.2); box-shadow: -22px 0 48px rgba(7, 12, 22, 0.2);
transform: translateX(100%); transform: translateX(100%);
transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1); transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
@@ -1363,7 +1363,7 @@
gap: 10px; gap: 10px;
padding: 0 24px; padding: 0 24px;
border-top: 1px solid rgba(34, 42, 54, 0.09); border-top: 1px solid rgba(34, 42, 54, 0.09);
background: #fff; background: var(--surface);
box-shadow: 0 -10px 24px rgba(20, 27, 38, 0.045); box-shadow: 0 -10px 24px rgba(20, 27, 38, 0.045);
} }
+9
View File
@@ -0,0 +1,9 @@
{
"name": "yingqing",
"private": true,
"scripts": {
"be": "bash scripts/dev.sh be",
"fe": "bash scripts/dev.sh fe",
"dev": "bash scripts/dev.sh dev"
}
}
Executable
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# 在仓库任意目录记这三条就够:
# npm run be 只开后端 http://127.0.0.1:8010
# npm run fe 只开前端 http://127.0.0.1:5173
# npm run dev 两个一起开(在项目根目录)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
MODE="${1:-all}"
PY="$ROOT/core/backend/.venv/bin/python"
listening() {
lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1
}
need_python() {
if [[ -x "$PY" ]]; then
return 0
fi
echo "后端环境还没装好。在项目里执行一次:"
echo " cd core/backend && python3.12 -m venv .venv && .venv/bin/pip install -r requirements.txt"
exit 1
}
start_backend() {
need_python
if listening 8010; then
echo "后端已经在跑: http://127.0.0.1:8010"
return 0
fi
echo "后端 → http://127.0.0.1:8010"
cd "$ROOT/core/backend"
exec "$PY" manage.py runserver 0.0.0.0:8010
}
start_frontend() {
if listening 5173; then
echo "前端已经在跑: http://127.0.0.1:5173"
return 0
fi
if [[ ! -d "$ROOT/core/frontend/node_modules" ]]; then
echo "第一次开前端,先装依赖…"
npm --prefix "$ROOT/core/frontend" install
fi
echo "前端 → http://127.0.0.1:5173"
cd "$ROOT/core/frontend"
exec npm run dev
}
case "$MODE" in
be|backend)
start_backend
;;
fe|frontend)
start_frontend
;;
all|dev)
need_python
trap 'kill 0' INT TERM
if listening 8010; then
echo "后端已经在跑: http://127.0.0.1:8010"
else
echo "后端 → http://127.0.0.1:8010"
(cd "$ROOT/core/backend" && "$PY" manage.py runserver 0.0.0.0:8010) &
fi
if listening 5173; then
echo "前端已经在跑: http://127.0.0.1:5173"
else
echo "前端 → http://127.0.0.1:5173"
(cd "$ROOT/core/frontend" && npm run dev) &
fi
wait
;;
*)
echo "用法: npm run be | npm run fe | npm run dev"
exit 1
;;
esac