导出模块
This commit is contained in:
@@ -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
658
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
|||||||
143
src/components/ChatAi/index.jsx
Normal file
143
src/components/ChatAi/index.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
// 添加货币符号映射
|
// 添加货币符号映射
|
||||||
@@ -105,11 +107,11 @@ const QuotationForm = () => {
|
|||||||
},
|
},
|
||||||
[currentCurrency]
|
[currentCurrency]
|
||||||
);
|
);
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
console.log(currentCurrency,'currency');
|
console.log(currentCurrency, 'currency');
|
||||||
|
|
||||||
},[currentCurrency])
|
}, [currentCurrency])
|
||||||
// 处理表单值变化
|
// 处理表单值变化
|
||||||
const handleValuesChange = (changedValues, allValues) => {
|
const handleValuesChange = (changedValues, allValues) => {
|
||||||
setFormValues(allValues);
|
setFormValues(allValues);
|
||||||
if (changedValues.currency) {
|
if (changedValues.currency) {
|
||||||
@@ -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,15 +469,23 @@ const QuotationForm = () => {
|
|||||||
返回
|
返回
|
||||||
</Button>
|
</Button>
|
||||||
{!isView && (
|
{!isView && (
|
||||||
<Button
|
<>
|
||||||
type="primary"
|
|
||||||
icon={<SaveOutlined />}
|
<Button
|
||||||
onClick={() => form.submit()}
|
type="primary"
|
||||||
loading={loading}
|
icon={<SaveOutlined />}
|
||||||
>
|
onClick={() => form.submit()}
|
||||||
保存
|
loading={loading}
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user