实现游戏共创 M1:作品级共创授权与改编血缘
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- game_distribution_game 表尾追加 fork_authorization(默认 forbidden,只升不降,旧行自动取默认)
- 新增 game_distribution_lineage 表,记录父作品、来源版本、根作品与代际,并按 parent/root/owner 建索引
- migration.rs 登记新表并注明血缘随迁移导出
- 新增 set_game_distribution_fork_authorization_and_return procedure 与幂等收据、期望值 CAS
- 创建作品时校验改编声明:来源存在、已公开、授权开放、来源版本等于当前公开版本;复用既有身份时拒绝携带血缘
- 公开与后台响应增量:forkAuthorization、forkCount、lineage、generation、forkedFromGameId、derivedCount
- 公开快照的作者名与头像改为读时联 user_account,不再落到「创作者」兜底文案
- api-server 新增 PUT /api/game-distribution/games/{game_id}/fork-authorization(Bearer、幂等键、期望值校验)
- 错误映射新增 FORK_ 前缀分支并置于通用子串分支之前,避免被误判成 409 或 404
- module-game-distribution 增加授权阶梯、代际计算与错误码,并补单测覆盖阶梯与错误码前缀
- 网页端详情页新增共创信息卡,我的作品页新增授权提升入口,补服务层调用与定向测试
- 重新生成 SpacetimeDB 绑定,登记 DTO 一致性脚本的共创类型
This commit is contained in:
@@ -7,9 +7,11 @@ import {
|
||||
import { ArrowLeft, Gamepad2, Maximize2 } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type {
|
||||
GameDistributionRecoveryAction,
|
||||
GameDistributionVersionSummary,
|
||||
import {
|
||||
GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS,
|
||||
type GameDistributionRecoveryAction,
|
||||
type GameDistributionVersionSummary,
|
||||
resolveGameForkAuthorization,
|
||||
} from '../../../packages/shared/src/contracts/gameDistribution';
|
||||
import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl';
|
||||
import {
|
||||
@@ -316,6 +318,11 @@ function GameDetailContent({
|
||||
? ''
|
||||
: resolveNonPublicCoverLabel(game.status, latestVersion?.status);
|
||||
const latestReviewReason = latestVersion?.reviewReason?.trim() ?? '';
|
||||
const forkAuthorization = resolveGameForkAuthorization(
|
||||
game.forkAuthorization,
|
||||
);
|
||||
const lineage = game.lineage ?? null;
|
||||
const forkCount = game.forkCount ?? 0;
|
||||
const mobilePlaybackBlocked = isMobileViewport && !game.deviceSupport.mobile;
|
||||
const playbackBlocked = !version || mobilePlaybackBlocked;
|
||||
return (
|
||||
@@ -341,6 +348,46 @@ function GameDetailContent({
|
||||
controlsLabel={resolveControlsLabel(version, game.inputModes)}
|
||||
versionLabel={versionLabel}
|
||||
rating={publiclyVisible ? <GameRatingSummary state={reviews} /> : null}
|
||||
infoCards={[
|
||||
{
|
||||
label: '共创',
|
||||
value: (
|
||||
<span className="game-fork-card">
|
||||
<span
|
||||
className={`game-fork-badge game-fork-badge--${forkAuthorization}`}
|
||||
>
|
||||
{GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[
|
||||
forkAuthorization
|
||||
]}
|
||||
</span>
|
||||
{lineage ? (
|
||||
<>
|
||||
{/*
|
||||
父作品被软删除时服务端不再回传它的标题与作者名(见
|
||||
module-game-distribution 的血缘快照),这里降级成「原作品已不可用」,
|
||||
既不猜测也不回显已下线作品的信息。
|
||||
*/}
|
||||
<span className="game-fork-lineage">
|
||||
{lineage.parentTitle
|
||||
? `第 ${lineage.generation} 代作品 · 改编自《${lineage.parentTitle}》`
|
||||
: `第 ${lineage.generation} 代作品 · 原作品已不可用`}
|
||||
</span>
|
||||
{lineage.parentAuthorName ? (
|
||||
<span className="game-fork-lineage__author">
|
||||
由 {lineage.parentAuthorName} 制作
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
{forkCount > 0 ? (
|
||||
<span className="game-fork-derived">
|
||||
已被改编 {forkCount} 次
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
actions={
|
||||
<>
|
||||
<PlatformActionButton
|
||||
|
||||
@@ -246,6 +246,73 @@ describe('GameDetailPage', () => {
|
||||
|
||||
expect(await screen.findByText('暂未发布可玩版本')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('展示共创授权、代际与溯源信息,且不提供改造入口', async () => {
|
||||
getGameMock.mockResolvedValueOnce(
|
||||
createGame({
|
||||
forkAuthorization: 'nonCommercial',
|
||||
forkCount: 3,
|
||||
lineage: {
|
||||
generation: 2,
|
||||
rootGameId: 'game-root',
|
||||
rootTitle: '根作品',
|
||||
parentGameId: 'game-parent',
|
||||
parentTitle: '原初之光',
|
||||
parentAuthorName: '原作者',
|
||||
},
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<GameDetailPage gameId="game-1" onBack={vi.fn()} onPlay={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('允许非商用共创')).toBeTruthy();
|
||||
expect(screen.getByText('第 2 代作品 · 改编自《原初之光》')).toBeTruthy();
|
||||
expect(screen.getByText('由 原作者 制作')).toBeTruthy();
|
||||
expect(screen.getByText('已被改编 3 次')).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: /改造/u })).toBeNull();
|
||||
});
|
||||
|
||||
it('旧数据缺少授权与血缘字段时只按禁止共创渲染', async () => {
|
||||
getGameMock.mockResolvedValueOnce(createGame());
|
||||
render(
|
||||
<GameDetailPage gameId="game-1" onBack={vi.fn()} onPlay={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('禁止共创')).toBeTruthy();
|
||||
expect(screen.queryByText(/代作品/u)).toBeNull();
|
||||
expect(screen.queryByText(/已被改编/u)).toBeNull();
|
||||
expect(screen.queryByText(/允许/u)).toBeNull();
|
||||
});
|
||||
|
||||
it('父作品已被软删除时不展示被删作品标题与作者名', async () => {
|
||||
getGameMock.mockResolvedValueOnce(
|
||||
createGame({
|
||||
forkAuthorization: 'full',
|
||||
forkCount: 0,
|
||||
lineage: {
|
||||
generation: 2,
|
||||
rootGameId: 'game-root',
|
||||
rootTitle: null,
|
||||
parentGameId: 'game-parent',
|
||||
parentTitle: null,
|
||||
parentAuthorName: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<GameDetailPage gameId="game-1" onBack={vi.fn()} onPlay={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('允许全开放共创')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText('第 2 代作品 · 原作品已不可用'),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText(/改编自/u)).toBeNull();
|
||||
// 血缘作者行(卡内 `.game-fork-lineage__author`)不该出现;hero 里的
|
||||
// 「由 测试作者 制作」是作品自身署名,不在断言范围内。
|
||||
expect(document.querySelector('.game-fork-lineage__author')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GameDetailPage 作者视角', () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
submitGameVersion,
|
||||
unpublishGame,
|
||||
updateMyGame,
|
||||
updateGameForkAuthorization,
|
||||
} from '../../services/gameDistributionClient';
|
||||
import { AuthUiContext } from '../auth/AuthUiContext';
|
||||
import { MyGamesPage } from './MyGamesPage';
|
||||
@@ -28,6 +29,7 @@ vi.mock('../../services/gameDistributionClient', () => ({
|
||||
submitGameVersion: vi.fn(),
|
||||
unpublishGame: vi.fn(),
|
||||
updateMyGame: vi.fn(),
|
||||
updateGameForkAuthorization: vi.fn(),
|
||||
}));
|
||||
|
||||
type AuthValue = NonNullable<ContextType<typeof AuthUiContext>>;
|
||||
@@ -115,6 +117,7 @@ beforeEach(() => {
|
||||
vi.mocked(submitGameVersion).mockReset();
|
||||
vi.mocked(unpublishGame).mockReset();
|
||||
vi.mocked(updateMyGame).mockReset();
|
||||
vi.mocked(updateGameForkAuthorization).mockReset();
|
||||
vi.mocked(listMyGames).mockResolvedValue([]);
|
||||
vi.mocked(loadVersionRecoveryActions).mockResolvedValue({});
|
||||
// 默认没有冻结素材:编辑弹窗打开时按“线上封面不可复用”处理,用例按需覆盖。
|
||||
@@ -590,3 +593,73 @@ test('删除作品需二次确认并带上当前修订号', async () => {
|
||||
expect(String(idempotencyKey)).toContain('game-delete');
|
||||
await waitFor(() => expect(listMyGames).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
test('把共创授权从禁止提升为非商用会携带期望档位并重新加载列表', async () => {
|
||||
vi.mocked(listMyGames).mockResolvedValue([
|
||||
createGame({ forkAuthorization: 'forbidden' }),
|
||||
]);
|
||||
vi.mocked(updateGameForkAuthorization).mockResolvedValue({
|
||||
ok: true,
|
||||
game: { id: 'game-1', forkAuthorization: 'nonCommercial' },
|
||||
});
|
||||
|
||||
renderPage(createAuthValue());
|
||||
|
||||
expect(await screen.findByText(/共创:禁止共创/)).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole('button', { name: '共创授权' }));
|
||||
|
||||
const nonCommercialOption = screen.getByRole('button', { name: '非商用' });
|
||||
expect((nonCommercialOption as HTMLButtonElement).disabled).toBe(false);
|
||||
fireEvent.click(nonCommercialOption);
|
||||
|
||||
// 首次点击只进入确认态,不发送请求。
|
||||
expect(updateGameForkAuthorization).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认提升' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateGameForkAuthorization).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
const [gameId, target, expected, idempotencyKey] =
|
||||
vi.mocked(updateGameForkAuthorization).mock.calls[0] ?? [];
|
||||
expect(gameId).toBe('game-1');
|
||||
expect(target).toBe('nonCommercial');
|
||||
expect(expected).toBe('forbidden');
|
||||
expect(String(idempotencyKey)).toContain('game-1');
|
||||
await waitFor(() => expect(listMyGames).toHaveBeenCalledTimes(2));
|
||||
expect(screen.getByText(/共创授权已更新为「允许非商用共创」/)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('共创授权当前档位与更低档位不可点', async () => {
|
||||
vi.mocked(listMyGames).mockResolvedValue([
|
||||
createGame({ forkAuthorization: 'nonCommercial' }),
|
||||
]);
|
||||
|
||||
renderPage(createAuthValue());
|
||||
|
||||
expect(await screen.findByText(/共创:允许非商用共创/)).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole('button', { name: '共创授权' }));
|
||||
|
||||
expect(
|
||||
(screen.getByRole('button', { name: '禁止' }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(true);
|
||||
expect(
|
||||
(screen.getByRole('button', { name: '非商用' }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(true);
|
||||
expect(
|
||||
(screen.getByRole('button', { name: '全开放' }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('共创授权已达全开放时入口变为只读展示', async () => {
|
||||
vi.mocked(listMyGames).mockResolvedValue([
|
||||
createGame({ forkAuthorization: 'full' }),
|
||||
]);
|
||||
|
||||
renderPage(createAuthValue());
|
||||
|
||||
expect(await screen.findByText('共创授权已达上限')).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: '共创授权' })).toBeNull();
|
||||
});
|
||||
|
||||
@@ -4,6 +4,14 @@ import { ArrowLeft, RefreshCcw } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { GameDistributionRecoveryAction } from '../../../packages/shared/src/contracts/gameDistribution';
|
||||
import {
|
||||
canUpgradeGameForkAuthorization,
|
||||
GAME_DISTRIBUTION_FORK_AUTHORIZATIONS,
|
||||
GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS,
|
||||
GAME_DISTRIBUTION_FORK_AUTHORIZATION_SHORT_LABELS,
|
||||
type GameDistributionForkAuthorization,
|
||||
resolveGameForkAuthorization,
|
||||
} from '../../../packages/shared/src/contracts/gameDistribution';
|
||||
import {
|
||||
cancelGameVersion,
|
||||
deleteMyGame,
|
||||
@@ -14,6 +22,7 @@ import {
|
||||
loadVersionRecoveryActions,
|
||||
submitGameVersion,
|
||||
unpublishGame,
|
||||
updateGameForkAuthorization,
|
||||
} from '../../services/gameDistributionClient';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
@@ -83,6 +92,12 @@ function createDeleteKey(gameId: string) {
|
||||
return `game-delete-${gameId}-${Date.now()}-${deleteKeySeed}`;
|
||||
}
|
||||
|
||||
let forkAuthorizationKeySeed = 0;
|
||||
function createForkAuthorizationKey(gameId: string) {
|
||||
forkAuthorizationKeySeed += 1;
|
||||
return `game-fork-authorization-${gameId}-${Date.now()}-${forkAuthorizationKeySeed}`;
|
||||
}
|
||||
|
||||
export type MyGameStatusFilter =
|
||||
| 'all'
|
||||
| 'published'
|
||||
@@ -126,6 +141,12 @@ const VERSION_FILTER_STATUSES: Record<
|
||||
closed: { cancelled: true, revoked: true },
|
||||
};
|
||||
|
||||
/** 行内授权设置的目标档位;两段式确认后才会发出请求。 */
|
||||
type ForkAuthorizationDraft = {
|
||||
gameId: string;
|
||||
target: GameDistributionForkAuthorization;
|
||||
};
|
||||
|
||||
function resolveVersionStatus(game: GameDistributionMyGame) {
|
||||
const latest = game.latestVersion;
|
||||
if (!latest) return null;
|
||||
@@ -169,6 +190,12 @@ export function MyGamesPage({
|
||||
const [statusFilter, setStatusFilter] = useState<MyGameStatusFilter>('all');
|
||||
const [versionFilter, setVersionFilter] =
|
||||
useState<MyGameVersionFilter>('all');
|
||||
const [forkAuthorizationEditorGameId, setForkAuthorizationEditorGameId] =
|
||||
useState('');
|
||||
const [pendingForkAuthorization, setPendingForkAuthorization] =
|
||||
useState<ForkAuthorizationDraft | null>(null);
|
||||
const [savingForkAuthorizationGameId, setSavingForkAuthorizationGameId] =
|
||||
useState('');
|
||||
const [notice, setNotice] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const canLoad = Boolean(authUi?.user && authUi.canAccessProtectedData);
|
||||
@@ -312,6 +339,38 @@ export function MyGamesPage({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateForkAuthorization(
|
||||
game: GameDistributionMyGame,
|
||||
target: GameDistributionForkAuthorization,
|
||||
expected: GameDistributionForkAuthorization,
|
||||
) {
|
||||
setSavingForkAuthorizationGameId(game.id);
|
||||
setForkAuthorizationEditorGameId('');
|
||||
setPendingForkAuthorization(null);
|
||||
setError('');
|
||||
setNotice('');
|
||||
try {
|
||||
await updateGameForkAuthorization(
|
||||
game.id,
|
||||
target,
|
||||
expected,
|
||||
createForkAuthorizationKey(game.id),
|
||||
);
|
||||
setNotice(
|
||||
`《${game.title}》的共创授权已更新为「${GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[target]}」。`,
|
||||
);
|
||||
await loadGames();
|
||||
} catch (updateError: unknown) {
|
||||
setError(
|
||||
updateError instanceof Error
|
||||
? updateError.message
|
||||
: '设置共创授权失败',
|
||||
);
|
||||
} finally {
|
||||
setSavingForkAuthorizationGameId('');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnpublish(game: GameDistributionMyGame) {
|
||||
setBusyGameId(game.id);
|
||||
setError('');
|
||||
@@ -492,6 +551,36 @@ export function MyGamesPage({
|
||||
const resumeLabel = resolveDraftResumeLabel(game);
|
||||
const versions = game.versions;
|
||||
const isHistoryOpen = expandedGameId === game.id;
|
||||
const forkAuthorization = resolveGameForkAuthorization(
|
||||
game.forkAuthorization,
|
||||
);
|
||||
const forkAuthorizationLabel =
|
||||
GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[
|
||||
forkAuthorization
|
||||
];
|
||||
const isForkAuthorizationEditing =
|
||||
forkAuthorizationEditorGameId === game.id;
|
||||
const forkAuthorizationTarget =
|
||||
pendingForkAuthorization?.gameId === game.id
|
||||
? pendingForkAuthorization.target
|
||||
: null;
|
||||
const isSavingForkAuthorization =
|
||||
savingForkAuthorizationGameId === game.id;
|
||||
const forkAuthorizationTargetLabel = forkAuthorizationTarget
|
||||
? GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[
|
||||
forkAuthorizationTarget
|
||||
]
|
||||
: '';
|
||||
const forkAuthorizationOptions =
|
||||
GAME_DISTRIBUTION_FORK_AUTHORIZATIONS.map((option) => ({
|
||||
option,
|
||||
label: GAME_DISTRIBUTION_FORK_AUTHORIZATION_SHORT_LABELS[option],
|
||||
fullLabel: GAME_DISTRIBUTION_FORK_AUTHORIZATION_LABELS[option],
|
||||
canUpgrade: canUpgradeGameForkAuthorization(
|
||||
forkAuthorization,
|
||||
option,
|
||||
),
|
||||
}));
|
||||
return (
|
||||
<article key={game.id} className="my-game-card">
|
||||
<GameCover game={game} compact />
|
||||
@@ -523,6 +612,8 @@ export function MyGamesPage({
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
<span>·</span>
|
||||
<span>共创:{forkAuthorizationLabel}</span>
|
||||
</div>
|
||||
{version?.reviewReason ? (
|
||||
<p className="my-game-card__reason">
|
||||
@@ -668,6 +759,100 @@ export function MyGamesPage({
|
||||
</PlatformActionButton>
|
||||
)
|
||||
) : null}
|
||||
{forkAuthorization === 'full' ? (
|
||||
<span className="my-game-status my-game-status--fork-full">
|
||||
共创授权已达上限
|
||||
</span>
|
||||
) : isForkAuthorizationEditing ? (
|
||||
forkAuthorizationTarget ? (
|
||||
<>
|
||||
<span className="my-game-card__fork-confirm">
|
||||
确认提升为「{forkAuthorizationTargetLabel}」?
|
||||
提升后不可撤销。
|
||||
</span>
|
||||
<PlatformActionButton
|
||||
tone="danger"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
disabled={isSavingForkAuthorization}
|
||||
onClick={() =>
|
||||
void handleUpdateForkAuthorization(
|
||||
game,
|
||||
forkAuthorizationTarget,
|
||||
forkAuthorization,
|
||||
)
|
||||
}
|
||||
>
|
||||
确认提升
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
disabled={isSavingForkAuthorization}
|
||||
onClick={() => setPendingForkAuthorization(null)}
|
||||
>
|
||||
取消
|
||||
</PlatformActionButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{forkAuthorizationOptions.map(
|
||||
({ option, label, canUpgrade, fullLabel }) => (
|
||||
<PlatformActionButton
|
||||
key={option}
|
||||
tone={
|
||||
option === forkAuthorization
|
||||
? 'primary'
|
||||
: 'secondary'
|
||||
}
|
||||
size="sm"
|
||||
shape="pill"
|
||||
disabled={
|
||||
isSavingForkAuthorization || !canUpgrade
|
||||
}
|
||||
title={
|
||||
canUpgrade
|
||||
? `提升为「${fullLabel}」`
|
||||
: '共创授权只能提升,不能降级'
|
||||
}
|
||||
onClick={() =>
|
||||
setPendingForkAuthorization({
|
||||
gameId: game.id,
|
||||
target: option,
|
||||
})
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</PlatformActionButton>
|
||||
),
|
||||
)}
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
onClick={() =>
|
||||
setForkAuthorizationEditorGameId('')
|
||||
}
|
||||
>
|
||||
取消
|
||||
</PlatformActionButton>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
disabled={isSavingForkAuthorization}
|
||||
onClick={() => {
|
||||
setPendingForkAuthorization(null);
|
||||
setForkAuthorizationEditorGameId(game.id);
|
||||
}}
|
||||
>
|
||||
共创授权
|
||||
</PlatformActionButton>
|
||||
)}
|
||||
<PlatformActionButton
|
||||
tone="danger"
|
||||
size="sm"
|
||||
|
||||
@@ -382,6 +382,43 @@
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
/*
|
||||
* 共创资料卡:详情页通过共享展示组件的 `infoCards` 槽位渲染,卡片外壳
|
||||
* (`.game-detail-info-card`)与内联 flex 的 value 包裹层都由共享组件提供。
|
||||
* 这里把卡内布局改成纵向栅格,免得授权徽章、代际与改编次数挤在一行里被侧栏裁掉;
|
||||
* 选择器前缀沿用共享组件的类名,保证优先级高于共享样式的 `.game-detail-info-card span`。
|
||||
*/
|
||||
.game-detail-info-card .game-fork-card {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
gap: 0.3rem;
|
||||
white-space: normal;
|
||||
}
|
||||
.game-detail-info-card .game-fork-badge {
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 999px;
|
||||
padding: 0.2rem 0.6rem;
|
||||
background: var(--platform-surface-fill);
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.game-detail-info-card .game-fork-badge--nonCommercial {
|
||||
background: var(--platform-success-bg);
|
||||
color: var(--platform-success-text);
|
||||
}
|
||||
.game-detail-info-card .game-fork-badge--full {
|
||||
background: rgba(199, 101, 61, 0.14);
|
||||
color: var(--platform-accent-strong);
|
||||
}
|
||||
.game-detail-info-card .game-fork-lineage {
|
||||
color: var(--platform-text-base);
|
||||
font-weight: 700;
|
||||
}
|
||||
.game-detail-info-card .game-fork-lineage__author,
|
||||
.game-detail-info-card .game-fork-derived {
|
||||
color: var(--platform-text-soft);
|
||||
}
|
||||
.game-loading-panel {
|
||||
display: grid;
|
||||
min-height: 18rem;
|
||||
@@ -727,6 +764,7 @@
|
||||
.my-game-card__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: auto;
|
||||
padding-top: 0.35rem;
|
||||
@@ -748,6 +786,16 @@
|
||||
background: var(--platform-button-danger-fill);
|
||||
color: var(--platform-button-danger-text);
|
||||
}
|
||||
.my-game-status--fork-full {
|
||||
background: rgba(199, 101, 61, 0.14);
|
||||
color: var(--platform-accent-strong);
|
||||
}
|
||||
.my-game-card__fork-confirm {
|
||||
flex: 1 1 100%;
|
||||
color: var(--platform-text-soft);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.my-game-card {
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user