Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/API_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

`ProjectOut` 的 `user_id`、`workflow_id`、`project_name`、`character_perspective`、`directional_movement`、精灵宽高、画风、参考图和时间字段,均在 `entities/project` 内显式映射为 camelCase。PR #75 没有项目更新端点,因此前端不声明 `ProjectApis.update`。

项目归属由后端从 access token 取:`ProjectCreate` 不含 `user_id`,`/projects` 各路由统一读 `request.state.current_user.id`,且 `/projects` 不在鉴权白名单里。因此 `CreateProjectInput` 没有 ownerId,请求体也不带 `user_id`——带了等于宣称调用方可以替别人建项目,那正是后端刚修掉的越权口子。`/projects/new` 的创建入口只看有没有 access token(`getApiAccessToken()`);登录模块尚未接入时保持禁用并写明需要登录。

后端枚举按下表映射:

| 后端值 | `character_perspective` | `directional_movement` |
Expand Down Expand Up @@ -54,7 +56,6 @@ Outfit、Action、Frame 没有独立端点。`outfit.characterId` 与 `action.ou

## 二、本轮明确不实现

- 新建项目流程:只保留禁用入口,后续单独实现。
- Workflow Editor 与生成流程:不在 Projects / 资产库模块内创建弹窗或复制生成逻辑。
- Action Template:后端没有模块、存储或 HTTP 接口,只保留带原因的禁用入口。
- 导出:PR #75 没有导出接口;PR #97 是尚未接入资产页的前端打包实现,只保留带原因的禁用入口。
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { HomePage } from '@/pages/home'
import { NotFoundPage } from '@/pages/not-found'
import { PlaytestPage } from '@/pages/playtest'
import { ProjectDetailPage } from '@/pages/project-detail'
import { ProjectCreatePage } from '@/pages/project-create'
import { ProjectsPage } from '@/pages/projects'
import { QuickStartPage } from '@/pages/quick-start'
import { WorkflowEditorPage } from '@/pages/workflow-editor'
Expand Down Expand Up @@ -34,6 +35,7 @@ export function AppRoutes() {
<Route path="/quick-start" element={<QuickStartPage />} />
<Route path="/quick-start/:runId" element={<QuickStartPage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/projects/new" element={<ProjectCreatePage />} />
<Route path="/workflow-editor/:runId" element={<WorkflowEditorPage />} />
<Route path="/workflow-editor/:runId/:stage" element={<WorkflowEditorPage />} />
<Route path="/playtest/:characterId/:outfitId" element={<PlaytestPage />} />
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/entities/project/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ describe('projectApis', () => {
})

await projectApis.create({
ownerId: '7',
workflowId: '9',
name: '点灯人',
perspective: 'isometric',
Expand All @@ -93,8 +92,8 @@ describe('projectApis', () => {
})

expect(request?.method).toBe('POST')
// 请求体不含 user_id:归属由后端从 access token 取,见 CreateProjectInput 的注释。
await expect(request?.json()).resolves.toEqual({
user_id: 7,
workflow_id: 9,
project_name: '点灯人',
character_perspective: 3,
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/entities/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ export interface Project {
updatedAt: string
}

/** 后端创建 Project 需要的完整字段;创建流程本身不属于资产库页面。 */
/**
* 后端创建 Project 需要的完整字段。
* 这里没有 ownerId:归属由后端从 access token 里取(`ProjectCreate` 不含 user_id),
* 请求体再带一个用户 ID 就等于宣称调用方可以替别人建项目。
*/
export interface CreateProjectInput {
ownerId: string
workflowId?: string | null
name: string
perspective: CharacterPerspective
Expand Down Expand Up @@ -163,7 +166,6 @@ export const projectApis: ProjectApis = {
const dto = await getApiClient().request<ProjectDto>('/projects', {
method: 'POST',
json: {
user_id: toBackendId(input.ownerId, 'ownerId'),
workflow_id:
input.workflowId === undefined
? undefined
Expand Down
146 changes: 145 additions & 1 deletion frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,151 @@ body,
.projects-dialog-backdrop,
.projects-dialog-panel,
.route-transition,
.action-reveal {
.action-reveal,
.project-pixel-mark i {
animation: none;
}
}

@keyframes project-pixel-field {
0%,
58%,
100% {
opacity: 0.5;
transform: scale(0.84);
}

16% {
opacity: 0.96;
transform: scale(1.08);
}
}

@keyframes project-pixel-frame {
0%,
100% {
opacity: 0.72;
transform: scale(0.86);
}

38% {
opacity: 1;
transform: scale(1.08);
}
}

@keyframes project-pixel-handle {
0%,
24%,
100% {
transform: scale(0.82);
}

42%,
58% {
transform: scale(1.18);
}
}

@keyframes project-pixel-cursor {
0%,
14%,
100% {
opacity: 0.28;
transform: translate(-5px, -5px) scale(0.86);
}

38% {
opacity: 1;
transform: translate(0, 0) scale(1);
}
}

/*
创建页左侧的装饰点阵。289 个格子共用四组动画,靠 animation-delay 错峰:
pixel-ring-N 是格子到中心的距离环,管底噪;pixel-phase-N 是 (x+y)%4,管实心笔画。
两组延迟都是负值,页面一进来就处在动画中段,不会先静止再启动。
*/
.project-pixel-mark {
display: grid;
grid-template-columns: repeat(17, 5px);
grid-template-rows: repeat(17, 5px);
gap: 8px;
place-content: center;
width: 248px;
height: 248px;
}

.project-pixel-mark i {
width: 5px;
height: 5px;
border-radius: 1px;
background: #bfc6bf;
opacity: 0.62;
box-shadow: 0 0 0 1px rgb(255 255 255 / 18%);
animation: project-pixel-field 4.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

.project-pixel-mark .pixel-ring-1 {
animation-delay: -0.14s;
}

.project-pixel-mark .pixel-ring-2 {
animation-delay: -0.28s;
}

.project-pixel-mark .pixel-ring-3 {
animation-delay: -0.42s;
}

.project-pixel-mark .pixel-ring-4 {
animation-delay: -0.56s;
}

.project-pixel-mark .pixel-ring-5 {
animation-delay: -0.7s;
}

.project-pixel-mark .pixel-ring-6 {
animation-delay: -0.84s;
}

.project-pixel-mark .pixel-ring-7 {
animation-delay: -0.98s;
}

.project-pixel-mark .pixel-ring-8 {
animation-delay: -1.12s;
}

.project-pixel-mark i.is-active {
background: #202120;
opacity: 0.9;
box-shadow: none;
animation: project-pixel-frame 3.4s ease-in-out infinite;
}

.project-pixel-mark i.is-active.pixel-phase-1 {
animation-delay: -0.18s;
}

.project-pixel-mark i.is-active.pixel-phase-2 {
animation-delay: -0.36s;
}

.project-pixel-mark i.is-active.pixel-phase-3 {
animation-delay: -0.54s;
}

.project-pixel-mark i.is-handle {
background: #050505;
opacity: 1;
box-shadow: 0 0 0 4px rgb(17 18 17 / 9%);
animation: project-pixel-handle 3.8s ease-in-out infinite;
}

.project-pixel-mark i.is-cursor {
background: #090a09;
opacity: 1;
animation: project-pixel-cursor 2.8s cubic-bezier(0.22, 1, 0.36, 1) infinite;
}
4 changes: 3 additions & 1 deletion frontend/src/pages/home/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ describe('HomePage', () => {
expect(screen.getByRole('heading', { name: /真正登场/ })).toBeTruthy()
expect(screen.getByRole('link', { name: /快速开始/ }).getAttribute('href')).toBe('/quick-start')
expect(screen.getByText('工作流画布')).toBeTruthy()
expect(screen.getByRole('link', { name: '创建新项目' }).getAttribute('href')).toBe('/projects')
expect(screen.getByRole('link', { name: '创建新项目' }).getAttribute('href')).toBe(
'/projects/new',
)
expect(screen.getByRole('link', { name: '继续已有项目' }).getAttribute('href')).toBe(
'/projects',
)
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/pages/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ export function HomePage() {
/>
{/*
画布入口必须先确定角色挂在哪个项目下,所以动作条悬停后分成新建与继续两条。
两条现在都落在 /projects 占位页;建项目、建 workflow run 再进
/workflow-editor/:runId 的真实链路等后端接口对齐后接上。
新建直达创建页;继续先落项目中心选项目,等项目级 WorkflowRun 历史页面就位后
再指到那里。两条都还进不了 /workflow-editor/:runId——建 workflow run 需要
WorkflowController,它目前只有接口没有实现。
*/}
<HomeChoiceCard
to="/projects"
Expand All @@ -98,7 +99,7 @@ export function HomePage() {
description="打开工作流画布,逐个节点决定生成什么、留下哪一版。"
actionLabel="打开画布"
actions={[
{ to: '/projects', label: '创建新项目' },
{ to: '/projects/new', label: '创建新项目' },
{ to: '/projects', label: '继续已有项目' },
]}
/>
Expand Down
Loading