init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
import { fetchJson, parseApiErrorMessage, saveJsonObject } from './jsonClient';
export const EDITOR_API_BASE_PATH = '/api/editor';
export const ASSETS_API_BASE_PATH = '/api/assets';
export const EDITOR_JSON_RESOURCE_IDS = {
itemOverrides: 'item-overrides',
npcVisualOverrides: 'npc-visual-overrides',
npcLayoutConfig: 'npc-layout-config',
characterOverrides: 'character-overrides',
monsterOverrides: 'monster-overrides',
sceneOverrides: 'scene-overrides',
sceneNpcOverrides: 'scene-npc-overrides',
stateFunctionOverrides: 'state-function-overrides',
} as const;
export type EditorJsonResourceId =
(typeof EDITOR_JSON_RESOURCE_IDS)[keyof typeof EDITOR_JSON_RESOURCE_IDS];
export const ASSET_API_PATHS = {
characterWorkflowCache: `${ASSETS_API_BASE_PATH}/character-workflow-cache`,
characterVisualGenerate: `${ASSETS_API_BASE_PATH}/character-visual/generate`,
characterVisualPublish: `${ASSETS_API_BASE_PATH}/character-visual/publish`,
characterVisualJobs: `${ASSETS_API_BASE_PATH}/character-visual/jobs`,
characterAnimationGenerate: `${ASSETS_API_BASE_PATH}/character-animation/generate`,
characterAnimationPublish: `${ASSETS_API_BASE_PATH}/character-animation/publish`,
characterAnimationJobs: `${ASSETS_API_BASE_PATH}/character-animation/jobs`,
characterAnimationImportVideo: `${ASSETS_API_BASE_PATH}/character-animation/import-video`,
characterAnimationTemplates: `${ASSETS_API_BASE_PATH}/character-animation/templates`,
} as const;
export const EDITOR_ITEM_CATALOG_API_PATH =
`${EDITOR_API_BASE_PATH}/catalog/items`;
export function buildEditorJsonApiPath(resourceId: EditorJsonResourceId) {
return `${EDITOR_API_BASE_PATH}/json/${resourceId}`;
}
export function fetchEditorJsonResource<T>(
resourceId: EditorJsonResourceId,
fallbackMessage = '读取失败',
) {
return fetchJson<T>(buildEditorJsonApiPath(resourceId), fallbackMessage);
}
export function saveEditorJsonResource(
resourceId: EditorJsonResourceId,
payload: Record<string, unknown>,
fallbackMessage = '保存失败',
) {
return saveJsonObject(
buildEditorJsonApiPath(resourceId),
payload,
fallbackMessage,
);
}
export async function postApiJson<T>(
url: string,
payload: Record<string, unknown>,
fallbackMessage: string,
) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const responseText = await response.text();
if (!response.ok) {
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
}
return responseText ? (JSON.parse(responseText) as T) : ({} as T);
}

View File

@@ -0,0 +1,49 @@
import {describe, expect, it} from 'vitest';
import {parseApiErrorMessage} from './jsonClient';
describe('parseApiErrorMessage', () => {
it('prefers api error detail messages for business failures', () => {
expect(
parseApiErrorMessage(
JSON.stringify({
ok: false,
data: null,
error: {
code: 'BAD_REQUEST',
message: '请求参数不合法',
details: {
message: 'big_fish 发布校验未通过:还缺少 16 个基础动作',
provider: 'spacetimedb',
},
},
meta: {
apiVersion: '2026-04-08',
},
}),
'Fallback failure',
),
).toBe('big_fish 发布校验未通过:还缺少 16 个基础动作');
});
it('prefers nested api error messages', () => {
expect(
parseApiErrorMessage(
JSON.stringify({
error: {
message: 'Detailed failure',
},
}),
'Fallback failure',
),
).toBe('Detailed failure');
});
it('falls back to the raw response text when the payload is not json', () => {
expect(parseApiErrorMessage('Plain text failure', 'Fallback failure')).toBe('Plain text failure');
});
it('uses the fallback when the response body is empty', () => {
expect(parseApiErrorMessage('', 'Fallback failure')).toBe('Fallback failure');
});
});

View File

@@ -0,0 +1,48 @@
import {
API_RESPONSE_ENVELOPE_HEADER,
API_RESPONSE_ENVELOPE_VERSION,
parseApiErrorMessage,
unwrapApiResponse,
} from '../../../packages/shared/src/http';
export async function fetchJson<T>(
url: string,
fallbackMessage = '请求失败',
): Promise<T> {
const response = await fetch(url, {
headers: {
[API_RESPONSE_ENVELOPE_HEADER]: API_RESPONSE_ENVELOPE_VERSION,
},
});
const responseText = await response.text();
if (!response.ok) {
throw new Error(parseApiErrorMessage(responseText, `${fallbackMessage}: ${response.status}`));
}
return responseText
? unwrapApiResponse<T>(JSON.parse(responseText) as T)
: ({} as T);
}
export async function saveJsonObject(
url: string,
payload: Record<string, unknown>,
fallbackMessage = '保存失败',
) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[API_RESPONSE_ENVELOPE_HEADER]: API_RESPONSE_ENVELOPE_VERSION,
},
body: JSON.stringify(payload, null, 2),
});
const responseText = await response.text();
if (!response.ok) {
throw new Error(parseApiErrorMessage(responseText, fallbackMessage));
}
}
export { parseApiErrorMessage };