- 产品列表:拖拽排序持久化、编辑弹窗、删除校验(活跃版本需迁移) - 项目列表:新建项目表单(产品选择+重名校验)、产品筛选、版本归属修复 - 版本列表:新建版本表单(迭代类型自动生成版本号)、状态/项目筛选、倒序排列 - 项目详情页:概览统计、人员墙(参与次数)、版本时间线(阶段流水线+进度条) - 全局优化:筛选栏统一为header下方固定行、按钮颜色改为主题蓝、API探测300ms、store缓存 - 数据持久化到localStorage,支持离线开发 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1';
|
|
|
|
let apiAvailable: boolean | null = null;
|
|
let probePromise: Promise<boolean> | null = null;
|
|
|
|
async function checkApi(): Promise<boolean> {
|
|
if (apiAvailable !== null) return apiAvailable;
|
|
if (probePromise) return probePromise;
|
|
probePromise = (async () => {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/products`, { method: 'HEAD', signal: AbortSignal.timeout(300) });
|
|
apiAvailable = res.ok;
|
|
} catch {
|
|
apiAvailable = false;
|
|
}
|
|
return apiAvailable;
|
|
})();
|
|
return probePromise;
|
|
}
|
|
|
|
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
|
const available = await checkApi();
|
|
if (!available) throw new Error('API 不可用');
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
...options,
|
|
}).catch(() => null);
|
|
if (!res) throw new Error('API 不可用');
|
|
if (!res.ok) {
|
|
const error = await res.json().catch(() => ({}));
|
|
throw new Error(error.message || `请求失败: ${res.status}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string) => request<T>(path),
|
|
post: <T>(path: string, data: unknown) =>
|
|
request<T>(path, { method: 'POST', body: JSON.stringify(data) }),
|
|
patch: <T>(path: string, data: unknown) =>
|
|
request<T>(path, { method: 'PATCH', body: JSON.stringify(data) }),
|
|
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
|
};
|