反合编辑器素材库分支到master

This commit is contained in:
2026-06-23 22:47:56 +08:00
70 changed files with 3622 additions and 219 deletions
+1
View File
@@ -5,6 +5,7 @@ name = "Genarrative"
[setup]
script = '''
cp "$CODEX_SOURCE_TREE_PATH/.env.secrets.local" "$CODEX_WORKTREE_PATH/.env.secrets.local"
git reset --hard
npm install
npm run codegraph:init
npm run codegraph:index
+2 -1
View File
@@ -1,6 +1,6 @@
# 图片画布编辑器 Lovart 化执行跟踪
更新时间:`2026-06-17`
更新时间:`2026-06-23`
## 目标
@@ -73,6 +73,7 @@
- `npm run check:encoding`
- `git diff --check`
- Headless Playwright smoke`http://127.0.0.1:10000/editor` 可展示画布、缩放菜单、底部工具栏和图片工具栏;只启动 `dev:web``/api/*` 代理 500 属于未启动后端的预期现象。
- 2026-06-23 规范图选择回归修正:角色规范槽只接受画布中的规范图,图标素材与 UI 设计规范槽只接受图标规范图;选择普通角色图、图标图或其它不合格图片时不写回引用,并在画板顶部显示 warning toast。已验证:`npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
- 2026-06-12 Lovart 布局修正 smoke`http://127.0.0.1:10000/editor` 已移除左侧竖向工具栏和右侧独立图层栏;素材、已生成文件、图层统一收在左侧可折叠面板,中央画布和周边面板保持浅色一体布局;截图留存于 `output/playwright/editor-left-integrated-light.png`
- 2026-06-12 外圈背景修正 smoke`http://127.0.0.1:10000/editor` 的编辑器宿主和画布根容器已铺成同一块白色工作台,不再通过圆角边框露出底部平台背景;截图留存于 `output/playwright/editor-no-outer-background.png`
- 2026-06-12 画布背景与原生菜单修正 smoke:`http://127.0.0.1:10000/editor` 已拦截编辑器区域右键菜单,禁用长按文本选择 / iOS callout,并移除画布网格线与棋盘格底纹;截图留存于 `output/playwright/editor-plain-background.png`
+20
View File
@@ -2,6 +2,8 @@ import type {
AdminUpsertCreationEntryEventBannersRequest,
AdminUpsertCreationEntryTypeConfigRequest,
AdminCreationEntryConfigResponse,
AdminDashboardQuery,
AdminDashboardResponse,
AdminDebugHttpRequest,
AdminDebugHttpResponse,
AdminDisableProfileRedeemCodeRequest,
@@ -145,6 +147,16 @@ export function getAdminOverview(token: string) {
return request<AdminOverviewResponse>('/admin/api/overview', { token });
}
export function getAdminDashboard(
token: string,
query: AdminDashboardQuery = {},
) {
return request<AdminDashboardResponse>(
`/admin/api/dashboard${buildDashboardQuery(query)}`,
{ token },
);
}
export function getAdminDatabaseTables(token: string) {
return request<AdminDatabaseTableListResponse>('/admin/api/database/tables', {
token,
@@ -425,6 +437,14 @@ function buildQueryString(query: AdminTrackingEventListQuery) {
return queryString ? `?${queryString}` : '';
}
function buildDashboardQuery(query: AdminDashboardQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'granularity', query.granularity);
appendQueryParam(params, 'anchor', query.anchor);
const queryString = params.toString();
return queryString ? `?${queryString}` : '';
}
function buildDatabaseTableRowsQuery(query: AdminDatabaseTableRowsQuery) {
const params = new URLSearchParams();
appendQueryParam(params, 'search', query.search);
+68
View File
@@ -54,6 +54,74 @@ export interface AdminOverviewResponse {
database: AdminDatabaseOverviewPayload;
}
export type AdminDashboardGranularity = 'day' | 'week' | 'month';
export interface AdminDashboardQuery {
granularity?: AdminDashboardGranularity;
anchor?: string;
}
export interface AdminDashboardResponse {
range: AdminDashboardRangePayload;
metrics: AdminDashboardMetricsPayload;
charts: AdminDashboardChartPayload[];
operations: AdminDashboardOperationsPayload;
warnings: string[];
generatedAt: string;
}
export interface AdminDashboardRangePayload {
granularity: AdminDashboardGranularity;
anchorDate: string;
periodStartDate: string;
periodEndDate: string;
periodLabel: string;
}
export interface AdminDashboardMetricsPayload {
generatedAssets: number;
consumedMudPoints: number;
totalRegisteredUsers: number;
visitUsers: number;
totalVisitUsers: number;
visitCount: number;
totalVisitCount: number;
currentUsers: number;
}
export interface AdminDashboardChartPayload {
id: string;
title: string;
unit: string;
total: number;
buckets: AdminDashboardChartBucketPayload[];
}
export interface AdminDashboardChartBucketPayload {
key: string;
label: string;
value: number;
}
export interface AdminDashboardOperationsPayload {
cards: AdminDashboardOperationMetricPayload[];
assetKindBreakdown: AdminDashboardBreakdownRowPayload[];
moduleVisitBreakdown: AdminDashboardBreakdownRowPayload[];
}
export interface AdminDashboardOperationMetricPayload {
id: string;
label: string;
value: number;
unit: string;
}
export interface AdminDashboardBreakdownRowPayload {
key: string;
label: string;
value: number;
}
export interface AdminServiceOverviewPayload {
bindHost: string;
bindPort: number;
+4
View File
@@ -20,6 +20,7 @@ import {
setStoredAdminToken,
} from '../auth/adminAuthStore';
import {AdminCreationEntrySwitchPage} from '../pages/AdminCreationEntrySwitchPage';
import {AdminDashboardPage} from '../pages/AdminDashboardPage';
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
@@ -173,6 +174,9 @@ export function AdminApp() {
onLogout={handleLogout}
onRouteChange={handleRouteChange}
>
{routeId === 'dashboard' ? (
<AdminDashboardPage token={token} onUnauthorized={handleUnauthorized} />
) : null}
{routeId === 'overview' ? (
<AdminOverviewPage token={token} onUnauthorized={handleUnauthorized} />
) : null}
+3 -1
View File
@@ -1,4 +1,5 @@
import {
Activity,
Bug,
BadgeDollarSign,
Coins,
@@ -30,7 +31,8 @@ interface AdminShellProps {
}
const routeIcons = {
overview: LayoutDashboard,
dashboard: LayoutDashboard,
overview: Activity,
tables: Database,
debug: Bug,
tracking: Table2,
@@ -2,6 +2,17 @@ import {expect, test} from 'vitest';
import {adminRoutes, resolveAdminRoute, routeHash} from './adminRoutes';
test('后台默认进入 Dashboard', () => {
expect(adminRoutes[0]).toEqual({
id: 'dashboard',
label: 'Dashboard',
hash: '#dashboard',
});
expect(resolveAdminRoute('')).toBe('dashboard');
expect(resolveAdminRoute('#unknown')).toBe('dashboard');
expect(routeHash('dashboard')).toBe('#dashboard');
});
// 中文注释:后台入口公告必须作为独立导航存在,避免公告表单被误藏在入口开关页。
test('后台入口公告路由可通过导航和 hash 访问', () => {
expect(adminRoutes).toContainEqual({
+6 -4
View File
@@ -1,5 +1,6 @@
/** 后台单页应用可导航的路由标识,入口公告独立于入口开关维护。 */
export type AdminRouteId =
| 'dashboard'
| 'overview'
| 'tables'
| 'debug'
@@ -22,7 +23,8 @@ export interface AdminRouteDefinition {
}
export const adminRoutes: AdminRouteDefinition[] = [
{id: 'overview', label: '总览', hash: '#overview'},
{id: 'dashboard', label: 'Dashboard', hash: '#dashboard'},
{id: 'overview', label: '服务总览', hash: '#overview'},
{id: 'tables', label: '表查询', hash: '#tables'},
{id: 'debug', label: 'API 调试', hash: '#debug'},
{id: 'tracking', label: '埋点数据', hash: '#tracking'},
@@ -37,12 +39,12 @@ export const adminRoutes: AdminRouteDefinition[] = [
{id: 'work-visibility', label: '作品可见性', hash: '#work-visibility'},
];
/** 根据地址栏 hash 解析后台路由,未知 hash 回落到总览页。 */
/** 根据地址栏 hash 解析后台路由,未知 hash 回落到 Dashboard。 */
export function resolveAdminRoute(hash: string): AdminRouteId {
const normalizedHash = hash.trim().toLowerCase().split('?')[0] ?? '';
return (
adminRoutes.find((route) => route.hash === normalizedHash)?.id ??
'overview'
'dashboard'
);
}
@@ -51,6 +53,6 @@ export function routeHash(routeId: AdminRouteId) {
return (
adminRoutes.find((route) => route.id === routeId)?.hash ??
adminRoutes[0]?.hash ??
'#overview'
'#dashboard'
);
}
@@ -0,0 +1,119 @@
/* @vitest-environment jsdom */
import {fireEvent, render, screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {beforeEach, expect, test, vi} from 'vitest';
import {getAdminDashboard} from '../api/adminApiClient';
import type {AdminDashboardResponse} from '../api/adminApiTypes';
import {AdminDashboardPage} from './AdminDashboardPage';
vi.mock('../api/adminApiClient', () => ({
formatAdminApiError: vi.fn((error: unknown) =>
error instanceof Error ? error.message : '请求失败',
),
getAdminDashboard: vi.fn(),
isAdminApiError: vi.fn(() => false),
}));
const dashboardResponse: AdminDashboardResponse = {
range: {
granularity: 'day',
anchorDate: '2026-06-23',
periodStartDate: '2026-06-23',
periodEndDate: '2026-06-23',
periodLabel: '2026-06-23',
},
metrics: {
generatedAssets: 12,
consumedMudPoints: 88,
totalRegisteredUsers: 1200,
visitUsers: 34,
totalVisitUsers: 456,
visitCount: 98,
totalVisitCount: 9876,
currentUsers: 7,
},
charts: [
{
id: 'generated-assets',
title: '生产素材',
unit: '个',
total: 12,
buckets: [{key: '2026-06-23', label: '2026-06-23', value: 12}],
},
],
operations: {
cards: [
{
id: 'period-generated-assets',
label: '生产素材',
value: 12,
unit: '个',
},
],
assetKindBreakdown: [
{key: 'editor_generated_image', label: '画板图片', value: 9},
],
moduleVisitBreakdown: [{key: 'auth', label: '认证', value: 30}],
},
warnings: [],
generatedAt: '2026-06-23T12:00:00Z',
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getAdminDashboard).mockResolvedValue(dashboardResponse);
});
test('Dashboard 默认加载今日指标并支持运营汇总页签', async () => {
const user = userEvent.setup();
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
expect(await screen.findByText('今日总生产素材数')).toBeTruthy();
expect(screen.getByText('总注册用户')).toBeTruthy();
expect(screen.getByText('当前使用人数(五分钟统计一次)')).toBeTruthy();
expect(screen.getByText('生产素材')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '运营汇总' }));
expect(screen.getByText('素材类型分布')).toBeTruthy();
expect(screen.getByText('画板图片')).toBeTruthy();
expect(screen.getByText('访问模块分布')).toBeTruthy();
});
test('Dashboard 选择周时带上 week granularity 和周一 anchor', async () => {
const user = userEvent.setup();
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
await screen.findByText('今日总生产素材数');
await user.click(screen.getByRole('button', { name: '周' }));
fireEvent.change(screen.getByLabelText('周'), {
target: {value: '2026-W27'},
});
await waitFor(() => {
expect(getAdminDashboard).toHaveBeenLastCalledWith('admin-token', {
granularity: 'week',
anchor: '2026-06-29',
});
});
});
test('Dashboard 选择月份时带上 month granularity 和月初 anchor', async () => {
const user = userEvent.setup();
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
await screen.findByText('今日总生产素材数');
await user.click(screen.getByRole('button', { name: '月' }));
fireEvent.change(screen.getByLabelText('月份'), {
target: {value: '2026-07'},
});
await waitFor(() => {
expect(getAdminDashboard).toHaveBeenLastCalledWith('admin-token', {
granularity: 'month',
anchor: '2026-07-01',
});
});
});
File diff suppressed because it is too large Load Diff
+248
View File
@@ -271,6 +271,216 @@ button:disabled {
gap: 16px;
}
.admin-dashboard-heading {
align-items: flex-start;
}
.admin-dashboard-actions {
display: flex;
flex-wrap: wrap;
align-items: end;
justify-content: flex-end;
gap: 10px;
}
.admin-dashboard-granularity {
width: 188px;
flex: 0 0 auto;
}
.admin-dashboard-date-field {
width: 170px;
}
.admin-dashboard-tabs {
display: inline-grid;
width: fit-content;
grid-template-columns: repeat(2, minmax(96px, 1fr));
gap: 6px;
border: 1px solid #e1ccbb;
border-radius: 8px;
background: #ffffff;
padding: 4px;
}
.admin-dashboard-tabs button {
min-height: 38px;
border: 0;
border-radius: 6px;
color: #755a49;
background: transparent;
font-weight: 800;
}
.admin-dashboard-tabs button[data-active="true"] {
color: #8f3f27;
background: #f4e5d7;
}
.admin-dashboard-metric-grid,
.admin-dashboard-chart-grid,
.admin-dashboard-operation-grid {
display: grid;
gap: 14px;
}
.admin-dashboard-metric-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.admin-dashboard-operation-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.admin-dashboard-chart-grid,
.admin-dashboard-operations {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.admin-dashboard-operations {
display: grid;
gap: 16px;
align-items: start;
}
.admin-dashboard-operations > .admin-panel:first-child {
grid-column: 1 / -1;
}
.admin-dashboard-metric-card {
gap: 8px;
min-height: 132px;
}
.admin-dashboard-metric-card[data-compact="true"] {
min-height: 112px;
}
.admin-dashboard-metric-card span {
color: #8f7868;
font-size: 13px;
font-weight: 750;
line-height: 1.35;
}
.admin-dashboard-metric-card strong {
color: #3d1f10;
font-size: 30px;
line-height: 1.1;
overflow-wrap: anywhere;
}
.admin-dashboard-metric-card small {
color: #a38f80;
font-size: 12px;
font-weight: 700;
}
.admin-dashboard-chart-card {
min-height: 286px;
}
.admin-dashboard-bars {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(18px, 1fr);
align-items: end;
gap: 8px;
min-height: 196px;
overflow-x: auto;
padding: 4px 2px 0;
}
.admin-dashboard-bar-item {
display: grid;
min-width: 18px;
gap: 7px;
align-items: end;
justify-items: center;
}
.admin-dashboard-bar-track {
position: relative;
display: flex;
width: 100%;
min-width: 18px;
height: 160px;
align-items: flex-end;
overflow: hidden;
border-radius: 7px;
background: #f4e5d7;
}
.admin-dashboard-bar-track span {
display: block;
width: 100%;
border-radius: 7px 7px 0 0;
background: linear-gradient(180deg, #c87955, #8f3f27);
}
.admin-dashboard-bar-item small {
max-width: 48px;
overflow: hidden;
color: #8f7868;
font-size: 11px;
font-weight: 700;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-dashboard-breakdown-panel {
min-height: 300px;
}
.admin-dashboard-breakdown-list {
display: grid;
gap: 12px;
}
.admin-dashboard-breakdown-row {
display: grid;
gap: 7px;
}
.admin-dashboard-breakdown-row > div:first-child {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.admin-dashboard-breakdown-row strong,
.admin-dashboard-breakdown-row span {
min-width: 0;
overflow-wrap: anywhere;
}
.admin-dashboard-breakdown-row strong {
color: #3d1f10;
font-size: 13px;
}
.admin-dashboard-breakdown-row span {
color: #8f7868;
font-size: 12px;
font-weight: 700;
}
.admin-dashboard-breakdown-track {
height: 8px;
overflow: hidden;
border-radius: 999px;
background: #f4e5d7;
}
.admin-dashboard-breakdown-track span {
display: block;
height: 100%;
border-radius: inherit;
background: #8f3f27;
}
.admin-two-column {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(320px, 1.1fr);
@@ -1048,6 +1258,8 @@ button:disabled {
}
.admin-overview-grid,
.admin-dashboard-chart-grid,
.admin-dashboard-operations,
.admin-two-column,
.admin-two-column-wide,
.admin-pricing-grid,
@@ -1058,6 +1270,25 @@ button:disabled {
grid-template-columns: 1fr;
}
.admin-dashboard-heading {
display: grid;
}
.admin-dashboard-actions {
justify-content: stretch;
}
.admin-dashboard-granularity,
.admin-dashboard-date-field,
.admin-dashboard-actions .admin-secondary-button {
width: 100%;
}
.admin-dashboard-metric-grid,
.admin-dashboard-operation-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.admin-field-compact {
max-width: none;
}
@@ -1116,6 +1347,23 @@ button:disabled {
gap: 3px;
}
.admin-dashboard-tabs {
width: 100%;
}
.admin-dashboard-metric-grid,
.admin-dashboard-operation-grid {
grid-template-columns: 1fr;
}
.admin-dashboard-metric-card {
min-height: 112px;
}
.admin-dashboard-chart-card {
min-height: 250px;
}
.admin-header-row {
grid-template-columns: 1fr;
}
+3 -1
View File
@@ -34,11 +34,13 @@ Expo React Native 移动壳和 Tauri 桌面壳的工程结构、同源 WebView
图片画布生成类面板的模型泥点默认 JSON、运行时 override、后台“模型定价”页面和主站动态下发口径见 [【编辑器】模型定价配置管理方案-2026-06-22.md](./%E3%80%90%E7%BC%96%E8%BE%91%E5%99%A8%E3%80%91%E6%A8%A1%E5%9E%8B%E5%AE%9A%E4%BB%B7%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88-2026-06-22.md)。
桌面端全站客服悬浮入口的全局接入点、固定层级、拖拽边界和移动端隐藏口径见 [【前端架构】全站客服悬浮入口接入约定-2026-06-23.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%85%A8%E7%AB%99%E5%AE%A2%E6%9C%8D%E6%82%AC%E6%B5%AE%E5%85%A5%E5%8F%A3%E6%8E%A5%E5%85%A5%E7%BA%A6%E5%AE%9A-2026-06-23.md)。
桌面端 `/creation` 创作工具主页、顶级“草稿”入口替换为“项目”、最近项目、新建项目和陶泥儿精选素材瀑布流的落地计划见 [【玩法创作】创作主页与项目入口改版计划-2026-06-18.md](./%E3%80%90%E7%8E%A9%E6%B3%95%E5%88%9B%E4%BD%9C%E3%80%91%E5%88%9B%E4%BD%9C%E4%B8%BB%E9%A1%B5%E4%B8%8E%E9%A1%B9%E7%9B%AE%E5%85%A5%E5%8F%A3%E6%94%B9%E7%89%88%E8%AE%A1%E5%88%92-2026-06-18.md)。
本地通过 SSH alias 管理多台服务器、查看硬件 / systemd / HTTP 健康状态并执行受控服务启停的 egui 桌面工具见 [【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md](./technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md)。
生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。Pingora 只作为独立二进制影子网关试点时,边界、路由口径与替换前验收见 [【开发运维】Pingora独立网关试点-2026-06-11.md](./technical/【开发运维】Pingora独立网关试点-2026-06-11.md)。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。Pingora 只作为独立二进制影子网关试点时,边界、路由口径与替换前验收见 [【开发运维】Pingora独立网关试点-2026-06-11.md](./technical/【开发运维】Pingora独立网关试点-2026-06-11.md)。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md),Dashboard 默认入口、运营指标和统计口径见 [【后台管理】Dashboard运营看板方案-2026-06-23.md](./technical/【后台管理】Dashboard运营看板方案-2026-06-23.md)
SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。
@@ -3689,3 +3689,11 @@
- 2026-06-22 调整:release 打包资源在 Windows WebView 内可能以 `http://tauri.localhost/index.html` 出现,这仍是 Tauri 内部资源,不允许被导航拦截交给系统浏览器;`shell/navigation.rs` 必须允许 `http` / `https``*.localhost` 留在 WebView。Windows release 二进制必须使用 GUI subsystem,避免正式包启动时额外弹出控制台窗口。
- 影响范围:`apps/desktop-shell/src-tauri/tauri.conf.json``apps/desktop-shell/scripts/check-config.mjs``scripts/dev.test.ts`、Expo / Tauri HostBridge 方案文档。
- 验证方式:`npm run test -- scripts/dev.test.ts -t "Linux 桌面壳显式指定 web-port"``cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml desktop_main_window_config_uses_dev_url_in_dev_builds desktop_webview_navigation_stays_on_packaged_or_same_origin_pages``npm run desktop-shell:typecheck``npm run check:native-shells``npm run check:encoding``git diff --check`
## 2026-06-23 后台默认入口切到 Dashboard 运营看板
- 背景:后台需要默认进入运营数据面板,而不是服务 / 数据库状态页;看板要同时支持日 / 周 / 月筛选,并展示生产素材、泥点消耗、注册、访问和当前使用人数。
- 决策:`apps/admin-web` 默认路由改为 `#dashboard`,原 `#overview` 保留为“服务总览”。Dashboard 统一通过 `GET /admin/api/dashboard` 读取 api-server 后端投影,不让前端绕过 BFF 直接访问 SpacetimeDB。后端不新增 SpacetimeDB schema,聚合现有 `editor_project_resource``profile_wallet_ledger``profile_dashboard_state``tracking_daily_stat``tracking_event`
- 指标口径:生产素材数统计 `editor_project_resource.source_type = generated`;消耗泥点数统计 `profile_wallet_ledger.source_type = asset_operation_consume` 的负向流水绝对值;访问次数只统计 `tracking_daily_stat.scope_kind = site`;访问人数和当前使用人数按登录用户去重,匿名访问人数需要未来补 visitor id 后才能统计。
- 影响范围:`/admin/api/dashboard``shared-contracts` admin DTO、`apps/admin-web` 默认路由和 Dashboard 页面、后台运营文档。
- 验证方式:`cargo test -p api-server --manifest-path server-rs/Cargo.toml admin``npm run admin-web:typecheck``npx vitest run apps/admin-web/src/pages/AdminDashboardPage.test.tsx apps/admin-web/src/app/adminRoutes.test.ts --reporter verbose``npm run check:encoding``git diff --check`
@@ -670,6 +670,14 @@
- 验证:`cargo test -p platform-image --test generated_asset_sheets --manifest-path server-rs/Cargo.toml` 通过,且 `cargo check -p api-server --manifest-path server-rs/Cargo.toml` 保持绿灯。
- 关联:`server-rs/crates/platform-image/src/generated_asset_sheets/``server-rs/crates/api-server/src/generated_asset_sheets.rs``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 图片画布图标素材切片不要把断开的高光阴影当独立图标
- 现象:图片画布生成图标素材或提取 UI 素材后,右侧素材库出现很小的废图;主体图标的阴影、反光、高光或小装饰不完整。
- 原因:图标 spritesheet 切片按 alpha 连通域识别素材,模型常把软阴影、高光、小星星等画成与主体断开的透明块;如果直接逐连通域出图,小碎片会抢占图标顺序,主体也会缺边缘装饰。
- 处理:在 `platform-image``sheet.rs` 里先合并靠近主体的辅助连通域,再过滤孤立小碎片,最后给裁剪框保留安全 padding。不要在前端素材卡或画布层里修已经切坏的 PNG。
- 验证:`cargo test -p platform-image generated_asset_sheets --manifest-path server-rs/Cargo.toml` 覆盖断开的高光合并和孤立小碎片过滤;调用方补跑 `cargo test -p api-server editor_icon --manifest-path server-rs/Cargo.toml`
- 关联:`server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs``server-rs/crates/api-server/src/editor_project.rs`
## UI spritesheet 不要依赖模型直接生成透明背景
- 现象:拼图或抓大鹅运行态解析 UI spritesheet 时,把整张背景图、棋盘格、叶子或装饰图也当作 UI 素材区域,按钮映射错乱;截图里常表现为底部按钮区只剩透明棋盘格或素材碎片。
@@ -0,0 +1,36 @@
# 全站客服悬浮入口接入约定
日期:2026-06-23
## 背景
主站需要在桌面端每个页面提供稳定可见的客服入口,包括 `/editor/canvas` 这类全屏画布页面。入口使用陶泥儿客服形象图片,不进入具体业务页布局,避免被滚动、画布缩放或面板层级影响。
## 接入口
- 全局 React 入口为 `src/components/common/FloatingFeedbackEntry.tsx`
-`src/main.tsx``RouteImageReadyGate` 内、当前路由组件之后渲染,覆盖主站 `resolveAppRoute` 管理的所有页面。
- 图片资产放在 `public/branding/taonier-feedback-entry.png`,并通过 `BRAND_ASSETS.taonierFeedbackEntry` 引用。
- 悬浮层样式使用 `.floating-feedback-entry`,通过 `position: fixed` 和最高层级保持在页面最上方。
## 交互约定
- 仅桌面端展示,媒体查询为 `(min-width: 1024px)`;移动端不渲染入口。
- 初始位置靠近页面左下角,随窗口滚动保持固定。
- 用户可以按住拖动,拖拽过程中实时夹在当前 viewport 内,保证图片完整可见,不被窗口边界遮挡。
- 窗口尺寸变化后重新夹取当前位置,避免缩小窗口后入口跑出视口。
- 点击入口使用新页面打开客服表单:
```text
https://kcnz41bksl1c.feishu.cn/share/base/form/shrcn1p9WpuZMDxrI5ysLLJhw7k
```
- 拖拽释放后短时间内的补发 click 会被吞掉,避免用户拖动入口时误打开表单。
## 验收口径
- 桌面端主站任意路由均能看到客服入口,画布页也可见。
- 滚动页面时入口位置相对视口不变。
- 拖到窗口四边时入口完整留在可见区域内。
- 移动端视口不渲染入口。
- 单击入口新开页面跳转到飞书表单。
@@ -0,0 +1,40 @@
# 后台 Dashboard 运营看板方案
## 范围
- 后台管理默认入口为 `#dashboard`,原服务 / 数据库状态页保留为 `#overview`,导航展示名为“服务总览”。
- Dashboard 由 `GET /admin/api/dashboard` 提供统一 BFF 投影,前端只展示后端返回的 `range``metrics``charts``operations``warnings`
- 本次不修改 SpacetimeDB schema,不新增统计表;读取现有 private 表后在 api-server 聚合。
## 查询参数
`GET /admin/api/dashboard?granularity=day|week|month&anchor=YYYY-MM-DD`
- `granularity` 默认为 `day`
- `anchor` 使用北京时间日历日期。`week``month` 选择包含该日期的自然周 / 自然月。
- 前端在日 / 周 / 月模式分别展示日期、周、月份选择器;周选择器会把选中的周转换为该周周一作为 `anchor`,月份选择器会转换为当月 1 日作为 `anchor`
- 前端每 5 分钟自动刷新一次,同时保留手动刷新按钮。
## 指标口径
- 生产素材数:`editor_project_resource``source_type = 'generated'` 的资源,按 `created_at` 映射到北京时间业务日。
- 消耗泥点数:`profile_wallet_ledger``source_type = asset_operation_consume``amount_delta < 0` 的流水绝对值,按 `created_at` 映射到北京时间业务日。
- 总注册用户:`profile_dashboard_state` 行数。
- 访问次数:`tracking_daily_stat``scope_kind = site` 的日聚合次数。它表示站点级成功路由 / 站点级事件,不把用户级钱包、任务、生成等业务操作混入访问次数。
- 访问人数:当前数据只具备登录用户维度,按 `tracking_daily_stat``scope_kind = user``scope_id` 去重;匿名访问人数需要未来补充稳定 visitor id 后才能统计。
- 当前使用人数:最近 5 分钟内 `tracking_event` 中有 `user_id` 的登录用户去重;不跟随页面选择的历史日 / 周 / 月。
- 运营汇总页签:复用同一时间窗,展示运营指标卡、素材类型分布和访问模块分布。
## 前后端文件
- 后端路由:`server-rs/crates/api-server/src/modules/admin.rs`
- 后端聚合:`server-rs/crates/api-server/src/admin.rs`
- 契约:`server-rs/crates/shared-contracts/src/admin.rs``apps/admin-web/src/api/adminApiTypes.ts`
- 前端页面:`apps/admin-web/src/pages/AdminDashboardPage.tsx`
- 后台路由:`apps/admin-web/src/app/adminRoutes.ts`
## 验证
- `cargo test -p api-server --manifest-path server-rs/Cargo.toml admin`
- `npm run admin-web:typecheck`
- `npx vitest run apps/admin-web/src/pages/AdminDashboardPage.test.tsx apps/admin-web/src/app/adminRoutes.test.ts --reporter verbose`
@@ -54,7 +54,7 @@ npm run check:server-rs-ddd
路由树由 `server-rs/crates/api-server/src/app.rs` 统一构造。当前主要分组:
- 健康检查:`GET /healthz`
- 后台管理:`/admin/api/*`,包括登录、概览、HTTP debug、埋点、表查询、创作入口开关、作品互动配置、作品可见性、兑换码、邀请码、任务配置和充值商品配置。
- 后台管理:`/admin/api/*`,包括登录、Dashboard 运营看板、概览、HTTP debug、埋点、表查询、创作入口开关、作品互动配置、作品可见性、兑换码、邀请码、任务配置和充值商品配置。Dashboard 指标口径见 [`docs/technical/【后台管理】Dashboard运营看板方案-2026-06-23.md`](./technical/【后台管理】Dashboard运营看板方案-2026-06-23.md)。
- 认证与账号:`/api/auth/*``/api/profile/me`,包括短信、密码、微信、refresh session、多端会话和登出。
- 个人中心:`/api/profile/*`,包括钱包流水、任务、领奖、充值、反馈、邀请和兑换等账号侧能力。
- 平台基础能力:`/api/llm/*``/api/speech/volcengine/*`,只保留通用 LLM 和语音代理。
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+22 -22
View File
@@ -1,7 +1,7 @@
{
"generatedBy": "VectorEngine gpt-image-2",
"generatedBy": "VectorEngine gpt-image-2 icon sheet v3 + local transparent slicing",
"generatedFor": "/creation 创作工具主页 image2 风格资源",
"styleReference": "用户提供参考图 codex-clipboard-5ee77a57-5ceb-46c7-a309-f6c8e374d989.png",
"styleReference": "用户提供参考图 codex-clipboard-fe833c5b-42e0-44e6-a0f7-76b9340ed7bf.png",
"constraints": [
"无图内文字",
"不替换现有 UI 文案",
@@ -16,102 +16,102 @@
{
"file": "/creation-home/brand-mascot.png",
"subject": "创作主页旧装饰头像,不作为产品 IP 形象",
"bytes": 265352
"bytes": 113204
},
{
"file": "/creation-home/feature-character.png",
"subject": "游戏角色",
"bytes": 257922
"bytes": 100268
},
{
"file": "/creation-home/feature-coding.png",
"subject": "游戏功能编码",
"bytes": 280325
"bytes": 95259
},
{
"file": "/creation-home/feature-effects.png",
"subject": "游戏特效",
"bytes": 246466
"bytes": 78890
},
{
"file": "/creation-home/feature-export.png",
"subject": "一键导出",
"bytes": 230725
"bytes": 88954
},
{
"file": "/creation-home/feature-marketing.png",
"subject": "游戏美宣",
"bytes": 272968
"bytes": 88415
},
{
"file": "/creation-home/feature-music.png",
"subject": "游戏音乐",
"bytes": 258027
"bytes": 67669
},
{
"file": "/creation-home/feature-scene.png",
"subject": "游戏场景",
"bytes": 284949
"bytes": 114222
},
{
"file": "/creation-home/feature-ui.png",
"subject": "游戏 UI",
"bytes": 280537
"bytes": 65351
},
{
"file": "/creation-home/feature-visual-spec.png",
"subject": "游戏视觉规范",
"bytes": 297305
"bytes": 82856
},
{
"file": "/creation-home/hero-community.png",
"subject": "玩家社区按钮图标",
"bytes": 250131
"bytes": 88168
},
{
"file": "/creation-home/hero-create.png",
"subject": "开始创作按钮图标",
"bytes": 242745
"bytes": 88664
},
{
"file": "/creation-home/nav-create.png",
"subject": "左侧导航创作",
"bytes": 278377
"bytes": 81621
},
{
"file": "/creation-home/nav-discover.png",
"subject": "左侧导航发现",
"bytes": 261177
"bytes": 109929
},
{
"file": "/creation-home/nav-profile.png",
"subject": "左侧导航我的",
"bytes": 288027
"bytes": 83915
},
{
"file": "/creation-home/nav-projects.png",
"subject": "左侧导航项目 / 草稿兜底",
"bytes": 247891
"bytes": 55869
},
{
"file": "/creation-home/nav-recommend.png",
"subject": "左侧导航推荐",
"bytes": 284543
"bytes": 76995
},
{
"file": "/creation-home/project-new.png",
"subject": "新建项目",
"bytes": 260908
"bytes": 43201
},
{
"file": "/creation-home/topbar-search.png",
"subject": "顶部搜索",
"bytes": 241211
"bytes": 83466
},
{
"file": "/creation-home/topbar-wallet.png",
"subject": "顶部泥点余额",
"bytes": 280555
"bytes": 95544
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Some files were not shown because too many files have changed in this diff Show More