style(code): 格式化代码缩进和布局样式
- 统一调整 admin.ts 文件中的接口定义缩进格式 - 优化 animated-theme-toggler.tsx 组件的代码结构和缩进 - 规范 app-config-modal.tsx 中 JSX 元素的嵌套格式 - 整理 app-providers.tsx 中的组件层级缩进 - 标准化 app-theme.ts 中的对象属性缩进格式
This commit is contained in:
@@ -13,79 +13,84 @@ export type ImageAsset = AssetBase<"image"> & { data: { dataUrl: string; storage
|
||||
export type Asset = TextAsset | ImageAsset;
|
||||
|
||||
type AssetBase<T extends AssetKind> = {
|
||||
id: string;
|
||||
kind: T;
|
||||
title: string;
|
||||
coverUrl: string;
|
||||
tags: string[];
|
||||
source?: string;
|
||||
note?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
id: string;
|
||||
kind: T;
|
||||
title: string;
|
||||
coverUrl: string;
|
||||
tags: string[];
|
||||
source?: string;
|
||||
note?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type AssetStore = {
|
||||
assets: Asset[];
|
||||
addAsset: (asset: Omit<Asset, "id" | "createdAt" | "updatedAt">) => string;
|
||||
updateAsset: (id: string, patch: Partial<Omit<Asset, "id" | "createdAt">>) => void;
|
||||
removeAsset: (id: string) => void;
|
||||
cleanupImages: (extra?: unknown) => void;
|
||||
assets: Asset[];
|
||||
addAsset: (asset: Omit<Asset, "id" | "createdAt" | "updatedAt">) => string;
|
||||
updateAsset: (id: string, patch: Partial<Omit<Asset, "id" | "createdAt">>) => void;
|
||||
removeAsset: (id: string) => void;
|
||||
cleanupImages: (extra?: unknown) => void;
|
||||
};
|
||||
|
||||
const ASSET_STORE_KEY = "infinite-canvas:asset_store";
|
||||
|
||||
const assetStorage: PersistStorage<AssetStore> = {
|
||||
getItem: async (name) => {
|
||||
const value = await localForageStorage.getItem(name);
|
||||
if (!value) return null;
|
||||
const parsed = JSON.parse(value) as StorageValue<AssetStore>;
|
||||
parsed.state.assets = await Promise.all(parsed.state.assets.map(async (asset) => {
|
||||
if (asset.kind !== "image") return asset;
|
||||
if (asset.data.storageKey) return {
|
||||
...asset,
|
||||
coverUrl: asset.coverUrl.startsWith("blob:") ? await resolveImageUrl(asset.data.storageKey, asset.coverUrl) : asset.coverUrl,
|
||||
data: { ...asset.data, dataUrl: await resolveImageUrl(asset.data.storageKey, asset.data.dataUrl) },
|
||||
};
|
||||
if (!asset.data.dataUrl.startsWith("data:image/")) return asset;
|
||||
const image = await uploadImage(asset.data.dataUrl);
|
||||
return { ...asset, coverUrl: asset.coverUrl.startsWith("data:image/") ? image.url : asset.coverUrl, data: { ...asset.data, dataUrl: image.url, storageKey: image.storageKey, bytes: image.bytes, mimeType: image.mimeType } };
|
||||
}));
|
||||
return parsed;
|
||||
},
|
||||
setItem: (name, value) => localForageStorage.setItem(name, JSON.stringify(value)),
|
||||
removeItem: (name) => localForageStorage.removeItem(name),
|
||||
getItem: async (name) => {
|
||||
const value = await localForageStorage.getItem(name);
|
||||
if (!value) return null;
|
||||
const parsed = JSON.parse(value) as StorageValue<AssetStore>;
|
||||
parsed.state.assets = await Promise.all(
|
||||
parsed.state.assets.map(async (asset) => {
|
||||
if (asset.kind !== "image") return asset;
|
||||
if (asset.data.storageKey)
|
||||
return {
|
||||
...asset,
|
||||
coverUrl: asset.coverUrl.startsWith("blob:") ? await resolveImageUrl(asset.data.storageKey, asset.coverUrl) : asset.coverUrl,
|
||||
data: { ...asset.data, dataUrl: await resolveImageUrl(asset.data.storageKey, asset.data.dataUrl) },
|
||||
};
|
||||
if (!asset.data.dataUrl.startsWith("data:image/")) return asset;
|
||||
const image = await uploadImage(asset.data.dataUrl);
|
||||
return { ...asset, coverUrl: asset.coverUrl.startsWith("data:image/") ? image.url : asset.coverUrl, data: { ...asset.data, dataUrl: image.url, storageKey: image.storageKey, bytes: image.bytes, mimeType: image.mimeType } };
|
||||
}),
|
||||
);
|
||||
return parsed;
|
||||
},
|
||||
setItem: (name, value) => localForageStorage.setItem(name, JSON.stringify(value)),
|
||||
removeItem: (name) => localForageStorage.removeItem(name),
|
||||
};
|
||||
|
||||
export const useAssetStore = create<AssetStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
assets: [],
|
||||
addAsset: (asset) => {
|
||||
const now = new Date().toISOString();
|
||||
const id = nanoid();
|
||||
set((state) => ({ assets: [{ ...asset, id, createdAt: now, updatedAt: now } as Asset, ...state.assets] }));
|
||||
return id;
|
||||
},
|
||||
updateAsset: (id, patch) => set((state) => ({
|
||||
assets: state.assets.map((asset) => asset.id === id ? { ...asset, ...patch, updatedAt: new Date().toISOString() } as Asset : asset),
|
||||
})),
|
||||
removeAsset: (id) => set((state) => {
|
||||
const assets = state.assets.filter((asset) => asset.id !== id);
|
||||
get().cleanupImages({ assets });
|
||||
return { assets };
|
||||
}),
|
||||
cleanupImages: (extra) => {
|
||||
window.setTimeout(async () => {
|
||||
const { useCanvasStore } = await import("@/app/(user)/canvas/stores/use-canvas-store");
|
||||
await cleanupUnusedImages({ assets: get().assets, projects: useCanvasStore.getState().projects, extra });
|
||||
}, 0);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: ASSET_STORE_KEY,
|
||||
storage: assetStorage,
|
||||
partialize: (state) => ({ assets: state.assets }) as StorageValue<AssetStore>["state"],
|
||||
},
|
||||
),
|
||||
persist(
|
||||
(set, get) => ({
|
||||
assets: [],
|
||||
addAsset: (asset) => {
|
||||
const now = new Date().toISOString();
|
||||
const id = nanoid();
|
||||
set((state) => ({ assets: [{ ...asset, id, createdAt: now, updatedAt: now } as Asset, ...state.assets] }));
|
||||
return id;
|
||||
},
|
||||
updateAsset: (id, patch) =>
|
||||
set((state) => ({
|
||||
assets: state.assets.map((asset) => (asset.id === id ? ({ ...asset, ...patch, updatedAt: new Date().toISOString() } as Asset) : asset)),
|
||||
})),
|
||||
removeAsset: (id) =>
|
||||
set((state) => {
|
||||
const assets = state.assets.filter((asset) => asset.id !== id);
|
||||
get().cleanupImages({ assets });
|
||||
return { assets };
|
||||
}),
|
||||
cleanupImages: (extra) => {
|
||||
window.setTimeout(async () => {
|
||||
const { useCanvasStore } = await import("@/app/(user)/canvas/stores/use-canvas-store");
|
||||
await cleanupUnusedImages({ assets: get().assets, projects: useCanvasStore.getState().projects, extra });
|
||||
}, 0);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: ASSET_STORE_KEY,
|
||||
storage: assetStorage,
|
||||
partialize: (state) => ({ assets: state.assets }) as StorageValue<AssetStore>["state"],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -8,116 +8,116 @@ import { apiGet } from "@/services/api/request";
|
||||
import type { AdminPublicSettings } from "@/services/api/admin";
|
||||
|
||||
export type AiConfig = {
|
||||
channelMode: "remote" | "local";
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
imageModel: string;
|
||||
textModel: string;
|
||||
systemPrompt: string;
|
||||
models: string[];
|
||||
quality: string;
|
||||
size: string;
|
||||
count: string;
|
||||
channelMode: "remote" | "local";
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
imageModel: string;
|
||||
textModel: string;
|
||||
systemPrompt: string;
|
||||
models: string[];
|
||||
quality: string;
|
||||
size: string;
|
||||
count: string;
|
||||
};
|
||||
|
||||
export const CONFIG_STORE_KEY = "infinite-canvas:ai_config_store";
|
||||
|
||||
export const defaultConfig: AiConfig = {
|
||||
channelMode: "local",
|
||||
baseUrl: "https://api.openai.com",
|
||||
apiKey: "",
|
||||
model: "gpt-image-2",
|
||||
imageModel: "gpt-image-2",
|
||||
textModel: "gpt-5.5",
|
||||
systemPrompt: "",
|
||||
models: [],
|
||||
quality: "auto",
|
||||
size: "1:1",
|
||||
count: "1",
|
||||
channelMode: "local",
|
||||
baseUrl: "https://api.openai.com",
|
||||
apiKey: "",
|
||||
model: "gpt-image-2",
|
||||
imageModel: "gpt-image-2",
|
||||
textModel: "gpt-5.5",
|
||||
systemPrompt: "",
|
||||
models: [],
|
||||
quality: "auto",
|
||||
size: "1:1",
|
||||
count: "1",
|
||||
};
|
||||
|
||||
type ConfigStore = {
|
||||
config: AiConfig;
|
||||
publicSettings: AdminPublicSettings | null;
|
||||
isPublicSettingsLoading: boolean;
|
||||
isConfigOpen: boolean;
|
||||
shouldPromptContinue: boolean;
|
||||
updateConfig: <K extends keyof AiConfig>(key: K, value: AiConfig[K]) => void;
|
||||
loadPublicSettings: () => Promise<void>;
|
||||
isAiConfigReady: (config: AiConfig, model: string) => boolean;
|
||||
openConfigDialog: (shouldPromptContinue?: boolean) => void;
|
||||
setConfigDialogOpen: (isOpen: boolean) => void;
|
||||
clearPromptContinue: () => void;
|
||||
config: AiConfig;
|
||||
publicSettings: AdminPublicSettings | null;
|
||||
isPublicSettingsLoading: boolean;
|
||||
isConfigOpen: boolean;
|
||||
shouldPromptContinue: boolean;
|
||||
updateConfig: <K extends keyof AiConfig>(key: K, value: AiConfig[K]) => void;
|
||||
loadPublicSettings: () => Promise<void>;
|
||||
isAiConfigReady: (config: AiConfig, model: string) => boolean;
|
||||
openConfigDialog: (shouldPromptContinue?: boolean) => void;
|
||||
setConfigDialogOpen: (isOpen: boolean) => void;
|
||||
clearPromptContinue: () => void;
|
||||
};
|
||||
|
||||
function resolveEffectiveConfig(config: AiConfig, modelChannel: AdminPublicSettings["modelChannel"] | null) {
|
||||
const channelMode = modelChannel?.allowCustomChannel ? config.channelMode : "remote";
|
||||
if (channelMode === "local" || !modelChannel) return { ...config, channelMode };
|
||||
const models = modelChannel.availableModels;
|
||||
return {
|
||||
...config,
|
||||
channelMode,
|
||||
models,
|
||||
model: models.includes(config.model) ? config.model : modelChannel.defaultModel,
|
||||
imageModel: models.includes(config.imageModel) ? config.imageModel : modelChannel.defaultImageModel || modelChannel.defaultModel,
|
||||
textModel: models.includes(config.textModel) ? config.textModel : modelChannel.defaultTextModel || modelChannel.defaultModel,
|
||||
systemPrompt: modelChannel.systemPrompt,
|
||||
};
|
||||
const channelMode = modelChannel?.allowCustomChannel ? config.channelMode : "remote";
|
||||
if (channelMode === "local" || !modelChannel) return { ...config, channelMode };
|
||||
const models = modelChannel.availableModels;
|
||||
return {
|
||||
...config,
|
||||
channelMode,
|
||||
models,
|
||||
model: models.includes(config.model) ? config.model : modelChannel.defaultModel,
|
||||
imageModel: models.includes(config.imageModel) ? config.imageModel : modelChannel.defaultImageModel || modelChannel.defaultModel,
|
||||
textModel: models.includes(config.textModel) ? config.textModel : modelChannel.defaultTextModel || modelChannel.defaultModel,
|
||||
systemPrompt: modelChannel.systemPrompt,
|
||||
};
|
||||
}
|
||||
|
||||
function isAiConfigReady(config: AiConfig, model: string) {
|
||||
return Boolean(model.trim()) && (config.channelMode === "remote" || Boolean(config.baseUrl.trim() && config.apiKey.trim()));
|
||||
return Boolean(model.trim()) && (config.channelMode === "remote" || Boolean(config.baseUrl.trim() && config.apiKey.trim()));
|
||||
}
|
||||
|
||||
export const useConfigStore = create<ConfigStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
config: defaultConfig,
|
||||
publicSettings: null,
|
||||
isPublicSettingsLoading: false,
|
||||
isConfigOpen: false,
|
||||
shouldPromptContinue: false,
|
||||
updateConfig: (key, value) =>
|
||||
set((state) => ({
|
||||
config: {
|
||||
...state.config,
|
||||
[key]: value,
|
||||
},
|
||||
})),
|
||||
loadPublicSettings: async () => {
|
||||
if (get().isPublicSettingsLoading) return;
|
||||
set({ isPublicSettingsLoading: true });
|
||||
try {
|
||||
set({ publicSettings: await apiGet<AdminPublicSettings>("/api/settings") });
|
||||
} finally {
|
||||
set({ isPublicSettingsLoading: false });
|
||||
}
|
||||
},
|
||||
isAiConfigReady: (config, model) => isAiConfigReady(config, model),
|
||||
openConfigDialog: (shouldPromptContinue = false) => set({ isConfigOpen: true, shouldPromptContinue }),
|
||||
setConfigDialogOpen: (isConfigOpen) => set({ isConfigOpen }),
|
||||
clearPromptContinue: () => set({ shouldPromptContinue: false }),
|
||||
}),
|
||||
{
|
||||
name: CONFIG_STORE_KEY,
|
||||
partialize: (state) => ({ config: state.config }),
|
||||
merge: (persisted, current) => {
|
||||
const config = { ...defaultConfig, ...((persisted as Partial<ConfigStore>).config || {}) };
|
||||
return { ...current, config: { ...config, channelMode: config.channelMode || "remote", imageModel: config.imageModel || config.model, textModel: config.textModel || config.model } };
|
||||
},
|
||||
},
|
||||
),
|
||||
persist(
|
||||
(set, get) => ({
|
||||
config: defaultConfig,
|
||||
publicSettings: null,
|
||||
isPublicSettingsLoading: false,
|
||||
isConfigOpen: false,
|
||||
shouldPromptContinue: false,
|
||||
updateConfig: (key, value) =>
|
||||
set((state) => ({
|
||||
config: {
|
||||
...state.config,
|
||||
[key]: value,
|
||||
},
|
||||
})),
|
||||
loadPublicSettings: async () => {
|
||||
if (get().isPublicSettingsLoading) return;
|
||||
set({ isPublicSettingsLoading: true });
|
||||
try {
|
||||
set({ publicSettings: await apiGet<AdminPublicSettings>("/api/settings") });
|
||||
} finally {
|
||||
set({ isPublicSettingsLoading: false });
|
||||
}
|
||||
},
|
||||
isAiConfigReady: (config, model) => isAiConfigReady(config, model),
|
||||
openConfigDialog: (shouldPromptContinue = false) => set({ isConfigOpen: true, shouldPromptContinue }),
|
||||
setConfigDialogOpen: (isConfigOpen) => set({ isConfigOpen }),
|
||||
clearPromptContinue: () => set({ shouldPromptContinue: false }),
|
||||
}),
|
||||
{
|
||||
name: CONFIG_STORE_KEY,
|
||||
partialize: (state) => ({ config: state.config }),
|
||||
merge: (persisted, current) => {
|
||||
const config = { ...defaultConfig, ...((persisted as Partial<ConfigStore>).config || {}) };
|
||||
return { ...current, config: { ...config, channelMode: config.channelMode || "remote", imageModel: config.imageModel || config.model, textModel: config.textModel || config.model } };
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export function useEffectiveConfig() {
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const modelChannel = useConfigStore((state) => state.publicSettings?.modelChannel || null);
|
||||
return useMemo(() => resolveEffectiveConfig(config, modelChannel), [config, modelChannel]);
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const modelChannel = useConfigStore((state) => state.publicSettings?.modelChannel || null);
|
||||
return useMemo(() => resolveEffectiveConfig(config, modelChannel), [config, modelChannel]);
|
||||
}
|
||||
|
||||
export function buildApiUrl(baseUrl: string, path: string) {
|
||||
const normalizedBaseUrl = baseUrl.trim().replace(/\/+$/, "");
|
||||
const apiBaseUrl = normalizedBaseUrl.endsWith("/v1") ? normalizedBaseUrl : `${normalizedBaseUrl}/v1`;
|
||||
return `${apiBaseUrl}${path}`;
|
||||
const normalizedBaseUrl = baseUrl.trim().replace(/\/+$/, "");
|
||||
const apiBaseUrl = normalizedBaseUrl.endsWith("/v1") ? normalizedBaseUrl : `${normalizedBaseUrl}/v1`;
|
||||
return `${apiBaseUrl}${path}`;
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ import { persist } from "zustand/middleware";
|
||||
export type ThemeName = "light" | "dark";
|
||||
|
||||
type ThemeStore = {
|
||||
theme: ThemeName;
|
||||
setTheme: (theme: ThemeName) => void;
|
||||
theme: ThemeName;
|
||||
setTheme: (theme: ThemeName) => void;
|
||||
};
|
||||
|
||||
export const useThemeStore = create<ThemeStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
theme: "dark",
|
||||
setTheme: (theme) => set({ theme }),
|
||||
}),
|
||||
{ name: "infinite-canvas:theme_store" },
|
||||
),
|
||||
persist(
|
||||
(set) => ({
|
||||
theme: "dark",
|
||||
setTheme: (theme) => set({ theme }),
|
||||
}),
|
||||
{ name: "infinite-canvas:theme_store" },
|
||||
),
|
||||
);
|
||||
|
||||
@@ -6,73 +6,73 @@ import { persist } from "zustand/middleware";
|
||||
import { AUTH_TOKEN_KEY, fetchCurrentUser, login, register, type AuthPayload, type AuthUser } from "@/services/api/auth";
|
||||
|
||||
type UserStore = {
|
||||
token: string;
|
||||
user: AuthUser | null;
|
||||
isReady: boolean;
|
||||
isLoading: boolean;
|
||||
setSession: (token: string, user: AuthUser) => void;
|
||||
clearSession: () => void;
|
||||
hydrateUser: () => Promise<void>;
|
||||
login: (payload: AuthPayload) => Promise<AuthUser>;
|
||||
register: (payload: AuthPayload) => Promise<AuthUser>;
|
||||
token: string;
|
||||
user: AuthUser | null;
|
||||
isReady: boolean;
|
||||
isLoading: boolean;
|
||||
setSession: (token: string, user: AuthUser) => void;
|
||||
clearSession: () => void;
|
||||
hydrateUser: () => Promise<void>;
|
||||
login: (payload: AuthPayload) => Promise<AuthUser>;
|
||||
register: (payload: AuthPayload) => Promise<AuthUser>;
|
||||
};
|
||||
|
||||
export const useUserStore = create<UserStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
token: "",
|
||||
user: null,
|
||||
isReady: false,
|
||||
isLoading: false,
|
||||
setSession: (token, user) => set({ token, user, isReady: true }),
|
||||
clearSession: () => set({ token: "", user: null, isReady: true }),
|
||||
hydrateUser: async () => {
|
||||
const token = get().token;
|
||||
if (!token) {
|
||||
set({ user: null, isReady: true });
|
||||
return;
|
||||
}
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const user = await fetchCurrentUser(token);
|
||||
if (user.role === "guest") {
|
||||
set({ token: "", user: null, isReady: true, isLoading: false });
|
||||
return;
|
||||
}
|
||||
set({ user, isReady: true, isLoading: false });
|
||||
} catch {
|
||||
set({ token: "", user: null, isReady: true, isLoading: false });
|
||||
}
|
||||
},
|
||||
login: async (payload) => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const session = await login(payload);
|
||||
set({ token: session.token, user: session.user, isReady: true, isLoading: false });
|
||||
return session.user;
|
||||
} catch (error) {
|
||||
set({ isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
register: async (payload) => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const session = await register(payload);
|
||||
set({ token: session.token, user: session.user, isReady: true, isLoading: false });
|
||||
return session.user;
|
||||
} catch (error) {
|
||||
set({ isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: AUTH_TOKEN_KEY,
|
||||
partialize: (state) => ({ token: state.token }),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) state.isReady = false;
|
||||
},
|
||||
},
|
||||
),
|
||||
persist(
|
||||
(set, get) => ({
|
||||
token: "",
|
||||
user: null,
|
||||
isReady: false,
|
||||
isLoading: false,
|
||||
setSession: (token, user) => set({ token, user, isReady: true }),
|
||||
clearSession: () => set({ token: "", user: null, isReady: true }),
|
||||
hydrateUser: async () => {
|
||||
const token = get().token;
|
||||
if (!token) {
|
||||
set({ user: null, isReady: true });
|
||||
return;
|
||||
}
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const user = await fetchCurrentUser(token);
|
||||
if (user.role === "guest") {
|
||||
set({ token: "", user: null, isReady: true, isLoading: false });
|
||||
return;
|
||||
}
|
||||
set({ user, isReady: true, isLoading: false });
|
||||
} catch {
|
||||
set({ token: "", user: null, isReady: true, isLoading: false });
|
||||
}
|
||||
},
|
||||
login: async (payload) => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const session = await login(payload);
|
||||
set({ token: session.token, user: session.user, isReady: true, isLoading: false });
|
||||
return session.user;
|
||||
} catch (error) {
|
||||
set({ isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
register: async (payload) => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const session = await register(payload);
|
||||
set({ token: session.token, user: session.user, isReady: true, isLoading: false });
|
||||
return session.user;
|
||||
} catch (error) {
|
||||
set({ isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: AUTH_TOKEN_KEY,
|
||||
partialize: (state) => ({ token: state.token }),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) state.isReady = false;
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user