导出模块

This commit is contained in:
liamzi
2024-12-31 13:40:08 +08:00
parent d43fca017c
commit d129178dbb
6 changed files with 865 additions and 131 deletions

View File

@@ -16,9 +16,14 @@
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@mantine/code-highlight": "^7.15.2",
"@mantine/core": "^7.15.2",
"@mantine/hooks": "^7.15.2",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@supabase/supabase-js": "^2.38.4", "@supabase/supabase-js": "^2.38.4",
"ai": "^4.0.22",
"antd": "^5.11.0", "antd": "^5.11.0",
"code-highlight": "^1.0.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dnd-kit": "^0.0.2", "dnd-kit": "^0.0.2",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
@@ -29,6 +34,7 @@
"react-icons": "^5.4.0", "react-icons": "^5.4.0",
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",
"react-router-dom": "^6.18.0", "react-router-dom": "^6.18.0",
"react-use": "^17.6.0",
"recharts": "^2.9.0", "recharts": "^2.9.0",
"styled-components": "^6.1.0", "styled-components": "^6.1.0",
"uuid": "^11.0.3" "uuid": "^11.0.3"

658
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,18 @@ import AppRoutes from "./routes/AppRoutes";
import { RootStoreProvider } from "./contexts/RootStore"; import { RootStoreProvider } from "./contexts/RootStore";
import { MantineProvider } from '@mantine/core';
import '@mantine/core/styles.css';
import '@mantine/code-highlight/styles.css';
const App = () => { const App = () => {
return ( return (
<BrowserRouter> <BrowserRouter>
<AuthProvider> <AuthProvider>
<RootStoreProvider> <RootStoreProvider>
<ThemeProvider> <ThemeProvider>
<MantineProvider>
<AppRoutes /> <AppRoutes />
</MantineProvider>
</ThemeProvider> </ThemeProvider>
</RootStoreProvider> </RootStoreProvider>
</AuthProvider> </AuthProvider>

View File

@@ -0,0 +1,143 @@
import { Button, Drawer, Input, Space, message } from 'antd';
import { useChat } from "ai/react";
import { CodeHighlight } from "@mantine/code-highlight";
import { DownloadOutlined } from '@ant-design/icons';
import { useEffect, useRef } from 'react';
export default function ChatAIDrawer({ open, onClose, onExport }) {
const STORAGE_KEY = 'chat_history';
const { messages, input, handleSubmit, handleInputChange, isLoading, setMessages } = useChat({
api: "https://test-ai-quirkyai.vercel.app/api/chat",
initialMessages: JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'),
});
const messagesEndRef = useRef(null);
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
}, [messages]);
// 新消息时自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// 修改导出函数,接收单条消息内容
const handleExport = (content) => {
try {
const jsonContent = JSON.parse(content);
onExport?.(jsonContent);
} catch (error) {
console.log(error);
message.error('导出失败,请重试');
}
};
return (
<Drawer
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>AI 助手</span>
<Button
size="small"
onClick={() => {
setMessages([]);
localStorage.removeItem(STORAGE_KEY);
message.success('历史记录已清空');
}}
>
清空历史
</Button>
</div>
}
placement="right"
width={600}
open={open}
onClose={onClose}
>
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 108px)' }}>
<div style={{
flex: 1,
overflowY: 'auto',
marginBottom: 16,
padding: '0 16px'
}}>
{messages.map((message) => (
<div
key={message.id}
style={{
marginBottom: 16,
backgroundColor: message.role === 'assistant' ? '#f0f9ff' : '#f8f9fa',
padding: 16,
borderRadius: 12,
boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
}}
>
<div style={{
fontWeight: 'bold',
marginBottom: 8,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
color: message.role === 'assistant' ? '#1677ff' : '#52525b'
}}>
<span>{message.role === 'assistant' ? 'AI 助手' : '用户'}</span>
{message.role === 'assistant' && (
<Button
type="link"
size="small"
icon={<DownloadOutlined />}
onClick={() => handleExport(message.content)}
>
导出
</Button>
)}
</div>
<div>
{message.role === "assistant" ? (
<CodeHighlight
code={message.content}
language="json"
copyLabel="复制代码"
copiedLabel="已复制!"
withLineNumbers
/>
) : (
<div style={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}>
{message.content}
</div>
)}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<form onSubmit={handleSubmit} style={{
padding: '16px',
borderTop: '1px solid #f0f0f0',
backgroundColor: '#fff'
}}>
<Space.Compact style={{ width: '100%' }}>
<Input
value={input}
placeholder="请输入您的问题..."
onChange={handleInputChange}
disabled={isLoading}
style={{ borderRadius: '6px 0 0 6px' }}
/>
<Button
type="primary"
htmlType="submit"
loading={isLoading}
style={{ borderRadius: '0 6px 6px 0' }}
>
发送
</Button>
</Space.Compact>
</form>
</div>
</Drawer>
);
}

View File

@@ -23,6 +23,8 @@ import { supabase } from "@/config/supabase";
import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import SectionList from '@/components/SectionList' import SectionList from '@/components/SectionList'
import ChatAIDrawer from '@/components/ChatAi';
const { Title } = Typography; const { Title } = Typography;
// 添加货币符号映射 // 添加货币符号映射
@@ -275,119 +277,7 @@ const QuotationForm = () => {
setTemplateModalVisible(false); setTemplateModalVisible(false);
}; };
// 添加模板选择弹窗内容渲染方法
const renderTemplateModalContent = () => (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{availableSections.map((section) => (
<div
key={section.id}
className="group relative bg-white dark:bg-gray-800 rounded-lg shadow-sm
border border-gray-200 dark:border-gray-700 hover:shadow-lg
transition-all duration-300 cursor-pointer"
onClick={() => handleUseTemplate(section)}
>
<div className="p-6">
{/* 标题和项目数量 */}
<div className="text-center mb-4">
<h3
className="text-lg font-medium text-gray-900 dark:text-gray-100
group-hover:text-blue-500 transition-colors"
>
{section.attributes.name}
</h3>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{section.attributes.items?.length || 0} 个项目
</div>
</div>
{/* 项目列表预览 */}
<div className="space-y-2 mt-4 border-t dark:border-gray-700 pt-4">
{(section.attributes.items || [])
.slice(0, 3)
.map((item, index) => (
<div
key={index}
className="flex justify-between items-center"
>
<span className="text-sm text-gray-600 dark:text-gray-300 truncate flex-1">
{item.name}
</span>
<span className="text-sm text-gray-500 dark:text-gray-400 ml-2">
{formatCurrency(item.price)}
</span>
</div>
))}
{(section.attributes.items || []).length > 3 && (
<div className="text-sm text-gray-500 dark:text-gray-400 text-center">
还有 {section.attributes.items.length - 3} 个项目...
</div>
)}
</div>
{/* 小节总金额 */}
<div className="mt-4 pt-4 border-t dark:border-gray-700 flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-300">
总金额
</span>
<span className="text-base font-medium text-blue-500">
{formatCurrency(
(section.attributes.items || []).reduce(
(sum, item) =>
sum + (item.price * (item.quantity || 1) || 0),
0
)
)}
</span>
</div>
</div>
{/* 悬边框效果 */}
<div
className="absolute inset-0 border-2 border-transparent
group-hover:border-blue-500 rounded-lg transition-colors duration-300"
/>
{/* 选择指示器 */}
<div
className="absolute top-3 right-3 opacity-0 group-hover:opacity-100
transition-opacity duration-300"
>
<Button
type="primary"
size="small"
className="flex items-center gap-1"
icon={<PlusOutlined />}
>
套用
</Button>
</div>
</div>
))}
</div>
{availableSections.length === 0 && (
<div className="text-center py-8">
<div className="text-gray-500 dark:text-gray-400">暂无可用模版</div>
</div>
)}
<Divider className="dark:border-gray-700" />
<div className="flex justify-center">
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={handleCreateCustom}
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500
dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400
transition-all duration-300"
>
自定义小节
</Button>
</div>
</div>
);
const fetchTemplateData = async () => { const fetchTemplateData = async () => {
try { try {
@@ -533,7 +423,25 @@ const QuotationForm = () => {
} }
}, [id, templateId]); }, [id, templateId]);
const [open, setOpen] = useState(false);
const handleExport = (data) => {
const _data={
...data,
key: uuidv4(),
}
const newSections = [...formValues.sections, _data];
console.log(newSections,'newSections');
form.setFieldValue('sections', newSections);
const currentFormValues = form.getFieldsValue();
setFormValues({
...currentFormValues,
sections: newSections
});
message.success('已添加新的服务项目');
setOpen(false);
};
return ( return (
<div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2"> <div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
@@ -561,6 +469,8 @@ const QuotationForm = () => {
返回 返回
</Button> </Button>
{!isView && ( {!isView && (
<>
<Button <Button
type="primary" type="primary"
icon={<SaveOutlined />} icon={<SaveOutlined />}
@@ -569,7 +479,13 @@ const QuotationForm = () => {
> >
保存 保存
</Button> </Button>
<Button onClick={() => setOpen(true)}>
AI 助手
</Button>
</>
)} )}
</Space> </Space>
</div> </div>
} }
@@ -655,6 +571,7 @@ const QuotationForm = () => {
} }
bordered={false} bordered={false}
> >
<SectionList <SectionList
type="quotation" type="quotation"
form={form} form={form}
@@ -667,7 +584,11 @@ const QuotationForm = () => {
</Card> </Card>
</Form> </Form>
</Card> </Card>
<ChatAIDrawer
open={open}
onClose={() => setOpen(false)}
onExport={handleExport}
/>
</div> </div>
); );
}; };

View File

@@ -3,6 +3,7 @@
@tailwind utilities; @tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap');
:root { :root {
--primary-color: #1677ff; --primary-color: #1677ff;
--primary-gradient: linear-gradient(45deg, #1677ff, #36cff0); --primary-gradient: linear-gradient(45deg, #1677ff, #36cff0);