auto-save 2026-05-15 16:10 (+1, ~4)
This commit is contained in:
@@ -1,12 +1,5 @@
|
|||||||
{
|
{
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
|
||||||
"files_changed": 7,
|
|
||||||
"hash": "042efdc",
|
|
||||||
"message": "auto-save 2026-05-14 00:42 (+4, ~3)",
|
|
||||||
"ts": "2026-05-14T00:43:18+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"files_changed": 7,
|
"files_changed": 7,
|
||||||
"hash": "e8a653e",
|
"hash": "e8a653e",
|
||||||
@@ -3251,6 +3244,13 @@
|
|||||||
"type": "session-heartbeat",
|
"type": "session-heartbeat",
|
||||||
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 15:59 (~4)",
|
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-15 15:59 (~4)",
|
||||||
"files_changed": 1
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-15T16:05:39+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-15 16:05 (~1)",
|
||||||
|
"hash": "ac36c4e",
|
||||||
|
"files_changed": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -591,7 +591,8 @@
|
|||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态:jobs、activeJobId、按 job 隔离的 selectedFrames/详情面板状态、clipboard、ReactFlow 节点和边;负责打开/找回画布工作面板。</td></tr>
|
<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>生产登录页:账号密码表单、保持登录、错误/成功状态,以及参考风格库 14 的四个动画角色互动。</td></tr>
|
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:账号密码表单、保持登录、错误/成功状态;把角色状态和眼神位移传给独立动画组件。</td></tr>
|
||||||
|
<tr><td><code>web/components/login/animated-login-characters.tsx</code></td><td>登录页四个几何角色组件:按配置渲染紫色高矩形、黑色矩形、橙色半圆、黄色圆角柱,以及眼睛和嘴型。</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/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/audio-strip.tsx</code></td><td>底部吸附音频条:可拖拽调整高度;播放原音频时移动指针,逐个高亮英文/中文字幕节点和对应波形,并在右侧固定显示按原音频时长生成的 SKG 英文产品口播和 MiniMax 随机英文配音。</td></tr>
|
||||||
<tr><td><code>web/components/lightbox.tsx</code></td><td>关键帧素材准备面板:清洗、统一主体候选、参考帧网格、六张主体重绘图、每帧去主体场景图、纵向 6 行产品融合镜头工作表和审核。</td></tr>
|
<tr><td><code>web/components/lightbox.tsx</code></td><td>关键帧素材准备面板:清洗、统一主体候选、参考帧网格、六张主体重绘图、每帧去主体场景图、纵向 6 行产品融合镜头工作表和审核。</td></tr>
|
||||||
@@ -941,6 +942,17 @@ SubjectAsset {
|
|||||||
<h2>变更记录</h2>
|
<h2>变更记录</h2>
|
||||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||||
<div class="changelog">
|
<div class="changelog">
|
||||||
|
<article class="change">
|
||||||
|
<header>
|
||||||
|
<h3>2026-05-15 · 登录页角色组件重构</h3>
|
||||||
|
<span class="tag rose">UI</span>
|
||||||
|
<span class="tag violet">Refactor</span>
|
||||||
|
</header>
|
||||||
|
<div class="body">
|
||||||
|
<p><strong>改动:</strong>新增 <code>web/components/login/animated-login-characters.tsx</code>,把登录页四个角色从 <code>web/app/login/page.tsx</code> 抽成独立组件;角色用配置数组渲染,眼睛、嘴型和黄色 SVG 嘴不再散落在页面主体。动效状态从全页 <code>data-mood</code> 改为挂在角色舞台 <code>login-character-stage</code> 上,样式作用域更清晰。</p>
|
||||||
|
<p><strong>影响:</strong><code>web/app/login/page.tsx</code>、<code>web/components/login/animated-login-characters.tsx</code>、<code>web/app/globals.css</code>、<code>docs/source-analysis.html</code>。</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
<article class="change">
|
<article class="change">
|
||||||
<header>
|
<header>
|
||||||
<h3>2026-05-15 · 登录页角色眼神幅度增强</h3>
|
<h3>2026-05-15 · 登录页角色眼神幅度增强</h3>
|
||||||
|
|||||||
@@ -362,84 +362,84 @@
|
|||||||
fill: none;
|
fill: none;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
}
|
}
|
||||||
.login-page[data-mood="typing"] .login-figure--purple {
|
.login-character-stage[data-mood="typing"] .login-figure--purple {
|
||||||
height: 430px;
|
height: 430px;
|
||||||
transform: skewX(-10deg) translateX(36px);
|
transform: skewX(-10deg) translateX(36px);
|
||||||
}
|
}
|
||||||
.login-page[data-mood="typing"] .login-figure--black {
|
.login-character-stage[data-mood="typing"] .login-figure--black {
|
||||||
transform: skewX(7deg) translateX(6px);
|
transform: skewX(7deg) translateX(6px);
|
||||||
}
|
}
|
||||||
.login-page[data-mood="typing"] .login-figure--orange {
|
.login-character-stage[data-mood="typing"] .login-figure--orange {
|
||||||
transform: skewX(-5deg);
|
transform: skewX(-5deg);
|
||||||
}
|
}
|
||||||
.login-page[data-mood="typing"] .login-figure--yellow {
|
.login-character-stage[data-mood="typing"] .login-figure--yellow {
|
||||||
transform: skewX(4deg);
|
transform: skewX(4deg);
|
||||||
}
|
}
|
||||||
.login-page[data-mood="typing"] .login-mouth--purple {
|
.login-character-stage[data-mood="typing"] .login-mouth--purple {
|
||||||
width: 7px;
|
width: 7px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
transform: translate(14px, -28px) skewX(10deg);
|
transform: translate(14px, -28px) skewX(10deg);
|
||||||
}
|
}
|
||||||
.login-page[data-mood="typing"] .login-mouth--orange {
|
.login-character-stage[data-mood="typing"] .login-mouth--orange {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transform: translateX(6px);
|
transform: translateX(6px);
|
||||||
}
|
}
|
||||||
.login-page[data-mood="peek"] .login-figure--purple,
|
.login-character-stage[data-mood="peek"] .login-figure--purple,
|
||||||
.login-page[data-mood="peek"] .login-figure--black {
|
.login-character-stage[data-mood="peek"] .login-figure--black {
|
||||||
transform: skewX(0deg) translateY(-10px);
|
transform: skewX(0deg) translateY(-10px);
|
||||||
}
|
}
|
||||||
.login-page[data-mood="peek"] .login-eyes--purple {
|
.login-character-stage[data-mood="peek"] .login-eyes--purple {
|
||||||
left: 40px;
|
left: 40px;
|
||||||
top: 14px;
|
top: 14px;
|
||||||
}
|
}
|
||||||
.login-page[data-mood="peek"] .login-eyes--black {
|
.login-character-stage[data-mood="peek"] .login-eyes--black {
|
||||||
left: 2px;
|
left: 2px;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
}
|
}
|
||||||
.login-page[data-mood="peek"] .login-eyes--orange {
|
.login-character-stage[data-mood="peek"] .login-eyes--orange {
|
||||||
left: 68px;
|
left: 68px;
|
||||||
top: 48px;
|
top: 48px;
|
||||||
}
|
}
|
||||||
.login-page[data-mood="peek"] .login-eyes--yellow {
|
.login-character-stage[data-mood="peek"] .login-eyes--yellow {
|
||||||
left: 10px;
|
left: 10px;
|
||||||
top: 28px;
|
top: 28px;
|
||||||
}
|
}
|
||||||
.login-page[data-mood="peek"] .login-eye::after,
|
.login-character-stage[data-mood="peek"] .login-eye::after,
|
||||||
.login-page[data-mood="peek"] .login-pupil::after {
|
.login-character-stage[data-mood="peek"] .login-pupil::after {
|
||||||
transform: translate(-8px, -6px);
|
transform: translate(-8px, -6px);
|
||||||
}
|
}
|
||||||
.login-page[data-mood="error"] .login-characters-container {
|
.login-character-stage[data-mood="error"] .login-characters-container {
|
||||||
animation: login-stage-breathe 7s ease-in-out infinite, login-shake 0.28s ease-in-out 2;
|
animation: login-stage-breathe 7s ease-in-out infinite, login-shake 0.28s ease-in-out 2;
|
||||||
}
|
}
|
||||||
.login-page[data-mood="error"] .login-eye {
|
.login-character-stage[data-mood="error"] .login-eye {
|
||||||
height: 10px;
|
height: 10px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
.login-page[data-mood="error"] .login-mouth--purple,
|
.login-character-stage[data-mood="error"] .login-mouth--purple,
|
||||||
.login-page[data-mood="error"] .login-mouth--orange {
|
.login-character-stage[data-mood="error"] .login-mouth--orange {
|
||||||
border-radius: 12px 12px 0 0;
|
border-radius: 12px 12px 0 0;
|
||||||
transform: translateY(10px);
|
transform: translateY(10px);
|
||||||
}
|
}
|
||||||
.login-page[data-mood="error"] .login-yellow-mouth path {
|
.login-character-stage[data-mood="error"] .login-yellow-mouth path {
|
||||||
d: path("M0 10 Q10 2, 20 10 Q30 18, 40 10 Q50 2, 60 10 Q70 18, 80 10");
|
d: path("M0 10 Q10 2, 20 10 Q30 18, 40 10 Q50 2, 60 10 Q70 18, 80 10");
|
||||||
}
|
}
|
||||||
.login-page[data-mood="success"] .login-characters-container {
|
.login-character-stage[data-mood="success"] .login-characters-container {
|
||||||
animation: login-stage-success 0.75s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
animation: login-stage-success 0.75s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||||
}
|
}
|
||||||
.login-page[data-mood="success"] .login-mouth--purple {
|
.login-character-stage[data-mood="success"] .login-mouth--purple {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border-radius: 0 0 15px 15px;
|
border-radius: 0 0 15px 15px;
|
||||||
}
|
}
|
||||||
.login-page[data-mood="success"] .login-mouth--orange {
|
.login-character-stage[data-mood="success"] .login-mouth--orange {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
border-radius: 0 0 16px 16px;
|
border-radius: 0 0 16px 16px;
|
||||||
}
|
}
|
||||||
.login-page[data-mood="success"] .login-yellow-mouth path {
|
.login-character-stage[data-mood="success"] .login-yellow-mouth path {
|
||||||
d: path("M0 6 Q20 18, 40 18 Q60 18, 80 6");
|
d: path("M0 6 Q20 18, 40 18 Q60 18, 80 6");
|
||||||
}
|
}
|
||||||
@keyframes login-stage-breathe {
|
@keyframes login-stage-breathe {
|
||||||
@@ -466,8 +466,8 @@
|
|||||||
}
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.login-characters-container,
|
.login-characters-container,
|
||||||
.login-page[data-mood="error"] .login-characters-container,
|
.login-character-stage[data-mood="error"] .login-characters-container,
|
||||||
.login-page[data-mood="success"] .login-characters-container {
|
.login-character-stage[data-mood="success"] .login-characters-container {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import type { CSSProperties, FormEvent } from "react"
|
import type { FormEvent } from "react"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
UserRound,
|
UserRound,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
import { AnimatedLoginCharacters, type LoginCharacterMood } from "@/components/login/animated-login-characters"
|
||||||
|
|
||||||
type LoginStatus = "idle" | "loading" | "success"
|
type LoginStatus = "idle" | "loading" | "success"
|
||||||
type LoginMood = "idle" | "typing" | "peek" | "error" | "success"
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [username, setUsername] = useState("")
|
const [username, setUsername] = useState("")
|
||||||
@@ -39,7 +39,7 @@ export default function LoginPage() {
|
|||||||
return () => window.removeEventListener("pointermove", onPointerMove)
|
return () => window.removeEventListener("pointermove", onPointerMove)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const mood: LoginMood = useMemo(() => {
|
const mood: LoginCharacterMood = useMemo(() => {
|
||||||
if (status === "success") return "success"
|
if (status === "success") return "success"
|
||||||
if (error) return "error"
|
if (error) return "error"
|
||||||
if (showPassword && activeField === "password") return "peek"
|
if (showPassword && activeField === "password") return "peek"
|
||||||
@@ -87,13 +87,6 @@ export default function LoginPage() {
|
|||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className="login-page relative min-h-screen overflow-hidden px-5 py-6 text-white sm:px-8 lg:px-10"
|
className="login-page relative min-h-screen overflow-hidden px-5 py-6 text-white sm:px-8 lg:px-10"
|
||||||
data-mood={mood}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--eye-x": `${eyeOffset.x}px`,
|
|
||||||
"--eye-y": `${eyeOffset.y}px`,
|
|
||||||
} as CSSProperties
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="relative z-10 mx-auto flex min-h-[calc(100vh-3rem)] w-full max-w-7xl items-center">
|
<div className="relative z-10 mx-auto flex min-h-[calc(100vh-3rem)] w-full max-w-7xl items-center">
|
||||||
<div className="grid w-full gap-5 lg:grid-cols-[minmax(0,1.08fr)_minmax(380px,460px)] lg:items-stretch">
|
<div className="grid w-full gap-5 lg:grid-cols-[minmax(0,1.08fr)_minmax(380px,460px)] lg:items-stretch">
|
||||||
@@ -115,42 +108,7 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="login-character-stage" aria-hidden="true">
|
<AnimatedLoginCharacters mood={mood} eyeOffset={eyeOffset} />
|
||||||
<div className="login-stage-grid" />
|
|
||||||
<div className="login-characters-container">
|
|
||||||
<div className="login-figure login-figure--purple">
|
|
||||||
<span className="login-eyes login-eyes--purple">
|
|
||||||
<span className="login-eye" />
|
|
||||||
<span className="login-eye" />
|
|
||||||
</span>
|
|
||||||
<span className="login-mouth login-mouth--purple" />
|
|
||||||
</div>
|
|
||||||
<div className="login-figure login-figure--black">
|
|
||||||
<span className="login-eyes login-eyes--black">
|
|
||||||
<span className="login-eye login-eye--small" />
|
|
||||||
<span className="login-eye login-eye--small" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="login-figure login-figure--orange">
|
|
||||||
<span className="login-eyes login-eyes--orange">
|
|
||||||
<span className="login-pupil" />
|
|
||||||
<span className="login-pupil" />
|
|
||||||
</span>
|
|
||||||
<span className="login-mouth login-mouth--orange" />
|
|
||||||
</div>
|
|
||||||
<div className="login-figure login-figure--yellow">
|
|
||||||
<span className="login-eyes login-eyes--yellow">
|
|
||||||
<span className="login-pupil" />
|
|
||||||
<span className="login-pupil" />
|
|
||||||
</span>
|
|
||||||
<span className="login-yellow-mouth">
|
|
||||||
<svg width="80" height="20" viewBox="0 0 80 20">
|
|
||||||
<path d="M0 10 Q10 10, 20 10 Q30 10, 40 10 Q50 10, 60 10 Q70 10, 80 10" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
{[
|
{[
|
||||||
|
|||||||
81
web/components/login/animated-login-characters.tsx
Normal file
81
web/components/login/animated-login-characters.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import type { CSSProperties } from "react"
|
||||||
|
|
||||||
|
export type LoginCharacterMood = "idle" | "typing" | "peek" | "error" | "success"
|
||||||
|
|
||||||
|
type EyeKind = "eye" | "small-eye" | "pupil"
|
||||||
|
|
||||||
|
type FigureSpec = {
|
||||||
|
id: "purple" | "black" | "orange" | "yellow"
|
||||||
|
eyeKind: EyeKind
|
||||||
|
mouth?: "purple" | "orange" | "yellow"
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIGURES: FigureSpec[] = [
|
||||||
|
{ id: "purple", eyeKind: "eye", mouth: "purple" },
|
||||||
|
{ id: "black", eyeKind: "small-eye" },
|
||||||
|
{ id: "orange", eyeKind: "pupil", mouth: "orange" },
|
||||||
|
{ id: "yellow", eyeKind: "pupil", mouth: "yellow" },
|
||||||
|
]
|
||||||
|
|
||||||
|
function Eyes({ figure, kind }: { figure: FigureSpec["id"]; kind: EyeKind }) {
|
||||||
|
const className = kind === "pupil" ? "login-pupil" : kind === "small-eye" ? "login-eye login-eye--small" : "login-eye"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`login-eyes login-eyes--${figure}`}>
|
||||||
|
<span className={className} />
|
||||||
|
<span className={className} />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Mouth({ type }: { type: NonNullable<FigureSpec["mouth"]> }) {
|
||||||
|
if (type === "yellow") {
|
||||||
|
return (
|
||||||
|
<span className="login-yellow-mouth">
|
||||||
|
<svg width="80" height="20" viewBox="0 0 80 20" aria-hidden="true">
|
||||||
|
<path d="M0 10 Q10 10, 20 10 Q30 10, 40 10 Q50 10, 60 10 Q70 10, 80 10" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span className={`login-mouth login-mouth--${type}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function Figure({ spec }: { spec: FigureSpec }) {
|
||||||
|
return (
|
||||||
|
<div className={`login-figure login-figure--${spec.id}`}>
|
||||||
|
<Eyes figure={spec.id} kind={spec.eyeKind} />
|
||||||
|
{spec.mouth ? <Mouth type={spec.mouth} /> : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnimatedLoginCharacters({
|
||||||
|
mood,
|
||||||
|
eyeOffset,
|
||||||
|
}: {
|
||||||
|
mood: LoginCharacterMood
|
||||||
|
eyeOffset: { x: number; y: number }
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="login-character-stage"
|
||||||
|
data-mood={mood}
|
||||||
|
aria-hidden="true"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--eye-x": `${eyeOffset.x}px`,
|
||||||
|
"--eye-y": `${eyeOffset.y}px`,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="login-stage-grid" />
|
||||||
|
<div className="login-characters-container">
|
||||||
|
{FIGURES.map((spec) => (
|
||||||
|
<Figure key={spec.id} spec={spec} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user