first commit

This commit is contained in:
zhaofengchao 2024-01-10 12:09:19 +08:00
parent c9afb6e2bf
commit 6f1c5d6ef5
53 changed files with 16658 additions and 0 deletions

9
client/.gitignore vendored Normal file
View 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
View file

@ -0,0 +1,2 @@
registry=https://registry.npmmirror.com/

18
client/.umirc.ts Normal file
View 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

File diff suppressed because it is too large Load diff

30
client/package.json Normal file
View 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
View 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
View 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

Binary file not shown.

BIN
client/src/assets/yay.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View 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;

View 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;
}

View 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;

View 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;

View 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"
}
}

View 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
View 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;

View file

@ -0,0 +1,14 @@
.filter {
height: 32px;
display: flex;
margin: 10px 0;
justify-content: space-between;
.search {
flex: 1;
}
.operate {
width: 200px;
}
}

View file

@ -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;

View file

@ -0,0 +1,3 @@
export default () => {
return <div></div>
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -0,0 +1,7 @@
.tab {
:global {
.ant-tabs-tabpane {
margin: 20px;
}
}
}

View 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;

View 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);

View 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;

View 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;

View 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;
}
}
}

View 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;

View 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;
}

View 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);

View 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;

View 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;

View 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;

View 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
View 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': '',
};

View 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);

View 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 localstoragelocalstorage
* namespace: string localstorage的keymanager时区分
* 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
View 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`,
};

View 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
View 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
View 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
View 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
View 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
View 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;
};

View 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 });

View 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
View 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
View 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();
}
};

View 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;

View 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 versionwebpack会根据打包命令写入版本号
*/
//检查远端版本是否跟本地一致
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>
)
});
}
};

View 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
View file

@ -0,0 +1,6 @@
{
"extends": "./src/.umi/tsconfig.json",
"@@/*": [
"src/.umi/*"
]
}

2
client/typings.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
import 'umi/typings';
declare module 'lodash'