reorganize prop and state

This commit is contained in:
2026-07-13 20:17:50 +08:00
parent 95c16a4168
commit d6f6bfc8b4
6 changed files with 384 additions and 220 deletions
+2 -1
View File
@@ -18,7 +18,8 @@
"lucide-react": "^0.546.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"vite": "^6.2.0"
"vite": "^6.2.0",
"zustand": "^5.0.14"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.14",
+66 -178
View File
@@ -28,11 +28,16 @@ import {
Zap,
} from 'lucide-react';
import HomeView, {
type HomeDraft,
type HomeAgentMode,
type HomeAgentModeItem,
type HomeAttachmentDraft,
type HomeShowcaseResource,
} from './view/home';
import { useLauncherHomeDraftStore } from './stores/useHomeDraftStore';
import {
getClientProfileDashboard,
getClientProfileWalletLedger,
} from './services/clientApi';
import type {
AuthEntryResponse,
@@ -51,7 +56,6 @@ import {
import type {
ProfileDashboardSummary,
ProfileWalletLedgerEntry,
ProfileWalletLedgerResponse,
} from '../../../packages/shared/src/contracts/runtime';
import {
createGameCreationAppManifest,
@@ -144,26 +148,6 @@ type PendingNonEmptyProject =
projectPath: string;
};
type EditorShowcaseResource = HomeShowcaseResource & {
resourceId: string;
showcaseId?: string | null;
label?: string | null;
imageSrc: string;
width?: number;
height?: number;
prompt?: string | null;
actualPrompt?: string | null;
assetKind?: string | null;
authorDisplayName?: string | null;
authorPublicUserCode?: string | null;
likeCount?: number | null;
};
type EditorShowcaseResourceListResponse = {
resources: EditorShowcaseResource[];
nextCursor?: string | null;
};
interface InitLocalProjectResult {
projectPath: string;
manifestPath: string;
@@ -1237,31 +1221,6 @@ async function logoutClientAuthSession() {
}
}
function getClientProfileDashboard() {
return requestAuthJson<ProfileDashboardSummary>(
'/api/profile/dashboard',
{ method: 'GET' },
'读取泥点余额失败',
);
}
function getClientProfileWalletLedger() {
return requestAuthJson<ProfileWalletLedgerResponse>(
'/api/profile/wallet-ledger',
{ method: 'GET' },
'读取泥点明细失败',
);
}
function listClientShowcaseResources() {
return requestAuthJson<EditorShowcaseResourceListResponse>(
'/api/editor/showcase/resources',
{ method: 'GET' },
'读取灵感推荐失败',
{ skipAuth: true },
);
}
const homeAgentModeItems: HomeAgentModeItem[] = [
{
mode: 'game',
@@ -1320,10 +1279,6 @@ function formatLocalDateTime(value: string | number | null | undefined) {
});
}
function createHomeAttachmentId(file: File, index: number) {
return `${file.name}:${file.size}:${file.lastModified}:${index}:${Date.now()}`;
}
function buildHomeConversationContent(
mode: HomeAgentMode,
prompt: string,
@@ -2153,12 +2108,6 @@ export function WorkspaceLauncher({
const [projectPath, setProjectPath] = useState(defaultProjectPath);
const [status, setStatus] = useState('请选择项目');
const [launcherView, setLauncherView] = useState<LauncherView>(initialView);
const [homeAgentMode, setHomeAgentMode] = useState<HomeAgentMode>('game');
const [homePrompt, setHomePrompt] = useState('');
const [homeAttachments, setHomeAttachments] = useState<HomeAttachmentDraft[]>(
[],
);
const [homeCreationBusy, setHomeCreationBusy] = useState(false);
const [currentProjectContext, setCurrentProjectContext] =
useState<LauncherProjectContext | null>(null);
const [recentWorkspaces, setRecentWorkspaces] =
@@ -2186,13 +2135,11 @@ export function WorkspaceLauncher({
title: string;
message: string;
} | null>(null);
const [showcaseResources, setShowcaseResources] = useState<
EditorShowcaseResource[]
>([]);
const [showcaseStatus, setShowcaseStatus] = useState('正在读取灵感');
const homeAttachmentInputRef = useRef<HTMLInputElement | null>(null);
const accountMenuRef = useRef<HTMLDivElement | null>(null);
const helpMenuRef = useRef<HTMLDivElement | null>(null);
const resetLauncherHomeDraft = useLauncherHomeDraftStore(
(state) => state.reset,
);
const launcherAgentChatAgents = deriveAgentStatusCards(seedManifest, null);
const [agentChatProjectPath, setAgentChatProjectPath] =
useState(defaultProjectPath);
@@ -2258,23 +2205,8 @@ export function WorkspaceLauncher({
return;
}
setProfileDashboard(null);
setWalletLedgerStatus(error instanceof Error ? error.message : '泥点读取失败');
});
void listClientShowcaseResources()
.then((response) => {
if (disposed) {
return;
}
setShowcaseResources(response.resources.slice(0, 6));
setShowcaseStatus(response.resources.length > 0 ? '已读取灵感' : '暂无灵感');
})
.catch((error) => {
if (disposed) {
return;
}
setShowcaseResources([]);
setShowcaseStatus(
error instanceof Error ? error.message : '灵感读取失败',
setWalletLedgerStatus(
error instanceof Error ? error.message : '泥点读取失败',
);
});
return () => {
@@ -2433,14 +2365,13 @@ export function WorkspaceLauncher({
) {
const trimmedProjectPath = validateProjectPath(nextProjectPath);
if (!trimmedProjectPath) {
return;
return '项目目录无效';
}
const invoke = resolveTauriInvoke();
if (!invoke) {
setStatus('需要在 Tauri App 内运行');
return;
return '需要在 Tauri App 内运行';
}
setHomeCreationBusy(true);
setStatus('正在创建项目');
try {
if (!skipNonEmptyCheck) {
@@ -2457,7 +2388,7 @@ export function WorkspaceLauncher({
attachments,
});
setStatus('目标文件夹不是空的');
return;
return '目标文件夹不是空的,请确认是否继续新建';
}
}
const result = await invoke<InitLocalProjectResult>(
@@ -2503,12 +2434,11 @@ export function WorkspaceLauncher({
recentRunStopReason: null,
createdAt: Date.now(),
});
setHomePrompt('');
setHomeAttachments([]);
return '已创建项目并进入项目开发';
} catch (error) {
setStatus(error instanceof Error ? error.message : String(error));
} finally {
setHomeCreationBusy(false);
const message = error instanceof Error ? error.message : String(error);
setStatus(message);
throw error;
}
}
@@ -2649,43 +2579,51 @@ export function WorkspaceLauncher({
void openProject(projectPath, 'open');
}
async function handleHomeSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const activeMode = homeAgentModeItems.find(
(item) => item.mode === homeAgentMode,
);
if (!homePrompt.trim() && homeAttachments.length === 0) {
setStatus(activeMode?.emptyPrompt ?? '请输入需求或上传附件');
return;
}
async function createHomeDraft(draft: HomeDraft) {
const invoke = resolveTauriInvoke();
if (!invoke) {
setStatus('需要在 Tauri App 内运行');
return;
throw new Error('需要在 Tauri App 内运行');
}
setHomeCreationBusy(true);
setStatus('请选择项目目录');
const selectedPath = await invoke<string | null>(
'pick_local_project_directory',
);
if (!selectedPath) {
return '已取消';
}
return createHomeProjectFromDirectory(
selectedPath,
draft.mode,
draft.prompt,
draft.attachments,
);
}
async function loadWalletLedger() {
setWalletLedgerStatus('正在读取明细');
try {
const selectedPath = await invoke<string | null>(
'pick_local_project_directory',
);
if (!selectedPath) {
setStatus('已取消');
return;
}
await createHomeProjectFromDirectory(
selectedPath,
homeAgentMode,
homePrompt,
homeAttachments,
const response = await getClientProfileWalletLedger();
setWalletLedger(response.entries.slice(0, 5));
setWalletLedgerStatus(
response.entries.length > 0 ? '已读取明细' : '暂无泥点明细',
);
} catch (error) {
setStatus(error instanceof Error ? error.message : String(error));
} finally {
setHomeCreationBusy(false);
setWalletLedger([]);
setWalletLedgerStatus(
error instanceof Error ? error.message : '明细读取失败',
);
}
}
function showLauncherNotice(title: string) {
setAccountMenuOpen(false);
setHelpMenuOpen(false);
setWalletPanelOpen(false);
setLauncherNotice({
title,
message: `${title}正在接入中,当前版本会先保留入口。`,
});
}
async function handlePickProjectDirectory() {
const invoke = resolveTauriInvoke();
if (!invoke) {
@@ -2733,53 +2671,6 @@ export function WorkspaceLauncher({
}
}
function handleHomeAttachmentSelection(event: ChangeEvent<HTMLInputElement>) {
const files = Array.from(event.currentTarget.files ?? []);
event.currentTarget.value = '';
if (files.length === 0) {
return;
}
setHomeAttachments((current) => [
...current,
...files.map((file, index) => ({
id: createHomeAttachmentId(file, index),
file,
})),
]);
}
function removeHomeAttachment(attachmentId: string) {
setHomeAttachments((current) =>
current.filter((attachment) => attachment.id !== attachmentId),
);
}
async function loadWalletLedger() {
setWalletLedgerStatus('正在读取明细');
try {
const response = await getClientProfileWalletLedger();
setWalletLedger(response.entries.slice(0, 5));
setWalletLedgerStatus(
response.entries.length > 0 ? '已读取明细' : '暂无泥点明细',
);
} catch (error) {
setWalletLedger([]);
setWalletLedgerStatus(
error instanceof Error ? error.message : '明细读取失败',
);
}
}
function showLauncherNotice(title: string) {
setAccountMenuOpen(false);
setHelpMenuOpen(false);
setWalletPanelOpen(false);
setLauncherNotice({
title,
message: `${title}正在接入中,当前版本会先保留入口。`,
});
}
function handleRecentWorkspaceRefresh() {
if (recentWorkspaces.length === 0 || recentWorkspaceRefreshing) {
return;
@@ -3241,7 +3132,9 @@ export function WorkspaceLauncher({
<div className="launcher-account-popover" role="menu">
<strong>{currentUser.displayName || '陶泥儿用户'}</strong>
<span>
{currentUser.phoneNumberMasked || currentUser.publicUserCode}
{currentUser.phoneNumberMasked ||
currentUser.publicUserCode ||
'账号信息读取中'}
</span>
<button
type="button"
@@ -3264,7 +3157,14 @@ export function WorkspaceLauncher({
>
</button>
<button type="button" role="menuitem" onClick={onLogout}>
<button
type="button"
role="menuitem"
onClick={() => {
resetLauncherHomeDraft();
onLogout();
}}
>
<LogOut size={14} aria-hidden="true" />
退
</button>
@@ -3394,21 +3294,9 @@ export function WorkspaceLauncher({
{launcherView === 'home' ? (
<HomeView
hasPromo={launcherNotifications.length > 0}
homeAgentMode={homeAgentMode}
homeAgentModeItems={homeAgentModeItems}
homePrompt={homePrompt}
homeAttachments={homeAttachments}
homeAttachmentInputRef={homeAttachmentInputRef}
homeCreationBusy={homeCreationBusy}
status={status}
recentProjectRows={recentProjectRows}
showcaseResources={showcaseResources}
showcaseStatus={showcaseStatus}
onHomeAgentModeChange={setHomeAgentMode}
onHomePromptChange={setHomePrompt}
onHomeAttachmentSelection={handleHomeAttachmentSelection}
onHomeAttachmentRemove={removeHomeAttachment}
onHomeSubmit={handleHomeSubmit}
onCreateDraft={createHomeDraft}
onProjectsOpen={() => setLauncherView('projects')}
onProjectOpen={(path) => {
setProjectPath(path);
@@ -0,0 +1,152 @@
import {
API_RESPONSE_ENVELOPE_HEADER,
API_RESPONSE_ENVELOPE_VERSION,
ProfileDashboardSummary,
ProfileWalletLedgerResponse,
unwrapApiResponse,
} from '../../../../packages/shared/src';
const ACCESS_TOKEN_STORAGE_KEY = 'genarrative.auth.access-token.v1';
const DEFAULT_CLIENT_AUTH_API_BASE_URL = 'http://127.0.0.1:8082';
export type EditorShowcaseResource = {
resourceId: string;
showcaseId?: string | null;
label?: string | null;
imageSrc: string;
width?: number;
height?: number;
prompt?: string | null;
actualPrompt?: string | null;
assetKind?: string | null;
authorDisplayName?: string | null;
authorPublicUserCode?: string | null;
likeCount?: number | null;
};
type EditorShowcaseResourceListResponse = {
resources: EditorShowcaseResource[];
nextCursor?: string | null;
};
export class ClientAuthRequestError extends Error {
readonly status: number | null;
readonly networkError: boolean;
constructor(
message: string,
options: { status?: number | null; networkError?: boolean } = {},
) {
super(message);
this.status = options.status ?? null;
this.networkError = options.networkError ?? false;
}
}
export function getStoredAuthAccessToken() {
return window.localStorage.getItem(ACCESS_TOKEN_STORAGE_KEY)?.trim() || '';
}
export function setStoredAuthAccessToken(token: string) {
const nextToken = token.trim();
if (nextToken) {
window.localStorage.setItem(ACCESS_TOKEN_STORAGE_KEY, nextToken);
return;
}
window.localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY);
}
export function clearStoredAuthAccessToken() {
window.localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY);
}
function resolveClientApiUrl(url: string) {
if (/^https?:\/\//iu.test(url)) {
return url;
}
if (import.meta.env.DEV) {
return url;
}
const isHttpPage =
window.location.protocol === 'http:' ||
window.location.protocol === 'https:';
if (!window.__TAURI__ && isHttpPage) {
return url;
}
return `${DEFAULT_CLIENT_AUTH_API_BASE_URL}${url}`;
}
async function readApiErrorMessage(response: Response, fallback: string) {
const text = await response.text();
if (!text.trim()) {
return fallback;
}
try {
unwrapApiResponse(JSON.parse(text) as unknown);
} catch (error) {
return error instanceof Error ? error.message : fallback;
}
return fallback;
}
export async function requestClientApi<T>(
url: string,
init: RequestInit,
fallbackMessage: string,
options: { skipAuth?: boolean } = {},
) {
const headers = new Headers(init.headers);
headers.set(API_RESPONSE_ENVELOPE_HEADER, API_RESPONSE_ENVELOPE_VERSION);
if (!options.skipAuth) {
const token = getStoredAuthAccessToken();
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
}
let response: Response;
try {
response = await fetch(resolveClientApiUrl(url), {
...init,
credentials: 'same-origin',
headers,
});
} catch {
throw new ClientAuthRequestError(
'无法连接登录服务,请确认配套后端或 API 代理已启动后重试',
{ networkError: true },
);
}
if (!response.ok) {
throw new ClientAuthRequestError(
await readApiErrorMessage(response, fallbackMessage),
{ status: response.status },
);
}
const text = await response.text();
return text ? unwrapApiResponse<T>(JSON.parse(text) as T) : (null as T);
}
export function getClientProfileDashboard() {
return requestClientApi<ProfileDashboardSummary>(
'/api/profile/dashboard',
{ method: 'GET' },
'读取泥点余额失败',
);
}
export function getClientProfileWalletLedger() {
return requestClientApi<ProfileWalletLedgerResponse>(
'/api/profile/wallet-ledger',
{ method: 'GET' },
'读取泥点明细失败',
);
}
export function listClientShowcaseResources() {
return requestClientApi<EditorShowcaseResourceListResponse>(
'/api/editor/showcase/resources',
{ method: 'GET' },
'读取灵感推荐失败',
{ skipAuth: true },
);
}
@@ -0,0 +1,44 @@
import { create } from 'zustand';
export type HomeAgentMode = 'game' | 'art' | 'doc';
export type HomeAttachmentDraft = {
id: string;
file: File;
};
export type HomeDraft = {
mode: HomeAgentMode;
prompt: string;
attachments: HomeAttachmentDraft[];
};
type UseHomeDraftStore = HomeDraft & {
setMode: (mode: HomeAgentMode) => void;
setPrompt: (prompt: string) => void;
addAttachments: (attachments: HomeAttachmentDraft[]) => void;
removeAttachment: (attachmentId: string) => void;
reset: () => void;
};
const initialHomeDraft: HomeDraft = {
mode: 'game',
prompt: '',
attachments: [],
};
// Keep non-serializable File objects available while the Home view is unmounted.
export const useLauncherHomeDraftStore = create<UseHomeDraftStore>((set) => ({
...initialHomeDraft,
setMode: (mode) => set({ mode }),
setPrompt: (prompt) => set({ prompt }),
addAttachments: (attachments) =>
set((state) => ({ attachments: [...state.attachments, ...attachments] })),
removeAttachment: (attachmentId) =>
set((state) => ({
attachments: state.attachments.filter(
(attachment) => attachment.id !== attachmentId,
),
})),
reset: () => set(initialHomeDraft),
}));
@@ -7,17 +7,23 @@ import {
X,
} from 'lucide-react';
import type {
ChangeEventHandler,
FormEventHandler,
RefObject,
FormEvent,
} from 'react';
import { useRef, useState } from 'react';
export type HomeAgentMode = 'game' | 'art' | 'doc';
import {
useLauncherHomeDraftStore,
type HomeAgentMode,
type HomeAttachmentDraft,
type HomeDraft,
} from '../../stores/useHomeDraftStore';
import { useHomeShowcase } from './useHomeShowcase';
export type HomeAttachmentDraft = {
id: string;
file: File;
};
export type {
HomeAgentMode,
HomeAttachmentDraft,
HomeDraft,
} from '../../stores/useHomeDraftStore';
export type HomeAgentModeItem = {
mode: HomeAgentMode;
@@ -46,45 +52,39 @@ export type HomeShowcaseResource = {
type HomeViewProps = {
hasPromo: boolean;
homeAgentMode: HomeAgentMode;
homeAgentModeItems: readonly HomeAgentModeItem[];
homePrompt: string;
homeAttachments: readonly HomeAttachmentDraft[];
homeAttachmentInputRef: RefObject<HTMLInputElement | null>;
homeCreationBusy: boolean;
status: string;
recentProjectRows: readonly HomeProjectRow[];
showcaseResources: readonly HomeShowcaseResource[];
showcaseStatus: string;
onHomeAgentModeChange: (mode: HomeAgentMode) => void;
onHomePromptChange: (value: string) => void;
onHomeAttachmentSelection: ChangeEventHandler<HTMLInputElement>;
onHomeAttachmentRemove: (attachmentId: string) => void;
onHomeSubmit: FormEventHandler<HTMLFormElement>;
onCreateDraft: (draft: HomeDraft) => Promise<string>;
onProjectsOpen: () => void;
onProjectOpen: (path: string) => void;
};
export default function HomeView({
hasPromo,
homeAgentMode,
homeAgentModeItems,
homePrompt,
homeAttachments,
homeAttachmentInputRef,
homeCreationBusy,
status,
recentProjectRows,
showcaseResources,
showcaseStatus,
onHomeAgentModeChange,
onHomePromptChange,
onHomeAttachmentSelection,
onHomeAttachmentRemove,
onHomeSubmit,
onCreateDraft,
onProjectsOpen,
onProjectOpen,
}: HomeViewProps) {
const homeAgentMode = useLauncherHomeDraftStore((state) => state.mode);
const homePrompt = useLauncherHomeDraftStore((state) => state.prompt);
const homeAttachments = useLauncherHomeDraftStore(
(state) => state.attachments,
);
const setHomeAgentMode = useLauncherHomeDraftStore((state) => state.setMode);
const setHomePrompt = useLauncherHomeDraftStore((state) => state.setPrompt);
const addHomeAttachments = useLauncherHomeDraftStore(
(state) => state.addAttachments,
);
const removeHomeAttachment = useLauncherHomeDraftStore(
(state) => state.removeAttachment,
);
const [homeCreationBusy, setHomeCreationBusy] = useState(false);
const [homeStatus, setHomeStatus] = useState('请选择创作模式');
const { resources: showcaseResources, status: showcaseStatus } =
useHomeShowcase();
const homeAttachmentInputRef = useRef<HTMLInputElement | null>(null);
const activeHomeMode =
homeAgentModeItems.find((item) => item.mode === homeAgentMode) ??
homeAgentModeItems[0];
@@ -95,6 +95,48 @@ export default function HomeView({
const ActiveHomeModeIcon = activeHomeMode.icon;
function handleHomeAttachmentSelection(
event: React.ChangeEvent<HTMLInputElement>,
) {
const files = Array.from(event.currentTarget.files ?? []);
event.currentTarget.value = '';
if (files.length === 0) {
return;
}
addHomeAttachments(
files.map((file, index) => ({
id: `${file.name}:${file.size}:${file.lastModified}:${index}:${Date.now()}`,
file,
})),
);
}
async function handleHomeSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!homePrompt.trim() && homeAttachments.length === 0) {
setHomeStatus(
homeAgentModeItems.find((item) => item.mode === homeAgentMode)
?.emptyPrompt ?? '请输入需求或上传附件',
);
return;
}
setHomeCreationBusy(true);
setHomeStatus('请选择项目目录');
try {
setHomeStatus(
await onCreateDraft({
mode: homeAgentMode,
prompt: homePrompt,
attachments: homeAttachments,
}),
);
} catch (error) {
setHomeStatus(error instanceof Error ? error.message : String(error));
} finally {
setHomeCreationBusy(false);
}
}
return (
<>
<section
@@ -132,7 +174,7 @@ export default function HomeView({
? 'border-violet-500 bg-[#f8f2ff] text-violet-600'
: 'border-[#d6d6d6] bg-white text-[#333]'
}`}
onClick={() => onHomeAgentModeChange(item.mode)}
onClick={() => setHomeAgentMode(item.mode)}
>
<ModeIcon size={14} aria-hidden="true" />
{item.label}
@@ -142,14 +184,14 @@ export default function HomeView({
</div>
<form
className="grid min-h-20 w-[min(488px,calc(100vw-110px))] grid-rows-[minmax(42px,auto)_auto_auto] rounded-[18px] border border-[#e1e4e8] bg-white px-3 pb-2 pt-[13px] shadow-[0_8px_24px_rgb(15_23_42_/_8%)] max-[720px]:w-[min(100%,calc(100vw-76px))]"
onSubmit={onHomeSubmit}
onSubmit={handleHomeSubmit}
>
<textarea
className="min-h-9 w-full resize-none border-0 bg-transparent p-0 text-[13px] text-[#171717] outline-0 placeholder:text-[#bababa]"
aria-label="创作想法"
placeholder={activeHomeMode.placeholder}
value={homePrompt}
onChange={(event) => onHomePromptChange(event.currentTarget.value)}
onChange={(event) => setHomePrompt(event.currentTarget.value)}
/>
{homeAttachments.length > 0 ? (
<div className="flex flex-wrap gap-1.5 py-2 pb-1" aria-label="附件队列">
@@ -163,7 +205,7 @@ export default function HomeView({
className="size-[18px] cursor-pointer border-0 bg-transparent p-0 text-gray-500"
type="button"
aria-label={`移除附件 ${attachment.file.name}`}
onClick={() => onHomeAttachmentRemove(attachment.id)}
onClick={() => removeHomeAttachment(attachment.id)}
>
<X size={12} aria-hidden="true" />
</button>
@@ -177,7 +219,7 @@ export default function HomeView({
className="pointer-events-none absolute size-px opacity-0"
type="file"
multiple
onChange={onHomeAttachmentSelection}
onChange={handleHomeAttachmentSelection}
/>
<button
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[#666]"
@@ -187,7 +229,7 @@ export default function HomeView({
>
<Upload size={15} aria-hidden="true" />
</button>
<span>{status}</span>
<span>{homeStatus}</span>
<button
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-[#f1f1f1] p-0 text-[#9a9a9a] disabled:cursor-not-allowed disabled:opacity-55"
type="submit"
@@ -207,7 +249,9 @@ export default function HomeView({
<header className="flex items-center justify-between gap-3">
<div>
<h2 className="m-0 text-[15px] text-gray-900"></h2>
<p className="mb-0 mt-1 text-[12px] text-[#8b8b8b]">{status}</p>
<p className="mb-0 mt-1 text-[12px] text-[#8b8b8b]">
{homeStatus}
</p>
</div>
<div className="launcher-project-list-actions">
<button
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
import {
listClientShowcaseResources,
type EditorShowcaseResource,
} from '../../services/clientApi';
export function useHomeShowcase() {
const [resources, setResources] = useState<EditorShowcaseResource[]>([]);
const [status, setStatus] = useState('正在读取灵感');
useEffect(() => {
let disposed = false;
void listClientShowcaseResources()
.then((response) => {
if (disposed) {
return;
}
setResources(response.resources.slice(0, 6));
setStatus(response.resources.length > 0 ? '已读取灵感' : '暂无灵感');
})
.catch((error) => {
if (disposed) {
return;
}
setResources([]);
setStatus(error instanceof Error ? error.message : '灵感读取失败');
});
return () => {
disposed = true;
};
}, []);
return { resources, status };
}