auto-save 2026-05-15 17:11 (~6)

This commit is contained in:
2026-05-15 17:11:52 +08:00
parent f3230ff99e
commit 6c9806ce98
6 changed files with 106 additions and 14 deletions

View File

@@ -1,12 +1,5 @@
{
"entries": [
{
"files_changed": 8,
"hash": "8c6ee1d",
"message": "auto-save 2026-05-14 01:45 (+6, ~2)",
"ts": "2026-05-14T01:46:08+08:00",
"type": "commit"
},
{
"files_changed": 2,
"message": "启动 Codex 接力会话 · 已载入 Claude / Codex 最近会话,等待下一条指令 · 分支 HEAD · 2 项未提交变更 · 最近提交auto-save 2026-05-14 01:45 (+6, ~2)",
@@ -3251,6 +3244,13 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 4 项未提交变更 · 最近提交auto-save 2026-05-15 17:00 (~3)",
"files_changed": 4
},
{
"ts": "2026-05-15T17:06:22+08:00",
"type": "commit",
"message": "auto-save 2026-05-15 17:06 (+1, ~4)",
"hash": "f3230ff",
"files_changed": 5
}
]
}

View File

@@ -21,7 +21,7 @@
- 管理后台:待定
- 服务器目录:`/opt/skg-marketing-studio`
- 生产启动:`docker compose -f docker-compose.prod.yml --env-file deploy/.env.production up -d --build`
- 生产架构:`web` 容器用 Nginx 承载 Next 静态导出;未登录访问工作台跳转 `/login/``/api/` 通过 Nginx `auth_request` 校验 FastAPI 会话 Cookie 后反代到 `skg-marketing-api:4291`Traefik 通过 `coolify` 外部网络接入 80/443
- 生产架构:`web` 容器用 Nginx 承载 Next 静态导出;`/login/``/_next/``/assets/` 等登录页必需静态资源公开访问;未登录访问工作台跳转 `/login/``/api/` 通过 Nginx `auth_request` 校验 FastAPI 会话 Cookie 后反代到 `skg-marketing-api:4291`Traefik 通过 `coolify` 外部网络接入 80/443
- 持久化目录:服务器 `./data/jobs` 挂载到后端 `/data/jobs`
- 登录凭证:用户名写下方快捷登录;密码明文备份只放服务器 `/root/skg-marketing-studio-login.txt`,生产环境变量 `WEB_AUTH_PASSWORD` / `WEB_AUTH_SESSION_SECRET` 只放服务器 `deploy/.env.production`

View File

@@ -87,6 +87,11 @@ server {
try_files $uri =404;
}
location /assets/ {
root /usr/share/nginx/html;
try_files $uri =404;
}
location ~* ^/(icon|apple-icon|favicon|manifest|placeholder).* {
root /usr/share/nginx/html;
try_files $uri =404;

View File

@@ -536,7 +536,7 @@
<tr>
<td>生产站点</td>
<td><code>https://marketing.skg.com</code></td>
<td>公司域名已解析到 VPS <code>76.13.31.179</code>。线上由既有 Coolify / Traefik 负责 HTTPS 入口,项目 <code>web</code> 容器用 Nginx 承载静态前端;未登录访问工作台跳转 <code>/login/</code><code>/api/</code> 通过 <code>auth_request</code> 校验 FastAPI 会话 Cookie 后再反代。</td>
<td>公司域名已解析到 VPS <code>76.13.31.179</code>。线上由既有 Coolify / Traefik 负责 HTTPS 入口,项目 <code>web</code> 容器用 Nginx 承载静态前端;<code>/login/</code><code>/_next/</code><code>/assets/</code> 为公开登录页资源,未登录访问工作台跳转 <code>/login/</code><code>/api/</code> 通过 <code>auth_request</code> 校验 FastAPI 会话 Cookie 后再反代。</td>
</tr>
<tr>
<td>生产部署</td>
@@ -591,8 +591,8 @@
<table>
<tbody>
<tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态jobs、activeJobId、按 job 隔离的 selectedFrames/详情面板状态、clipboard、ReactFlow 节点和边;负责打开/找回画布工作面板。</td></tr>
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:账号密码表单、保持登录、错误/成功状态;左侧展示区改为高级产品入口页结构,使用本地 SKG 颈部按摩仪产品图、黑白/香槟金视觉、Secure Studio 胶囊和素材/声音/成片状态栏。</td></tr>
<tr><td><code>web/components/login/animated-login-characters.tsx</code></td><td>登录页旧版四个几何角色组件:仍保留在代码中作为历史/备用组件,但当前生产登录页主视觉已不再挂载</td></tr>
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:账号密码表单、保持登录、错误/成功状态;左侧展示区改为高级产品入口页结构,使用本地 SKG 颈部按摩仪产品图、黑白/香槟金视觉、Secure Studio 胶囊和素材/声音/成片状态栏;同时保留动态角色作为 Live Studio Modules 小组件</td></tr>
<tr><td><code>web/components/login/animated-login-characters.tsx</code></td><td>登录页四个几何动态角色组件:当前以小型 Live Studio Modules 方式挂在产品区,保留鼠标眼神跟随、输入、显示密码、错误和成功状态反馈</td></tr>
<tr><td><code>web/components/nodes/index.tsx</code></td><td>DAG 节点定义Input、VisualLab、Audio、Compose以及画布工作面板 KeyframePanel / VideoFramePanel旧 Keyframe/Storyboard/VideoGen 组件保留但不再挂主画布。</td></tr>
<tr><td><code>web/components/audio-strip.tsx</code></td><td>底部吸附音频条:可拖拽调整高度;播放原音频时移动指针,逐个高亮英文/中文字幕节点和对应波形,并在右侧固定显示按原音频时长生成的 SKG 英文产品口播和 MiniMax 随机英文配音。</td></tr>
<tr><td><code>web/components/lightbox.tsx</code></td><td>关键帧素材准备面板:清洗、统一主体候选、参考帧网格、六张主体重绘图、每帧去主体场景图、纵向 6 行产品融合镜头工作表和审核。</td></tr>
@@ -949,8 +949,8 @@ SubjectAsset {
</header>
<div class="body">
<p><strong>问题:</strong>上一版虽然吸收了官网元素,但仍偏浅灰卡片拼接,卡通角色舞台削弱了公司级工具和高端健康硬件的质感。</p>
<p><strong>改动:</strong>生产登录页主视觉改为真实 SKG 颈部按摩仪产品摄影卡,采用黑白/香槟金主轴、超大标题、克制胶囊状态、素材/声音/成片三段式状态栏;几何角色组件不再作为当前登录页主视觉挂载。新增本地静态图 <code>web/public/assets/skg-g7-pro-neck-massager.png</code>避免远程官网图在生产登录页破图</p>
<p><strong>影响:</strong><code>web/app/login/page.tsx</code><code>web/app/globals.css</code><code>web/public/assets/skg-g7-pro-neck-massager.png</code><code>docs/source-analysis.html</code></p>
<p><strong>改动:</strong>生产登录页主视觉改为真实 SKG 颈部按摩仪产品摄影卡,采用黑白/香槟金主轴、超大标题、克制胶囊状态、素材/声音/成片三段式状态栏;几何角色组件不再抢占主视觉,但作为 <code>Live Studio Modules</code> 小组件保留,继续响应鼠标眼神跟随、输入、显示密码、错误和成功状态。新增本地静态图 <code>web/public/assets/skg-g7-pro-neck-massager.png</code>并在生产 Nginx 放行 <code>/assets/</code>,避免登录页产品图被登录保护拦截</p>
<p><strong>影响:</strong><code>web/app/login/page.tsx</code><code>web/app/globals.css</code><code>web/public/assets/skg-g7-pro-neck-massager.png</code><code>deploy/nginx.conf</code><code>RULES.md</code><code>docs/source-analysis.html</code></p>
</div>
</article>
<article class="change">

View File

@@ -296,6 +296,50 @@
font-weight: 600;
line-height: 1.05;
}
.login-dynamic-dock {
position: absolute;
left: 0;
bottom: 118px;
width: 252px;
border: 1px solid rgba(255, 255, 255, 0.72);
border-radius: 8px;
background: rgba(255, 255, 255, 0.64);
padding: 12px;
box-shadow: 0 22px 52px rgba(40, 40, 40, 0.12);
backdrop-filter: blur(18px);
}
.login-dynamic-dock__label {
display: block;
margin-bottom: 8px;
color: rgba(40, 40, 40, 0.46);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.login-dynamic-dock .login-character-stage {
min-height: 116px;
border-color: rgba(40, 40, 40, 0.08);
background:
linear-gradient(90deg, rgba(40, 40, 40, 0.04) 1px, transparent 1px),
linear-gradient(180deg, rgba(40, 40, 40, 0.04) 1px, transparent 1px),
rgba(255, 255, 255, 0.5);
background-size: 22px 22px, 22px 22px, auto;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.84);
}
.login-dynamic-dock .login-character-stage::after {
height: 48%;
background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.7));
}
.login-dynamic-dock .login-stage-grid {
inset: 12px;
border-color: rgba(40, 40, 40, 0.08);
}
.login-dynamic-dock .login-characters-container {
left: 50%;
bottom: 0;
transform: translateX(-50%) scale(0.28);
}
.login-studio-chip {
position: absolute;
display: inline-flex;
@@ -816,6 +860,18 @@
.login-product-caption b {
font-size: 22px;
}
.login-dynamic-dock {
left: 0;
bottom: 132px;
width: 210px;
padding: 10px;
}
.login-dynamic-dock .login-character-stage {
min-height: 94px;
}
.login-dynamic-dock .login-characters-container {
transform: translateX(-50%) scale(0.22);
}
.login-studio-chip {
font-size: 12px;
}

View File

@@ -1,7 +1,7 @@
"use client"
import type { FormEvent } from "react"
import { useState } from "react"
import { useEffect, useMemo, useState } from "react"
import {
AlertCircle,
ArrowRight,
@@ -14,6 +14,7 @@ import {
Sparkles,
UserRound,
} from "lucide-react"
import { AnimatedLoginCharacters, type LoginCharacterMood } from "@/components/login/animated-login-characters"
type LoginStatus = "idle" | "loading" | "success"
@@ -25,11 +26,33 @@ export default function LoginPage() {
const [password, setPassword] = useState("")
const [remember, setRemember] = useState(true)
const [showPassword, setShowPassword] = useState(false)
const [activeField, setActiveField] = useState<"username" | "password" | null>(null)
const [error, setError] = useState("")
const [status, setStatus] = useState<LoginStatus>("idle")
const [eyeOffset, setEyeOffset] = useState({ x: 0, y: 0 })
useEffect(() => {
const onPointerMove = (event: PointerEvent) => {
const centerX = window.innerWidth / 2
const centerY = window.innerHeight / 2
const nextX = Math.max(-1, Math.min(1, (event.clientX - centerX) / centerX))
const nextY = Math.max(-1, Math.min(1, (event.clientY - centerY) / centerY))
setEyeOffset({ x: nextX * 8, y: nextY * 5.5 })
}
window.addEventListener("pointermove", onPointerMove)
return () => window.removeEventListener("pointermove", onPointerMove)
}, [])
const disabled = status === "loading" || status === "success"
const mood: LoginCharacterMood = useMemo(() => {
if (status === "success") return "success"
if (error) return "error"
if (showPassword && activeField === "password") return "peek"
if (activeField || username || password) return "typing"
return "idle"
}, [activeField, error, password, showPassword, status, username])
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
setError("")
@@ -95,6 +118,10 @@ export default function LoginPage() {
<span>G7 Pro Fold</span>
<b>Neck Massager</b>
</div>
<div className="login-dynamic-dock">
<span className="login-dynamic-dock__label">Live Studio Modules</span>
<AnimatedLoginCharacters mood={mood} eyeOffset={eyeOffset} />
</div>
<div className="login-studio-chip login-studio-chip--visual">
<Sparkles className="h-4 w-4" />
<span>Visual Asset Flow</span>
@@ -141,6 +168,8 @@ export default function LoginPage() {
disabled={disabled}
autoComplete="username"
placeholder="请输入账号"
onFocus={() => setActiveField("username")}
onBlur={() => setActiveField(null)}
onChange={(event) => {
setUsername(event.target.value)
if (error) setError("")
@@ -160,6 +189,8 @@ export default function LoginPage() {
type={showPassword ? "text" : "password"}
autoComplete="current-password"
placeholder="请输入密码"
onFocus={() => setActiveField("password")}
onBlur={() => setActiveField(null)}
onChange={(event) => {
setPassword(event.target.value)
if (error) setError("")