first commit
This commit is contained in:
parent
c9afb6e2bf
commit
6f1c5d6ef5
53 changed files with 16658 additions and 0 deletions
9
client/.gitignore
vendored
Normal file
9
client/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/node_modules
|
||||
/.env.local
|
||||
/.umirc.local.ts
|
||||
/config/config.local.ts
|
||||
/src/.umi
|
||||
/src/.umi-production
|
||||
/src/.umi-test
|
||||
/dist
|
||||
.swc
|
||||
2
client/.npmrc
Normal file
2
client/.npmrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
registry=https://registry.npmmirror.com/
|
||||
|
||||
18
client/.umirc.ts
Normal file
18
client/.umirc.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { defineConfig } from "umi";
|
||||
import routes from './routes'
|
||||
|
||||
export default defineConfig({
|
||||
outputPath: 'dist',
|
||||
// alias: { '@': './src' },
|
||||
routes,
|
||||
npmClient: 'npm',
|
||||
base: '/',
|
||||
publicPath: '/client/dist/',
|
||||
hash: true,
|
||||
history: {
|
||||
type: 'hash',
|
||||
},
|
||||
plugins: ['@umijs/plugins/dist/dva'],
|
||||
dva: {}
|
||||
});
|
||||
|
||||
14375
client/package-lock.json
generated
Normal file
14375
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
30
client/package.json
Normal file
30
client/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"private": true,
|
||||
"author": "zhaofengchao <13723060510@163.com>",
|
||||
"scripts": {
|
||||
"dev": "umi dev",
|
||||
"build": "umi build",
|
||||
"postinstall": "umi setup",
|
||||
"setup": "umi setup",
|
||||
"start": "npm run dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@ant-design/pro-layout": "^7.17.16",
|
||||
"antd": "^5.12.7",
|
||||
"axios": "^1.6.3",
|
||||
"classnames": "^2.5.1",
|
||||
"i18next": "^23.7.16",
|
||||
"lodash": "^4.17.21",
|
||||
"react-i18next": "^14.0.0",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"umi": "^4.0.90",
|
||||
"umi-request": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@umijs/plugins": "^4.1.0",
|
||||
"typescript": "^5.0.3"
|
||||
}
|
||||
}
|
||||
27
client/reducer.js
Normal file
27
client/reducer.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React, { useReducer } from 'react'
|
||||
const CHANGE_LOCALE = 'CHANGE_LOCALE'
|
||||
|
||||
const mainContext = React.createContext()
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case CHANGE_LOCALE:
|
||||
return { ...state, locale: action.locale || 'zh' }
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const ContextProvider = (props) => {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
locale: 'zh'
|
||||
})
|
||||
return (
|
||||
<mainContext.Provider value={{ state, dispatch }}>
|
||||
{props.children}
|
||||
</mainContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export { reducer, mainContext, ContextProvider }
|
||||
|
||||
80
client/routes.js
Normal file
80
client/routes.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
component: '@/pages/login',
|
||||
layout: false
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: '@/layouts', // 默认页面
|
||||
redirect: '/knowledge',
|
||||
// wrappers: [
|
||||
// '@/wrappers/auth',
|
||||
// ]
|
||||
},
|
||||
|
||||
{
|
||||
id: 2,
|
||||
name: '知识库',
|
||||
icon: 'home',
|
||||
auth: [3, 4, 100],
|
||||
path: '/knowledge',
|
||||
component: '@/pages/knowledge',
|
||||
pathname: 'knowledge'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '知识库',
|
||||
icon: 'home',
|
||||
auth: [3, 4, 100],
|
||||
path: '/knowledge/add',
|
||||
component: '@/pages/add-knowledge',
|
||||
pathname: 'knowledge',
|
||||
// routes: [{
|
||||
// id: 3,
|
||||
// name: '设置',
|
||||
// icon: 'home',
|
||||
// auth: [3, 4, 100],
|
||||
// path: '/knowledge/add/setting',
|
||||
// component: '@/pages/setting',
|
||||
// pathname: "setting"
|
||||
// }, {
|
||||
// id: 1,
|
||||
// name: '文件',
|
||||
// icon: 'file',
|
||||
// auth: [3, 4, 100],
|
||||
// path: '/knowledge/add/file',
|
||||
// component: '@/pages/file',
|
||||
// pathname: 'file'
|
||||
// },]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '聊天',
|
||||
icon: 'home',
|
||||
auth: [3, 4, 100],
|
||||
path: '/chat',
|
||||
component: '@/pages/chat',
|
||||
pathname: "chat"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '设置',
|
||||
icon: 'home',
|
||||
auth: [3, 4, 100],
|
||||
path: '/setting',
|
||||
component: '@/pages/setting',
|
||||
pathname: "setting"
|
||||
},
|
||||
{
|
||||
path: '/*',
|
||||
component: '@/pages/404',
|
||||
layout: false
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
|
||||
module.exports = routes;
|
||||
BIN
client/src/assets/1.pdf
Normal file
BIN
client/src/assets/1.pdf
Normal file
Binary file not shown.
BIN
client/src/assets/yay.jpg
Normal file
BIN
client/src/assets/yay.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
38
client/src/layouts/components/user/index.tsx
Normal file
38
client/src/layouts/components/user/index.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Button, Dropdown, } from 'antd';
|
||||
import { history } from 'umi'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
|
||||
const App: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const logout = () => { history.push('/login') }
|
||||
const toSetting = () => { history.push('/setting') }
|
||||
const items: MenuProps['items'] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<Button type="text" onClick={logout}>{t('header.logout')}</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<Button type="text" onClick={toSetting}>{t('header.setting')}</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
}, []);
|
||||
|
||||
return (<>
|
||||
<Dropdown menu={{ items }} placement="bottomLeft" arrow>
|
||||
<img
|
||||
style={{ width: '50px', height: '50px', borderRadius: '25px' }}
|
||||
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
|
||||
/>
|
||||
</Dropdown>
|
||||
</>)
|
||||
}
|
||||
|
||||
export default App;
|
||||
33
client/src/layouts/index.less
Normal file
33
client/src/layouts/index.less
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
.navs {
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.layout {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
height: 40px;
|
||||
padding: 0 30px;
|
||||
margin: 0 5px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checked {
|
||||
color: #1677ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
82
client/src/layouts/index.tsx
Normal file
82
client/src/layouts/index.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { history, Outlet, useLocation, useNavigate } from 'umi';
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import classnames from 'classnames'
|
||||
import '../locales/config';
|
||||
import {
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Layout, Button, theme, Space, } from 'antd';
|
||||
import styles from './index.less'
|
||||
import User from './components/user'
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
|
||||
const App: React.FC = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
token: { colorBgContainer, borderRadiusLG },
|
||||
} = theme.useToken();
|
||||
const [current, setCurrent] = useState('knowledge');
|
||||
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
if (location.pathname !== '/') {
|
||||
const path = location.pathname.split('/')
|
||||
setCurrent(path[1]);
|
||||
}
|
||||
console.log(location.pathname.split('/'))
|
||||
}, [location.pathname])
|
||||
|
||||
const handleChange = (path: string) => {
|
||||
setCurrent(path)
|
||||
navigate(path);
|
||||
};
|
||||
const tagsData = [{ path: '/knowledge', name: 'knowledge' }, { path: '/chat', name: 'chat' }];
|
||||
|
||||
return (
|
||||
<Layout className={styles.layout} >
|
||||
<Layout>
|
||||
<Header style={{ padding: 0, background: colorBgContainer, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
width: 64,
|
||||
height: 64,
|
||||
}}
|
||||
/>
|
||||
<Space size={[0, 8]} wrap>
|
||||
{tagsData.map((item) =>
|
||||
(<span key={item.name} className={classnames(styles['tag'], {
|
||||
[styles['checked']]: current === item.name
|
||||
})} onClick={() => handleChange(item.path)}>
|
||||
{item.name}
|
||||
</span>)
|
||||
)}
|
||||
</Space>
|
||||
<User ></User>
|
||||
</Header>
|
||||
<Content
|
||||
style={{
|
||||
margin: '24px 16px',
|
||||
padding: 24,
|
||||
minHeight: 280,
|
||||
background: colorBgContainer,
|
||||
borderRadius: borderRadiusLG,
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout >
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
24
client/src/locales/config.ts
Normal file
24
client/src/locales/config.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import translation_en from './en.json';
|
||||
import translation_zh from './zh.json';
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: translation_en,
|
||||
},
|
||||
zh: {
|
||||
translation: translation_zh,
|
||||
},
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: 'zh',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
20
client/src/locales/en.json
Normal file
20
client/src/locales/en.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"header": {
|
||||
"register": "Register",
|
||||
"signin": "Sign In",
|
||||
"home": "Home",
|
||||
"setting": "用户设置",
|
||||
"logout": "登出"
|
||||
},
|
||||
"footer": {
|
||||
"detail": "All rights reserved @ React"
|
||||
},
|
||||
"layout": {
|
||||
"file": "file",
|
||||
"knowledge": "knowledge",
|
||||
"chat": "chat"
|
||||
},
|
||||
"setting": {
|
||||
"btn": "en"
|
||||
}
|
||||
}
|
||||
20
client/src/locales/zh.json
Normal file
20
client/src/locales/zh.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"header": {
|
||||
"register": "注册",
|
||||
"signin": "登陆",
|
||||
"home": "首页",
|
||||
"setting": "user setting",
|
||||
"logout": "logout"
|
||||
},
|
||||
"footer": {
|
||||
"detail": "版权所有 @ React"
|
||||
},
|
||||
"layout": {
|
||||
"file": "文件",
|
||||
"knowledge": "知识库",
|
||||
"chat": "聊天"
|
||||
},
|
||||
"setting": {
|
||||
"btn": "中文"
|
||||
}
|
||||
}
|
||||
20
client/src/pages/404.js
Normal file
20
client/src/pages/404.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Button, Result } from 'antd';
|
||||
import React from 'react';
|
||||
import { history } from 'umi';
|
||||
|
||||
const NoFoundPage = () => {
|
||||
return (
|
||||
<Result
|
||||
status= "404"
|
||||
title = "404"
|
||||
subTitle = "页面未找到,请输入正确的地址。"
|
||||
extra = {
|
||||
< Button type = "primary" onClick = {() => history.push('/')}>
|
||||
返回主页
|
||||
< /Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoFoundPage;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.filter {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
margin: 10px 0;
|
||||
justify-content: space-between;
|
||||
|
||||
.search {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.operate {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { Space, Table, Tag, Input, Button } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { debounce } from 'lodash';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import styles from './idnex.less'
|
||||
|
||||
interface DataType {
|
||||
key: string;
|
||||
name: string;
|
||||
age: number;
|
||||
address: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const columns: ColumnsType<DataType> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text) => <a>{text}</a>,
|
||||
},
|
||||
{
|
||||
title: 'Age',
|
||||
dataIndex: 'age',
|
||||
key: 'age',
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
dataIndex: 'address',
|
||||
key: 'address',
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
key: 'tags',
|
||||
dataIndex: 'tags',
|
||||
render: (_, { tags }) => (
|
||||
<>
|
||||
{tags.map((tag) => {
|
||||
let color = tag.length > 5 ? 'geekblue' : 'green';
|
||||
if (tag === 'loser') {
|
||||
color = 'volcano';
|
||||
}
|
||||
return (
|
||||
<Tag color={color} key={tag}>
|
||||
{tag.toUpperCase()}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<a>Invite {record.name}</a>
|
||||
<a>Delete</a>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const data: DataType[] = [
|
||||
{
|
||||
key: '1',
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
tags: ['nice', 'developer'],
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
tags: ['loser'],
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sydney No. 1 Lake Park',
|
||||
tags: ['cool', 'teacher'],
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const changeValue = (value: string) => {
|
||||
{
|
||||
console.log(value)
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
const debounceChange = debounce(changeValue, 300)
|
||||
const debounceCallback = useCallback((value: string) => debounceChange(value), [])
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const value = e.target.value
|
||||
setLoading(true)
|
||||
setInputValue(value)
|
||||
debounceCallback(e.target.value)
|
||||
|
||||
}
|
||||
return <>
|
||||
<div className={styles.filter}>
|
||||
<div className="search">
|
||||
<Input placeholder="搜索" value={inputValue} allowClear onChange={handleInputChange} />
|
||||
</div>
|
||||
<div className="operate">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => { }}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Table columns={columns} dataSource={data} loading={loading} />
|
||||
</>
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default () => {
|
||||
return <div>知识库搜索</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.tags {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.preset {
|
||||
display: flex;
|
||||
height: 80px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 100px;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.4);
|
||||
margin: 10px 0px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Form, Input, InputNumber, Radio, Select, Tag, Space, Avatar, Divider, List, Skeleton } from 'antd';
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import styles from './index.less'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 8 },
|
||||
wrapperCol: { span: 16 },
|
||||
labelAlign: 'left' as const
|
||||
};
|
||||
const { Option } = Select
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
const validateMessages = {
|
||||
required: '${label} is required!',
|
||||
types: {
|
||||
email: '${label} is not a valid email!',
|
||||
number: '${label} is not a valid number!',
|
||||
},
|
||||
number: {
|
||||
range: '${label} must be between ${min} and ${max}',
|
||||
},
|
||||
};
|
||||
/* eslint-enable no-template-curly-in-string */
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
console.log(values);
|
||||
};
|
||||
interface DataType {
|
||||
gender: string;
|
||||
name: {
|
||||
title: string;
|
||||
first: string;
|
||||
last: string;
|
||||
};
|
||||
email: string;
|
||||
picture: {
|
||||
large: string;
|
||||
medium: string;
|
||||
thumbnail: string;
|
||||
};
|
||||
nat: string;
|
||||
}
|
||||
const tags = [{ title: '研报' }, { title: '法律' }, { title: '简历' }, { title: '说明书' }, { title: '书籍' }, { title: '演讲稿' }]
|
||||
|
||||
const App: React.FC = () => {
|
||||
|
||||
return <Form
|
||||
{...layout}
|
||||
name="nest-messages"
|
||||
onFinish={onFinish}
|
||||
style={{ maxWidth: 600 }}
|
||||
validateMessages={validateMessages}
|
||||
>
|
||||
<Form.Item name={['user', 'name']} label="知识库名称" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={['user', 'introduction']} label="知识库描述">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<Form.Item name="radio-group" label="可见权限">
|
||||
<Radio.Group>
|
||||
<Radio value="a">只有我</Radio>
|
||||
<Radio value="b">所有团队成员</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="select"
|
||||
label="Embedding 模型"
|
||||
hasFeedback
|
||||
rules={[{ required: true, message: 'Please select your country!' }]}
|
||||
>
|
||||
<Select placeholder="Please select a country">
|
||||
<Option value="china">China</Option>
|
||||
<Option value="usa">U.S.A</Option>
|
||||
</Select>
|
||||
<div style={{ marginTop: '5px' }}>
|
||||
修改Embedding 模型,请去<span style={{ color: '#1677ff' }}>设置</span>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Space size={[0, 8]} wrap>
|
||||
<div className={styles.tags}>
|
||||
{
|
||||
tags.map(item => {
|
||||
return (<Tag key={item.title}>{item.title}</Tag>)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</Space>
|
||||
<Space size={[0, 8]} wrap>
|
||||
|
||||
</Space>
|
||||
<div className={styles.preset}>
|
||||
<div className={styles.left}>
|
||||
xxxxx文章
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
预估份数
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item wrapperCol={{ ...layout.wrapperCol, offset: 8 }}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存并处理
|
||||
</Button>
|
||||
<Button htmlType="button" style={{ marginLeft: '20px' }}>
|
||||
取消
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default App;
|
||||
7
client/src/pages/add-knowledge/index.less
Normal file
7
client/src/pages/add-knowledge/index.less
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.tab {
|
||||
:global {
|
||||
.ant-tabs-tabpane {
|
||||
margin: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
client/src/pages/add-knowledge/index.tsx
Normal file
51
client/src/pages/add-knowledge/index.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React, { useState } from 'react';
|
||||
import type { RadioChangeEvent } from 'antd';
|
||||
import { Radio, Space, Tabs } from 'antd';
|
||||
import {
|
||||
ToolOutlined,
|
||||
BarsOutlined,
|
||||
SearchOutlined
|
||||
} from '@ant-design/icons';
|
||||
import File from './components/knowledge-file'
|
||||
import Setting from './components/knowledge-setting'
|
||||
import Search from './components/knowledge-search'
|
||||
import styles from './index.less'
|
||||
|
||||
|
||||
const App: React.FC = () => {
|
||||
type keyType = 'setting' | 'file' | 'search'
|
||||
const [activeKey, setActiveKey] = useState<keyType>('file')
|
||||
// type tab = { label: string, icon: Element, tag: string }
|
||||
const tabs = [{ label: '配置', icon: <ToolOutlined />, tag: 'setting' }, { label: '知识库', icon: <BarsOutlined />, tag: 'file' }, { label: '搜索测试', icon: <SearchOutlined />, tag: 'search' }]
|
||||
|
||||
const onTabClick = (activeKey: keyType) => {
|
||||
setActiveKey(activeKey)
|
||||
}
|
||||
// type stringKey = Record<string, Element>
|
||||
|
||||
const mapComponent = {
|
||||
file: <File />,
|
||||
setting: <Setting />,
|
||||
search: <Search />
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
tabPosition='left'
|
||||
activeKey={activeKey}
|
||||
onTabClick={(activeKey: keyType, e: KeyboardEvent<Element> | MouseEvent<Element, MouseEvent>) => { onTabClick(activeKey) }}
|
||||
className={styles.tab}
|
||||
items={tabs.map((item) => {
|
||||
return {
|
||||
label: item.label,
|
||||
icon: item.icon,
|
||||
key: item.tag,
|
||||
children: mapComponent[activeKey] as Element,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
19
client/src/pages/chat/index.tsx
Normal file
19
client/src/pages/chat/index.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React, { FC } from 'react';
|
||||
import { IndexModelState, ConnectProps, Loading, connect } from 'umi';
|
||||
|
||||
interface PageProps extends ConnectProps {
|
||||
index: IndexModelState;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const IndexPage: FC<PageProps> = ({ index, dispatch }) => {
|
||||
const { name } = index;
|
||||
return <div>chat: {name}</div>;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ index, loading }: { index: IndexModelState; loading: Loading }) => ({
|
||||
index,
|
||||
loading: loading.models.index,
|
||||
}),
|
||||
)(IndexPage);
|
||||
52
client/src/pages/chat/model.ts
Normal file
52
client/src/pages/chat/model.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { Effect, ImmerReducer, Reducer, Subscription } from 'umi';
|
||||
|
||||
export interface IndexModelState {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IndexModelType {
|
||||
namespace: 'index';
|
||||
state: IndexModelState;
|
||||
effects: {
|
||||
query: Effect;
|
||||
};
|
||||
reducers: {
|
||||
save: Reducer<IndexModelState>;
|
||||
// 启用 immer 之后
|
||||
// save: ImmerReducer<IndexModelState>;
|
||||
};
|
||||
subscriptions: { setup: Subscription };
|
||||
}
|
||||
|
||||
const IndexModel: IndexModelType = {
|
||||
namespace: 'index',
|
||||
state: {
|
||||
name: 'kate',
|
||||
},
|
||||
|
||||
effects: {
|
||||
*query({ payload }, { call, put }) { },
|
||||
},
|
||||
reducers: {
|
||||
save(state, action) {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
},
|
||||
// 启用 immer 之后
|
||||
// save(state, action) {
|
||||
// state.name = action.payload;
|
||||
// },
|
||||
},
|
||||
subscriptions: {
|
||||
setup({ dispatch, history }) {
|
||||
return history.listen((query) => {
|
||||
console.log(query)
|
||||
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default IndexModel;
|
||||
50
client/src/pages/file/index.tsx
Normal file
50
client/src/pages/file/index.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { Button, Upload } from 'antd';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [fileList, setFileList] = useState([{
|
||||
uid: '0',
|
||||
name: 'xxx.png',
|
||||
status: 'uploading',
|
||||
percent: 10,
|
||||
}])
|
||||
const obj = {
|
||||
uid: '-1',
|
||||
name: 'yyy.png',
|
||||
status: 'done',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
}
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setFileList((fileList) => {
|
||||
const percent = fileList[0]?.percent
|
||||
if (percent + 10 >= 100) {
|
||||
clearInterval(timer)
|
||||
return [obj]
|
||||
}
|
||||
const list = [{ ...fileList[0], percent: percent + 10 }]
|
||||
console.log(list)
|
||||
return list
|
||||
|
||||
})
|
||||
}, 300)
|
||||
}, [])
|
||||
return (
|
||||
|
||||
<>
|
||||
<Upload
|
||||
action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188"
|
||||
listType="picture"
|
||||
fileList={[...fileList]}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>Upload</Button>
|
||||
</Upload>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default App;
|
||||
35
client/src/pages/knowledge/index.less
Normal file
35
client/src/pages/knowledge/index.less
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
.container {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.context {
|
||||
flex: 1
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 20px;
|
||||
|
||||
.text {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
:global {
|
||||
.ant-card-body {
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
88
client/src/pages/knowledge/index.tsx
Normal file
88
client/src/pages/knowledge/index.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import React, { useState, } from 'react';
|
||||
import { useNavigate } from 'umi'
|
||||
import { Card, List, Popconfirm, message, FloatButton } from 'antd';
|
||||
import { MinusSquareOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import styles from './index.less'
|
||||
|
||||
const data = [
|
||||
{
|
||||
title: 'Title 1',
|
||||
text: '1',
|
||||
des: '111'
|
||||
},
|
||||
{
|
||||
title: 'Title 2',
|
||||
text: '2',
|
||||
des: '111'
|
||||
},
|
||||
{
|
||||
title: 'Title 3',
|
||||
text: '3',
|
||||
des: '111'
|
||||
},
|
||||
{
|
||||
title: 'Title 4',
|
||||
text: '4',
|
||||
des: '111'
|
||||
},
|
||||
];
|
||||
const dd = [{
|
||||
title: 'Title 4',
|
||||
text: '4',
|
||||
des: '111'
|
||||
}]
|
||||
const App: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const [datas, setDatas] = useState(data)
|
||||
const confirm = (index: number) => {
|
||||
console.log(index);
|
||||
setDatas(datas => datas.filter((item, i) => i !== index))
|
||||
message.success('Click on Yes');
|
||||
};
|
||||
const handleAddKnowledge = () => {
|
||||
// setDatas((datas) => [...datas, ...dd])
|
||||
navigate('add');
|
||||
}
|
||||
return (<><FloatButton onClick={handleAddKnowledge} icon={<PlusOutlined />} type="primary" style={{ right: 24, top: 100 }} />
|
||||
<List
|
||||
grid={{ gutter: 16, column: 4 }}
|
||||
dataSource={datas}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<span className={styles.context}>
|
||||
content
|
||||
</span>
|
||||
<span className={styles.delete}>
|
||||
<Popconfirm
|
||||
title="Delete the task"
|
||||
description="Are you sure to delete this task?"
|
||||
onConfirm={() => { confirm(index) }}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<span className={styles.text}>
|
||||
<MinusSquareOutlined />{item.text}
|
||||
</span>
|
||||
<span className={styles.text}>
|
||||
<MinusSquareOutlined />{item.des}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
/></>
|
||||
)
|
||||
};
|
||||
|
||||
export default App;
|
||||
17
client/src/pages/login/index.less
Normal file
17
client/src/pages/login/index.less
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
@import '../../theme/vars';
|
||||
|
||||
|
||||
|
||||
.modal {
|
||||
width: 500px;
|
||||
height: 400px;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
left: 50%;
|
||||
border-radius: 4px;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 1px solid rgba(0, 0, 0, 0.4);
|
||||
padding-right: 70px;
|
||||
padding-top: 100px;
|
||||
}
|
||||
90
client/src/pages/login/index.tsx
Normal file
90
client/src/pages/login/index.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { connect } from 'umi';
|
||||
import { Input, Form, Button, Checkbox } from 'antd';
|
||||
// import md5 from 'md5';
|
||||
import styles from './index.less';
|
||||
|
||||
const View = ({
|
||||
loginModel,
|
||||
dispatch,
|
||||
location,
|
||||
}) => {
|
||||
const onFinish = (params: any) => {
|
||||
console.log('Success:', params);
|
||||
params.mail = params.username.replace(/^\s+|\s+$/g, '');
|
||||
params.passwordMd5 = params.password.replace(/^\s+|\s+$/g, '');
|
||||
|
||||
dispatch({
|
||||
type: 'loginModel/login',
|
||||
payload: {
|
||||
mail: params.mail,
|
||||
authority: 1,
|
||||
passwordMd5: params.passwordMd5
|
||||
// passwordMd5: md5(params.passwordMd5).toLocaleUpperCase()
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log('Failed:', errorInfo);
|
||||
};
|
||||
|
||||
|
||||
type FieldType = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
remember?: string;
|
||||
};
|
||||
|
||||
const onJump = () => {
|
||||
window.location.href = 'http://www.martechlab.cn/';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.loginContainer}>
|
||||
<div className={styles.modal}>
|
||||
<Form
|
||||
name="basic"
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
style={{ maxWidth: 600 }}
|
||||
initialValues={{ remember: true }}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label="mail"
|
||||
name="username"
|
||||
rules={[{ required: true, message: 'Please input your mail!' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Password"
|
||||
name="password"
|
||||
rules={[{ required: true, message: 'Please input your password!' }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
|
||||
{/* <Form.Item<FieldType>
|
||||
name="remember"
|
||||
valuePropName="checked"
|
||||
wrapperCol={{ offset: 8, span: 16 }}
|
||||
>
|
||||
<Checkbox>Remember me</Checkbox>
|
||||
</Form.Item> */}
|
||||
|
||||
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
|
||||
<Button type="primary" htmlType="submit" >
|
||||
Submit
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(({ loginModel, loading }) => ({ loginModel, loading }))(View);
|
||||
55
client/src/pages/login/model.ts
Normal file
55
client/src/pages/login/model.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { message } from 'antd';
|
||||
import store from '@/utils/persistStore';
|
||||
import { addParam } from '@/utils';
|
||||
import loginService from '@/services/loginService';
|
||||
|
||||
const Model = {
|
||||
namespace: 'loginModel',
|
||||
state: {
|
||||
list: [],
|
||||
info: {},
|
||||
visible: false,
|
||||
pagination: {},
|
||||
campaignInfo: {}
|
||||
},
|
||||
subscriptions: {
|
||||
setup({ dispatch, history }) {
|
||||
history.listen(location => { });
|
||||
}
|
||||
},
|
||||
effects: {
|
||||
*login({ payload = {} }, { call, put }) {
|
||||
console.log(111)
|
||||
const { code, data, errorMessage } = yield call(loginService.login, payload);
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 300);
|
||||
// if (code === 0) {
|
||||
// message.success('登录成功!');
|
||||
// const name = data.name;
|
||||
// const token = data.token;
|
||||
// const role = data.role;
|
||||
// const title = data.title;
|
||||
// const userInfo = {
|
||||
// role: data.role,
|
||||
// title: data.title,
|
||||
// name: data.name || data.Name,
|
||||
// };
|
||||
// store.token = token;
|
||||
// store.userInfo = userInfo;
|
||||
// setTimeout(() => {
|
||||
// window.location.href = '/file';
|
||||
// }, 300);
|
||||
// }
|
||||
}
|
||||
},
|
||||
reducers: {
|
||||
updateState(state, { payload }) {
|
||||
return {
|
||||
...state,
|
||||
...payload
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
export default Model;
|
||||
20
client/src/pages/setting/index.tsx
Normal file
20
client/src/pages/setting/index.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
import i18n from 'i18next';
|
||||
import { Button } from 'antd'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
|
||||
function Index() {
|
||||
const changeLang = (val: string) => { // 改变状态里的 语言 进行切换
|
||||
i18n.changeLanguage(val);
|
||||
}
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Button type="primary" onClick={() => i18n.changeLanguage(i18n.language == 'en' ? 'zh' : 'en')}>{t('setting.btn')}</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Index;
|
||||
33
client/src/services/chatService.ts
Normal file
33
client/src/services/chatService.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import api from '@/utils/api';
|
||||
import registerServer from '@/utils/registerServer';
|
||||
import request from '@/utils/request';
|
||||
|
||||
const {
|
||||
create_account,
|
||||
update_account,
|
||||
account_detail,
|
||||
getUserDetail, } = api;
|
||||
|
||||
const chatService = registerServer(
|
||||
{
|
||||
createAccount: {
|
||||
url: create_account,
|
||||
method: 'post'
|
||||
},
|
||||
updateAccount: {
|
||||
url: update_account,
|
||||
method: 'post'
|
||||
},
|
||||
getAccountDetail: {
|
||||
url: account_detail,
|
||||
method: 'post'
|
||||
},
|
||||
getUserDetail: {
|
||||
url: getUserDetail,
|
||||
method: 'post'
|
||||
}
|
||||
},
|
||||
request
|
||||
);
|
||||
|
||||
export default chatService;
|
||||
18
client/src/services/loginService.ts
Normal file
18
client/src/services/loginService.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import api from '@/utils/api';
|
||||
import registerServer from '@/utils/registerServer';
|
||||
import request from '@/utils/request';
|
||||
|
||||
const {
|
||||
login, } = api;
|
||||
|
||||
const chatService = registerServer(
|
||||
{
|
||||
login: {
|
||||
url: login,
|
||||
method: 'post'
|
||||
}
|
||||
},
|
||||
request
|
||||
);
|
||||
|
||||
export default chatService;
|
||||
11
client/src/theme/theme.ts
Normal file
11
client/src/theme/theme.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
'primary-color': '#338AFF', // '#338AFF',
|
||||
'border-radius-base': '4px',
|
||||
// 'menu-dark-color': '',
|
||||
// 'menu-dark-danger-color': '',
|
||||
'menu-dark-bg': '#092140',
|
||||
'menu-dark-item-active-bg': '#092140'
|
||||
|
||||
// 'menu-dark-arrow-color': '',
|
||||
// 'menu-dark-inline-submenu-bg': '',
|
||||
};
|
||||
9
client/src/theme/vars.less
Normal file
9
client/src/theme/vars.less
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
@header-height: 64px;
|
||||
@menu-width: 200px;
|
||||
@menu-small-width: 83px;
|
||||
@layout-bg: #f2f3f6;
|
||||
@logo-font-size: 22px;
|
||||
@border-color: #d9d9d9;
|
||||
@dashboard-desc-color: #9d9fa2;
|
||||
@primary-color: #338aff;
|
||||
@primary-color-light: rgba(21, 65, 255, 0.5);
|
||||
155
client/src/utils/StorageManager.ts
Normal file
155
client/src/utils/StorageManager.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { debounce } from 'lodash';
|
||||
import semver from 'semver';
|
||||
import merge from 'lodash/merge';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
/**
|
||||
* 统一localStorage存储管理器
|
||||
*
|
||||
* 参数说明:
|
||||
* lazy: boolean 是否懒加载,未开启懒加载在初始化时就读取localstorage,开启懒加载将在第一次获取或设置值时读取localstorage
|
||||
* namespace: string localstorage的key,定义多个manager时区分
|
||||
* version: string 'x.x.x'格式,采用语义化版本控制,用于重置用户设备中的localstorage
|
||||
*
|
||||
* 使用说明:
|
||||
* 创建一个初始结构,可用来初始化用户存储,或者重置用户存储, 格式类似于redux里的state
|
||||
* const store = {
|
||||
* token: '',
|
||||
* };
|
||||
*
|
||||
* 创建manager对象, 此处传入version便于标明版本号,方便后续升级重置用户数据
|
||||
* const manager = new StorageManager(store, { version: '0.0.0' });
|
||||
*
|
||||
* 直接设置属性,将存入localstorage, 多次设置只会存入最后的值,此处使用debounce减少io操作
|
||||
* manager.token = '123456'
|
||||
* manager.token = '123'
|
||||
* manager.token = '456'
|
||||
*
|
||||
* 直接读取值,获取到的的localstorage中的值
|
||||
* console.log(manager.token)
|
||||
*
|
||||
*/
|
||||
class StorageManager {
|
||||
constructor(store, options) {
|
||||
options = Object.assign(
|
||||
{},
|
||||
{
|
||||
lazy: true,
|
||||
namespace: 'STORAGE_MANAGER',
|
||||
version: '0.0.0'
|
||||
},
|
||||
options
|
||||
);
|
||||
this.namespace = options.namespace;
|
||||
this.isLazy = options.lazy;
|
||||
this._store = store;
|
||||
this.loaded = false;
|
||||
this.version = options.version;
|
||||
|
||||
this.cache = {};
|
||||
this.initStorage();
|
||||
}
|
||||
|
||||
initStorage() {
|
||||
if (!isPlainObject(this._store)) {
|
||||
throw new Error('store should be a plain object');
|
||||
}
|
||||
if (this.checkStore()) {
|
||||
this.setItem(this.namespace, this.buildData(this._store));
|
||||
}
|
||||
this.initStore();
|
||||
if (!this.isLazy) {
|
||||
this.fillCache();
|
||||
}
|
||||
}
|
||||
|
||||
initStore() {
|
||||
const keys = Object.keys(this._store);
|
||||
let i = keys.length;
|
||||
while (i--) {
|
||||
this.proxy(keys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
proxy(key) {
|
||||
Object.defineProperty(this, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
if (!this.loaded && this.isLazy) {
|
||||
this.fillCache();
|
||||
}
|
||||
return this.cache[key];
|
||||
},
|
||||
set: val => {
|
||||
if (!this.loaded && this.isLazy) {
|
||||
this.fillCache();
|
||||
}
|
||||
this.cache[key] = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
observe(data) {
|
||||
if (Object.prototype.toString.call(data) !== '[object Object]') {
|
||||
return;
|
||||
}
|
||||
let keys = Object.keys(data);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
this.defineReactive(data, keys[i], data[keys[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
defineReactive(data, key, val) {
|
||||
this.observe(val);
|
||||
Object.defineProperty(data, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
return val;
|
||||
},
|
||||
set: newVal => {
|
||||
if (val === newVal) {
|
||||
return;
|
||||
}
|
||||
val = newVal;
|
||||
this.observe(newVal);
|
||||
this.debounceSet();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fillCache() {
|
||||
this.cache = merge({}, this._store, this.getItem(this.namespace).data);
|
||||
this.loaded = true;
|
||||
this.observe(this.cache);
|
||||
}
|
||||
|
||||
checkStore() {
|
||||
const item = this.getItem(this.namespace);
|
||||
return !!(!item || semver.lt(item.version, this.version));
|
||||
}
|
||||
|
||||
buildData(data) {
|
||||
return {
|
||||
version: this.version,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
debounceSet() {
|
||||
return debounce(this.setItem, 200)(this.namespace, this.buildData(this.cache));
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(key));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
export default StorageManager;
|
||||
36
client/src/utils/api.ts
Normal file
36
client/src/utils/api.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import config from '@/utils/config';
|
||||
|
||||
const host = window.location.host;
|
||||
|
||||
let api_host = `//mp.test.amnetapi.com/mp/v1`;
|
||||
|
||||
|
||||
// api_host = '//mpcompany3.test.amnetapi.com/mp/v1';
|
||||
let login = '//mp41.test.amnetapi.com/mp/v1/user/ssoLogin';
|
||||
// sso_host = `//test120-sso.amnetapi.com`;
|
||||
|
||||
export { api_host };
|
||||
|
||||
export default {
|
||||
icp: config.COPY_RIGHT_TEXT,
|
||||
|
||||
upload: `${api_host}/upload`,
|
||||
uploadZip: `${api_host}/uploadZip`,
|
||||
segment_upload: `${api_host}/uploadPopulation`,
|
||||
|
||||
// 用户
|
||||
login: login,
|
||||
user: `${api_host}/user/validate`,
|
||||
getUrl: `${api_host}/requestGetUrl`,
|
||||
getAdPermits: `${api_host}/adServer/getAdPermits`,
|
||||
|
||||
//子用户管理
|
||||
account_list: `${api_host}/user/getUserList`,
|
||||
create_account: `${api_host}/user/createUserAccountSso`,
|
||||
update_account: `${api_host}/user/updateUserAccountSso`,
|
||||
account_detail: `${api_host}/user/getUserDetail`,
|
||||
getUserDetail: `${api_host}/user/getUserDetail`,
|
||||
account_status: `${api_host}/user/updateAccountStatus`,
|
||||
sign_agreement: `${api_host}/user/updateUserSignAgreement`,
|
||||
|
||||
};
|
||||
35
client/src/utils/config.ts
Normal file
35
client/src/utils/config.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
export default {
|
||||
API_HOST: 'mpapi.martechlab.cn',
|
||||
SSO_LOGIN_HOST: 'sso.martechlab.cn',
|
||||
SYS_ADMIN_HOST: 'sysadmin.martechlab.cn',
|
||||
PRODUCTION_HOST: 'adv.martechlab.cn',
|
||||
|
||||
EASY_REPORT_API_HOST: 'api.easyreport.fancydsp.com',
|
||||
|
||||
HIDE_MENU_IDS: [],
|
||||
|
||||
BROWSER_TITLE_TEXT: 'docgpt',
|
||||
|
||||
COPY_RIGHT_TEXT: '沪xxxxx',
|
||||
|
||||
HIDE_QRCODE: false,
|
||||
HIDE_DOC: false,
|
||||
HIDE_WEB_SITE: false,
|
||||
HIDE_COPY_RIGHT: false,
|
||||
HIDE_VERSION: false,
|
||||
|
||||
LOGO_WIDTH: '116px',
|
||||
LOGO_HEIGHT: '19px',
|
||||
|
||||
UMI_PUBLIC_PATH_HOST: 'mpcdn.martechlab.cn',
|
||||
|
||||
WHITE_API: [
|
||||
'api.easyreport.fancydsp.com',
|
||||
'sso.martechlab.cn',
|
||||
'test41-sso.amnetapi.com',
|
||||
'test41-sysadmin.amnetapi.com',
|
||||
'sysadmin.martechlab.cn'
|
||||
],
|
||||
pages: ['/login'],
|
||||
};
|
||||
20
client/src/utils/date.ts
Normal file
20
client/src/utils/date.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import moment from 'moment';
|
||||
|
||||
export function today() {
|
||||
return formatDate(moment());
|
||||
}
|
||||
|
||||
export function lastDay() {
|
||||
return formatDate(moment().subtract(1, 'days'));
|
||||
}
|
||||
|
||||
export function lastWeek() {
|
||||
return formatDate(moment().subtract(1, 'weeks'));
|
||||
}
|
||||
|
||||
export function formatDate(date) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
return moment(date).format('YYYY-MM-DD');
|
||||
}
|
||||
241
client/src/utils/helper.ts
Normal file
241
client/src/utils/helper.ts
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import { Modal, Form } from 'antd';
|
||||
import router from 'umi/router';
|
||||
|
||||
export function getMonospaceLength(content = '') {
|
||||
let total = 0;
|
||||
for (let c of content) {
|
||||
if (/[\x00-\xff]/.test(c)) {
|
||||
total += 0.5;
|
||||
} else {
|
||||
total += 1;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
export const pickRecordFormList = function (list = [], ids = [], key = 'id') {
|
||||
let results = [];
|
||||
for (let id of ids) {
|
||||
let find = list.find(record => record[key] === id);
|
||||
if (find != null) {
|
||||
results.push(find);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
export function extractFieldsValues(fields) {
|
||||
let results = {};
|
||||
Object.keys(fields).forEach(key => {
|
||||
results[key] = fields[key]?.value;
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
export function getTableData(response) {
|
||||
const { code, data } = response;
|
||||
const { count = 0, list = [] } = data || {};
|
||||
if (code === 0) {
|
||||
return {
|
||||
total: count,
|
||||
list
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
total: 0,
|
||||
list: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const getReportData = response => {
|
||||
const { code, data } = response;
|
||||
const { rows, total, aggregation = {} } = data || {};
|
||||
if (code === 0 && rows.length > 0) {
|
||||
return {
|
||||
total,
|
||||
list: [{ name: '合计', ...aggregation }, ...rows]
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
total: 0,
|
||||
list: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 大数字转换,将大额数字转换为万、千万、亿等
|
||||
* @param value 数字值
|
||||
*/
|
||||
export const bigNumberTransform = value => {
|
||||
if (!value || parseInt(value) < 1000) {
|
||||
return value;
|
||||
}
|
||||
const newValue = ['', '', ''];
|
||||
let fr = 1000;
|
||||
let num = 3;
|
||||
let text1 = '';
|
||||
let fm = 1;
|
||||
while (value / fr >= 1) {
|
||||
fr *= 10;
|
||||
num += 1;
|
||||
// console.log('数字', value / fr, 'num:', num)
|
||||
}
|
||||
if (num <= 4) {
|
||||
// 千
|
||||
newValue[0] = parseInt(value / 1000) + '';
|
||||
newValue[1] = '千';
|
||||
} else if (num <= 8) {
|
||||
// 万
|
||||
text1 = parseInt(num - 4) / 3 > 1 ? '千万' : '万';
|
||||
// tslint:disable-next-line:no-shadowed-variable
|
||||
fm = text1 === '万' ? 10000 : 10000000;
|
||||
if (value % fm === 0) {
|
||||
newValue[0] = parseInt(value / fm) + '';
|
||||
} else {
|
||||
newValue[0] = parseFloat(value / fm).toFixed(1) + '';
|
||||
}
|
||||
newValue[1] = text1;
|
||||
} else if (num <= 16) {
|
||||
// 亿
|
||||
text1 = (num - 8) / 3 > 1 ? '千亿' : '亿';
|
||||
text1 = (num - 8) / 4 > 1 ? '万亿' : text1;
|
||||
text1 = (num - 8) / 7 > 1 ? '千万亿' : text1;
|
||||
// tslint:disable-next-line:no-shadowed-variable
|
||||
fm = 1;
|
||||
if (text1 === '亿') {
|
||||
fm = 100000000;
|
||||
} else if (text1 === '千亿') {
|
||||
fm = 100000000000;
|
||||
} else if (text1 === '万亿') {
|
||||
fm = 1000000000000;
|
||||
} else if (text1 === '千万亿') {
|
||||
fm = 1000000000000000;
|
||||
}
|
||||
if (value % fm === 0) {
|
||||
newValue[0] = parseInt(value / fm) + '';
|
||||
} else {
|
||||
newValue[0] = parseFloat(value / fm).toFixed(1) + '';
|
||||
}
|
||||
newValue[1] = text1;
|
||||
}
|
||||
if (value < 1000) {
|
||||
newValue[0] = value + '';
|
||||
newValue[1] = '';
|
||||
}
|
||||
return newValue.join('');
|
||||
};
|
||||
|
||||
export const handleCancel = route => {
|
||||
Modal.confirm({
|
||||
title: '确认返回?',
|
||||
content: '当前处于编辑状态,返回无法保存当前已编辑内容',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
if (route) {
|
||||
router.push(route);
|
||||
} else {
|
||||
router.goBack();
|
||||
}
|
||||
},
|
||||
onCancel() {}
|
||||
});
|
||||
};
|
||||
|
||||
export function createFormData(values) {
|
||||
let formData = {};
|
||||
Object.keys(values || {}).forEach(fieldName => {
|
||||
formData[fieldName] = Form.createFormField({
|
||||
value: values[fieldName]
|
||||
});
|
||||
});
|
||||
return formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字符串,超出一定字数后转化为...
|
||||
* @param {String} text String 文本原文
|
||||
* @param {Number} limit Number 文本转换的阈值
|
||||
* @return {String} String 转化后的文本
|
||||
*/
|
||||
const getShortString = (text, limit) => {
|
||||
let _temp = text || '';
|
||||
const _limit = parseInt(limit, 10);
|
||||
if (_temp.length > _limit) {
|
||||
_temp = _temp.slice(0, _limit) + '...';
|
||||
}
|
||||
return _temp;
|
||||
};
|
||||
|
||||
export const FormatString = {
|
||||
getShortString
|
||||
};
|
||||
|
||||
export function GetQueryString(payload = {}) {
|
||||
let newPayload = {};
|
||||
Object.keys(payload)
|
||||
.sort()
|
||||
.forEach(key => {
|
||||
newPayload[key] = payload[key];
|
||||
});
|
||||
return JSON.stringify(newPayload);
|
||||
}
|
||||
|
||||
export function ParseQueryString(queryStr) {
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(queryStr);
|
||||
} catch (e) {
|
||||
payload = {};
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function GetUrlQueryString(params = {}) {
|
||||
return Object.keys(params)
|
||||
.map(k => {
|
||||
if (Array.isArray(params[k])) {
|
||||
return params[k].map(v => `${encodeURIComponent(k)}[]=${v}`).join('&');
|
||||
} else {
|
||||
return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
|
||||
}
|
||||
})
|
||||
.join('&');
|
||||
}
|
||||
|
||||
export function IsSameProperty(record, property) {
|
||||
let isSame = true;
|
||||
for (let key in property) {
|
||||
if (record[key] !== property[key]) {
|
||||
isSame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isSame;
|
||||
}
|
||||
|
||||
export function getFormContainer(ref) {
|
||||
if (ref && ref.current && ref.current.closest) {
|
||||
return ref.current.closest('form');
|
||||
} else {
|
||||
return document.getElementById('form-container');
|
||||
}
|
||||
}
|
||||
|
||||
export const toTransformSize = size => {
|
||||
size = parseInt(size, 10);
|
||||
if (typeof size !== 'number' || size === 0 || !size) return '-';
|
||||
|
||||
if (size < 1024) return `${size} B`;
|
||||
|
||||
const sizeKB = Math.floor(size / 1024);
|
||||
if (sizeKB < 1024) return `${sizeKB} KB`;
|
||||
|
||||
const sizeMB = Math.floor(sizeKB / 1024);
|
||||
if (sizeMB < 1024) return `${sizeMB} MB`;
|
||||
|
||||
const sizeGB = Math.floor(sizeMB / 1024);
|
||||
return `${sizeGB} GB`;
|
||||
};
|
||||
14
client/src/utils/hooks.ts
Normal file
14
client/src/utils/hooks.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
export const usePagination = function (defaultPage: number, defaultPageSize: number, total: number) {
|
||||
const [page = 1, setPage] = useState(defaultPage);
|
||||
const [pageSize = 10, setPageSize] = useState(defaultPageSize);
|
||||
return {
|
||||
page,
|
||||
pageSize,
|
||||
count: total,
|
||||
setPage,
|
||||
setPageSize,
|
||||
nextPage: () => setPage(page + 1)
|
||||
};
|
||||
};
|
||||
155
client/src/utils/index.ts
Normal file
155
client/src/utils/index.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* @param {String} url
|
||||
* @param {Boolean} isNoCaseSensitive 是否区分大小写
|
||||
* @return {Object}
|
||||
*/
|
||||
// import numeral from 'numeral';
|
||||
import store from '@/utils/persistStore';
|
||||
|
||||
export const parseQuery = (url = window.location.search, isNoCaseSensitive: boolean) => {
|
||||
return window.g_history.location.query;
|
||||
let arr, part;
|
||||
|
||||
const query = {};
|
||||
//去掉首位空格
|
||||
if (!(url || '').replace(/^\s+|\s+$/, '')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
url = url.replace(/\S*\?/, '');
|
||||
|
||||
if (url) {
|
||||
if (isNoCaseSensitive) {
|
||||
url = url.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
arr = url.split('&');
|
||||
for (let i in arr) {
|
||||
part = arr[i].split('=');
|
||||
query[part[0]] = decodeURIComponent(part[1]);
|
||||
}
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
export const parseQueryForPath = url => {
|
||||
if (!url) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let arr, part;
|
||||
|
||||
const query = {};
|
||||
//去掉首位空格
|
||||
if (!(url || '').replace(/^\s+|\s+$/, '')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
url = url.replace(/\S*\?/, '');
|
||||
|
||||
if (url) {
|
||||
arr = url.split('&');
|
||||
for (let i in arr) {
|
||||
part = arr[i].split('=');
|
||||
query[part[0]] = decodeURIComponent(part[1]);
|
||||
}
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
export const param = paramObj => {
|
||||
let str = [];
|
||||
for (let i in paramObj) {
|
||||
if (typeof paramObj[i] !== 'undefined') {
|
||||
str.push(i + '=' + encodeURIComponent(paramObj[i]));
|
||||
}
|
||||
}
|
||||
return str.join('&');
|
||||
};
|
||||
|
||||
export const addParam = (url, params) => {
|
||||
let SEARCH_REG = /\?([^#]*)/,
|
||||
HASH_REG = /#(.*)/,
|
||||
searchStr;
|
||||
|
||||
url = url || '';
|
||||
let search = {},
|
||||
searchMatch = url.match(SEARCH_REG);
|
||||
|
||||
if (searchMatch) {
|
||||
search = parseQuery(searchMatch[0]);
|
||||
}
|
||||
|
||||
//合并当前search参数
|
||||
search = Object.assign(search, params);
|
||||
|
||||
searchStr = '?' + param(search);
|
||||
|
||||
//是否存在search
|
||||
if (SEARCH_REG.test(url)) {
|
||||
url = url.replace(SEARCH_REG, searchStr);
|
||||
} else {
|
||||
//是否存在hash
|
||||
if (HASH_REG.test(url)) {
|
||||
url = url.replace(HASH_REG, searchStr + '#' + url.match(HASH_REG)[1]);
|
||||
} else {
|
||||
url += searchStr;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
const downloadWithIframe = (url: string) => {
|
||||
const id = `downloadIframe${new Date().getTime()}`;
|
||||
let iframe = document.createElement('iframe');
|
||||
iframe.id = id;
|
||||
iframe.onload = function () {
|
||||
console.log('开始加载');
|
||||
};
|
||||
document.body.appendChild(iframe);
|
||||
iframe.style.cssText = 'display:none';
|
||||
const iframeDoc = iframe.contentWindow.document;
|
||||
iframeDoc.open(); // 打开流
|
||||
iframeDoc.write(`<iframe src="${url}"></iframe>`);
|
||||
iframeDoc.write('<script>');
|
||||
iframeDoc.write(
|
||||
`window.onload = function() { setTimeout(function() {parent.document.body.removeChild(parent.document.getElementById("${id}"))}, 20000)}`
|
||||
);
|
||||
iframeDoc.write('</script>');
|
||||
iframeDoc.close(); // 关闭流
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export const getReportVersion = () => {
|
||||
const erVersion = window.localStorage.getItem('EASY_REPORT_VERSION');
|
||||
let version = undefined;
|
||||
if (window.location.host === 'adv.martechlab.cn') {
|
||||
version = '';
|
||||
} else if (erVersion) {
|
||||
version = erVersion;
|
||||
}
|
||||
return version;
|
||||
};
|
||||
|
||||
export const delay = timeout => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
export const formatRequestUrlByDomainPrefix = url => {
|
||||
let prefix = store.domainPrefix || '';
|
||||
if (prefix) {
|
||||
prefix = `//mp${prefix}.`;
|
||||
url = url.slice(2).split('.').slice(1).join('.');
|
||||
}
|
||||
return `${prefix}${url}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
parseQuery,
|
||||
downloadWithIframe,
|
||||
formatRequestUrlByDomainPrefix
|
||||
};
|
||||
51
client/src/utils/menu.ts
Normal file
51
client/src/utils/menu.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { formatMessage } from 'umi-plugin-react/locale';
|
||||
import { check } from '@/components/Authorize/getAuth';
|
||||
import memoize from 'lodash/memoize';
|
||||
|
||||
function formatter(data, parentName) {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return data
|
||||
.map(item => {
|
||||
if (!item.name || !item.path) {
|
||||
return null;
|
||||
}
|
||||
let locale = parentName ? `${parentName}.${item.name}` : `menu.${item.name}`;
|
||||
const result = {
|
||||
...item,
|
||||
locale: formatMessage({
|
||||
id: locale,
|
||||
defaultMessage: item.name
|
||||
})
|
||||
};
|
||||
if (item.routes) {
|
||||
result.children = formatter(item.routes, locale);
|
||||
}
|
||||
delete result.routes;
|
||||
return result;
|
||||
})
|
||||
.filter(item => item);
|
||||
}
|
||||
|
||||
export const getMenuData = memoize(formatter);
|
||||
|
||||
const getSubMenu = item => {
|
||||
// doc: add hideChildrenInMenu
|
||||
if (item.children && item.children.some(child => child.name)) {
|
||||
return {
|
||||
...item,
|
||||
children: getAuthorizedMenuData(item.children) // eslint-disable-line
|
||||
};
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
export const getAuthorizedMenuData = (menuData, currentAuth) => {
|
||||
return menuData.map(item => check(item.auth, currentAuth, getSubMenu(item))).filter(item => item);
|
||||
};
|
||||
|
||||
export const getAuthorizeByHeader = (menuData, path) => {
|
||||
const formatMenus = menuData.filter(d => d.path === path || path?.includes(d.extraKey));
|
||||
return formatMenus;
|
||||
};
|
||||
35
client/src/utils/persistStore.ts
Normal file
35
client/src/utils/persistStore.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import StorageManager from './StorageManager';
|
||||
|
||||
// 定义初始数据结构,发生改变时必须升级版本号否则会导致数据不一致
|
||||
const store = {
|
||||
token: '',
|
||||
domainPrefix: '',
|
||||
userInfo: {},
|
||||
customColumns: {
|
||||
campaignTable: [],
|
||||
adUnitTable: [],
|
||||
creativeTable: [],
|
||||
vendorTable: [],
|
||||
dashboardWapper: [],
|
||||
toutiaoVendorTable: [],
|
||||
toutiaoCampaignTable: [],
|
||||
toutiaoAdUnitTable: [],
|
||||
toutiaoCreativeTable: [],
|
||||
materialVideosTable: [],
|
||||
tencentCampaignTable: [],
|
||||
tencentCreativeTable: [],
|
||||
tencentAdUnitTable: [],
|
||||
tencentVendorTable: []
|
||||
},
|
||||
vendorAuth: [],
|
||||
warningRuleIds: [],
|
||||
warningDate: '',
|
||||
ksOldCreateTimer: '',
|
||||
ttOldCreateTimer: '',
|
||||
gdtCreateTimer: ''
|
||||
};
|
||||
|
||||
// 数据结构改变时改变
|
||||
const version = '1.1.1';
|
||||
|
||||
export default new StorageManager(store, { version });
|
||||
22
client/src/utils/registerServer.ts
Normal file
22
client/src/utils/registerServer.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const registerServer = (opt, request) => {
|
||||
let server = {};
|
||||
for (let key in opt) {
|
||||
server[key] = params => {
|
||||
if (opt[key].method === 'post' || opt[key].method === 'POST') {
|
||||
return request(opt[key].url, {
|
||||
method: opt[key].method,
|
||||
data: params
|
||||
});
|
||||
}
|
||||
|
||||
if (opt[key].method === 'get' || opt[key].method === 'GET') {
|
||||
return request.get(opt[key].url, {
|
||||
params
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
return server;
|
||||
};
|
||||
|
||||
export default registerServer;
|
||||
130
client/src/utils/request.ts
Normal file
130
client/src/utils/request.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* request 网络请求工具
|
||||
* 更详细的 api 文档: https://github.com/umijs/umi-request
|
||||
*/
|
||||
import { extend } from 'umi-request';
|
||||
import { notification, message } from 'antd';
|
||||
import store from '@/utils/persistStore';
|
||||
import config from '@/utils/config';
|
||||
import _ from 'lodash';
|
||||
|
||||
import api from '@/utils/api';
|
||||
const { login } = api;
|
||||
|
||||
const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动中断请求。errorHandler 抛出的error message
|
||||
|
||||
|
||||
|
||||
const codeMessage = {
|
||||
200: '服务器成功返回请求的数据。',
|
||||
201: '新建或修改数据成功。',
|
||||
202: '一个请求已经进入后台排队(异步任务)。',
|
||||
204: '删除数据成功。',
|
||||
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
|
||||
401: '用户没有权限(令牌、用户名、密码错误)。',
|
||||
403: '用户得到授权,但是访问是被禁止的。',
|
||||
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
|
||||
406: '请求的格式不可得。',
|
||||
410: '请求的资源被永久删除,且不会再得到的。',
|
||||
422: '当创建一个对象时,发生一个验证错误。',
|
||||
500: '服务器发生错误,请检查服务器。',
|
||||
502: '网关错误。',
|
||||
503: '服务不可用,服务器暂时过载或维护。',
|
||||
504: '网关超时。'
|
||||
};
|
||||
|
||||
/**
|
||||
* 异常处理程序
|
||||
*/
|
||||
const errorHandler = (error: any) => {
|
||||
const { response } = error;
|
||||
// 手动中断请求 abort
|
||||
if (error.message === ABORT_REQUEST_ERR_MESSAGE) {
|
||||
console.log('user abort request');
|
||||
} else {
|
||||
if (response && response.status) {
|
||||
const errorText = codeMessage[response.status] || response.statusText;
|
||||
const { status, url } = response;
|
||||
notification.error({
|
||||
message: `请求错误 ${status}: ${url}`,
|
||||
description: errorText,
|
||||
top: 65
|
||||
});
|
||||
} else if (!response) {
|
||||
notification.error({
|
||||
description: '您的网络发生异常,无法连接服务器',
|
||||
message: '网络异常',
|
||||
top: 65
|
||||
});
|
||||
}
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* 配置request请求时的默认参数
|
||||
*/
|
||||
const request = extend({
|
||||
errorHandler, // 默认错误处理
|
||||
// credentials: 'include', // 默认请求是否带上cookie
|
||||
timeout: 3000000
|
||||
});
|
||||
|
||||
request.interceptors.request.use((url, options) => {
|
||||
let prefix = '';
|
||||
console.log(url)
|
||||
return {
|
||||
url,
|
||||
options: {
|
||||
...options,
|
||||
headers: {
|
||||
...(options.skipToken ? undefined : { Authorization: 'Bearer ' + store.token }),
|
||||
...options.headers
|
||||
},
|
||||
interceptors: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/*
|
||||
* 请求response拦截器
|
||||
* */
|
||||
request.interceptors.response.use(async (response, request) => {
|
||||
const data = await response.clone().json();
|
||||
if (data.code && data.code !== 0) {
|
||||
const payload = {
|
||||
token: request.headers.Authorization,
|
||||
res: data,
|
||||
body: request.data,
|
||||
url: response.url
|
||||
};
|
||||
}
|
||||
// response 拦截
|
||||
if (data.code === 401 || data.Code === 401) {
|
||||
notification.error({
|
||||
message: data.errorMessage,
|
||||
description: data.errorMessage,
|
||||
duration: 3,
|
||||
top: 65
|
||||
});
|
||||
store.domainPrefix = '';
|
||||
} else if (data.code !== 0) {
|
||||
if (data.code === 100) {
|
||||
//code为100 时账户名或者密码错误, 为了跟之前弹窗一样所以用message
|
||||
message.error(data.errorMessage);
|
||||
} else {
|
||||
notification.error({
|
||||
message: `提示 : ${data.code}`,
|
||||
description: data.errorMessage,
|
||||
duration: 3,
|
||||
top: 65
|
||||
});
|
||||
}
|
||||
|
||||
return response; //这里return response, 是为了避免modal里面报code undefined
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
});
|
||||
|
||||
export default request;
|
||||
12
client/src/utils/rules.ts
Normal file
12
client/src/utils/rules.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { getMonospaceLength } from '@/utils/helper';
|
||||
|
||||
export const monospace = (rule, value, callback) => {
|
||||
const length = getMonospaceLength(value);
|
||||
if (rule.max > 0 && length > rule.max) {
|
||||
callback(`长度不能超过${rule.max}!`);
|
||||
} else if (rule.min > 0 && length < rule.min) {
|
||||
callback(`长度不能小于${rule.min}!`);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
40
client/src/utils/upladFileProgress.ts
Normal file
40
client/src/utils/upladFileProgress.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
const UpladFile = props => {
|
||||
let { xhr, options } = props;
|
||||
const { file, url, uploadData, headers, callback, getProgress } = options;
|
||||
|
||||
var form = new FormData(); // FormData 对象
|
||||
form.append('file', file); // 文件对象
|
||||
for (let key in uploadData) {
|
||||
if (uploadData.hasOwnProperty(key)) {
|
||||
form.append(key, uploadData[key]);
|
||||
}
|
||||
}
|
||||
xhr = new XMLHttpRequest(); // XMLHttpRequest 对象
|
||||
xhr.open('post', url, true); //post方式,url为服务器请求地址,true 该参数规定请求是否异步处理。
|
||||
for (let key in headers) {
|
||||
if (headers.hasOwnProperty(key)) {
|
||||
xhr.setRequestHeader(key, headers[key]);
|
||||
}
|
||||
}
|
||||
xhr.onload = evt => callback(true, JSON.parse(evt.target.response)); //请求完成
|
||||
xhr.onerror = evt => callback(false, JSON.parse(evt.target.response)); //请求失败
|
||||
xhr.upload.onprogress = evt => {
|
||||
if (evt.lengthComputable) {
|
||||
const rate = Math.round((evt.loaded / evt.total) * 100);
|
||||
if (getProgress) getProgress(file.uid, rate);
|
||||
}
|
||||
};
|
||||
xhr.send(form); //开
|
||||
};
|
||||
|
||||
const cancleUploadFile = xhr => {
|
||||
xhr.abort();
|
||||
};
|
||||
|
||||
const upladFileProgress = props => {
|
||||
const { file, url, uploadData, headers, callback, getProgress } = props;
|
||||
let xhr;
|
||||
UpladFile({ xhr, options: { file, url, uploadData, headers, callback, getProgress } });
|
||||
};
|
||||
|
||||
export default upladFileProgress;
|
||||
78
client/src/utils/version.ts
Normal file
78
client/src/utils/version.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/* global VERSION */
|
||||
import api from '@/utils/api';
|
||||
import { extend } from 'umi-request';
|
||||
import { notification, Button } from 'antd';
|
||||
import config from '@/utils/config';
|
||||
|
||||
const { get_simple_version } = api;
|
||||
const host = window.location.host;
|
||||
const NOTIFICATION_KEY = 'VERSION';
|
||||
|
||||
// npm run build shiyi version
|
||||
export const LOCAL_VERSION = VERSION;
|
||||
|
||||
/**
|
||||
* 这里必须重新extend一个request,不然跟全局的request为同一个对象,
|
||||
* */
|
||||
const request = extend({
|
||||
errorHandler: () => {} // 默认错误处理
|
||||
});
|
||||
|
||||
let timer;
|
||||
let timeLoad = () => {
|
||||
timer = setInterval(async () => {
|
||||
try {
|
||||
const { code, publishVersion } = await request(get_simple_version);
|
||||
code === 0 && publishVersion && checkVersion(publishVersion);
|
||||
} catch (e) {}
|
||||
}, 60000);
|
||||
};
|
||||
|
||||
if (host === 'adv.martechlab.cn' || host === 'shiyi.martechlab.cn') {
|
||||
if (!config.HIDE_VERSION) {
|
||||
timeLoad();
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
*
|
||||
* @param publishVersion 接口取到的version
|
||||
* @param LOCAL_VERSION 本地version,每次构建打包,webpack会根据打包命令写入版本号
|
||||
*/
|
||||
|
||||
//检查远端版本是否跟本地一致
|
||||
const checkVersion = publishVersion => {
|
||||
if (LOCAL_VERSION?.slice(0, 3) !== publishVersion?.slice(0, 3)) {
|
||||
clearInterval(timer);
|
||||
//不相等证明有弹窗提醒
|
||||
notification.info({
|
||||
key: NOTIFICATION_KEY,
|
||||
top: 50,
|
||||
duration: null,
|
||||
message: <span>发现版本更新</span>,
|
||||
onClose: () => timeLoad(),
|
||||
description: (
|
||||
<div>
|
||||
<div>发现版本更新,是否刷新页面获取;</div>
|
||||
<div style={{ color: 'orange' }}>注意!若刷新页面当前输入内容不做保存!</div>
|
||||
</div>
|
||||
),
|
||||
btn: (
|
||||
<div>
|
||||
<Button
|
||||
style={{ marginRight: 15 }}
|
||||
onClick={() => {
|
||||
notification.close(NOTIFICATION_KEY);
|
||||
timeLoad();
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>{' '}
|
||||
<Button type="primary" onClick={() => history.go(0)}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
12
client/src/wrappers/auth.tsx
Normal file
12
client/src/wrappers/auth.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Navigate, Outlet } from 'umi'
|
||||
|
||||
export default (props) => {
|
||||
// const { isLogin } = useAuth();
|
||||
console.log(props)
|
||||
const isLogin = false
|
||||
if (isLogin) {
|
||||
return <Outlet />;
|
||||
} else {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
}
|
||||
6
client/tsconfig.json
Normal file
6
client/tsconfig.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "./src/.umi/tsconfig.json",
|
||||
"@@/*": [
|
||||
"src/.umi/*"
|
||||
]
|
||||
}
|
||||
2
client/typings.d.ts
vendored
Normal file
2
client/typings.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import 'umi/typings';
|
||||
declare module 'lodash'
|
||||
Loading…
Add table
Reference in a new issue