fix:优化数据
This commit is contained in:
@@ -18,17 +18,20 @@ export interface CustomerRead {
|
||||
id: number;
|
||||
name: string;
|
||||
contact_info: string | null;
|
||||
tags: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CustomerCreate {
|
||||
name: string;
|
||||
contact_info?: string | null;
|
||||
tags?: string | null;
|
||||
}
|
||||
|
||||
export interface CustomerUpdate {
|
||||
name?: string;
|
||||
contact_info?: string | null;
|
||||
tags?: string | null;
|
||||
}
|
||||
|
||||
export interface RequirementAnalyzeRequest {
|
||||
@@ -69,7 +72,68 @@ export interface FinanceSyncResult {
|
||||
}
|
||||
|
||||
export interface FinanceSyncResponse {
|
||||
items: FinanceSyncResult[];
|
||||
status: string;
|
||||
new_files: number;
|
||||
details: FinanceSyncResult[];
|
||||
}
|
||||
|
||||
export interface FinanceRecordRead {
|
||||
id: number;
|
||||
month: string;
|
||||
type: string;
|
||||
file_name: string;
|
||||
file_path: string;
|
||||
amount: number | null;
|
||||
billing_date: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface TemplateInfo {
|
||||
name: string;
|
||||
type: "excel" | "word";
|
||||
size: number;
|
||||
uploaded_at: number;
|
||||
}
|
||||
|
||||
export interface AIConfig {
|
||||
id?: string;
|
||||
name?: string;
|
||||
provider: string;
|
||||
api_key: string;
|
||||
base_url: string;
|
||||
model_name: string;
|
||||
temperature: number;
|
||||
system_prompt_override: string;
|
||||
}
|
||||
|
||||
export interface AIConfigListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
model_name: string;
|
||||
base_url: string;
|
||||
api_key_configured: boolean;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface AIConfigCreate {
|
||||
name?: string;
|
||||
provider?: string;
|
||||
api_key?: string;
|
||||
base_url?: string;
|
||||
model_name?: string;
|
||||
temperature?: number;
|
||||
system_prompt_override?: string;
|
||||
}
|
||||
|
||||
export interface AIConfigUpdate {
|
||||
name?: string;
|
||||
provider?: string;
|
||||
api_key?: string;
|
||||
base_url?: string;
|
||||
model_name?: string;
|
||||
temperature?: number;
|
||||
system_prompt_override?: string;
|
||||
}
|
||||
|
||||
export interface ProjectRead {
|
||||
@@ -84,6 +148,7 @@ export interface ProjectRead {
|
||||
}
|
||||
|
||||
export interface ProjectUpdate {
|
||||
raw_requirement?: string | null;
|
||||
ai_solution_md?: string | null;
|
||||
status?: string;
|
||||
}
|
||||
@@ -112,10 +177,15 @@ async function request<T>(
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
let detail = text;
|
||||
let detail: string = text;
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
detail = j.detail ?? text;
|
||||
const raw = j.detail ?? text;
|
||||
if (Array.isArray(raw) && raw.length > 0) {
|
||||
detail = raw.map((x: { msg?: string }) => x.msg ?? JSON.stringify(x)).join("; ");
|
||||
} else {
|
||||
detail = typeof raw === "string" ? raw : JSON.stringify(raw);
|
||||
}
|
||||
} catch {
|
||||
// keep text
|
||||
}
|
||||
@@ -176,7 +246,10 @@ export async function downloadFileAsBlob(
|
||||
// --------------- Customers ---------------
|
||||
|
||||
export const customersApi = {
|
||||
list: () => request<CustomerRead[]>("/customers/"),
|
||||
list: (params?: { q?: string }) =>
|
||||
request<CustomerRead[]>(
|
||||
params?.q ? `/customers/?q=${encodeURIComponent(params.q)}` : "/customers/"
|
||||
),
|
||||
get: (id: number) => request<CustomerRead>(`/customers/${id}`),
|
||||
create: (body: CustomerCreate) =>
|
||||
request<CustomerRead>("/customers/", {
|
||||
@@ -207,10 +280,11 @@ export const projectsApi = {
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
generateQuote: (projectId: number) =>
|
||||
request<QuoteGenerateResponse>(`/projects/${projectId}/generate_quote`, {
|
||||
method: "POST",
|
||||
}),
|
||||
generateQuote: (projectId: number, template?: string | null) =>
|
||||
request<QuoteGenerateResponse>(
|
||||
`/projects/${projectId}/generate_quote${template ? `?template=${encodeURIComponent(template)}` : ""}`,
|
||||
{ method: "POST" }
|
||||
),
|
||||
|
||||
generateContract: (projectId: number, body: ContractGenerateRequest) =>
|
||||
request<ContractGenerateResponse>(
|
||||
@@ -222,20 +296,285 @@ export const projectsApi = {
|
||||
),
|
||||
};
|
||||
|
||||
export async function listProjects(): Promise<ProjectRead[]> {
|
||||
return request<ProjectRead[]>("/projects/");
|
||||
export async function listProjects(params?: {
|
||||
customer_tag?: string;
|
||||
}): Promise<ProjectRead[]> {
|
||||
const searchParams: Record<string, string> = {};
|
||||
if (params?.customer_tag?.trim()) searchParams.customer_tag = params.customer_tag.trim();
|
||||
return request<ProjectRead[]>("/projects/", {
|
||||
params: searchParams,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getProject(projectId: number): Promise<ProjectRead> {
|
||||
return request<ProjectRead>(`/projects/${projectId}`);
|
||||
}
|
||||
|
||||
// --------------- Settings / Templates ---------------
|
||||
|
||||
export const templatesApi = {
|
||||
list: () => request<TemplateInfo[]>("/settings/templates"),
|
||||
|
||||
upload: async (file: File): Promise<{ name: string; path: string }> => {
|
||||
const base = apiBase();
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const res = await fetch(`${base}/settings/templates/upload`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
let detail = text;
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
detail = j.detail ?? text;
|
||||
} catch {
|
||||
// keep text
|
||||
}
|
||||
throw new Error(detail);
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
};
|
||||
|
||||
// --------------- AI Settings ---------------
|
||||
|
||||
export const aiSettingsApi = {
|
||||
get: () => request<AIConfig>("/settings/ai"),
|
||||
list: () => request<AIConfigListItem[]>("/settings/ai/list"),
|
||||
getById: (id: string) => request<AIConfig>(`/settings/ai/${id}`),
|
||||
create: (body: AIConfigCreate) =>
|
||||
request<AIConfig>("/settings/ai", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
update: (id: string, body: AIConfigUpdate) =>
|
||||
request<AIConfig>(`/settings/ai/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
delete: (id: string) =>
|
||||
request<void>(`/settings/ai/${id}`, { method: "DELETE" }),
|
||||
activate: (id: string) =>
|
||||
request<AIConfig>(`/settings/ai/${id}/activate`, { method: "POST" }),
|
||||
test: (configId?: string) =>
|
||||
request<{ status: string; message: string }>(
|
||||
configId ? `/settings/ai/test?config_id=${encodeURIComponent(configId)}` : "/settings/ai/test",
|
||||
{ method: "POST" }
|
||||
),
|
||||
};
|
||||
|
||||
// --------------- Email Configs (multi-account sync) ---------------
|
||||
|
||||
export interface EmailConfigRead {
|
||||
id: string;
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
mailbox: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface EmailConfigCreate {
|
||||
host: string;
|
||||
port?: number;
|
||||
user: string;
|
||||
password: string;
|
||||
mailbox?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface EmailConfigUpdate {
|
||||
host?: string;
|
||||
port?: number;
|
||||
user?: string;
|
||||
password?: string;
|
||||
mailbox?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface EmailFolder {
|
||||
raw: string;
|
||||
decoded: string;
|
||||
}
|
||||
|
||||
export const emailConfigsApi = {
|
||||
list: () => request<EmailConfigRead[]>("/settings/email"),
|
||||
create: (body: EmailConfigCreate) =>
|
||||
request<EmailConfigRead>("/settings/email", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
update: (id: string, body: EmailConfigUpdate) =>
|
||||
request<EmailConfigRead>(`/settings/email/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
delete: (id: string) =>
|
||||
request<void>(`/settings/email/${id}`, { method: "DELETE" }),
|
||||
/** List mailbox folders for an account (to pick custom label). Use decoded as mailbox value. */
|
||||
listFolders: (configId: string) =>
|
||||
request<{ folders: EmailFolder[] }>(`/settings/email/${configId}/folders`),
|
||||
};
|
||||
|
||||
// --------------- Cloud Docs (快捷入口) ---------------
|
||||
|
||||
export interface CloudDocLinkRead {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface CloudDocLinkCreate {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface CloudDocLinkUpdate {
|
||||
name?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export const cloudDocsApi = {
|
||||
list: () => request<CloudDocLinkRead[]>("/settings/cloud-docs"),
|
||||
create: (body: CloudDocLinkCreate) =>
|
||||
request<CloudDocLinkRead>("/settings/cloud-docs", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
update: (id: string, body: CloudDocLinkUpdate) =>
|
||||
request<CloudDocLinkRead>(`/settings/cloud-docs/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
delete: (id: string) =>
|
||||
request<void>(`/settings/cloud-docs/${id}`, { method: "DELETE" }),
|
||||
};
|
||||
|
||||
// --------------- Cloud Doc Config (API 凭证) ---------------
|
||||
|
||||
export interface CloudDocConfigRead {
|
||||
feishu: { app_id: string; app_secret_configured: boolean };
|
||||
yuque: { token_configured: boolean; default_repo: string };
|
||||
tencent: { client_id: string; client_secret_configured: boolean };
|
||||
}
|
||||
|
||||
export interface CloudDocConfigUpdate {
|
||||
feishu?: { app_id?: string; app_secret?: string };
|
||||
yuque?: { token?: string; default_repo?: string };
|
||||
tencent?: { client_id?: string; client_secret?: string };
|
||||
}
|
||||
|
||||
export const cloudDocConfigApi = {
|
||||
get: () => request<CloudDocConfigRead>("/settings/cloud-doc-config"),
|
||||
update: (body: CloudDocConfigUpdate) =>
|
||||
request<CloudDocConfigRead>("/settings/cloud-doc-config", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
};
|
||||
|
||||
// --------------- Push to Cloud ---------------
|
||||
|
||||
export interface PushToCloudRequest {
|
||||
platform: "feishu" | "yuque" | "tencent";
|
||||
title?: string;
|
||||
body_md?: string;
|
||||
}
|
||||
|
||||
export interface PushToCloudResponse {
|
||||
url: string;
|
||||
cloud_doc_id: string;
|
||||
}
|
||||
|
||||
export function pushProjectToCloud(
|
||||
projectId: number,
|
||||
body: PushToCloudRequest
|
||||
): Promise<PushToCloudResponse> {
|
||||
return request<PushToCloudResponse>(`/projects/${projectId}/push-to-cloud`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
// --------------- Portal Links (快捷门户) ---------------
|
||||
|
||||
export interface PortalLinkRead {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface PortalLinkCreate {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface PortalLinkUpdate {
|
||||
name?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export const portalLinksApi = {
|
||||
list: () => request<PortalLinkRead[]>("/settings/portal-links"),
|
||||
create: (body: PortalLinkCreate) =>
|
||||
request<PortalLinkRead>("/settings/portal-links", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
update: (id: string, body: PortalLinkUpdate) =>
|
||||
request<PortalLinkRead>(`/settings/portal-links/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
delete: (id: string) =>
|
||||
request<void>(`/settings/portal-links/${id}`, { method: "DELETE" }),
|
||||
};
|
||||
|
||||
// --------------- Finance ---------------
|
||||
|
||||
export const financeApi = {
|
||||
sync: () =>
|
||||
request<FinanceSyncResponse>("/finance/sync", { method: "POST" }),
|
||||
|
||||
/** Upload invoice (PDF/image); returns created record with AI-extracted amount/date. */
|
||||
uploadInvoice: async (file: File): Promise<FinanceRecordRead> => {
|
||||
const base = apiBase();
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const res = await fetch(`${base}/finance/upload`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
let detail = text;
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
detail = j.detail ?? text;
|
||||
} catch {
|
||||
// keep text
|
||||
}
|
||||
throw new Error(detail);
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
/** Update amount/billing_date of a record (e.g. after manual review). */
|
||||
updateRecord: (id: number, body: { amount?: number | null; billing_date?: string | null }) =>
|
||||
request<FinanceRecordRead>(`/finance/records/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
/** List distinct months with records (YYYY-MM). */
|
||||
listMonths: () => request<string[]>("/finance/months"),
|
||||
|
||||
/** List records for a month (YYYY-MM). */
|
||||
listRecords: (month: string) =>
|
||||
request<FinanceRecordRead[]>(`/finance/records?month=${encodeURIComponent(month)}`),
|
||||
|
||||
/** Returns the URL to download the zip (or use downloadFile with the path). */
|
||||
getDownloadUrl: (month: string) => `${apiBase()}/finance/download/${month}`,
|
||||
|
||||
|
||||
22
frontend/lib/portal-config.ts
Normal file
22
frontend/lib/portal-config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 快捷门户地址配置
|
||||
* 通过环境变量 NEXT_PUBLIC_* 覆盖,未设置时使用默认值
|
||||
*/
|
||||
const DEFAULTS = {
|
||||
/** 国家税务总局门户 */
|
||||
TAX_GATEWAY_URL: "https://www.chinatax.gov.cn",
|
||||
/** 电子税务局(如上海电子税务局) */
|
||||
TAX_PORTAL_URL: "https://etax.shanghai.chinatax.gov.cn:8443/",
|
||||
/** 公积金管理中心 */
|
||||
HOUSING_FUND_PORTAL_URL: "https://www.shzfgjj.cn/static/unit/web/",
|
||||
} as const;
|
||||
|
||||
export const portalConfig = {
|
||||
taxGatewayUrl:
|
||||
process.env.NEXT_PUBLIC_TAX_GATEWAY_URL ?? DEFAULTS.TAX_GATEWAY_URL,
|
||||
taxPortalUrl:
|
||||
process.env.NEXT_PUBLIC_TAX_PORTAL_URL ?? DEFAULTS.TAX_PORTAL_URL,
|
||||
housingFundPortalUrl:
|
||||
process.env.NEXT_PUBLIC_HOUSING_FUND_PORTAL_URL ??
|
||||
DEFAULTS.HOUSING_FUND_PORTAL_URL,
|
||||
} as const;
|
||||
Reference in New Issue
Block a user