Merge branch 'master' into feat/sound_opt
This commit is contained in:
@@ -46,6 +46,7 @@ module.exports = {
|
||||
{
|
||||
files: [
|
||||
'apps/admin-web/src/pages/*.tsx',
|
||||
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog.tsx',
|
||||
'src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx',
|
||||
],
|
||||
rules: {
|
||||
@@ -72,6 +73,7 @@ module.exports = {
|
||||
'src/components/image-editor/**/*.{ts,tsx}',
|
||||
'src/components/project/**/*.{ts,tsx}',
|
||||
'src/components/platform-entry/PlatformActiveProfileView*.tsx',
|
||||
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog*.tsx',
|
||||
'src/components/platform-entry/PlatformEntryActiveFlowShell*.tsx',
|
||||
'src/components/platform-entry/PlatformEntryFlowShell.tsx',
|
||||
'src/components/platform-entry/PlatformProfileApiKeysModal.tsx',
|
||||
|
||||
@@ -2300,6 +2300,14 @@
|
||||
- 验证:deploy 工作区应直接出现 `build/<version>/web.tar.gz` 与 `web.tar.gz.sha256`;后续仍由 `scripts/deploy/production-web-deploy.sh` 执行 checksum 校验和解压 smoke。
|
||||
- 关联:`jenkins/Jenkinsfile.production-web-deploy`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
|
||||
## Copy Artifact Production 模式下来源 Job 必须显式授权
|
||||
|
||||
- 现象:Deploy / Publish / Import 在 `copyArtifacts` 立即报 `Unable to find project for artifact copy: <job>`,但 Jenkins 中的来源 Job、指定构建号和归档产物都存在。
|
||||
- 原因:Copy Artifact 已启用推荐的 `Production` 模式,但产物生产者的 Jenkinsfile 没有 `copyArtifactPermission`;插件会把权限不足伪装成“找不到项目”。
|
||||
- 处理:在产物生产者的 Declarative Pipeline `options` 内精确授权固定消费者:Stdb Build 授权 Stdb Publish,API Build 授权 API Deploy,Web Build 授权 Web Deploy,Database Export 授权 Database Import。不使用 `*`,不通过全局 `Job/Read` 扩权,不把插件退回 Migration 模式规避。
|
||||
- 验证:运行 `npm run check:production-ops`;上线后先运行一次四个产物生产者中本次需要的 Job,确认 live `config.xml` 出现 `CopyArtifactPermissionProperty`,再重跑消费者。
|
||||
- 关联:`jenkins/Jenkinsfile.production-stdb-module-build`、`jenkins/Jenkinsfile.production-api-build`、`jenkins/Jenkinsfile.production-web-build`、`jenkins/Jenkinsfile.production-database-export`、`scripts/check-production-ops-guardrails.mjs`。
|
||||
|
||||
## Jenkins 生产流水线拉 Git 统一走本机 SSH
|
||||
|
||||
- 后续更新:2026-07-14 起所有生产 Job 的 `Pipeline script from SCM` 和 Jenkinsfile 内部 checkout 统一使用本机 SSH 地址 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git` 与凭据 `genarrative-local-gitea-ssh`,不再保留局域网 IP、HTTP 内网地址或公网 fallback。
|
||||
|
||||
@@ -21,7 +21,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台,把 A
|
||||
- 小程序 WebView 外壳:`miniprogram/`。
|
||||
- 法律文本:`media/files/user_agreement.md`、`media/files/privacy_policy.md`、`media/files/disclaimer.md`。
|
||||
|
||||
桌面端侧边栏的一级入口为 `创作 / 项目 / 我的`;移动端底部 dock 只保留 `我的`。`/creation` 是桌面端独立创作工具主页,`/project` 是桌面端画布项目入口,`/profile` 是桌面端和移动端共用的“我的”稳定路由,继续承载账号、钱包、统计和通用设置等平台公共能力。移动端直达 `/creation`、`/project` 或 `/editor/canvas`,以及从首页触发项目 / 画布动作时,只显示桌面端创作提示,不挂载对应工具页面。
|
||||
桌面端侧边栏的一级入口为 `创作 / 项目 / 我的`;移动端底部 dock 只保留 `我的`,根入口默认展示“我的”并保持该 Tab 选中。移动端每次进入站点壳时先显示原有 IP 欢迎遮罩,明确“移动端仅支持作品展示”,遮罩只能通过“好”按钮关闭,关闭后本次页面生命周期内不再重复,刷新后重新显示。`/creation` 是桌面端独立创作工具主页,`/project` 是桌面端画布项目入口,`/profile` 是桌面端和移动端共用的“我的”稳定路由,继续承载账号、钱包、统计和通用设置等平台公共能力。移动端直达 `/creation`、`/project` 或 `/editor/canvas`,以及从首页触发项目 / 画布动作时,只显示与移除前一致的桌面端创作主页提示,不挂载对应工具页面;提示页底部继续保留唯一的“我的”Tab 作为返回入口。
|
||||
|
||||
## 当前后端路线
|
||||
|
||||
|
||||
@@ -583,6 +583,8 @@ cat /var/lib/genarrative/health-patrol/status.json
|
||||
|
||||
如需接外部告警,可在 `/etc/genarrative/health-patrol.env` 配置 `GENARRATIVE_HEALTH_PATROL_WEBHOOK_URL`;脚本只会在 `WARNING` 或 `CRITICAL` 时向该 webhook 发送 JSON。未配置 webhook 时,告警来源是 systemd 失败状态、journal 和状态文件。
|
||||
|
||||
Jenkins Copy Artifact 必须保持 `Production` 权限模式;产物生产者要在 Jenkinsfile 中用 `copyArtifactPermission` 精确授权消费者,不能依赖 Migration 模式或全局 `Job/Read`。固定映射为 `Genarrative-Stdb-Module-Build` → `Genarrative-Stdb-Module-Publish`、`Genarrative-Api-Build` → `Genarrative-Api-Deploy`、`Genarrative-Web-Build` → `Genarrative-Web-Deploy`、`Genarrative-Database-Export` → `Genarrative-Database-Import`。如果 `copyArtifacts` 报 `Unable to find project for artifact copy`,但来源 Job、指定构建号和归档都实际存在,先检查来源 Job 的 `CopyArtifactPermissionProperty`;修复 Jenkinsfile 后必须先运行一次产物生产者,让 Declarative Pipeline 把 Job property 写回 Jenkins,再重跑 Deploy / Publish / Import。`npm run check:production-ops` 会防止四条白名单再次丢失。
|
||||
|
||||
`Genarrative-Web-Build` 的主站构建失败若出现 Rollup 报错 `"xxx" is not exported by "src/services/publicWorkCode.ts"`,优先按前端公开作品号工具缺失处理,而不是排查 Jenkins 节点环境。修复时要让 `publicWorkCode.ts` 的 `build<Play>PublicWorkCode` 与 `isSame<Play>PublicWorkCode` 成对导出,并补 `src/services/publicWorkCode.test.ts` 覆盖对应玩法前缀;随后用 `npm run build:production-release -- --component web --name <临时名>` 复现 Jenkins web 构建路径。
|
||||
|
||||
`Genarrative-Web-Build` 会把 `build/<version>/web.tar.gz`、`web.tar.gz.sha256`、`release-manifest.json` 和 `scripts/deploy/production-web-deploy.sh` 直接归档为 Jenkins 构建产物;`Genarrative-Web-Deploy` 只通过 `copyArtifacts` 从指定上游构建复制这些产物和部署脚本,不再在目标机器 checkout Git,再执行随构建归档的 `scripts/deploy/production-web-deploy.sh`。Web 发布不再读取构建机本地缓存目录,也不再通过 release agent `rsync` 回构建机拉取大包;如果 deploy 找不到 `web.tar.gz`,应先检查上游 Web Build 是否按同一 `BUILD_VERSION` 成功归档产物。
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- `/project` 展示当前账号的图片编辑器项目,项目卡继续进入 `/editor/canvas`。
|
||||
- `/profile` 是“我的”稳定路由,保留头像与昵称编辑、陶泥号复制、泥点余额与账单、累计统计、泥点充值、兑换码、玩家社区、反馈与建议、通用设置、开发者 API Key 和法律信息等平台公共能力。
|
||||
- 桌面顶栏保留现役项目 / 素材搜索、泥点入口和账号胶囊。搜索只筛选当前编辑器项目与已读取的公开编辑器素材,不恢复旧公开作品号搜索、旧广场、旧作品详情或旧运行态。
|
||||
- 桌面端公共侧边栏固定显示“创作 / 项目 / 我的”;移动端底部 dock 只保留“我的”,不暴露“创作 / 项目”。移动端直达 `/creation`、`/project` 或 `/editor/canvas` 时显示桌面端创作提示,不挂载创作主页、项目列表或图片画布;从移动端首页触发项目或画布动作时也只显示同一提示。
|
||||
- 桌面端公共侧边栏固定显示“创作 / 项目 / 我的”;移动端底部 dock 只保留“我的”,不暴露“创作 / 项目”,根入口默认展示“我的”并保持该 Tab 选中。移动端每次进入站点壳时先显示移除前的 IP 欢迎遮罩,明确“移动端仅支持作品展示”;遮罩不允许通过背景或 Escape 关闭,只能点击“好”,关闭后本次页面生命周期内不再重复,刷新后重新显示。移动端直达 `/creation`、`/project` 或 `/editor/canvas` 时显示移除前的桌面端创作主页提示,不挂载创作主页、项目列表或图片画布;提示页底部继续保留唯一的“我的”Tab 作为返回入口。三类工具统一阻断沿用 2026-08-03 的现役产品边界,不回退为旧源码只拦截 `/creation` 的较弱行为。
|
||||
|
||||
现役入口和公共资料能力只能依赖 `creation-home`、`project`、`image-editor`、公共组件及 `services/platform-entry` 等现役模块。Vite 模块门禁会拒绝 `components/rpg-entry`、`services/rpg-entry`、旧玩法目录和旧平台业务模块进入依赖图;Tailwind `@source`、TypeScript `include`、ESLint ignore 或 Vite watch ignore 都不能替代这条运行时依赖门禁。
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ pipeline {
|
||||
disableConcurrentBuilds()
|
||||
skipDefaultCheckout(true)
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
copyArtifactPermission('Genarrative-Api-Deploy')
|
||||
}
|
||||
|
||||
environment {
|
||||
|
||||
@@ -5,6 +5,7 @@ pipeline {
|
||||
disableConcurrentBuilds()
|
||||
skipDefaultCheckout(true)
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
copyArtifactPermission('Genarrative-Database-Import')
|
||||
}
|
||||
|
||||
environment {
|
||||
|
||||
@@ -7,6 +7,7 @@ pipeline {
|
||||
disableConcurrentBuilds()
|
||||
skipDefaultCheckout(true)
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
copyArtifactPermission('Genarrative-Stdb-Module-Publish')
|
||||
}
|
||||
|
||||
environment {
|
||||
|
||||
@@ -7,6 +7,7 @@ pipeline {
|
||||
disableConcurrentBuilds()
|
||||
skipDefaultCheckout(true)
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
copyArtifactPermission('Genarrative-Web-Deploy')
|
||||
}
|
||||
|
||||
environment {
|
||||
|
||||
@@ -23,6 +23,11 @@ const checks = [
|
||||
includes: 'cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml',
|
||||
reason: 'API 生产构建必须执行 api-server 全 target 编译检查。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-api-build',
|
||||
includes: "copyArtifactPermission('Genarrative-Api-Deploy')",
|
||||
reason: 'Copy Artifact Production 模式下,API Build 必须显式授权 API Deploy 读取归档。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-stdb-module-build',
|
||||
includes: 'npm run check:rustfmt',
|
||||
@@ -38,6 +43,12 @@ const checks = [
|
||||
includes: 'npm run check:admin-account-procedures',
|
||||
reason: 'Stdb module 生产构建必须执行管理员 procedure smoke。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-stdb-module-build',
|
||||
includes: "copyArtifactPermission('Genarrative-Stdb-Module-Publish')",
|
||||
reason:
|
||||
'Copy Artifact Production 模式下,Stdb Build 必须显式授权 Stdb Publish 读取归档。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-web-build',
|
||||
includes: 'npm run check:production-ops',
|
||||
@@ -63,6 +74,17 @@ const checks = [
|
||||
includes: 'npm run test',
|
||||
reason: 'Web 生产构建必须执行当前维护范围内的 Vitest。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-web-build',
|
||||
includes: "copyArtifactPermission('Genarrative-Web-Deploy')",
|
||||
reason: 'Copy Artifact Production 模式下,Web Build 必须显式授权 Web Deploy 读取归档。',
|
||||
},
|
||||
{
|
||||
file: 'jenkins/Jenkinsfile.production-database-export',
|
||||
includes: "copyArtifactPermission('Genarrative-Database-Import')",
|
||||
reason:
|
||||
'Copy Artifact Production 模式下,Database Export 必须显式授权 Database Import 读取归档。',
|
||||
},
|
||||
{
|
||||
file: 'server-rs/crates/spacetime-module/src/migration.rs',
|
||||
includes:
|
||||
|
||||
@@ -311,5 +311,10 @@ describe('retired creation template asset boundary', () => {
|
||||
isForbiddenPublicAssetPath('/generated-editor-images/asset.png'),
|
||||
).toBe(true);
|
||||
expect(isForbiddenPublicAssetPath('/creation-home/logo.png')).toBe(false);
|
||||
expect(
|
||||
isForbiddenPublicAssetPath(
|
||||
'/branding/mobile-home-welcome-taonier-ip.png',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
PlatformActiveMobileWelcomeDialog,
|
||||
shouldOpenActiveMobileWelcomeDialog,
|
||||
shouldShowActiveMobileWelcomeDialog,
|
||||
} from './PlatformActiveMobileWelcomeDialog';
|
||||
|
||||
test('shows the active mobile welcome when entering the mobile site shell', () => {
|
||||
expect(
|
||||
shouldShowActiveMobileWelcomeDialog({
|
||||
isDesktopLayout: false,
|
||||
selectionStage: 'platform',
|
||||
platformTab: 'platform',
|
||||
pathname: '/',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldShowActiveMobileWelcomeDialog({
|
||||
isDesktopLayout: true,
|
||||
selectionStage: 'platform',
|
||||
platformTab: 'platform',
|
||||
pathname: '/',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldShowActiveMobileWelcomeDialog({
|
||||
isDesktopLayout: false,
|
||||
selectionStage: 'profile',
|
||||
platformTab: 'profile',
|
||||
pathname: '/profile',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldShowActiveMobileWelcomeDialog({
|
||||
isDesktopLayout: false,
|
||||
selectionStage: 'creation-home',
|
||||
platformTab: 'creation-home',
|
||||
pathname: '/creation',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('keeps the active mobile welcome dismissed across site navigation', () => {
|
||||
expect(
|
||||
shouldOpenActiveMobileWelcomeDialog({
|
||||
isDesktopLayout: false,
|
||||
selectionStage: 'platform',
|
||||
platformTab: 'platform',
|
||||
pathname: '/',
|
||||
isDismissed: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldOpenActiveMobileWelcomeDialog({
|
||||
isDesktopLayout: false,
|
||||
selectionStage: 'profile',
|
||||
platformTab: 'profile',
|
||||
pathname: '/profile',
|
||||
isDismissed: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('renders the restored copy and closes with the acknowledge button', () => {
|
||||
const onClose = vi.fn();
|
||||
|
||||
render(
|
||||
<PlatformActiveMobileWelcomeDialog
|
||||
open
|
||||
platformThemeClass="platform-theme--light"
|
||||
onClose={onClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '欢迎' })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('heading', { level: 2, name: '欢迎' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector('.platform-mobile-home-welcome-dialog__copy')
|
||||
?.textContent,
|
||||
).toBe(
|
||||
'尼嚎,新陶泥er!移动端仅支持作品展示体验创作工具请使用电脑端访问该地址',
|
||||
);
|
||||
expect(
|
||||
document
|
||||
.querySelector('.platform-mobile-home-welcome-dialog__icon')
|
||||
?.getAttribute('src'),
|
||||
).toBe('/branding/mobile-home-welcome-taonier-ip.png');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '好' }));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
|
||||
type MobileHomeWelcomeDecisionInput = {
|
||||
isDesktopLayout: boolean;
|
||||
selectionStage: string;
|
||||
platformTab: string;
|
||||
pathname: string;
|
||||
};
|
||||
|
||||
type MobileHomeWelcomeOpenInput = MobileHomeWelcomeDecisionInput & {
|
||||
isDismissed: boolean;
|
||||
};
|
||||
|
||||
type PlatformActiveMobileWelcomeDialogProps = {
|
||||
open: boolean;
|
||||
platformThemeClass: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function shouldShowActiveMobileWelcomeDialog({
|
||||
isDesktopLayout,
|
||||
}: MobileHomeWelcomeDecisionInput) {
|
||||
return !isDesktopLayout;
|
||||
}
|
||||
|
||||
export function shouldOpenActiveMobileWelcomeDialog({
|
||||
isDismissed,
|
||||
...decisionInput
|
||||
}: MobileHomeWelcomeOpenInput) {
|
||||
return (
|
||||
!isDismissed && shouldShowActiveMobileWelcomeDialog(decisionInput)
|
||||
);
|
||||
}
|
||||
|
||||
export function PlatformActiveMobileWelcomeDialog({
|
||||
open,
|
||||
platformThemeClass,
|
||||
onClose,
|
||||
}: PlatformActiveMobileWelcomeDialogProps) {
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={open}
|
||||
title="欢迎"
|
||||
onClose={onClose}
|
||||
showHeader={false}
|
||||
showCloseButton={false}
|
||||
closeOnBackdrop={false}
|
||||
closeOnEscape={false}
|
||||
size="sm"
|
||||
overlayClassName={`platform-theme ${platformThemeClass} platform-mobile-home-welcome-overlay !items-center !p-4`}
|
||||
panelClassName="platform-remap-surface platform-mobile-home-welcome-dialog"
|
||||
bodyClassName="platform-mobile-home-welcome-dialog__body"
|
||||
footerClassName="platform-mobile-home-welcome-dialog__footer"
|
||||
footer={
|
||||
<PlatformActionButton
|
||||
onClick={onClose}
|
||||
size="md"
|
||||
shape="pill"
|
||||
fullWidth
|
||||
className="min-h-11"
|
||||
>
|
||||
好
|
||||
</PlatformActionButton>
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="/branding/mobile-home-welcome-taonier-ip.png"
|
||||
alt=""
|
||||
className="platform-mobile-home-welcome-dialog__icon"
|
||||
/>
|
||||
<h2 className="platform-mobile-home-welcome-dialog__title">欢迎</h2>
|
||||
<p className="platform-mobile-home-welcome-dialog__copy">
|
||||
尼嚎,新陶泥er!
|
||||
<br />
|
||||
移动端仅支持作品展示
|
||||
<br />
|
||||
体验创作工具请使用电脑端访问该地址
|
||||
</p>
|
||||
</UnifiedModal>
|
||||
);
|
||||
}
|
||||
@@ -483,6 +483,15 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['我的']);
|
||||
expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
|
||||
expect(
|
||||
within(navigation)
|
||||
.getByRole('button', { name: '我的' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
expect(
|
||||
screen.queryByRole('main', { name: '陶泥儿创作主页' }),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.click(within(navigation).getByRole('button', { name: '我的' }));
|
||||
expect(window.location.pathname).toBe('/profile');
|
||||
@@ -491,6 +500,48 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('restores the original blocking welcome on mobile until acknowledged', async () => {
|
||||
responsiveMock.isDesktopLayout = false;
|
||||
const { rerender } = render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="platform"
|
||||
setSelectionStage={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '欢迎' })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'尼嚎,新陶泥er!移动端仅支持作品展示体验创作工具请使用电脑端访问该地址',
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
document
|
||||
.querySelector('.platform-mobile-home-welcome-dialog__icon')
|
||||
?.getAttribute('src'),
|
||||
).toBe('/branding/mobile-home-welcome-taonier-ip.png');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '好' }));
|
||||
expect(screen.queryByRole('dialog', { name: '欢迎' })).toBeNull();
|
||||
expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
|
||||
const navigation = screen.getByRole('navigation', {
|
||||
name: '移动平台导航',
|
||||
});
|
||||
expect(
|
||||
within(navigation)
|
||||
.getByRole('button', { name: '我的' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
|
||||
rerender(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="profile"
|
||||
setSelectionStage={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByRole('dialog', { name: '欢迎' })).toBeNull();
|
||||
});
|
||||
|
||||
it.each(['creation-home', 'project', 'image-editor'] as const)(
|
||||
'shows the desktop guide instead of mounting the %s page on mobile',
|
||||
async (selectionStage) => {
|
||||
@@ -506,25 +557,52 @@ describe('PlatformEntryActiveFlowShell', () => {
|
||||
expect(
|
||||
await screen.findByRole('main', { name: '桌面端创作提示' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByRole('main', { name: '陶泥儿创作主页' })).toBeNull();
|
||||
expect(screen.getByText('请在桌面端打开创作主页')).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByRole('main', { name: '陶泥儿创作主页' }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('main', { name: '项目' })).toBeNull();
|
||||
expect(screen.queryByRole('main', { name: '图片画布编辑器' })).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('main', { name: '图片画布编辑器' }),
|
||||
).toBeNull();
|
||||
const navigation = screen.getByRole('navigation', {
|
||||
name: '移动平台导航',
|
||||
});
|
||||
expect(
|
||||
within(navigation)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['我的']);
|
||||
expect(
|
||||
within(navigation)
|
||||
.getByRole('button', { name: '我的' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it('shows the desktop guide instead of the editor after opening a canvas tool from the mobile root', async () => {
|
||||
it('returns from the mobile tool guide through the only profile tab', async () => {
|
||||
responsiveMock.isDesktopLayout = false;
|
||||
render(<StatefulPlatformEntryFlowShell initialStage="creation-home" />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '好' }));
|
||||
const navigation = screen.getByRole('navigation', {
|
||||
name: '移动平台导航',
|
||||
});
|
||||
fireEvent.click(within(navigation).getByRole('button', { name: '我的' }));
|
||||
|
||||
expect(window.location.pathname).toBe('/profile');
|
||||
expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('does not mount creation actions on the mobile root', async () => {
|
||||
responsiveMock.isDesktopLayout = false;
|
||||
|
||||
render(<StatefulPlatformEntryFlowShell initialStage="platform" />);
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: '打开音乐工具' }),
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('main', { name: '桌面端创作提示' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByRole('main', { name: '图片画布编辑器' })).toBeNull();
|
||||
expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: '打开音乐工具' })).toBeNull();
|
||||
expect(screen.queryByRole('main', { name: '陶泥儿创作主页' })).toBeNull();
|
||||
});
|
||||
|
||||
it('switches between creation and projects and passes search to the active page', async () => {
|
||||
|
||||
@@ -30,6 +30,10 @@ import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { FLOATING_FEEDBACK_FORM_URL } from '../common/floatingFeedbackEntryModel';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import {
|
||||
PlatformActiveMobileWelcomeDialog,
|
||||
shouldOpenActiveMobileWelcomeDialog,
|
||||
} from './PlatformActiveMobileWelcomeDialog';
|
||||
import {
|
||||
resolveActivePublicUserCode,
|
||||
resolveActiveUserAvatarLabel,
|
||||
@@ -137,6 +141,30 @@ function ActiveBottomNavButton({
|
||||
);
|
||||
}
|
||||
|
||||
function MobileProfileDock({
|
||||
active,
|
||||
onOpenProfile,
|
||||
}: {
|
||||
active: boolean;
|
||||
onOpenProfile: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="platform-mobile-bottom-dock min-w-0 shrink-0 lg:hidden">
|
||||
<nav
|
||||
className="platform-bottom-nav grid grid-cols-1"
|
||||
aria-label="移动平台导航"
|
||||
>
|
||||
<ActiveBottomNavButton
|
||||
active={active}
|
||||
icon={UserRound}
|
||||
label="我的"
|
||||
onClick={onOpenProfile}
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActivePlatformBrand() {
|
||||
return (
|
||||
<span
|
||||
@@ -191,7 +219,7 @@ function MobileCreationDesktopGuide({
|
||||
<Monitor aria-hidden="true" className="h-7 w-7" />
|
||||
</span>
|
||||
<h1 className="mt-4 text-xl font-black leading-tight text-[var(--platform-text-strong)]">
|
||||
请在桌面端打开创作工具
|
||||
请在桌面端打开创作主页
|
||||
</h1>
|
||||
<p className="mt-3 text-sm font-semibold leading-6 text-[var(--platform-text-base)]">
|
||||
图片画布、项目管理和素材库需要更大的操作空间。
|
||||
@@ -224,7 +252,21 @@ export function PlatformEntryFlowShellImpl({
|
||||
const [activeSearchKeyword, setActiveSearchKeyword] = useState('');
|
||||
const [isMobileDesktopGuideOpen, setIsMobileDesktopGuideOpen] =
|
||||
useState(false);
|
||||
const [isMobileHomeWelcomeDismissed, setIsMobileHomeWelcomeDismissed] =
|
||||
useState(false);
|
||||
const isDesktopLayout = usePlatformDesktopLayout();
|
||||
const platformThemeClass =
|
||||
authUi?.platformTheme === 'dark'
|
||||
? 'platform-theme--dark'
|
||||
: 'platform-theme--light';
|
||||
const shouldOpenMobileHomeWelcome = shouldOpenActiveMobileWelcomeDialog({
|
||||
isDesktopLayout,
|
||||
selectionStage,
|
||||
platformTab: selectionStage,
|
||||
pathname:
|
||||
typeof window === 'undefined' ? '/' : window.location.pathname,
|
||||
isDismissed: isMobileHomeWelcomeDismissed,
|
||||
});
|
||||
const currentWalletOwnerUserId =
|
||||
authUi?.canAccessProtectedData && authUi.user?.id ? authUi.user.id : null;
|
||||
const walletOwnerUserId = usePlatformWalletStore(
|
||||
@@ -276,7 +318,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
void refreshDashboard();
|
||||
}, [refreshDashboard]);
|
||||
|
||||
const isProfileStage = selectionStage === 'profile';
|
||||
const isProfileStage =
|
||||
selectionStage === 'profile' ||
|
||||
(!isDesktopLayout && selectionStage === 'platform');
|
||||
|
||||
const profileCenter = usePlatformProfileCenterController({
|
||||
activeTab: isProfileStage ? 'profile' : 'project',
|
||||
@@ -348,15 +392,23 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
if ((!isDesktopLayout && isMobileDesktopGuideOpen) || isMobileToolStage) {
|
||||
return (
|
||||
<MobileCreationDesktopGuide
|
||||
onReturnHome={() => {
|
||||
setIsMobileDesktopGuideOpen(false);
|
||||
if (selectionStage === 'platform') {
|
||||
return;
|
||||
}
|
||||
setSelectionStage('platform', { path: '/' });
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<MobileCreationDesktopGuide
|
||||
onReturnHome={() => {
|
||||
setIsMobileDesktopGuideOpen(false);
|
||||
if (selectionStage === 'platform') {
|
||||
return;
|
||||
}
|
||||
setSelectionStage('platform', { path: '/' });
|
||||
}}
|
||||
/>
|
||||
<MobileProfileDock active={false} onOpenProfile={openProfile} />
|
||||
<PlatformActiveMobileWelcomeDialog
|
||||
open={shouldOpenMobileHomeWelcome}
|
||||
platformThemeClass={platformThemeClass}
|
||||
onClose={() => setIsMobileHomeWelcomeDismissed(true)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -580,19 +632,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div className="platform-mobile-bottom-dock min-w-0 shrink-0 lg:hidden">
|
||||
<nav
|
||||
className="platform-bottom-nav grid grid-cols-1"
|
||||
aria-label="移动平台导航"
|
||||
>
|
||||
<ActiveBottomNavButton
|
||||
active={isProfileStage}
|
||||
icon={UserRound}
|
||||
label="我的"
|
||||
onClick={openProfile}
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
<MobileProfileDock
|
||||
active={isProfileStage}
|
||||
onOpenProfile={openProfile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -661,6 +704,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
orderId={profileCenter.wechatRechargeOrderConfirmationState.orderId}
|
||||
/>
|
||||
) : null}
|
||||
<PlatformActiveMobileWelcomeDialog
|
||||
open={shouldOpenMobileHomeWelcome}
|
||||
platformThemeClass={platformThemeClass}
|
||||
onClose={() => setIsMobileHomeWelcomeDismissed(true)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -190,7 +190,6 @@ const ACTIVE_TAILWIND_SOURCES = `@import 'tailwindcss' source(none);
|
||||
const RETIRED_PUBLIC_ASSET_PATHS = [
|
||||
'/audio',
|
||||
'/branding/jump-hop-taonier-character.png',
|
||||
'/branding/mobile-home-welcome-taonier-ip.png',
|
||||
'/anthro-cat-illustrations',
|
||||
'/bark-battle-assets',
|
||||
'/character',
|
||||
|
||||
@@ -66,6 +66,7 @@ export default defineConfig({
|
||||
'src/components/image-editor/**/*.test.tsx',
|
||||
'src/components/project/**/*.test.ts',
|
||||
'src/components/project/**/*.test.tsx',
|
||||
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog.test.tsx',
|
||||
'src/components/platform-entry/PlatformActiveProfileView.test.tsx',
|
||||
'src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx',
|
||||
'src/components/platform-entry/PlatformProfileModalShell.test.tsx',
|
||||
|
||||
Reference in New Issue
Block a user