提交 f5a609cd authored 作者: 王鹏飞's avatar 王鹏飞

feat(data): 新增内置数据集功能

- 添加创建、删除和列出内置数据集的 API 接口 - 实现数据集创建和删除的 React Hook - 开发数据集列表和表单组件 - 集成 AI 聊天功能 - 优化文件上传组件
上级 1dd30bce
......@@ -24,6 +24,7 @@
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/blueimp-md5": "^2.18.2",
"@types/node": "^22.13.9",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
......@@ -1421,6 +1422,13 @@
"react": "^18 || ^19"
}
},
"node_modules/@types/blueimp-md5": {
"version": "2.18.2",
"resolved": "https://registry.npmjs.org/@types/blueimp-md5/-/blueimp-md5-2.18.2.tgz",
"integrity": "sha512-dJ9yRry9Olt5GAWlgCtE5dK9d/Dfhn/V7hna86eEO2Pn76+E8Y0S0n61iEUEGhWXXgtKtHxtZLVNwL8X+vLHzg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
......
......@@ -26,6 +26,7 @@
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/blueimp-md5": "^2.18.2",
"@types/node": "^22.13.9",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
......
......@@ -6,11 +6,11 @@ import { useQuery } from '@tanstack/react-query'
export interface QueryParams {
[key: string]: any
page: number
pageSize: number
'per-page': number
}
export interface AppListProps extends TableProps {
fetchApi?: (params: QueryParams) => Promise<{ list: any[]; total: number }>
fetchApi?: (params?: QueryParams) => Promise<{ list: any[]; total: number }>
filters?: any[]
filterAside?: React.ReactNode
}
......@@ -24,13 +24,12 @@ const AppList = forwardRef<AppListRef, AppListProps>((props, ref) => {
const { fetchApi, filters = [], filterAside, dataSource = [], ...rest } = props
const [form] = Form.useForm()
const [queryParams, setQueryParams] = useState<QueryParams>({ page: 1, pageSize: 10 })
const [queryParams, setQueryParams] = useState<QueryParams>({ page: 1, 'per-page': 10 })
const { data, refetch, isLoading } = useQuery({
queryKey: ['appList', queryParams],
queryFn: async () => {
const result = fetchApi ? await fetchApi(queryParams) : { list: dataSource || [], total: dataSource?.length || 0 }
return { ...result, list: result.list as any[] }
return fetchApi ? await fetchApi(queryParams) : { list: dataSource || [], total: dataSource?.length || 0 }
},
})
......@@ -48,12 +47,12 @@ const AppList = forwardRef<AppListRef, AppListProps>((props, ref) => {
const handleReset = () => {
form.resetFields()
setQueryParams({ page: 1, pageSize: 10 })
setQueryParams({ page: 1, 'per-page': 10 })
}
// 处理分页
const handlePageChange = (page: number, pageSize: number) => {
setQueryParams((prev) => ({ ...prev, page, pageSize }))
setQueryParams((prev) => ({ ...prev, page, 'per-page': pageSize }))
}
const pagination = {
......
import { upload } from '@/utils/upload'
import { PlusOutlined } from '@ant-design/icons'
import { Button, Upload } from 'antd'
export default function AppUpload({ value, onChange, ...props }: any) {
const generateFileList = (file: any) => {
if (!file || !file.name || !file.url) return []
return [
{
uid: Date.now().toString(),
name: file.name,
status: 'done',
url: file.url,
},
]
}
const fileList = generateFileList(value)
const [file] = fileList
// 自定义上传
const customRequest = async ({ onSuccess, onError, file }: any) => {
try {
const url = await upload(file)
onChange?.({ name: file.name, url })
onSuccess(url)
} catch (error) {
onError(error)
}
}
return (
<Upload customRequest={customRequest} fileList={fileList} showUploadList={false} maxCount={1} {...props}>
<Button type="primary" shape="circle" size="large" icon={<PlusOutlined />} />
<span style={{ marginLeft: '10px' }}>{file?.name}</span>
</Upload>
)
}
......@@ -4,10 +4,22 @@ import { CircleArrowLeft, CircleArrowRight } from 'lucide-react'
import './AIChat.scss'
import { OpenAIOutlined, SendOutlined } from '@ant-design/icons'
import TextArea from 'antd/es/input/TextArea'
import { useAI } from '@/hooks/useAI'
import { useAI, AIMessage } from '@/hooks/useAI'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
export const MessageItem = ({ message }: { message: AIMessage }) => {
return (
<div className={`message-item ${message.role}`}>
<div className="message-box">
<div className="message-content">
<Markdown remarkPlugins={[remarkGfm]}>{message.content}</Markdown>
</div>
</div>
</div>
)
}
export default function AIChat() {
const [collapsed, setCollapsed] = useState(false)
......@@ -30,18 +42,6 @@ export default function AIChat() {
post({ content })
}
const messageItemRender = (message) => {
return (
<div className={`message-item ${message.role}`}>
<div className="message-box">
<div className="message-content">
<Markdown remarkPlugins={[remarkGfm]}>{message.content}</Markdown>
</div>
</div>
</div>
)
}
if (collapsed) {
return (
<Card
......@@ -51,7 +51,7 @@ export default function AIChat() {
<div className="ai-chat-container">
<div className="message-scroll">
{messages.map((message) => {
return messageItemRender(message)
return <MessageItem message={message}></MessageItem>
})}
</div>
<Select value={ai} options={options} onChange={setAI} style={{ width: '100%' }}></Select>
......
......@@ -3,24 +3,35 @@ import md5 from 'blueimp-md5'
import axios from 'axios'
import { fetchEventSource } from '@fortaine/fetch-event-source'
export interface AIOption {
label: string
value: string
}
export interface AIMessage {
id?: string
role: 'user' | 'assistant'
content: string
}
export function useAI() {
const options = [
const options: AIOption[] = [
{ label: '文心一言', value: 'yiyan' },
{ label: 'DeepSeek', value: 'deepseek' },
{ label: '通义千问', value: 'qwen' },
{ label: '天工', value: 'tiangong' },
]
const [ai, setAI] = useState(localStorage.getItem('ai') || 'yiyan')
const [messages, setMessages] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [ai, setAI] = useState<string>(localStorage.getItem('ai') || 'yiyan')
const [messages, setMessages] = useState<AIMessage[]>([])
const [isLoading, setIsLoading] = useState<boolean>(false)
useEffect(() => {
localStorage.setItem('ai', ai)
}, [ai])
const post = useCallback(
async (data) => {
async (data: { content: string }) => {
setIsLoading(true)
setMessages((prevMessages) => [...prevMessages, { role: 'user', content: data.content }])
try {
......@@ -46,10 +57,10 @@ export function useAI() {
setIsLoading(false)
}
},
[ai] // 依赖 `ai`,当 `ai` 变化时,重新创建 `post`
[ai]
)
async function yiyan(data) {
async function yiyan(data: any) {
const getAccessToken = async () => {
const AK = 'wY7bvMpkWeZbDVq9w3EDvpjU'
const SK = 'XJwpiJWxs5HXkOtbo6tQrvYPZFJAWdAy'
......@@ -67,7 +78,7 @@ export function useAI() {
setMessages((prevMessages) => [...prevMessages, { role: 'assistant', content: resp.data.result }])
}
async function deepseek(data) {
async function deepseek(data: any) {
const apiKey = 'sk-f1a6f0a7013241de8393cb2cb108e777'
const resp = await axios.post(
'/api/deepseek/chat/completions',
......@@ -85,7 +96,7 @@ export function useAI() {
}
}
async function qwen(data) {
async function qwen(data: any) {
const apiKey = 'sk-afd0fcdb53bf4058b2068b8548820150'
const resp = await axios.post(
'/api/qwen/compatible-mode/v1/chat/completions',
......@@ -103,10 +114,10 @@ export function useAI() {
}
}
async function tiangong(data) {
async function tiangong(data: any) {
const appKey = 'a8701b73637562d33a53c668a90ee3be'
const appSecret = 'e191593f486bb88a39c634f46926762dddc97b9082e192af'
const timestamp = Math.floor(Date.now() / 1000)
const timestamp = Math.floor(Date.now() / 1000).toString()
const sign = md5(`${appKey}${appSecret}${timestamp}`)
return await fetchEventSource('/api/tiangong/sky-saas-writing/api/v1/chat', {
......@@ -116,14 +127,6 @@ export function useAI() {
chat_history: [{ role: 'user', content: data.content }],
stream_resp_type: 'update',
}),
async onopen(response) {
console.log(response)
if (response.ok) {
return response
} else {
throw response
}
},
onmessage(res) {
console.log(res.data)
const message = JSON.parse(res.data)
......
import httpRequest from '@/utils/axios'
import type { CreateDatasetParams } from './types'
// 创建内置数据集
export function createDataset(data: CreateDatasetParams) {
return httpRequest.post('/api/bi/v1/data/built-in/create', data)
}
// 删除内置数据集
export function deleteDataset(data: { id: string }) {
return httpRequest.post('/api/bi/v1/data/built-in/delete', data)
}
// 内置数据集列表
export function getDatasetList(params?: Partial<{ name: string; industry: string; page: number; 'per-page': number }>) {
return httpRequest.get('/api/bi/v1/data/built-in/list', { params })
}
import { Button, Form, Input, Modal, Select } from 'antd'
import { useMapStore } from '@/stores/map'
import AppUpload from '@/components/AppUpload'
import { useCreateDataset } from '../query'
import { useState } from 'react'
export default function FormButtonModal({ title, children }: { title: string; children?: React.ReactNode }) {
const [open, setOpen] = useState(false)
const [form] = Form.useForm()
const getMapValuesByKey = useMapStore((state) => state.getMapValuesByKey)
const industryList = getMapValuesByKey('bi_data_industry')
const dataSourceList = getMapValuesByKey('bi_data_source')
const sensitivityLevelList = getMapValuesByKey('bi_data_sensitivity_level')
const accessPermissionsList = getMapValuesByKey('bi_data_access_permissions')
const { mutate, isPending } = useCreateDataset()
const handleOk = () => {
form.validateFields().then((values) => {
const params = { ...values, file: JSON.stringify(values.file) }
mutate(params, {
onSuccess: () => setOpen(false),
})
})
}
return (
<>
{children ? (
children
) : (
<Button type="primary" onClick={() => setOpen(true)}>
{title}
</Button>
)}
<Modal
title={title}
open={open}
onOk={handleOk}
onCancel={() => setOpen(false)}
confirmLoading={isPending}
okText="保存"
destroyOnClose>
<Form form={form} labelCol={{ span: 5 }} preserve={false}>
<Form.Item label="数据集名称" name="name" rules={[{ required: true, message: '请输入数据集名称' }]}>
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="所属行业" name="industry" rules={[{ required: true, message: '请选择所属行业' }]}>
<Select options={industryList}></Select>
</Form.Item>
<Form.Item label="数据来源" name="source" rules={[{ required: true, message: '请选择数据来源' }]}>
<Select options={dataSourceList}></Select>
</Form.Item>
<Form.Item label="敏感等级" name="sensitivity_level" rules={[{ required: true, message: '请选择敏感等级' }]}>
<Select options={sensitivityLevelList}></Select>
</Form.Item>
<Form.Item label="访问权限" name="access_permissions" rules={[{ required: true, message: '请选择访问权限' }]}>
<Select options={accessPermissionsList}></Select>
</Form.Item>
<Form.Item label="说明" name="describe">
<Input.TextArea rows={4} placeholder="请输入" />
</Form.Item>
<Form.Item name="file" label={null} rules={[{ required: true, message: '请上传文件' }]}>
<AppUpload accept=".xlsx,.csv" />
</Form.Item>
</Form>
</Modal>
</>
)
}
import { Form, Input, Modal, Select } from 'antd'
import { useMapStore } from '@/stores/map'
export default function FormModal(props) {
const getMapValuesByKey = useMapStore((state) => state.getMapValuesByKey)
const industryList = getMapValuesByKey('bi_data_industry')
const dataSourceList = getMapValuesByKey('bi_data_source')
const sensitivityLevelList = getMapValuesByKey('bi_data_sensitivity_level')
const accessPermissionsList = getMapValuesByKey('bi_data_access_permissions')
return (
<Modal title="添加数据集" {...props}>
<Form labelCol={{ span: 4 }}>
<Form.Item label="数据集名称" name="name">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="所属行业" name="industry">
<Select options={industryList}></Select>
</Form.Item>
<Form.Item label="数据来源" name="data_source">
<Select options={dataSourceList}></Select>
</Form.Item>
<Form.Item label="敏感等级" name="level">
<Select options={sensitivityLevelList}></Select>
</Form.Item>
<Form.Item label="访问权限" name="access">
<Select options={accessPermissionsList}></Select>
</Form.Item>
<Form.Item label="说明" name="desc">
<Input.TextArea rows={4} placeholder="请输入" />
</Form.Item>
</Form>
</Modal>
)
}
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { createDataset, deleteDataset } from './api'
import type { CreateDatasetParams } from './types'
import { message } from 'antd'
// 创建
export function useCreateDataset() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: CreateDatasetParams) => createDataset(data),
onSuccess: () => {
message.success('创建成功')
queryClient.invalidateQueries({ queryKey: ['appList'] })
},
})
}
// 删除
export function useDeleteDataset() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: { id: string }) => deleteDataset(data),
onSuccess: () => {
message.success('删除成功')
queryClient.invalidateQueries({ queryKey: ['appList'] })
},
})
}
export interface CreateDatasetParams {
experiment_id: string
name: string
industry: string
source: string
sensitivity_level: string
access_permissions: string
describe: string
file: string
}
export interface DatasetListItem {
experiment_id: string
name: string
industry: string
source: string
sensitivity_level: string
access_permissions: string
describe: string
file: string
}
......@@ -2,28 +2,16 @@ import { lazy, useState } from 'react'
import { Button, Card, Input, Select } from 'antd'
import AppList, { AppListProps } from '@/components/AppList'
import { useMapStore } from '@/stores/map'
import { getDatasetList } from '../api'
import { useDeleteDataset } from '../query'
const FormModal = lazy(() => import('../components/FormModal'))
const FormButtonModal = lazy(() => import('../components/FormButtonModal'))
const ViewDataModal = lazy(() => import('@/components/data/ViewDataModal'))
export default function DataWriteBuilt() {
const getMapValuesByKey = useMapStore((state) => state.getMapValuesByKey)
const industryList = getMapValuesByKey('bi_data_industry')
const [isModalOpen, setIsModalOpen] = useState(false)
const showModal = () => {
setIsModalOpen(true)
}
const handleOk = () => {
setIsModalOpen(false)
}
const handleCancel = () => {
setIsModalOpen(false)
}
const [viewModalIsOpen, setViewModalIsOpen] = useState(false)
const handleView = (record: any) => {
......@@ -31,11 +19,16 @@ export default function DataWriteBuilt() {
setViewModalIsOpen(true)
}
const { mutate } = useDeleteDataset()
const handleRemove = (record: any) => {
console.log(record)
mutate({ id: record.id })
}
const listOptions: AppListProps = {
fetchApi: async (params) => {
const { data } = await getDatasetList(params)
return { ...data }
},
filters: [
{
name: 'industry',
......@@ -56,16 +49,16 @@ export default function DataWriteBuilt() {
width: 62,
align: 'center',
},
{ title: '数据集名称', dataIndex: 'name' },
{ title: '数据量', dataIndex: 'count' },
{ title: '所属行业', dataIndex: 'industry' },
{ title: '数据来源', dataIndex: 'sourc' },
{ title: '敏感等级', dataIndex: 'level' },
{ title: '访问权限', dataIndex: 'auth' },
{ title: '说明', dataIndex: 'desc' },
{ title: '创建人', dataIndex: 'create' },
{ title: '创建时间', dataIndex: 'create_time' },
{ title: '更新时间', dataIndex: 'update_time' },
{ title: '数据集名称', dataIndex: 'name', align: 'center' },
{ title: '数据量', dataIndex: 'number', align: 'center' },
{ title: '所属行业', dataIndex: 'industry_name', align: 'center' },
{ title: '数据来源', dataIndex: 'source_name', align: 'center' },
{ title: '敏感等级', dataIndex: 'sensitivity_level_name', align: 'center' },
{ title: '访问权限', dataIndex: 'access_permissions_name', align: 'center' },
{ title: '说明', dataIndex: 'describe', align: 'center' },
{ title: '创建人', dataIndex: 'created_operator_name', align: 'center' },
{ title: '创建时间', dataIndex: 'created_time', align: 'center' },
{ title: '更新时间', dataIndex: 'updated_time', align: 'center' },
{
title: '操作',
key: 'x',
......@@ -93,16 +86,7 @@ export default function DataWriteBuilt() {
}
return (
<Card className="app-card" title="内置数据集管理">
<AppList
bordered
{...listOptions}
filterAside={
<Button type="primary" onClick={showModal}>
添加数据集
</Button>
}></AppList>
<FormModal open={isModalOpen} onOk={handleOk} onCancel={handleCancel}></FormModal>
<AppList bordered {...listOptions} filterAside={<FormButtonModal title="添加数据集"></FormButtonModal>}></AppList>
<ViewDataModal open={viewModalIsOpen} onCancel={() => setViewModalIsOpen(false)}></ViewDataModal>
</Card>
......
......@@ -45,12 +45,10 @@ export default function DataWriteUpload() {
</Form.Item>
</Form>
<Flex align="center" gap={100} style={{ margin: '10px 0 30px' }}>
<div>
<Upload {...props}>
<Button type="primary" shape="circle" size="large" icon={<PlusOutlined />}></Button>
</Upload>
<Upload {...props}>
<Button type="primary" shape="circle" size="large" icon={<PlusOutlined />}></Button>
<span style={{ marginLeft: '10px' }}>{file?.name}</span>
</div>
</Upload>
<p>共计:{data.length}条数据</p>
<Button type="primary">保存</Button>
</Flex>
......
......@@ -9,6 +9,28 @@ const axiosInstance = axios.create({
// },
})
// 请求拦截
axiosInstance.interceptors.request.use(
function (config) {
const params = new URLSearchParams(window.location.search)
if (config.method === 'get')
config.params = Object.assign({}, config.params, {
experiment_id: params.get('experiment_id') || '7028276368903241728',
})
if (config.method === 'post')
config.data = Object.assign({}, config.data, {
experiment_id: params.get('experiment_id') || '7028276368903241728',
})
return config
},
function (error) {
return Promise.reject(error)
}
)
axiosInstance.interceptors.response.use(
(response) => {
const { data } = response
......
import axios from 'axios'
import md5 from 'blueimp-md5'
import { getSignature, uploadFile } from '@/api/base'
export async function upload(blob: Blob | File) {
let fileType = 'png'
if (blob instanceof File && blob.name) {
const matches = blob.name.match(/\.(\w+)$/)
if (matches) {
fileType = matches[1]
}
} else if (blob.type) {
const mimeType = blob.type.split('/').pop()
if (mimeType) {
fileType = mimeType
}
}
const key = 'upload/saas-bi/' + md5(new Date().getTime() + Math.random().toString(36).slice(-8)) + '.' + fileType
const response: any = await getSignature()
const params = {
key,
host: response.host,
OSSAccessKeyId: response.accessid,
policy: response.policy,
signature: response.signature,
success_action_status: '200',
file: blob,
url: `${response.host}/${key}`,
}
await uploadFile(params)
return params.url
}
export async function uploadFileByUrl(url: string) {
const res = await axios.get(url, { responseType: 'blob' })
return upload(res.data)
}
......@@ -10,6 +10,11 @@ export default defineConfig({
open: true,
host: 'dev.ezijing.com',
proxy: {
'/api/bi': {
target: 'http://local-com-resource-api.bi.ezijing.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/bi/, ''),
},
'/api': 'https://saas-lab.ezijing.com',
},
},
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论