feat(motions): 动作预览——骨架缩略图 + 管理端 3D 回放(契约 data-models 0.6.0)
按《DSH 开发文档 v1.6》4.11(T-WEB-014,用户标注「重要」): 服务端: - 抽出公共 PNG 编码器 src/common/png.ts(模型缩略图改为复用,零原生依赖不变) - 新增 src/motions/motion-thumbnail.ts:关键点序列 -> 正视图骨架 PNG (复用 33 点 PARENT_TABLE 连线,包围盒按整段计算,低置信度点不画) - motions 增 thumbnail_asset_id(迁移 AddMotionThumbnail)+ AssetKind.MotionThumbnail - /preprocess 生成缩略图并落库,失败只记 warning;MotionDto 增 thumbnailUrl(契约 0.6.0) 管理端: - 新增 MotionPreview:懒加载 NDJSON -> 骨架线框 + 关节实例球体 支持播放/暂停、上一帧/下一帧、时间轴拖动、0.25x-2x 变速、循环 叠加帧号/当前时间/总时长,按整段包围盒自动取景 three.js 单独分包(lazy + Suspense),主包 241 kB 不涨 - 动作列表新增 40px 骨架缩略图列(点击开预览)+ 预览按钮 + 内联预览面板 - 新增 admin/src/lib/keypoint-sequence.ts 与 lib/skeleton-topology.ts 测试:新增 8 项;规模 18 文件 / 237 项。版本 0.5.0。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { lazy, Suspense, useState, type FormEvent } from 'react';
|
||||
import { apiUrl, errorMessage } from '../api/client';
|
||||
import {
|
||||
deleteMotion,
|
||||
@@ -13,6 +13,10 @@ import { formatNumber } from '../lib/format';
|
||||
import { MOTION_FORMAT_LABELS, PREPROCESS_LABELS, STATUS_OPTIONS } from '../lib/labels';
|
||||
import { hasErrors, parseOptionalNumber, requireText, validateSourceFields, type FieldErrors } from '../lib/validation';
|
||||
import { useAsync } from '../lib/useAsync';
|
||||
const MotionPreview = lazy(async () => {
|
||||
const module = await import('../components/MotionPreview');
|
||||
return { default: module.MotionPreview };
|
||||
});
|
||||
import {
|
||||
ConfirmDialog,
|
||||
Empty,
|
||||
@@ -228,6 +232,7 @@ export function MotionsPage() {
|
||||
const [filters, setFilters] = useState<MotionFilters>({ q: '', format: '', status: '', page: 1 });
|
||||
const [qInput, setQInput] = useState('');
|
||||
const [uploadOpen, setUploadOpen] = useState(false);
|
||||
const [previewId, setPreviewId] = useState<number | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Motion | null>(null);
|
||||
const [busyId, setBusyId] = useState<number | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
@@ -383,6 +388,8 @@ export function MotionsPage() {
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>预览</th>
|
||||
|
||||
<th>名称</th>
|
||||
<th>关联曲目</th>
|
||||
<th>格式</th>
|
||||
@@ -398,6 +405,31 @@ export function MotionsPage() {
|
||||
const keypointsReady = motion.preprocessState === 'done';
|
||||
return (
|
||||
<tr key={motion.id}>
|
||||
<td>
|
||||
|
||||
{motion.thumbnailUrl ? (
|
||||
|
||||
<img
|
||||
|
||||
className="thumb-40 thumb-clickable"
|
||||
|
||||
src={motion.thumbnailUrl}
|
||||
|
||||
alt={`${motion.name} 首帧骨架`}
|
||||
|
||||
title="预处理时生成的骨架缩略图(点击预览)"
|
||||
|
||||
onClick={() => setPreviewId(previewId === motion.id ? null : motion.id)}
|
||||
|
||||
/>
|
||||
|
||||
) : (
|
||||
|
||||
<span className="muted small" title="预处理后生成">—</span>
|
||||
|
||||
)}
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<div className="cell-title">{motion.name}</div>
|
||||
<div className="muted small">#{motion.id}</div>
|
||||
@@ -425,6 +457,23 @@ export function MotionsPage() {
|
||||
<StatusBadge status={motion.status} />
|
||||
</td>
|
||||
<td className="col-actions">
|
||||
<button
|
||||
|
||||
type="button"
|
||||
|
||||
className="link-btn"
|
||||
|
||||
disabled={!keypointsReady}
|
||||
|
||||
onClick={() => setPreviewId(previewId === motion.id ? null : motion.id)}
|
||||
|
||||
title={keypointsReady ? '关键点 3D 回放(播放 / 逐帧 / 时间轴 / 变速 / 循环)' : '预处理完成后才可预览'}
|
||||
|
||||
>
|
||||
|
||||
{previewId === motion.id ? '收起预览' : '预览'}
|
||||
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
@@ -473,6 +522,20 @@ export function MotionsPage() {
|
||||
onChange={(page) => setFilters((previous) => ({ ...previous, page }))}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{previewId !== null ? (
|
||||
<div className="panel-body motion-preview-panel">
|
||||
<header className="panel-head">
|
||||
<h3>动作预览 #{previewId}</h3>
|
||||
<button type="button" className="link-btn" onClick={() => setPreviewId(null)}>
|
||||
关闭
|
||||
</button>
|
||||
</header>
|
||||
<Suspense fallback={<Loading text="正在加载 3D 预览…" />}>
|
||||
<MotionPreview motionId={previewId} />
|
||||
</Suspense>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{uploadOpen ? (
|
||||
|
||||
Reference in New Issue
Block a user