feat: 项目优化,丛商对接完成

This commit is contained in:
Ben
2025-05-16 18:37:15 +08:00
parent 432ca043c0
commit 26c10f3bc6
66 changed files with 414 additions and 192 deletions

View File

@@ -4,6 +4,7 @@
"": {
"name": "cs-auto-report",
"dependencies": {
"@ant-design/icons": "5.x",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-http": "~2",

View File

@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/png" href="/app-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>商报告生成器</title>
<title>商报告生成器</title>
</head>
<body>

View File

@@ -1,17 +1,20 @@
{
"name": "cs-auto-report",
"name": "dev_report",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "index.html",
"scripts": {
"dev": "tauri dev",
"vite:dev": "vite",
"tauri": "tauri",
"lint": "bunx @biomejs/biome lint ./src",
"format": "bunx @biomejs/biome format --write ./src",
"check": "bunx @biomejs/biome check --apply ./src"
"check": "bunx @biomejs/biome check --apply ./src",
"build": "vite build"
},
"dependencies": {
"@ant-design/icons": "5.x",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-http": "~2",

BIN
public/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

28
src-tauri/Cargo.lock generated
View File

@@ -2,6 +2,20 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "DevReport"
version = "0.1.0"
dependencies = [
"git2",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-opener",
]
[[package]]
name = "addr2line"
version = "0.24.2"
@@ -665,20 +679,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "cs-auto-report"
version = "0.1.0"
dependencies = [
"git2",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-opener",
]
[[package]]
name = "cssparser"
version = "0.27.2"

View File

@@ -1,5 +1,5 @@
[package]
name = "cs-auto-report"
name = "DevReport"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]

View File

@@ -11,7 +11,11 @@
"dialog:default",
{
"identifier": "http:default",
"allow": [{ "url": "https://api.deepseek.com" }],
"allow": [
{ "url": "https://api.deepseek.com" },
{ "url": "http://192.168.1.105:*" },
{ "url": "http://im.congshangyun.com:*" }
],
"deny": [{ "url": "https://private.tauri.app" }]
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,8 +1,8 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "cs-auto-report",
"productName": "DevReport",
"version": "0.1.0",
"identifier": "com.cs-auto-report.app",
"identifier": "com.DevReport.app",
"build": {
"beforeDevCommand": "bun run vite:dev",
"devUrl": "http://localhost:1420",
@@ -12,7 +12,7 @@
"app": {
"windows": [
{
"title": "商报告生成器",
"title": "商报告生成器",
"width": 1200,
"height": 800
}

View File

@@ -19,7 +19,7 @@ const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
const [reportType, setReportType] = useState<ReportType>(
ReportType.WEEKLY_REPORT,
);
const [useAI, setUseAI] = useState(false);
const [useAI, setUseAI] = useState(true);
const handleSubmit = () => {
onSubmit(reportType, useAI);

View File

@@ -0,0 +1,116 @@
import {useState, forwardRef, useImperativeHandle} from 'react';
import {Input, Button, Modal, message} from 'antd';
import {ImportOutlined, ExclamationCircleFilled} from "@ant-design/icons";
import { tauriPost } from "../../utils/http.ts";
interface PublishToCongShangProps {
reportContent?: string;
}
interface PublishToCongShangRef {
handleInitThisWorkVal: (key: string) => void;
}
const {TextArea} = Input;
const { confirm } = Modal;
const PublishToCongShang = forwardRef<PublishToCongShangRef, PublishToCongShangProps>(
({reportContent = ''}, ref) => {
const [thisWorkVal, setThisWorkVal] = useState(reportContent || '');
const [nextPlanVal, setNextPlanVal] = useState('');
const handleImport = () => {
if (!reportContent){
message.warning("无内容可导入");
return;
}
if (thisWorkVal){
confirm({
title: '提醒',
icon: <ExclamationCircleFilled />,
content: '内容已存在,确定覆盖吗',
onOk() {
setThisWorkVal(reportContent)
},
onCancel() {
console.log('取消覆盖');
},
});
return;
}
setThisWorkVal(reportContent)
};
useImperativeHandle(ref, () => ({
handleInitThisWorkVal: (newVal) => {
if (thisWorkVal) return;
setThisWorkVal(newVal)
},
}));
const handleSubmit = async () => {
console.log(thisWorkVal, nextPlanVal);
const congShangId = localStorage.getItem('congShangId');
try {
const response :any = await tauriPost('/apiWebServer', {
realAction: 'report.SPReport/apiInsert',
content: thisWorkVal,
plantowork: nextPlanVal,
stat: 1,
reporttype: '2',
teamid: 26,
sysuserid: congShangId,
spapi_rows: [
{
spapi_rows: [
{
"id": 1035046,
"type": 1,
"name": "田颖",
"num": 1,
"teamid": 26,
}
]
},
{
spapi_rows: []
}
],
})
console.log('Response:', response);
if (response.errcode !== 0) {
message.error(response.message || response.errmsg || '系统错误');
return;
}
message.success("提交成功");
} catch (error) {
console.error('Error submitting data:', error);
}
};
return (
<div className="flex flex-col gap-y-4">
<div>
<div className="flex items-center justify-between">
<div></div>
<ImportOutlined className="cursor-pointer" onClick={handleImport}/>
</div>
<TextArea autoSize={{minRows: 4, maxRows: 20}} value={thisWorkVal}
onChange={(e) => setThisWorkVal(e.target.value)}/>
</div>
<div>
<p></p>
<TextArea autoSize={{minRows: 4, maxRows: 8}} value={nextPlanVal}
onChange={(e) => setNextPlanVal(e.target.value)}/>
</div>
<Button type="primary" onClick={handleSubmit}></Button>
</div>
);
});
export default PublishToCongShang;

View File

@@ -1,8 +1,7 @@
import type React from 'react';
import { useState } from 'react';
import { Card, Tabs, Typography, message, Button } from "antd";
import { processWithDeepSeek } from '../../utils/deepseekApi';
import {useState, useImperativeHandle, forwardRef, useEffect, useRef} from 'react';
import {Button, Card, Drawer, Tabs, Typography} from "antd";
import CommitList from './CommitList';
import PublishToCongShang from "../PublishToCongShang";
interface CommitInfo {
id: string;
@@ -17,82 +16,90 @@ interface ReportPreviewProps {
reportContent?: string;
}
const { TabPane } = Tabs;
export interface ReportPreviewRef {
handleChangeActiveTab: (key: string) => void;
}
const { Title, Paragraph } = Typography;
const ReportPreview: React.FC<ReportPreviewProps> = ({
selectedRepos,
commits = [],
reportContent = '',
}) => {
const [isProcessing, setIsProcessing] = useState(false);
const [processedContent, setProcessedContent] = useState<string>('');
const ReportPreview = forwardRef<ReportPreviewRef, ReportPreviewProps>(
({ selectedRepos, commits = [], reportContent = '' }, ref) => {
const [activeTabKey, setActiveTabKey] = useState<string>("commits");
const previewDivRef = useRef<HTMLDivElement>(null);
const handleAICleanup = async () => {
if (!reportContent) return;
const handleChangeActiveTab = (key: string) => {
setActiveTabKey(key);
};
setIsProcessing(true);
try {
const apiKey = localStorage.getItem("userKey") || "";
let fullResponse = '';
const aiResponse = await processWithDeepSeek(reportContent, apiKey, (chunk) => {
fullResponse += chunk;
setProcessedContent(fullResponse);
});
message.success("AI整理完成");
return fullResponse;
} catch (error) {
message.error("AI整理失败");
console.error("DeepSeek处理失败:", error);
return reportContent;
} finally {
setIsProcessing(false);
useImperativeHandle(ref, () => ({
handleChangeActiveTab,
}));
useEffect(() => {
if (previewDivRef.current) {
previewDivRef.current.scrollTop = previewDivRef.current.scrollHeight;
}
}, [reportContent]);
const [isOpen, setIsOpen] = useState(false);
if (selectedRepos.length === 0) {
return (
<Card className="size-full flex items-center justify-center">
<Typography.Text type="secondary">Git仓库以预览报告内容</Typography.Text>
</Card>
);
}
};
if (selectedRepos.length === 0) {
return (
<Card className="size-full flex items-center justify-center">
<Typography.Text type="secondary">Git仓库以预览报告内容</Typography.Text>
</Card>
);
}
return (
<Card className="size-full">
<Tabs defaultActiveKey="commits">
<TabPane tab="提交记录" key="commits" className="h-full">
const tabItems = [
{
key: "commits",
label: "提交记录",
children: (
<div className="h-[calc(100vh-100px)] overflow-auto">
<CommitList commits={commits} />
</div>
</TabPane>
<TabPane tab="报告预览" key="preview" className="h-full">
<div className="h-[calc(100vh-100px)] overflow-auto">
),
},
{
key: "preview",
label: "报告预览",
children: (
<div className="flex flex-col">
<div className="flex justify-between items-center mb-4">
<Title level={4}></Title>
<Button
type="primary"
loading={isProcessing}
onClick={handleAICleanup}
>
AI整理
</Button>
<Button onClick={() => setIsOpen(true)}></Button>
</div>
<div className="h-[calc(100vh-140px)] overflow-auto" ref={previewDivRef}>
<Paragraph
style={{ whiteSpace: "pre-wrap" }}
>
{reportContent || "暂无报告内容"}
</Paragraph>
</div>
<Paragraph style={{ whiteSpace: "pre-wrap" }}>
{reportContent || "暂无报告内容"}
</Paragraph>
</div>
</TabPane>
<TabPane tab="AI整理预览" key="previeww" className="h-full">
<div className="h-[calc(100vh-100px)] overflow-auto">
<Paragraph style={{ whiteSpace: "pre-wrap" }}>
{processedContent || "暂无内容"}
</Paragraph>
</div>
</TabPane>
</Tabs>
</Card>
);
};
),
},
];
return (
<Card className="size-full">
<Drawer
closable
title="发布到丛商"
open={isOpen}
onClose={() => setIsOpen(false)}
>
<PublishToCongShang reportContent={reportContent} />
</Drawer>
<Tabs
activeKey={activeTabKey}
items={tabItems}
onChange={(activeKey: string) => {
setActiveTabKey(activeKey)
}}
/>
</Card>
);
});
export default ReportPreview;

View File

@@ -9,16 +9,21 @@ interface SettingProps {
const Setting: React.FC<SettingProps> = ({ className }) => {
const [email, setEmail] = useState<string>('');
const [key, setKey] = useState<string>('');
const [congShangId, setCongShangId] = useState<string>('');
useEffect(() => {
const savedEmail = localStorage.getItem('userEmail');
const savedKey = localStorage.getItem('userKey');
const savedCongShangId = localStorage.getItem('congShangId');
if (savedEmail) {
setEmail(savedEmail);
}
if (savedKey) {
setKey(savedKey);
}
if (savedCongShangId) {
setCongShangId(savedCongShangId);
}
}, []);
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -33,6 +38,12 @@ const Setting: React.FC<SettingProps> = ({ className }) => {
localStorage.setItem('userKey', value);
};
const handleChangeCongShangId = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setCongShangId(value);
localStorage.setItem('congShangId', value);
};
return (
<Card
title="设置"
@@ -51,6 +62,12 @@ const Setting: React.FC<SettingProps> = ({ className }) => {
onChange={handleKeyChange}
placeholder="请输入您的Key"
/>
<Input
addonBefore="丛商ID"
value={congShangId}
onChange={handleChangeCongShangId}
placeholder="请输入您的丛商ID"
/>
</Space>
</Card>
);

View File

@@ -1,9 +1,14 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import zhCN from 'antd/locale/zh_CN';
import {ConfigProvider} from "antd";
import 'dayjs/locale/zh-cn';
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
</React.StrictMode>,
);

View File

@@ -1,12 +1,13 @@
import { invoke } from '@tauri-apps/api/core';
import type React from 'react';
import { useState } from 'react';
import { message } from 'antd';
import GitRepo from '../../components/GitRepo';
import Setting from '../../components/Setting';
import ReportPreview from '../../components/ReportPreview';
import type { ReportType } from "../../types/types";
import { getTimeRange } from '../../utils/timeUtils';
import {invoke} from "@tauri-apps/api/core";
import type React from "react";
import {useRef, useState} from "react";
import {message} from "antd";
import GitRepo from "../../components/GitRepo";
import Setting from "../../components/Setting";
import ReportPreview, {ReportPreviewRef} from "../../components/ReportPreview";
import type {ReportType} from "../../types/types";
import {getTimeRange} from "../../utils/timeUtils";
import {processWithDeepSeek} from '../../utils/deepseekApi';
interface GitRepoData {
repoPath: string;
@@ -24,66 +25,104 @@ const Home: React.FC = () => {
const [repos, setRepos] = useState<GitRepoData[]>([]);
const [selectedRepos, setSelectedRepos] = useState<string[]>([]);
const [commits, setCommits] = useState<CommitInfo[]>([]);
const [reportContent, setReportContent] = useState<string>('');
const [reportContent, setReportContent] = useState<string>("");
const [messageApi, contextHolder] = message.useMessage();
const reportPreviewRef = useRef<ReportPreviewRef>(null);
const handleAddRepo = async (repo: GitRepoData) => {
try {
// TODO: 调用Tauri后端API验证仓库路径
setRepos([...repos, repo]);
message.success('仓库添加成功');
message.success("仓库添加成功");
} catch (error) {
message.error('添加仓库失败');
message.error("添加仓库失败");
}
};
const handleGenerateReport = async (reportType: ReportType, useAI: boolean) => {
const handleGenerateReport = async (
reportType: ReportType,
useAI: boolean,
) => {
if (selectedRepos.length === 0) {
message.warning('请先选择仓库');
message.warning("请先选择仓库");
return;
}
messageApi.open({
type: 'loading',
content: '获取git提交记录中...',
duration: 0,
});
try {
const commits = await Promise.all(
selectedRepos.map(async (repoPath) => {
const repoName = repos.find(r => r.repoPath === repoPath)?.name || '';
const repoName =
repos.find((r) => r.repoPath === repoPath)?.name || "";
const userEmail = localStorage.getItem('userEmail');
const result = await invoke<CommitInfo[]>("get_commits", {
const userEmail = localStorage.getItem("userEmail");
return await invoke<CommitInfo[]>("get_commits", {
repoPath,
repoName,
authorFilter: userEmail || null,
timeRange: reportType? getTimeRange(reportType): null,
timeRange: reportType ? getTimeRange(reportType) : null,
});
return result;
})
}),
);
messageApi.destroy();
console.log(commits);
const flatCommits = commits.flat();
setCommits(flatCommits);
let reportText = `已生成 ${selectedRepos.length} 个仓库的报告内容\n\n` +
flatCommits.map(commit =>
`- ${commit.message} (${commit.author}${new Date(parseInt(commit.date) * 1000).toLocaleDateString()})`
).join('\n');
const reportText = `已生成 ${selectedRepos.length} 个仓库的报告内容\n\n${flatCommits
.map(
(commit) =>
`- ${commit.message} (${commit.author}${new Date(Number.parseInt(commit.date) * 1000).toLocaleDateString()})`,
)
.join("\n")}`;
reportPreviewRef.current?.handleChangeActiveTab("preview");
if (useAI) {
messageApi.open({
type: 'loading',
content: '正在思考...',
duration: 0,
});
try {
const aiResponse = await invoke<string>("process_with_deepseek", {
content: reportText
const apiKey = localStorage.getItem("userKey") || "";
let fullResponse = '';
await processWithDeepSeek(reportText, apiKey, (chunk) => {
if (!fullResponse) {
messageApi.destroy();
messageApi.open({
type: 'loading',
content: '正在生成...',
duration: 0,
});
}
fullResponse += chunk;
setReportContent(fullResponse);
});
reportText = aiResponse;
} catch (error) {
console.error("DeepSeek处理失败:", error);
} finally {
messageApi.destroy();
}
}else {
setReportContent(reportText);
}
setReportContent(reportText);
message.success("报告生成成功");
message.success("报告生成完毕");
} catch (error) {
message.error("报告生成失败");
setCommits([]);
setReportContent('');
setReportContent("");
}
};
@@ -91,13 +130,13 @@ const Home: React.FC = () => {
setSelectedRepos(selected);
if (selected.length === 0) {
setCommits([]);
setReportContent('');
setReportContent("");
}
};
return (
<div className="flex size-full overflow-hidden">
{contextHolder}
<div className="h-full w-80 flex flex-col">
<div>
<Setting />
@@ -112,6 +151,7 @@ const Home: React.FC = () => {
</div>
<div className="flex-1 h-full overflow-hidden">
<ReportPreview
ref={reportPreviewRef}
selectedRepos={selectedRepos}
commits={commits}
reportContent={reportContent}

View File

@@ -5,10 +5,6 @@ export const processWithDeepSeek = async (
apiKey: string,
streamFn: (content: string) => void,
): Promise<string> => {
// 预处理git提交记录
const processedContent = content.includes("commit ")
? `请将以下git提交记录整理成结构化的报告格式:\n\n${content}\n\n报告要求:\n1. 按时间顺序列出重要提交\n2. 总结主要变更内容\n3. 分析代码变更趋势`
: content;
const openai = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
@@ -19,11 +15,11 @@ export const processWithDeepSeek = async (
messages: [
{
role: "system",
content: "You are a helpful assistant.",
content: "请将以下工作内容整理成简洁的报告:\n\n# 工作内容报告\n\n## 已完成任务\n1. 列出具体完成的工作任务\n2. 说明主要修改点\n\n请使用Markdown格式输出保持简洁明了。",
},
{
role: "user",
content: processedContent,
content: content,
},
],
model: "deepseek-chat",

View File

@@ -1,65 +1,98 @@
import { fetch } from "@tauri-apps/plugin-http";
import { message } from "antd";
import {fetch} from "@tauri-apps/plugin-http";
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
// const baseURL = `http://192.168.1.105:3000`
const baseURL = `http://im.congshangyun.com:3000`
interface RequestOptions<T = any> {
url: string;
method?: HttpMethod;
headers?: Record<string, string>;
params?: Record<string, string>;
data?: T;
responseType?: 'json' | 'text' | 'blob';
const BODY_TYPE = {
Form: 'Form',
Json: 'Json',
Text: 'Text',
Bytes: 'Bytes',
}
export async function httpRequest<T = any, R = any>(options: RequestOptions<T>): Promise<R> {
const {
url,
method = 'GET',
headers = {},
params = {},
data,
responseType = 'json'
} = options;
const commonOptions = {
timeout: 60,
}
try {
const queryString = new URLSearchParams(params).toString();
const fullUrl = queryString ? `${url}?${queryString}` : url;
const isAbsoluteURL = (url: string): boolean => {
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
}
const response = await fetch<R>(fullUrl, {
method,
headers: {
'Content-Type': 'application/json',
...headers
},
body: data ? JSON.stringify(data) : undefined,
response: responseType
});
console.log(headers);
console.log(response)
const combineURLs = (baseURL: string, relativeURL: string): string => {
return relativeURL
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
: baseURL
}
return response.data;
} catch (error) {
console.error('HTTP请求失败:', error);
message.error('请求失败,请稍后重试');
throw error;
const buildFullPath = (baseURL: string, requestedURL: string) => {
if (baseURL && !isAbsoluteURL(requestedURL)) {
return combineURLs(baseURL, requestedURL)
}
return requestedURL
}
export async function get<T = any, R = any>(url: string, params?: Record<string, string>, options?: Omit<RequestOptions, 'url' | 'method' | 'params'>): Promise<R> {
return httpRequest<T, R>({
url,
method: 'GET',
params,
...options
});
const http = (url: string, options: any = {}) => {
if (!options.headers) options.headers = {}
// 解析 body 类型并设置 Content-Type
if (options?.body) {
if (options.body.type === BODY_TYPE.Form) {
options.headers['Content-Type'] = 'multipart/form-data'
} else if (options.body.type === BODY_TYPE.Json) {
options.headers['Content-Type'] = 'application/json'
options.body = JSON.stringify(options.body.body)
} else if (options.body.type === BODY_TYPE.Text) {
options.headers['Content-Type'] = 'text/plain'
options.body = options.body.body.toString()
} else {
// 默认处理为 JSON
options.headers['Content-Type'] = 'application/json'
options.body = JSON.stringify(options.body)
}
}
options = { ...commonOptions, ...options }
return fetch(buildFullPath(baseURL, url), options)
.then(async (response: any) => {
let responseBody = '';
if (response.body && typeof response.body.getReader === 'function') {
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
responseBody += decoder.decode(value, { stream: true });
}
// 完成解码后处理最终字符串
responseBody += decoder.decode(); // flush remaining input
} else {
responseBody = response.data || 'No data';
}
try {
return JSON.parse(responseBody);
} catch (e) {
console.warn('Response is not JSON, returning raw text:', e);
return { data: responseBody };
}
})
.catch((err) => {
console.error(err)
return Promise.reject(err)
})
}
export async function post<T = any, R = any>(url: string, data?: T, options?: Omit<RequestOptions<T>, 'url' | 'method' | 'data'>): Promise<R> {
return httpRequest<T, R>({
url,
method: 'POST',
data,
...options
});
export const tauriGet = (url: string, options: any = {}) => {
options.method = 'GET'
return http(url, options)
}
export const tauriPost = (url: string, body: any, options: any = {}) => {
options.method = 'POST'
options.body = {
type: BODY_TYPE.Json,
body: body
}
return http(url, options)
}