feat(coding): add project identity UX
This commit is contained in:
@@ -48,9 +48,11 @@ import { useSettingsStore } from '@/stores/settings';
|
||||
import { useAuthStore, type AuthUser } from '@/stores/auth';
|
||||
import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
import { isCanonicalCodingProjectId } from '@/lib/coding-projects';
|
||||
import type { CodingProjectSummary } from '@/types/coding-project';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import type { ProjectType } from '../../../shared/project-config';
|
||||
import type { ProjectIdentityChoice } from '../../../shared/coding-project-contracts';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type OpenDialogResult = {
|
||||
@@ -70,6 +72,7 @@ type ProjectEntryError = {
|
||||
};
|
||||
|
||||
type ProjectDirectoryMode = 'use-selected-directory' | 'create-child-directory';
|
||||
type ProjectIdentityKind = ProjectIdentityChoice['kind'];
|
||||
|
||||
function getFolderName(pathValue: string): string {
|
||||
const trimmed = pathValue.trim();
|
||||
@@ -164,6 +167,8 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
const [newProjectSelectedPath, setNewProjectSelectedPath] = useState('');
|
||||
const [newProjectDirectoryMode, setNewProjectDirectoryMode] = useState<ProjectDirectoryMode>('use-selected-directory');
|
||||
const [newProjectType, setNewProjectType] = useState<ProjectType>('mini_game');
|
||||
const [newProjectIdentityKind, setNewProjectIdentityKind] = useState<ProjectIdentityKind>('create');
|
||||
const [newProjectIdentityProjectId, setNewProjectIdentityProjectId] = useState('');
|
||||
const [createProjectError, setCreateProjectError] = useState<string | null>(null);
|
||||
const [creatingProject, setCreatingProject] = useState(false);
|
||||
const [projectEntryError, setProjectEntryError] = useState<ProjectEntryError | null>(null);
|
||||
@@ -394,6 +399,8 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
setNewProjectSelectedPath('');
|
||||
setNewProjectDirectoryMode('use-selected-directory');
|
||||
setNewProjectType('mini_game');
|
||||
setNewProjectIdentityKind('create');
|
||||
setNewProjectIdentityProjectId('');
|
||||
setCreateProjectError(null);
|
||||
setCreateDialogOpen(true);
|
||||
};
|
||||
@@ -402,6 +409,8 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
if (creatingProject) return;
|
||||
setCreateDialogOpen(false);
|
||||
setNewProjectType('mini_game');
|
||||
setNewProjectIdentityKind('create');
|
||||
setNewProjectIdentityProjectId('');
|
||||
setCreateProjectError(null);
|
||||
};
|
||||
|
||||
@@ -454,12 +463,20 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
setCreateProjectError('请选择项目路径');
|
||||
return;
|
||||
}
|
||||
const boundProjectId = newProjectIdentityProjectId.trim();
|
||||
if (newProjectIdentityKind === 'bind' && !isCanonicalCodingProjectId(boundProjectId)) {
|
||||
setCreateProjectError('请输入有效的项目 ID(小写 UUID)。');
|
||||
return;
|
||||
}
|
||||
|
||||
setCreatingProject(true);
|
||||
setCreateProjectError(null);
|
||||
try {
|
||||
const project = await createProject({
|
||||
projectType: newProjectType,
|
||||
identity: newProjectIdentityKind === 'create'
|
||||
? { kind: 'create' }
|
||||
: { kind: 'bind', projectId: boundProjectId },
|
||||
...(newProjectDirectoryMode === 'create-child-directory'
|
||||
? { parentPath: selectedPath, projectName }
|
||||
: { projectPath: selectedPath }),
|
||||
@@ -744,6 +761,81 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
void confirmCreateProject();
|
||||
}}
|
||||
>
|
||||
<fieldset className="space-y-2">
|
||||
<legend className="text-xs font-semibold">项目身份</legend>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<label
|
||||
className={cn(
|
||||
'cursor-pointer rounded-xl border border-foreground/10 bg-white p-3 text-sm font-semibold',
|
||||
newProjectIdentityKind === 'create' ? 'ring-2 ring-brand/35 shadow-float' : 'hover:shadow-float',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="project-identity"
|
||||
value="create"
|
||||
aria-label="创建新的项目 ID"
|
||||
checked={newProjectIdentityKind === 'create'}
|
||||
onChange={() => {
|
||||
setNewProjectIdentityKind('create');
|
||||
setCreateProjectError(null);
|
||||
}}
|
||||
disabled={creatingProject}
|
||||
/>
|
||||
<span className="ml-2">创建新的项目 ID</span>
|
||||
<span className="mt-2 block text-[11px] font-medium leading-5 text-muted-foreground">
|
||||
由主进程生成一个新的小写 UUID,不会发起云端数据创建。
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
className={cn(
|
||||
'cursor-pointer rounded-xl border border-foreground/10 bg-white p-3 text-sm font-semibold',
|
||||
newProjectIdentityKind === 'bind' ? 'ring-2 ring-brand/35 shadow-float' : 'hover:shadow-float',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="project-identity"
|
||||
value="bind"
|
||||
aria-label="绑定已有项目 ID"
|
||||
checked={newProjectIdentityKind === 'bind'}
|
||||
onChange={() => {
|
||||
setNewProjectIdentityKind('bind');
|
||||
setCreateProjectError(null);
|
||||
}}
|
||||
disabled={creatingProject}
|
||||
/>
|
||||
<span className="ml-2">绑定已有项目 ID</span>
|
||||
<span className="mt-2 block text-[11px] font-medium leading-5 text-muted-foreground">
|
||||
仅接受规范的小写 UUID;绑定不会把项目 ID 当作登录凭据。
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{newProjectIdentityKind === 'bind' ? (
|
||||
<div className="space-y-2 rounded-xl border border-foreground/10 bg-surface-subtle p-3">
|
||||
<label className="block text-xs font-semibold" htmlFor="new-project-identity-id">
|
||||
已有项目 ID
|
||||
</label>
|
||||
<input
|
||||
id="new-project-identity-id"
|
||||
value={newProjectIdentityProjectId}
|
||||
onChange={(event) => {
|
||||
setNewProjectIdentityProjectId(event.target.value);
|
||||
setCreateProjectError(null);
|
||||
}}
|
||||
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
className="w-full rounded-md border border-foreground/15 bg-white px-3 py-2 font-mono text-sm font-medium outline-none focus:ring-2 focus:ring-brand/20"
|
||||
disabled={creatingProject}
|
||||
/>
|
||||
<p className="text-[11px] font-medium leading-5 text-muted-foreground">
|
||||
同一账号绑定相同项目 ID 会共享开发数据;项目 ID 不是凭据。
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="space-y-2">
|
||||
<legend className="text-xs font-semibold">项目类型</legend>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
|
||||
Reference in New Issue
Block a user