feat: 项目优化,丛商对接完成
1
bun.lock
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
After Width: | Height: | Size: 8.9 KiB |
28
src-tauri/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "cs-auto-report"
|
||||
name = "DevReport"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
|
||||
@@ -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" }]
|
||||
}
|
||||
]
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 51 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 24 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
116
src/components/PublishToCongShang/index.tsx
Normal 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;
|
||||
@@ -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,39 +16,33 @@ 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;
|
||||
|
||||
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);
|
||||
}
|
||||
const handleChangeActiveTab = (key: string) => {
|
||||
setActiveTabKey(key);
|
||||
};
|
||||
|
||||
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">
|
||||
@@ -57,42 +50,56 @@ const ReportPreview: React.FC<ReportPreviewProps> = ({
|
||||
</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>
|
||||
<Paragraph style={{ whiteSpace: "pre-wrap" }}>
|
||||
<div className="h-[calc(100vh-140px)] overflow-auto" ref={previewDivRef}>
|
||||
<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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<App />
|
||||
</ConfigProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
try {
|
||||
const aiResponse = await invoke<string>("process_with_deepseek", {
|
||||
content: reportText
|
||||
messageApi.open({
|
||||
type: 'loading',
|
||||
content: '正在思考...',
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||