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

chore: update

上级 10aacd61
VITE_LOGIN_URL=https://login.ezijing.com/auth/login/index
{
"globals": {
"createRef": true,
"forwardRef": true,
"lazy": true,
"memo": true,
"startTransition": true,
"useCallback": true,
"useContext": true,
"useDebugValue": true,
"useDeferredValue": true,
"useEffect": true,
"useHref": true,
"useId": true,
"useImperativeHandle": true,
"useInRouterContext": true,
"useInsertionEffect": true,
"useLayoutEffect": true,
"useLocation": true,
"useMemo": true,
"useNavigate": true,
"useNavigationType": true,
"useOutlet": true,
"useOutletContext": true,
"useParams": true,
"useReducer": true,
"useRef": true,
"useResolvedPath": true,
"useRoutes": true,
"useState": true,
"useSyncExternalStore": true,
"useTransition": true
}
}
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const createRef: typeof import('react')['createRef']
const forwardRef: typeof import('react')['forwardRef']
const lazy: typeof import('react')['lazy']
const memo: typeof import('react')['memo']
const startTransition: typeof import('react')['startTransition']
const useCallback: typeof import('react')['useCallback']
const useContext: typeof import('react')['useContext']
const useDebugValue: typeof import('react')['useDebugValue']
const useDeferredValue: typeof import('react')['useDeferredValue']
const useEffect: typeof import('react')['useEffect']
const useHref: typeof import('react-router')['useHref']
const useId: typeof import('react')['useId']
const useImperativeHandle: typeof import('react')['useImperativeHandle']
const useInRouterContext: typeof import('react-router')['useInRouterContext']
const useInsertionEffect: typeof import('react')['useInsertionEffect']
const useLayoutEffect: typeof import('react')['useLayoutEffect']
const useLocation: typeof import('react-router')['useLocation']
const useMemo: typeof import('react')['useMemo']
const useNavigate: typeof import('react-router')['useNavigate']
const useNavigationType: typeof import('react-router')['useNavigationType']
const useOutlet: typeof import('react-router')['useOutlet']
const useOutletContext: typeof import('react-router')['useOutletContext']
const useParams: typeof import('react-router')['useParams']
const useReducer: typeof import('react')['useReducer']
const useRef: typeof import('react')['useRef']
const useResolvedPath: typeof import('react-router')['useResolvedPath']
const useRoutes: typeof import('react-router')['useRoutes']
const useState: typeof import('react')['useState']
const useSyncExternalStore: typeof import('react')['useSyncExternalStore']
const useTransition: typeof import('react')['useTransition']
}
差异被折叠。
......@@ -11,14 +11,16 @@
},
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@fortaine/fetch-event-source": "^3.0.6",
"@tanstack/react-query": "^5.67.1",
"antd": "^5.24.3",
"axios": "^1.8.1",
"classnames": "^2.5.1",
"blueimp-md5": "^2.19.0",
"lucide-react": "^0.477.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^7.3.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zustand": "^5.0.3"
},
"devDependencies": {
......@@ -33,7 +35,6 @@
"eslint-plugin-react-refresh": "^0.4.6",
"sass-embedded": "^1.85.1",
"typescript": "^5.2.2",
"unplugin-auto-import": "^19.1.1",
"vite": "^5.2.0",
"vite-plugin-mkcert": "^1.17.7"
}
......
......@@ -81,6 +81,7 @@ textarea:focus {
html,
body,
#root {
width: 100%;
height: 100%;
}
.app-card {
......
......@@ -28,3 +28,8 @@ export async function uploadFile(data: Record<string, any>) {
})
return data
}
// 获取公共字典列表
export function getMapList() {
return httpRequest.get('/api/resource/v1/util/get-data-dictionary-list')
}
import { useState, useImperativeHandle, forwardRef } from 'react'
import { Table, Form, Button, Space, Flex } from 'antd'
import type { TableProps } from 'antd'
import { useQuery } from '@tanstack/react-query'
export interface QueryParams {
[key: string]: any
page: number
pageSize: number
}
export interface AppListProps extends TableProps {
fetchApi?: (params: QueryParams) => Promise<{ list: any[]; total: number }>
filters?: any[]
filterAside?: React.ReactNode
}
export interface AppListRef {
refresh: () => void
reset: () => void
}
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 { 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[] }
},
})
// 暴露方法
useImperativeHandle(ref, () => ({
refresh: refetch,
reset: handleReset,
}))
// 提交筛选
const handleSearch = (values: Record<string, any>) => {
console.log(values)
setQueryParams((prev) => ({ ...prev, ...values, page: 1 }))
}
const handleReset = () => {
form.resetFields()
setQueryParams({ page: 1, pageSize: 10 })
}
// 处理分页
const handlePageChange = (page: number, pageSize: number) => {
setQueryParams((prev) => ({ ...prev, page, pageSize }))
}
const pagination = {
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: [10, 20, 30, 40, 50],
showTotal: (total: number) => `共${total}条数据`,
onChange: handlePageChange,
}
const filterElement = (
<div className="app-list-filter" style={{ marginBottom: 20 }}>
<Flex justify="space-between">
<Form layout="inline" autoComplete="off" form={form} onFinish={handleSearch}>
{filters.map((item, index) => (
<Form.Item label={item.label} name={item.name} key={index}>
{item.element}
</Form.Item>
))}
<Space>
<Button type="primary" htmlType="button" ghost onClick={handleReset}>
重置
</Button>
<Button type="primary" htmlType="submit">
查询
</Button>
</Space>
</Form>
{filterAside}
</Flex>
</div>
)
return (
<div className="app-list">
{filters.length > 0 && filterElement}
<div className="app-list-table">
<Table rowKey="id" dataSource={data?.list} loading={isLoading} pagination={pagination} {...rest} />
</div>
</div>
)
})
export default AppList
.ai-chat {
height: 100%;
width: 400px;
.ai-chat-container {
height: 100%;
display: flex;
flex-direction: column;
}
.ant-card-extra {
cursor: pointer;
}
.ant-card-body {
height: calc(100% - 56px);
}
.input-container {
margin-top: 10px;
fill: #fff;
border: 1px solid #e0e3e6;
border-radius: 16px;
stroke-width: 2px;
stroke: #d9d9d9;
filter: drop-shadow(0px 0px 11px rgba(0, 0, 0, 0.05));
transition: border-color 0.3s;
overflow: hidden;
}
.input-box {
display: flex;
align-items: center;
padding: 10px;
background-color: #fff;
.edit-area {
flex: 1;
position: relative;
max-height: 160px;
overflow: hidden auto;
}
.pre-prompt {
position: absolute;
top: 0;
left: 0;
z-index: 1;
line-height: 1.7;
background-color: #e6eaf0;
border-radius: 4px;
padding: 4px 8px 3px;
font-size: 14px;
font-weight: 500;
}
.content {
padding: 0;
border: 0;
background: none;
box-shadow: none;
font-size: 14px;
line-height: 30px;
}
.input-tools {
align-self: flex-end;
display: flex;
align-items: center;
gap: 10px;
}
}
.message-scroll {
margin: 10px 0;
// height: 600px;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
.message-item {
display: flex;
margin: 10px 0;
&.user {
justify-content: flex-end;
.message-box {
background-color: #0000000d;
}
}
&.assistant {
.message-box {
background-color: #fae3e5;
}
}
.message-box {
max-width: 75%;
border-radius: 12px;
}
.message-content {
padding: 16px;
// max-height: 300px;
// overflow-y: auto;
}
.message-tools {
padding: 16px;
border-top: 1px solid #ececec;
}
.result-item {
margin-top: 12px;
p {
margin-bottom: 12px;
word-break: break-word !important;
}
}
}
}
import { useState, KeyboardEvent } from 'react'
import { Button, Card, FloatButton, Select } from 'antd'
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'
export default function AIChat() {
const [collapsed, setCollapsed] = useState(true)
const toggleCollapsed = () => {
setCollapsed(!collapsed)
}
const { ai, setAI, options, post, messages, isLoading } = useAI()
const [content, setContent] = useState('')
const handleEnterSearch = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSearch()
}
}
const handleSearch = () => {
setContent('')
post({ content })
}
const messageItemRender = (message) => {
return (
<div className={`message-item ${message.role}`}>
<div className="message-box">
<div className="message-content">{message.content}</div>
</div>
</div>
)
}
if (collapsed) {
return (
<Card
className="app-card ai-chat"
title="AI对话"
extra={<span onClick={toggleCollapsed}>{collapsed ? <CircleArrowRight /> : <CircleArrowLeft />}</span>}>
<div className="ai-chat-container">
<div className="message-scroll">
{messages.map((message) => {
return messageItemRender(message)
})}
</div>
<Select value={ai} options={options} onChange={setAI} style={{ width: '100%' }}></Select>
<div className="input-container">
<div className="input-box">
<div className="edit-area">
<TextArea
className="content"
autoSize
value={content}
placeholder="今天需要我做些什么?shift+enter换行"
onChange={(e) => setContent(e.target.value)}
onKeyDown={handleEnterSearch}
/>
</div>
<div className="input-tools">
<Button
type="primary"
size="large"
icon={<SendOutlined />}
onClick={handleSearch}
loading={isLoading}
/>
</div>
</div>
<div className="upload-list"></div>
</div>
</div>
</Card>
)
} else {
return <FloatButton icon={<OpenAIOutlined />} onClick={toggleCollapsed} />
}
}
import { Button, Card, Flex } from 'antd'
import { ReactNode } from 'react'
export default function DataWrap({
title,
buttons,
children,
}: {
title: string
buttons: ReactNode
children: ReactNode
}) {
return (
<Flex gap={20} style={{ height: '100%' }}>
<Card className="app-card" title={title} style={{ flex: 1, overflowX: 'hidden' }}>
<Flex justify="space-between" style={{ marginBottom: '20px' }}>
<Flex wrap gap={10}>
{buttons}
</Flex>
<Button>查看我的数据集</Button>
</Flex>
{children}
</Card>
</Flex>
)
}
import { Table, TableProps } from 'antd'
export default function DataRender(props: TableProps) {
const pagination = {
showSizeChanger: true,
pageSize: 100,
pageSizeOptions: [100, 200, 500],
showTotal: (total: number) => `共${total}条数据`,
}
return (
<Table
bordered
scroll={{ x: 'max-content', y: 600 }}
tableLayout="auto"
size="middle"
pagination={pagination}
{...props}
/>
)
}
import { Button, Card, Flex } from 'antd'
import AIChat from '@/components/ai/AIChat'
import ViewData from './ViewData'
import { ReactNode } from 'react'
export default function DataWrap({ title, buttons }: { title: string; buttons: ReactNode }) {
return (
<Flex gap={20} style={{ height: '100%' }}>
<Card className="app-card" title={title} style={{ flex: 1, overflowX: 'hidden' }}>
<Flex justify="space-between" style={{ marginBottom: '20px' }}>
<Flex wrap gap={10}>
{buttons}
</Flex>
<Button>查看字段详细信息</Button>
</Flex>
<ViewData />
</Card>
<AIChat></AIChat>
</Flex>
)
}
差异被折叠。
差异被折叠。
......@@ -2,11 +2,16 @@ import { Link, NavLink, useLocation } from 'react-router'
import { Menu, Popover, Button } from 'antd'
import { CaretDownOutlined } from '@ant-design/icons'
import './AppHeader.scss'
import { useUserStore } from '@/stores/user'
export default function AppHeader({ title = 'AI数据分析实验室' }) {
const location = useLocation()
const defaultSelectedKeys = [location.pathname]
const user = useUserStore((state) => state.info)
const name = user?.name || user?.username
const avatar = user?.avatar || 'https://webapp-pub.ezijing.com/website/base/images/avatar.svg'
const menus = [
{ name: '首页', path: '/' },
{ name: '我的实验', path: '/data' },
......@@ -40,11 +45,11 @@ export default function AppHeader({ title = 'AI数据分析实验室' }) {
content={
<div className="app-header-user">
<div className="app-header-user-avatar">
<img src="https://webapp-pub.ezijing.com/website/base/images/avatar.svg" />
<img src={avatar} />
</div>
<div className="app-header-user-main">
<h3>王鹏飞</h3>
<p>wangpengfei@ezijing.com</p>
<h3>{name}</h3>
<p>{user?.email}</p>
</div>
<div className="app-header-user-buttons">
<Button shape="round">退出登录</Button>
......@@ -53,9 +58,9 @@ export default function AppHeader({ title = 'AI数据分析实验室' }) {
}>
<div className="user">
<div className="avatar">
<img src="https://webapp-pub.ezijing.com/website/base/images/avatar.svg" />
<img src={avatar} />
</div>
<p>王鹏飞</p>
<p>{name}</p>
<CaretDownOutlined />
</div>
</Popover>
......
import { Outlet } from 'react-router'
import AppHeader from './AppHeader'
import './AppLayout.scss'
import { useUserQuery, useMapQuery } from '@/hooks/useQuery'
export default function AppLayout() {
const { isPending: queryUserIsPending } = useUserQuery()
const { isPending: queryMapIsPending } = useMapQuery()
if (queryUserIsPending || queryMapIsPending) {
return
}
return (
<div className="app-layout">
<AppHeader />
......
......@@ -59,5 +59,7 @@
}
&-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
}
......@@ -43,9 +43,9 @@ export default function DataLayout() {
{ name: '我的数据集', path: '/data/write/my' },
{ name: '数据复制', path: '/data/write/copy' },
{ name: '数据导入', path: '/data/write/upload' },
{ name: '数据爬虫', path: '/data/write/crawler' },
{ name: 'API数据采集', path: '/data/write/api' },
{ name: '数据库对接', path: '/data/write/db' },
{ name: '数据爬虫', path: '/data/write/crawler', disabled: true },
{ name: 'API数据采集', path: '/data/write/api', disabled: true },
{ name: '数据库对接', path: '/data/write/db', disabled: true },
{ name: '内置数据集管理', path: '/data/write/built' },
],
},
......@@ -54,13 +54,12 @@ export default function DataLayout() {
name: '数据理解与探索',
path: '/data/read',
children: [
{ name: '数据理解', path: '/data/read/crawler' },
{ name: '数据探索', path: '/data/read/api' },
{ name: '数据理解', path: '/data/read/understanding' },
{ name: '数据探索', path: '/data/read/exploration' },
],
},
{
icon: <Filter />,
name: '数据预处理',
path: '/data/preprocess',
children: [
......@@ -74,19 +73,18 @@ export default function DataLayout() {
{ name: '数据去标点', path: '/data/preprocess/symbol' },
{ name: '数据类型转换', path: '/data/preprocess/type' },
{ name: '数据排序', path: '/data/preprocess/sort' },
{ name: '数据拼接', path: '/data/preprocess/concat' },
{ name: '数据拼接', path: '/data/preprocess/splice' },
],
},
{
icon: <Bolt />,
name: '数据加工',
path: '/data/process',
children: [
{ name: '值映射', path: '/data/process/mapping' },
{ name: '数据分箱', path: '/data/process/binning' },
{ name: '数据分组', path: '/data/process/group' },
{ name: '数据脱敏', path: '/data/process/4' },
{ name: '数据脱敏', path: '/data/process/desensitization' },
{ name: '日期计算', path: '/data/process/date' },
{ name: '文本计算', path: '/data/process/string' },
{ name: '数值计算', path: '/data/process/number' },
......@@ -116,7 +114,7 @@ export default function DataLayout() {
name: '数据可视化组件',
path: '/data/chart',
children: [
{ name: '柱状图', path: '/data/chart/1' },
{ name: '柱状图', path: '/data/chart/bar' },
{ name: '折线图', path: '/data/chart/2' },
{ name: '饼状图', path: '/data/chart/3' },
{ name: '雷达图', path: '/data/chart/4' },
......
import { useState, useEffect, useCallback } from 'react'
import md5 from 'blueimp-md5'
import axios from 'axios'
import { fetchEventSource } from '@fortaine/fetch-event-source'
export function useAI() {
const options = [
{ 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)
useEffect(() => {
localStorage.setItem('ai', ai)
}, [ai])
const post = useCallback(
async (data) => {
setIsLoading(true)
setMessages((prevMessages) => [...prevMessages, { role: 'user', content: data.content }])
try {
switch (ai) {
case 'yiyan':
await yiyan(data)
break
case 'deepseek':
await deepseek(data)
break
case 'qwen':
await qwen(data)
break
case 'tiangong':
await tiangong(data)
break
default:
throw new Error('未找到对应的 AI 配置')
}
} catch (err) {
console.error('AI 请求失败:', err)
} finally {
setIsLoading(false)
}
},
[ai] // 依赖 `ai`,当 `ai` 变化时,重新创建 `post`
)
async function yiyan(data) {
const getAccessToken = async () => {
const AK = 'wY7bvMpkWeZbDVq9w3EDvpjU'
const SK = 'XJwpiJWxs5HXkOtbo6tQrvYPZFJAWdAy'
const resp = await axios.post(
`/api/qianfan/oauth/2.0/token?grant_type=client_credentials&client_id=${AK}&client_secret=${SK}`
)
return resp.data.access_token
}
const resp = await axios.post(
`/api/qianfan/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=${await getAccessToken()}`,
{
messages: [{ role: 'user', content: data.content }],
}
)
setMessages((prevMessages) => [...prevMessages, { role: 'assistant', content: resp.data.result }])
}
async function deepseek(data) {
const apiKey = 'sk-f1a6f0a7013241de8393cb2cb108e777'
const resp = await axios.post(
'/api/deepseek/chat/completions',
{
model: 'deepseek-chat',
messages: [{ role: 'user', content: data.content }],
},
{
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
}
)
if (resp.data) {
const [choice = {}] = resp.data.choices
setMessages((prevMessages) => [...prevMessages, { role: 'assistant', content: choice.message.content }])
}
}
async function qwen(data) {
const apiKey = 'sk-afd0fcdb53bf4058b2068b8548820150'
const resp = await axios.post(
'/api/qwen/compatible-mode/v1/chat/completions',
{
model: 'qwen-max',
messages: [{ role: 'user', content: data.content }],
},
{
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
}
)
if (resp.data) {
const [choice = {}] = resp.data.choices
setMessages((prevMessages) => [...prevMessages, { role: 'assistant', content: choice.message.content }])
}
}
async function tiangong(data) {
const appKey = 'a8701b73637562d33a53c668a90ee3be'
const appSecret = 'e191593f486bb88a39c634f46926762dddc97b9082e192af'
const timestamp = Math.floor(Date.now() / 1000)
const sign = md5(`${appKey}${appSecret}${timestamp}`)
return await fetchEventSource('/api/tiangong/sky-saas-writing/api/v1/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json', app_key: appKey, sign, timestamp, stream: 'true' },
body: JSON.stringify({
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)
if (message.type !== 1) return
setMessages((prevMessages) => {
const messageId = message.conversation_id
const messageIndex = prevMessages.findIndex((message) => message.id === messageId)
const content = message?.arguments?.[0]?.messages?.[0]?.text || ''
if (messageIndex === -1) {
return [...prevMessages, { id: messageId, role: 'assistant', content }]
} else {
return prevMessages.map((msg) => (msg.id === messageId ? { ...msg, content } : msg))
}
})
setIsLoading(false)
},
onerror(err) {
setIsLoading(false)
throw err
},
})
}
return { ai, setAI, options, post, messages, isLoading }
}
import { useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import { getUser, getMapList } from '@/api/base'
import { useUserStore } from '@/stores/user'
import { useMapStore } from '@/stores/map'
export function useUserQuery() {
const { setUser } = useUserStore()
const query = useQuery({ queryKey: ['user'], queryFn: getUser, select: (res) => res.data })
useEffect(() => {
if (query.data) {
setUser(query.data)
}
}, [query.data, setUser])
return query
}
export function useMapQuery() {
const { setMap } = useMapStore()
const query = useQuery({ queryKey: ['dictionary'], queryFn: getMapList, select: (res) => res.data })
useEffect(() => {
if (query.data) {
setMap(query.data)
}
}, [query.data, setMap])
return query
}
......@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ConfigProvider } from 'antd'
import zhCN from 'antd/locale/zh_CN'
import App from './App.tsx'
const queryClient = new QueryClient()
......@@ -18,7 +19,7 @@ const antdTheme = {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ConfigProvider theme={antdTheme}>
<ConfigProvider theme={antdTheme} locale={zhCN}>
<BrowserRouter>
<App />
</BrowserRouter>
......
import ChartWrap from '@/components/data/ChartWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <ChartWrap title="可视化:柱状图" buttons={<Button type="primary">新建柱状图</Button>}></ChartWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return (
<DataWrap
title="数据预处理:逻辑错误值处理"
buttons={
<>
<Button type="primary">处理逻辑错误值</Button>
</>
}></DataWrap>
)
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return (
<DataWrap
title="数据预处理:过大值处理"
buttons={
<>
<Button type="primary">处理过大值</Button>
</>
}></DataWrap>
)
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return (
<DataWrap
title="数据预处理:过小值处理"
buttons={
<>
<Button type="primary">处理过小值</Button>
</>
}></DataWrap>
)
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return (
<DataWrap
title="数据预处理:缺失值处理"
buttons={
<>
<Button type="primary">处理缺失值</Button>
</>
}></DataWrap>
)
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return (
<DataWrap
title="数据预处理:重复值处理"
buttons={
<>
<Button type="primary">处理重复值</Button>
</>
}></DataWrap>
)
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据预处理:数据排序" buttons={<Button type="primary">数据排序</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return (
<DataWrap
title="数据预处理:数据去空格"
buttons={
<>
<Button type="primary">数据去空格</Button>
</>
}></DataWrap>
)
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据预处理:数据拼接" buttons={<Button type="primary">数据拼接</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return (
<DataWrap
title="数据预处理:数据拆分"
buttons={
<>
<Button type="primary">数据拆分</Button>
</>
}></DataWrap>
)
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return (
<DataWrap
title="数据预处理:数据去标点"
buttons={
<>
<Button type="primary">数据去标点</Button>
</>
}></DataWrap>
)
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据预处理:数据类型转换" buttons={<Button type="primary">数据类型转换</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据加工:数据分箱" buttons={<Button type="primary">数据分箱</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据加工:日期计算" buttons={<Button type="primary">日期计算</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据加工:数据脱敏" buttons={<Button type="primary">数据脱敏</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据加工:数据分组" buttons={<Button type="primary">数据分组</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据加工:逻辑计算" buttons={<Button type="primary">逻辑计算</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据加工:值映射" buttons={<Button type="primary">值映射</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据加工:数值计算" buttons={<Button type="primary">数值计算</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据加工:数据透视" buttons={<Button type="primary">数据透视</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataProcess() {
return <DataWrap title="数据加工:文本计算" buttons={<Button type="primary">文本计算</Button>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataWriteUpload() {
const buttonsRender = () => {
const buttons = [
{ name: '缺失值' },
{ name: '重复值' },
{ name: '过大值' },
{ name: '过小值' },
{ name: '逻辑错误值' },
{ name: '最大值' },
{ name: '最小值' },
{ name: '平均值' },
{ name: '中位数' },
{ name: '众数' },
{ name: '1/4位数' },
{ name: '3/4位数' },
{ name: '方差' },
{ name: '标准差' },
{ name: '极差' },
]
return buttons.map((item) => {
return (
<Button type="primary" key={item.name}>
{item.name}
</Button>
)
})
}
return <DataWrap title="数据探索" buttons={<>{buttonsRender()}</>}></DataWrap>
}
import DataWrap from '@/components/data/DataWrap'
import { Button } from 'antd'
export default function DataWriteUpload() {
return (
<DataWrap
title="数据理解"
buttons={
<>
<Button type="primary">一键理解数据字段</Button>
<Button type="primary">一键梳理字段关系</Button>
</>
}></DataWrap>
)
}
......@@ -12,11 +12,44 @@ export const routes: RouteObject[] = [
children: [
{ index: true, element: <Navigate to="dashboard" /> },
{ path: 'dashboard', Component: lazy(() => import('./dashboard/views/Index')) },
// 数据采集
{ path: 'write', element: <Navigate to="my" /> },
{ path: 'write/my', Component: lazy(() => import('./write/my/views/Index')) },
{ path: 'write/copy', Component: lazy(() => import('./write/copy/views/Index')) },
{ path: 'write/upload', Component: lazy(() => import('./write/upload/views/Index')) },
{ path: 'write/copy', Component: lazy(() => import('./write/copy/views/Index')) },
{ path: 'write/built', Component: lazy(() => import('./write/built/views/Index')) },
// 数据理解与探索
{ path: 'read', element: <Navigate to="understanding" /> },
{ path: 'read/understanding', Component: lazy(() => import('./read/understanding/views/Index')) },
{ path: 'read/exploration', Component: lazy(() => import('./read/exploration/views/Index')) },
// 数据预处理
{ path: 'preprocess', element: <Navigate to="null" /> },
{ path: 'preprocess/null', Component: lazy(() => import('./preprocess/null/views/Index')) },
{ path: 'preprocess/repeat', Component: lazy(() => import('./preprocess/repeat/views/Index')) },
{ path: 'preprocess/max', Component: lazy(() => import('./preprocess/max/views/Index')) },
{ path: 'preprocess/min', Component: lazy(() => import('./preprocess/min/views/Index')) },
{ path: 'preprocess/error', Component: lazy(() => import('./preprocess/error/views/Index')) },
{ path: 'preprocess/split', Component: lazy(() => import('./preprocess/split/views/Index')) },
{ path: 'preprocess/space', Component: lazy(() => import('./preprocess/space/views/Index')) },
{ path: 'preprocess/symbol', Component: lazy(() => import('./preprocess/symbol/views/Index')) },
{ path: 'preprocess/type', Component: lazy(() => import('./preprocess/type/views/Index')) },
{ path: 'preprocess/sort', Component: lazy(() => import('./preprocess/sort/views/Index')) },
{ path: 'preprocess/splice', Component: lazy(() => import('./preprocess/splice/views/Index')) },
// 数据预处理
{ path: 'process', element: <Navigate to="binning" /> },
{ path: 'process/binning', Component: lazy(() => import('./process/binning/views/Index')) },
{ path: 'process/mapping', Component: lazy(() => import('./process/mapping/views/Index')) },
{ path: 'process/group', Component: lazy(() => import('./process/group/views/Index')) },
{ path: 'process/desensitization', Component: lazy(() => import('./process/desensitization/views/Index')) },
{ path: 'process/date', Component: lazy(() => import('./process/date/views/Index')) },
{ path: 'process/string', Component: lazy(() => import('./process/string/views/Index')) },
{ path: 'process/number', Component: lazy(() => import('./process/number/views/Index')) },
{ path: 'process/logic', Component: lazy(() => import('./process/logic/views/Index')) },
{ path: 'process/perspective', Component: lazy(() => import('./process/perspective/views/Index')) },
// 数据可视化组件
{ path: 'chart', element: <Navigate to="bar" /> },
{ path: 'chart/bar', Component: lazy(() => import('./chart/bar/views/Index')) },
],
},
],
......
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 { lazy, useState } from 'react'
import { Button, Card, Input, Select } from 'antd'
import AppList, { AppListProps } from '@/components/AppList'
import { useMapStore } from '@/stores/map'
const FormModal = lazy(() => import('../components/FormModal'))
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) => {
console.log(record)
setViewModalIsOpen(true)
}
const handleRemove = (record: any) => {
console.log(record)
}
const listOptions: AppListProps = {
filters: [
{
name: 'industry',
element: <Select placeholder="请选择所属行业" options={industryList} />,
},
{
name: 'name',
element: <Input placeholder="请输入数据集名称" />,
},
],
columns: [
{
title: '序号',
key: 'index',
render(_value, _record, index) {
return index + 1
},
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: '操作',
key: 'x',
width: 160,
align: 'center',
render(_value, record) {
return (
<>
<Button color="primary" variant="text" onClick={() => handleView(record)}>
查阅
</Button>
<Button color="danger" variant="text" onClick={() => handleRemove(record)}>
删除
</Button>
</>
)
},
},
],
dataSource: [
{ id: 1, name: '《商务数据分析基础》数据集' },
{ id: 2, name: '智慧交通数据集' },
{ id: 3, name: '信用卡数字营销数据集' },
],
}
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>
<ViewDataModal open={viewModalIsOpen} onCancel={() => setViewModalIsOpen(false)}></ViewDataModal>
</Card>
)
}
import { Form, Input, Modal } from 'antd'
export default function FormModal(props) {
return (
<Modal title="复制数据集" destroyOnClose {...props}>
<Form labelCol={{ span: 4 }} preserve={false}>
<Form.Item label="数据集名称" name="name">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item label="原始数据集">电子商务案例数据集</Form.Item>
<Form.Item label="数据量">2000</Form.Item>
<Form.Item label="所属行业">电子商务</Form.Item>
<Form.Item label="数据来源">案例数据</Form.Item>
<Form.Item label="敏感等级">L1一般敏感</Form.Item>
<Form.Item label="访问权限">完全开放</Form.Item>
<Form.Item label="创建人">张三疯</Form.Item>
<Form.Item label="创建时间">2024-12-12 13:13:13</Form.Item>
<Form.Item label="更新时间">2024-12-12 13:13:13</Form.Item>
</Form>
</Modal>
)
}
import { Card } from 'antd'
import { Button, Card, Input, Select } from 'antd'
import AppList, { AppListProps } from '@/components/AppList'
import { useMapStore } from '@/stores/map'
import { lazy, useState } from 'react'
export default function DataWriteUpload() {
const CopyModal = lazy(() => import('../components/CopyModal'))
const ViewDataModal = lazy(() => import('@/components/data/ViewDataModal'))
export default function DataWriteCopy() {
const getMapValuesByKey = useMapStore((state) => state.getMapValuesByKey)
const industryList = getMapValuesByKey('bi_data_industry')
const generateDataSource = (count: number) => {
return Array.from({ length: count }, (_, index) => ({
id: index,
key: index,
name: `数据集名称 ${index + 1}`,
count: Math.floor(Math.random() * 1000),
industry: `行业 ${index + 1}`,
source: `来源 ${index + 1}`,
level: `等级 ${index + 1}`,
auth: `权限 ${index + 1}`,
desc: `说明 ${index + 1}`,
create: `创建人 ${index + 1}`,
create_time: new Date().toLocaleString(),
update_time: new Date().toLocaleString(),
}))
}
const [viewModalIsOpen, setViewModalIsOpen] = useState(false)
const handleView = (record: any) => {
console.log(record)
setViewModalIsOpen(true)
}
const [copyModalIsOpen, setCopyModalIsOpen] = useState(false)
const handleCopy = (record: any) => {
console.log(record)
setCopyModalIsOpen(true)
}
const listOptions: AppListProps = {
filters: [
{
name: 'industry',
element: <Select placeholder="请选择所属行业" options={industryList} />,
},
{
name: 'name',
element: <Input placeholder="请输入数据集名称" />,
},
],
columns: [
{
title: '序号',
key: 'index',
render(_value, _record, index) {
return index + 1
},
width: 62,
align: 'center',
},
{ title: '数据集名称', dataIndex: 'name', align: 'center' },
{ title: '数据量', dataIndex: 'count', align: 'center' },
{ title: '所属行业', dataIndex: 'industry', align: 'center' },
{ title: '数据来源', dataIndex: 'source', align: 'center' },
{ title: '敏感等级', dataIndex: 'level', align: 'center' },
{ title: '访问权限', dataIndex: 'auth', align: 'center' },
{ title: '说明', dataIndex: 'desc', align: 'center' },
{ title: '创建人', dataIndex: 'create', align: 'center' },
{ title: '创建时间', dataIndex: 'create_time', align: 'center' },
{ title: '更新时间', dataIndex: 'update_time', align: 'center' },
{
title: '操作',
key: 'x',
width: 160,
align: 'center',
render(_value, record) {
return (
<>
<Button color="primary" variant="text" onClick={() => handleView(record)}>
查阅
</Button>
<Button color="primary" variant="text" onClick={() => handleCopy(record)}>
复制
</Button>
</>
)
},
},
],
dataSource: generateDataSource(20),
}
return (
<div>
<Card className="app-card" title="数据复制"></Card>
</div>
<Card className="app-card" title="数据复制">
<AppList bordered {...listOptions}></AppList>
<CopyModal open={copyModalIsOpen} onCancel={() => setCopyModalIsOpen(false)}></CopyModal>
<ViewDataModal open={viewModalIsOpen} onCancel={() => setViewModalIsOpen(false)}></ViewDataModal>
</Card>
)
}
import { Card } from 'antd'
import { Button, Card, Empty, Flex, Space } from 'antd'
import { Link } from 'react-router'
export default function DataWriteMy() {
const isEmpty = true
// 无数据渲染
const emptyRender = () => {
return (
<>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="无数据"></Empty>
<Flex justify="center" align="middle">
<Space>
<Link to="/data/write/copy">
<Button type="primary">复制数据</Button>
</Link>
<Link to="/data/write/upload">
<Button type="primary">导入数据</Button>
</Link>
<Button type="primary" disabled>
爬取数据
</Button>
<Button type="primary" disabled>
本地数据库连接
</Button>
<Button type="primary" disabled>
API数据获取
</Button>
</Space>
</Flex>
</>
)
}
export default function DataWriteUpload() {
return (
<div>
<Card className="app-card" title="我的数据集"></Card>
</div>
<Card className="app-card" title="我的数据集">
<Flex justify="space-between" align="middle">
<h4>数据集名称:电子商务案例分析数据集</h4>
<Button type="primary" disabled={isEmpty}>
数据质量分析报告
</Button>
</Flex>
{!isEmpty && (
<Flex justify="space-between" align="middle">
<p>《商务数据分析基础》数据集-电子商务2025.xlsx</p>
<p>共计:1000条数据</p>
</Flex>
)}
{isEmpty && emptyRender()}
</Card>
)
}
import { Card } from 'antd'
import { Button, Card, Flex, Form, Input, Upload } from 'antd'
import type { UploadProps } from 'antd'
import { InboxOutlined, PlusOutlined } from '@ant-design/icons'
import { useState } from 'react'
import { read, utils } from 'xlsx'
import DataRender from '@/components/data/DataRender'
export default function DataWriteUpload() {
const [file, setFile] = useState<File | null>(null)
const [data, setData] = useState<any[]>([])
const columns: any =
data.length > 0
? Object.keys(data[0]).map((key) => ({
title: key,
dataIndex: key,
align: 'center',
}))
: []
console.log(columns)
console.log(data)
const props: UploadProps = {
showUploadList: false,
accept: '.xlsx,.csv',
beforeUpload: async (file) => {
setFile(file)
const data = await file.arrayBuffer()
const workbook = read(data)
const sheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[sheetName]
const jsonData = utils.sheet_to_json(worksheet)
setData(jsonData)
return false
},
}
return (
<div>
<Card className="app-card" title="数据导入"></Card>
</div>
<Card className="app-card" title="数据导入">
{file ? (
<div>
<Form layout="inline">
<Form.Item label="数据集名称">
<Input placeholder="请输入" style={{ width: '220px' }}></Input>
</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>
<span style={{ marginLeft: '10px' }}>{file?.name}</span>
</div>
<p>共计:{data.length}条数据</p>
<Button type="primary">保存</Button>
</Flex>
<DataRender dataSource={data} columns={columns}></DataRender>
</div>
) : (
<Upload.Dragger {...props}>
<div style={{ padding: '100px 0' }}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p>此操作将会把上传的.xlsx文件或.csv文件,导入到后台数据库中,以便于后续操作处理。</p>
<p>
需要注意的是,每个实验中,一个账号只能同时操作一个文件,
<span style={{ color: 'var(--main-color)' }}>
如果重复上传,那么最新一次的上传数据将会替换原先上传的数据,导致原先对数据做的所有操作均会无效!
</span>
</p>
</div>
</Upload.Dragger>
)}
</Card>
)
}
import { create } from 'zustand'
import { getMapList } from '@/api/base'
interface State {
mapList: MapState[]
setMap: (data: MapState[]) => void
getMap: () => Promise<void>
getMapValuesByKey: (key: string) => MapValues[]
getNameByValue: (value: string, list: MapValues[]) => string
}
interface MapState {
id: string
key: string
name: string
remark: string
values: MapValues[]
}
interface MapValues {
data_dictionary_id: string
id: string
label: string
remark: string
sort: string
value: string
}
export const useMapStore = create<State>((set, get) => ({
mapList: [],
setMap: (data: MapState[]) => {
set({ mapList: data })
},
getMap: async () => {
const { data } = await getMapList()
set({ mapList: data })
},
getMapValuesByKey: (key) => {
return get().mapList.find((map) => map.key === key)?.values || []
},
getNameByValue: (value, list) => {
return list.find((item) => item.value == value)?.label || value
},
}))
import { getUser } from '@/api/base'
import { create } from 'zustand'
import { getUser } from '@/api/base'
// 用户信息
export interface UserType {
id: string
mobile: string
name: string
email: string
username: string
avatar: string
}
// 角色信息(1学员;5教师; 6管理员)
export interface RoleType {
id: 1 | 5 | 6
name: string
}
export const useUserStore = create((set) => ({
user: null,
// 权限信息
export interface PermissionType {
desc: string
effect_uris: string
id: string
name: string
parent_id: string
system_tag: number
type: number
tag: string
}
interface State {
info: UserType | null
role: RoleType | null
permissions: PermissionType[]
setUser: (data: any) => void
getUser: () => Promise<void>
}
export const useUserStore = create<State>((set) => ({
info: null,
role: null,
permissions: [],
setUser: (data) => {
set({ info: data.info, role: data.role, permissions: data.permissions.permissions })
},
getUser: async () => {
const res = await getUser()
set({ user: res.data })
const { data } = await getUser()
set({ info: data.info, role: data.role, permissions: data.permissions.permissions })
},
}))
import axios from 'axios'
import { message } from 'antd'
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'https://api.example.com',
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
// timeout: 60000,
// withCredentials: true,
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded',
// },
})
axiosInstance.interceptors.response.use(
(response) => response,
(response) => {
const { data } = response
// 未登录
if (data.code === 4001) {
location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}`
return Promise.reject(data)
}
if (data.code === 1 || data.code === -1) {
message.error(data.message || data.msg)
return Promise.reject(data)
}
return data
},
(error) => {
console.error('API Error:', error)
return Promise.reject(error)
if (error.response) {
const { status, message } = error.response.data
// 未登录
if (status === 403) {
location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}`
} else {
message.error(message || error.message)
console.error(`${status}: ${message}`)
}
} else {
console.error(error)
}
return Promise.reject(error.response || error)
}
)
......
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { fileURLToPath, URL } from 'node:url'
import AutoImport from 'unplugin-auto-import/vite'
import mkcert from 'vite-plugin-mkcert'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
AutoImport({
imports: ['react', 'react-router'],
dts: true,
eslintrc: { enabled: true },
}),
mkcert(),
],
plugins: [react(), mkcert()],
server: {
open: true,
host: 'dev.ezijing.com',
proxy: {
'/api': 'https://saas-dml.ezijing.com',
'/api': 'https://saas-lab.ezijing.com',
},
},
resolve: {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论