feat: 项目优化,丛商对接完成
1
bun.lock
@@ -4,6 +4,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "cs-auto-report",
|
"name": "cs-auto-report",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "5.x",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2",
|
"@tauri-apps/plugin-dialog": "^2",
|
||||||
"@tauri-apps/plugin-http": "~2",
|
"@tauri-apps/plugin-http": "~2",
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>从商报告生成器</title>
|
<title>丛商报告生成器</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "cs-auto-report",
|
"name": "dev_report",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "index.html",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"vite:dev": "vite",
|
"vite:dev": "vite",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"lint": "bunx @biomejs/biome lint ./src",
|
"lint": "bunx @biomejs/biome lint ./src",
|
||||||
"format": "bunx @biomejs/biome format --write ./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": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "5.x",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2",
|
"@tauri-apps/plugin-dialog": "^2",
|
||||||
"@tauri-apps/plugin-http": "~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.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
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]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.24.2"
|
version = "0.24.2"
|
||||||
@@ -665,20 +679,6 @@ dependencies = [
|
|||||||
"typenum",
|
"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]]
|
[[package]]
|
||||||
name = "cssparser"
|
name = "cssparser"
|
||||||
version = "0.27.2"
|
version = "0.27.2"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cs-auto-report"
|
name = "DevReport"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
"dialog:default",
|
"dialog:default",
|
||||||
{
|
{
|
||||||
"identifier": "http: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" }]
|
"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",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "cs-auto-report",
|
"productName": "DevReport",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.cs-auto-report.app",
|
"identifier": "com.DevReport.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run vite:dev",
|
"beforeDevCommand": "bun run vite:dev",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:1420",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "从商报告生成器",
|
"title": "丛商报告生成器",
|
||||||
"width": 1200,
|
"width": 1200,
|
||||||
"height": 800
|
"height": 800
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
|
|||||||
const [reportType, setReportType] = useState<ReportType>(
|
const [reportType, setReportType] = useState<ReportType>(
|
||||||
ReportType.WEEKLY_REPORT,
|
ReportType.WEEKLY_REPORT,
|
||||||
);
|
);
|
||||||
const [useAI, setUseAI] = useState(false);
|
const [useAI, setUseAI] = useState(true);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
onSubmit(reportType, useAI);
|
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, useImperativeHandle, forwardRef, useEffect, useRef} from 'react';
|
||||||
import { useState } from 'react';
|
import {Button, Card, Drawer, Tabs, Typography} from "antd";
|
||||||
import { Card, Tabs, Typography, message, Button } from "antd";
|
|
||||||
import { processWithDeepSeek } from '../../utils/deepseekApi';
|
|
||||||
import CommitList from './CommitList';
|
import CommitList from './CommitList';
|
||||||
|
import PublishToCongShang from "../PublishToCongShang";
|
||||||
|
|
||||||
interface CommitInfo {
|
interface CommitInfo {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,82 +16,90 @@ interface ReportPreviewProps {
|
|||||||
reportContent?: string;
|
reportContent?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
export interface ReportPreviewRef {
|
||||||
|
handleChangeActiveTab: (key: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
const { Title, Paragraph } = Typography;
|
const { Title, Paragraph } = Typography;
|
||||||
|
|
||||||
const ReportPreview: React.FC<ReportPreviewProps> = ({
|
const ReportPreview = forwardRef<ReportPreviewRef, ReportPreviewProps>(
|
||||||
selectedRepos,
|
({ selectedRepos, commits = [], reportContent = '' }, ref) => {
|
||||||
commits = [],
|
const [activeTabKey, setActiveTabKey] = useState<string>("commits");
|
||||||
reportContent = '',
|
const previewDivRef = useRef<HTMLDivElement>(null);
|
||||||
}) => {
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
|
||||||
const [processedContent, setProcessedContent] = useState<string>('');
|
|
||||||
|
|
||||||
const handleAICleanup = async () => {
|
const handleChangeActiveTab = (key: string) => {
|
||||||
if (!reportContent) return;
|
setActiveTabKey(key);
|
||||||
|
};
|
||||||
|
|
||||||
setIsProcessing(true);
|
useImperativeHandle(ref, () => ({
|
||||||
try {
|
handleChangeActiveTab,
|
||||||
const apiKey = localStorage.getItem("userKey") || "";
|
}));
|
||||||
let fullResponse = '';
|
|
||||||
const aiResponse = await processWithDeepSeek(reportContent, apiKey, (chunk) => {
|
useEffect(() => {
|
||||||
fullResponse += chunk;
|
if (previewDivRef.current) {
|
||||||
setProcessedContent(fullResponse);
|
previewDivRef.current.scrollTop = previewDivRef.current.scrollHeight;
|
||||||
});
|
}
|
||||||
message.success("AI整理完成");
|
}, [reportContent]);
|
||||||
return fullResponse;
|
|
||||||
} catch (error) {
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
message.error("AI整理失败");
|
|
||||||
console.error("DeepSeek处理失败:", error);
|
if (selectedRepos.length === 0) {
|
||||||
return reportContent;
|
return (
|
||||||
} finally {
|
<Card className="size-full flex items-center justify-center">
|
||||||
setIsProcessing(false);
|
<Typography.Text type="secondary">请选择Git仓库以预览报告内容</Typography.Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
const tabItems = [
|
||||||
|
{
|
||||||
if (selectedRepos.length === 0) {
|
key: "commits",
|
||||||
return (
|
label: "提交记录",
|
||||||
<Card className="size-full flex items-center justify-center">
|
children: (
|
||||||
<Typography.Text type="secondary">请选择Git仓库以预览报告内容</Typography.Text>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="size-full">
|
|
||||||
<Tabs defaultActiveKey="commits">
|
|
||||||
<TabPane tab="提交记录" key="commits" className="h-full">
|
|
||||||
<div className="h-[calc(100vh-100px)] overflow-auto">
|
<div className="h-[calc(100vh-100px)] overflow-auto">
|
||||||
<CommitList commits={commits} />
|
<CommitList commits={commits} />
|
||||||
</div>
|
</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">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<Title level={4}>报告内容</Title>
|
<Title level={4}>报告内容</Title>
|
||||||
<Button
|
<Button onClick={() => setIsOpen(true)}>发布到丛商</Button>
|
||||||
type="primary"
|
</div>
|
||||||
loading={isProcessing}
|
<div className="h-[calc(100vh-140px)] overflow-auto" ref={previewDivRef}>
|
||||||
onClick={handleAICleanup}
|
<Paragraph
|
||||||
>
|
style={{ whiteSpace: "pre-wrap" }}
|
||||||
AI整理
|
>
|
||||||
</Button>
|
{reportContent || "暂无报告内容"}
|
||||||
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
<Paragraph style={{ whiteSpace: "pre-wrap" }}>
|
|
||||||
{reportContent || "暂无报告内容"}
|
|
||||||
</Paragraph>
|
|
||||||
</div>
|
</div>
|
||||||
</TabPane>
|
),
|
||||||
<TabPane tab="AI整理预览" key="previeww" className="h-full">
|
},
|
||||||
<div className="h-[calc(100vh-100px)] overflow-auto">
|
];
|
||||||
<Paragraph style={{ whiteSpace: "pre-wrap" }}>
|
|
||||||
{processedContent || "暂无内容"}
|
return (
|
||||||
</Paragraph>
|
<Card className="size-full">
|
||||||
</div>
|
<Drawer
|
||||||
</TabPane>
|
closable
|
||||||
</Tabs>
|
title="发布到丛商"
|
||||||
</Card>
|
open={isOpen}
|
||||||
);
|
onClose={() => setIsOpen(false)}
|
||||||
};
|
>
|
||||||
|
<PublishToCongShang reportContent={reportContent} />
|
||||||
|
</Drawer>
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTabKey}
|
||||||
|
items={tabItems}
|
||||||
|
onChange={(activeKey: string) => {
|
||||||
|
setActiveTabKey(activeKey)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default ReportPreview;
|
export default ReportPreview;
|
||||||
@@ -9,16 +9,21 @@ interface SettingProps {
|
|||||||
const Setting: React.FC<SettingProps> = ({ className }) => {
|
const Setting: React.FC<SettingProps> = ({ className }) => {
|
||||||
const [email, setEmail] = useState<string>('');
|
const [email, setEmail] = useState<string>('');
|
||||||
const [key, setKey] = useState<string>('');
|
const [key, setKey] = useState<string>('');
|
||||||
|
const [congShangId, setCongShangId] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedEmail = localStorage.getItem('userEmail');
|
const savedEmail = localStorage.getItem('userEmail');
|
||||||
const savedKey = localStorage.getItem('userKey');
|
const savedKey = localStorage.getItem('userKey');
|
||||||
|
const savedCongShangId = localStorage.getItem('congShangId');
|
||||||
if (savedEmail) {
|
if (savedEmail) {
|
||||||
setEmail(savedEmail);
|
setEmail(savedEmail);
|
||||||
}
|
}
|
||||||
if (savedKey) {
|
if (savedKey) {
|
||||||
setKey(savedKey);
|
setKey(savedKey);
|
||||||
}
|
}
|
||||||
|
if (savedCongShangId) {
|
||||||
|
setCongShangId(savedCongShangId);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -33,6 +38,12 @@ const Setting: React.FC<SettingProps> = ({ className }) => {
|
|||||||
localStorage.setItem('userKey', value);
|
localStorage.setItem('userKey', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeCongShangId = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setCongShangId(value);
|
||||||
|
localStorage.setItem('congShangId', value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title="设置"
|
title="设置"
|
||||||
@@ -51,6 +62,12 @@ const Setting: React.FC<SettingProps> = ({ className }) => {
|
|||||||
onChange={handleKeyChange}
|
onChange={handleKeyChange}
|
||||||
placeholder="请输入您的Key"
|
placeholder="请输入您的Key"
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
addonBefore="丛商ID"
|
||||||
|
value={congShangId}
|
||||||
|
onChange={handleChangeCongShangId}
|
||||||
|
placeholder="请输入您的丛商ID"
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
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(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ConfigProvider locale={zhCN}>
|
||||||
|
<App />
|
||||||
|
</ConfigProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import {invoke} from "@tauri-apps/api/core";
|
||||||
import type React from 'react';
|
import type React from "react";
|
||||||
import { useState } from 'react';
|
import {useRef, useState} from "react";
|
||||||
import { message } from 'antd';
|
import {message} from "antd";
|
||||||
import GitRepo from '../../components/GitRepo';
|
import GitRepo from "../../components/GitRepo";
|
||||||
import Setting from '../../components/Setting';
|
import Setting from "../../components/Setting";
|
||||||
import ReportPreview from '../../components/ReportPreview';
|
import ReportPreview, {ReportPreviewRef} from "../../components/ReportPreview";
|
||||||
import type { ReportType } from "../../types/types";
|
import type {ReportType} from "../../types/types";
|
||||||
import { getTimeRange } from '../../utils/timeUtils';
|
import {getTimeRange} from "../../utils/timeUtils";
|
||||||
|
import {processWithDeepSeek} from '../../utils/deepseekApi';
|
||||||
|
|
||||||
interface GitRepoData {
|
interface GitRepoData {
|
||||||
repoPath: string;
|
repoPath: string;
|
||||||
@@ -24,66 +25,104 @@ const Home: React.FC = () => {
|
|||||||
const [repos, setRepos] = useState<GitRepoData[]>([]);
|
const [repos, setRepos] = useState<GitRepoData[]>([]);
|
||||||
const [selectedRepos, setSelectedRepos] = useState<string[]>([]);
|
const [selectedRepos, setSelectedRepos] = useState<string[]>([]);
|
||||||
const [commits, setCommits] = useState<CommitInfo[]>([]);
|
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) => {
|
const handleAddRepo = async (repo: GitRepoData) => {
|
||||||
try {
|
try {
|
||||||
// TODO: 调用Tauri后端API验证仓库路径
|
// TODO: 调用Tauri后端API验证仓库路径
|
||||||
setRepos([...repos, repo]);
|
setRepos([...repos, repo]);
|
||||||
message.success('仓库添加成功');
|
message.success("仓库添加成功");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('添加仓库失败');
|
message.error("添加仓库失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGenerateReport = async (reportType: ReportType, useAI: boolean) => {
|
const handleGenerateReport = async (
|
||||||
|
reportType: ReportType,
|
||||||
|
useAI: boolean,
|
||||||
|
) => {
|
||||||
if (selectedRepos.length === 0) {
|
if (selectedRepos.length === 0) {
|
||||||
message.warning('请先选择仓库');
|
message.warning("请先选择仓库");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messageApi.open({
|
||||||
|
type: 'loading',
|
||||||
|
content: '获取git提交记录中...',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const commits = await Promise.all(
|
const commits = await Promise.all(
|
||||||
selectedRepos.map(async (repoPath) => {
|
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 userEmail = localStorage.getItem("userEmail");
|
||||||
const result = await invoke<CommitInfo[]>("get_commits", {
|
return await invoke<CommitInfo[]>("get_commits", {
|
||||||
repoPath,
|
repoPath,
|
||||||
repoName,
|
repoName,
|
||||||
authorFilter: userEmail || null,
|
authorFilter: userEmail || null,
|
||||||
timeRange: reportType? getTimeRange(reportType): null,
|
timeRange: reportType ? getTimeRange(reportType) : null,
|
||||||
});
|
});
|
||||||
return result;
|
}),
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
messageApi.destroy();
|
||||||
console.log(commits);
|
console.log(commits);
|
||||||
|
|
||||||
const flatCommits = commits.flat();
|
const flatCommits = commits.flat();
|
||||||
setCommits(flatCommits);
|
setCommits(flatCommits);
|
||||||
|
|
||||||
let reportText = `已生成 ${selectedRepos.length} 个仓库的报告内容\n\n` +
|
const reportText = `已生成 ${selectedRepos.length} 个仓库的报告内容\n\n${flatCommits
|
||||||
flatCommits.map(commit =>
|
.map(
|
||||||
`- ${commit.message} (${commit.author} 于 ${new Date(parseInt(commit.date) * 1000).toLocaleDateString()})`
|
(commit) =>
|
||||||
).join('\n');
|
`- ${commit.message} (${commit.author} 于 ${new Date(Number.parseInt(commit.date) * 1000).toLocaleDateString()})`,
|
||||||
|
)
|
||||||
|
.join("\n")}`;
|
||||||
|
|
||||||
|
reportPreviewRef.current?.handleChangeActiveTab("preview");
|
||||||
|
|
||||||
if (useAI) {
|
if (useAI) {
|
||||||
|
messageApi.open({
|
||||||
|
type: 'loading',
|
||||||
|
content: '正在思考...',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const aiResponse = await invoke<string>("process_with_deepseek", {
|
const apiKey = localStorage.getItem("userKey") || "";
|
||||||
content: reportText
|
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) {
|
} catch (error) {
|
||||||
console.error("DeepSeek处理失败:", error);
|
console.error("DeepSeek处理失败:", error);
|
||||||
|
} finally {
|
||||||
|
messageApi.destroy();
|
||||||
}
|
}
|
||||||
|
}else {
|
||||||
|
setReportContent(reportText);
|
||||||
}
|
}
|
||||||
|
|
||||||
setReportContent(reportText);
|
message.success("报告生成完毕");
|
||||||
message.success("报告生成成功");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error("报告生成失败");
|
message.error("报告生成失败");
|
||||||
setCommits([]);
|
setCommits([]);
|
||||||
setReportContent('');
|
setReportContent("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,13 +130,13 @@ const Home: React.FC = () => {
|
|||||||
setSelectedRepos(selected);
|
setSelectedRepos(selected);
|
||||||
if (selected.length === 0) {
|
if (selected.length === 0) {
|
||||||
setCommits([]);
|
setCommits([]);
|
||||||
setReportContent('');
|
setReportContent("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full overflow-hidden">
|
<div className="flex size-full overflow-hidden">
|
||||||
|
{contextHolder}
|
||||||
<div className="h-full w-80 flex flex-col">
|
<div className="h-full w-80 flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<Setting />
|
<Setting />
|
||||||
@@ -112,6 +151,7 @@ const Home: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 h-full overflow-hidden">
|
<div className="flex-1 h-full overflow-hidden">
|
||||||
<ReportPreview
|
<ReportPreview
|
||||||
|
ref={reportPreviewRef}
|
||||||
selectedRepos={selectedRepos}
|
selectedRepos={selectedRepos}
|
||||||
commits={commits}
|
commits={commits}
|
||||||
reportContent={reportContent}
|
reportContent={reportContent}
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ export const processWithDeepSeek = async (
|
|||||||
apiKey: string,
|
apiKey: string,
|
||||||
streamFn: (content: string) => void,
|
streamFn: (content: string) => void,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
// 预处理git提交记录
|
|
||||||
const processedContent = content.includes("commit ")
|
|
||||||
? `请将以下git提交记录整理成结构化的报告格式:\n\n${content}\n\n报告要求:\n1. 按时间顺序列出重要提交\n2. 总结主要变更内容\n3. 分析代码变更趋势`
|
|
||||||
: content;
|
|
||||||
|
|
||||||
const openai = new OpenAI({
|
const openai = new OpenAI({
|
||||||
baseURL: "https://api.deepseek.com/v1",
|
baseURL: "https://api.deepseek.com/v1",
|
||||||
@@ -19,11 +15,11 @@ export const processWithDeepSeek = async (
|
|||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content: "You are a helpful assistant.",
|
content: "请将以下工作内容整理成简洁的报告:\n\n# 工作内容报告\n\n## 已完成任务\n1. 列出具体完成的工作任务\n2. 说明主要修改点\n\n请使用Markdown格式输出,保持简洁明了。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: processedContent,
|
content: content,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
model: "deepseek-chat",
|
model: "deepseek-chat",
|
||||||
|
|||||||
@@ -1,65 +1,98 @@
|
|||||||
import { fetch } from "@tauri-apps/plugin-http";
|
import {fetch} from "@tauri-apps/plugin-http";
|
||||||
import { message } from "antd";
|
|
||||||
|
|
||||||
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> {
|
const BODY_TYPE = {
|
||||||
url: string;
|
Form: 'Form',
|
||||||
method?: HttpMethod;
|
Json: 'Json',
|
||||||
headers?: Record<string, string>;
|
Text: 'Text',
|
||||||
params?: Record<string, string>;
|
Bytes: 'Bytes',
|
||||||
data?: T;
|
|
||||||
responseType?: 'json' | 'text' | 'blob';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function httpRequest<T = any, R = any>(options: RequestOptions<T>): Promise<R> {
|
const commonOptions = {
|
||||||
const {
|
timeout: 60,
|
||||||
url,
|
}
|
||||||
method = 'GET',
|
|
||||||
headers = {},
|
|
||||||
params = {},
|
|
||||||
data,
|
|
||||||
responseType = 'json'
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
try {
|
const isAbsoluteURL = (url: string): boolean => {
|
||||||
const queryString = new URLSearchParams(params).toString();
|
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
|
||||||
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
}
|
||||||
|
|
||||||
const response = await fetch<R>(fullUrl, {
|
const combineURLs = (baseURL: string, relativeURL: string): string => {
|
||||||
method,
|
return relativeURL
|
||||||
headers: {
|
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
|
||||||
'Content-Type': 'application/json',
|
: baseURL
|
||||||
...headers
|
}
|
||||||
},
|
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
|
||||||
response: responseType
|
|
||||||
});
|
|
||||||
console.log(headers);
|
|
||||||
console.log(response)
|
|
||||||
|
|
||||||
return response.data;
|
const buildFullPath = (baseURL: string, requestedURL: string) => {
|
||||||
} catch (error) {
|
if (baseURL && !isAbsoluteURL(requestedURL)) {
|
||||||
console.error('HTTP请求失败:', error);
|
return combineURLs(baseURL, requestedURL)
|
||||||
message.error('请求失败,请稍后重试');
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
return requestedURL
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get<T = any, R = any>(url: string, params?: Record<string, string>, options?: Omit<RequestOptions, 'url' | 'method' | 'params'>): Promise<R> {
|
const http = (url: string, options: any = {}) => {
|
||||||
return httpRequest<T, R>({
|
if (!options.headers) options.headers = {}
|
||||||
url,
|
|
||||||
method: 'GET',
|
// 解析 body 类型并设置 Content-Type
|
||||||
params,
|
if (options?.body) {
|
||||||
...options
|
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> {
|
export const tauriGet = (url: string, options: any = {}) => {
|
||||||
return httpRequest<T, R>({
|
options.method = 'GET'
|
||||||
url,
|
return http(url, options)
|
||||||
method: 'POST',
|
}
|
||||||
data,
|
export const tauriPost = (url: string, body: any, options: any = {}) => {
|
||||||
...options
|
options.method = 'POST'
|
||||||
});
|
options.body = {
|
||||||
|
type: BODY_TYPE.Json,
|
||||||
|
body: body
|
||||||
|
}
|
||||||
|
return http(url, options)
|
||||||
}
|
}
|
||||||