实现小游戏与小程序项目创建发布

This commit is contained in:
2026-08-09 13:31:06 +08:00
parent a6fe14a8d6
commit 493b31cff0
26 changed files with 1827 additions and 39 deletions

View File

@@ -39,6 +39,7 @@ import type { OpencodeProject } from '@/types/opencode';
import { buildConfiguredModelOptions } from '@/lib/model-options';
import { useProjectConfigStore } from '@/stores/project-config';
import { EMPTY_AGENT_USER_PROFILE, getProfileAccountKey, isUserProfileComplete, useUserProfileStore, type UserGender } from '@/stores/user-profile';
import type { ProjectType } from '../../../shared/project-config';
import { toast } from 'sonner';
type OpenDialogResult = {
@@ -162,6 +163,7 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
const [newProjectName, setNewProjectName] = useState('');
const [newProjectSelectedPath, setNewProjectSelectedPath] = useState('');
const [newProjectDirectoryMode, setNewProjectDirectoryMode] = useState<ProjectDirectoryMode>('use-selected-directory');
const [newProjectType, setNewProjectType] = useState<ProjectType>('mini_game');
const [newProjectDefaultModel, setNewProjectDefaultModel] = useState('');
const [createProjectError, setCreateProjectError] = useState<string | null>(null);
const [creatingProject, setCreatingProject] = useState(false);
@@ -391,6 +393,7 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
setNewProjectName('');
setNewProjectSelectedPath('');
setNewProjectDirectoryMode('use-selected-directory');
setNewProjectType('mini_game');
setNewProjectDefaultModel('');
setCreateProjectError(null);
setCreateDialogOpen(true);
@@ -399,6 +402,7 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
const closeCreateProject = () => {
if (creatingProject) return;
setCreateDialogOpen(false);
setNewProjectType('mini_game');
setNewProjectDefaultModel('');
setCreateProjectError(null);
};
@@ -460,6 +464,7 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
const project = await createProject({
directoryMode: newProjectDirectoryMode,
selectedPath,
projectType: newProjectType,
...(newProjectDirectoryMode === 'create-child-directory' ? { projectName } : {}),
...(newProjectDefaultModel ? { defaultModel: newProjectDefaultModel } : {}),
});
@@ -666,6 +671,46 @@ export function Sidebar({ sidebarPeekOpen = false, onSidebarPeekChange }: Sideba
void confirmCreateProject();
}}
>
<fieldset className="space-y-2">
<legend className="text-xs font-semibold"></legend>
<div className="grid gap-2 sm:grid-cols-3">
{([
['mini_game', '小游戏', '适合实时互动、Canvas 和关卡玩法;创建后可直接提交审核。'],
['mini_program', '小程序', '适合页面、表单和轻量工具;创建后可直接提交审核。'],
['custom', '自定义项目', '只创建项目空间,不配置默认发布方式。'],
] as const).map(([value, title, description]) => (
<label
key={value}
className={cn(
'min-h-28 cursor-pointer rounded-xl bg-white p-3 shadow-soft transition-[box-shadow,transform] active:scale-[0.96]',
newProjectType === value
? 'ring-2 ring-brand/35 shadow-float'
: 'ring-1 ring-foreground/10 hover:shadow-float',
)}
>
<span className="flex items-center gap-2 text-sm font-semibold">
<input
type="radio"
name="project-type"
value={value}
aria-label={title}
checked={newProjectType === value}
onChange={() => {
setNewProjectType(value);
setCreateProjectError(null);
}}
disabled={creatingProject}
/>
{title}
</span>
<span className="mt-2 block text-[11px] font-medium leading-5 text-muted-foreground [text-wrap:pretty]">
{description}
</span>
</label>
))}
</div>
</fieldset>
<fieldset className="space-y-2">
<legend className="text-xs font-semibold"></legend>
<div className="grid gap-2 sm:grid-cols-2">

View File

@@ -12,6 +12,7 @@ import {
type WorksProjectMetadataInput,
} from '@/lib/works-square';
import type { OpencodeProject } from '@/types/opencode';
import type { ProjectType } from '../../../shared/project-config';
type PublishPhase = 'idle' | 'submitting' | 'polling' | 'succeeded' | 'failed' | 'uncertain';
@@ -23,6 +24,7 @@ type BuildVersion = {
type ProjectPublishActionProps = {
project: OpencodeProject;
projectType: Exclude<ProjectType, 'custom'>;
};
const BUILD_POLL_INTERVAL_MS = 2_000;
@@ -63,7 +65,7 @@ function buttonLabel(phase: PublishPhase): string {
return '一键提交审核';
}
export function ProjectPublishAction({ project }: ProjectPublishActionProps) {
export function ProjectPublishAction({ project, projectType }: ProjectPublishActionProps) {
const [phase, setPhase] = useState<PublishPhase>('idle');
const [failure, setFailure] = useState<WorksPublishFailure | null>(null);
const pollGeneration = useRef(0);
@@ -79,7 +81,7 @@ export function ProjectPublishAction({ project }: ProjectPublishActionProps) {
title,
summary: `${title},由 Makelore 创建的作品。`,
cover_url: null,
category: 'web',
category: projectType,
age_band: null,
difficulty: null,
};

View File

@@ -19,8 +19,13 @@ const PUBLISH_FAILURES: Record<string, WorksPublishFailure> = {
},
PROJECT_FILE_MISSING: {
title: '项目文件不完整',
reason: '发布所需的基础文件缺失。',
nextStep: '请让开发助手补齐 package.json、package-lock.json 和 index.html 后重试。',
reason: '项目模板中的发布文件被删除或损坏。',
nextStep: '请让开发助手修复当前项目模板后重试。',
},
PROJECT_TYPE_UNPUBLISHABLE: {
title: '这个项目没有配置发布方式',
reason: '自定义项目只创建工作空间,不会自动生成平台发布模板。',
nextStep: '如需一键提交,请新建小游戏或小程序项目。',
},
PROJECT_FILE_INVALID: {
title: '项目配置无法读取',

View File

@@ -324,7 +324,17 @@ export function ProjectConfiguration() {
<Button type="button" variant="outline" onClick={() => setDeleteDialogOpen(true)} className="h-12 border border-foreground/15 bg-accent-soft px-5 font-semibold text-foreground shadow-soft"><Trash2 className="mr-2 h-5 w-5" /></Button>
<div className="ml-auto flex flex-wrap items-start justify-end gap-3">
<Button onClick={() => void submit()} disabled={saving} className="project-save-button h-12 border border-brand/20 bg-brand px-6 font-semibold text-primary-foreground"><Check className="mr-2 h-5 w-5" />{saving ? '保存中…' : draft.initialized ? '保存项目配置' : '确认并完成初始化'}</Button>
<ProjectPublishAction key={activeProject.id} project={activeProject} />
{draft.projectType === 'mini_game' || draft.projectType === 'mini_program' ? (
<ProjectPublishAction
key={activeProject.id}
project={activeProject}
projectType={draft.projectType}
/>
) : (
<p className="max-w-sm self-center text-right text-xs font-medium leading-5 text-muted-foreground [text-wrap:pretty]">
</p>
)}
</div>
</div>
</div>

View File

@@ -60,6 +60,7 @@ import {
getOpencodeErrorKind,
type OpencodeErrorKind,
} from '../../shared/opencode-error-kind';
import type { ProjectType } from '../../shared/project-config';
export type { OpencodeErrorKind } from '../../shared/opencode-error-kind';
@@ -80,6 +81,7 @@ export type ProjectDirectoryMode = 'use-selected-directory' | 'create-child-dire
export interface ProjectCreationInput {
directoryMode: ProjectDirectoryMode;
selectedPath: string;
projectType: ProjectType;
projectName?: string;
defaultModel?: string | null;
}
@@ -3956,6 +3958,7 @@ export const useOpencodeStore = create<OpencodeState>((set, get) => ({
...(input.directoryMode === 'use-selected-directory'
? { projectPath: input.selectedPath }
: { parentPath: input.selectedPath, projectName: input.projectName }),
projectType: input.projectType,
...(input.defaultModel !== undefined ? { defaultModel: input.defaultModel } : {}),
}),
});