init add first commit
This commit is contained in:
commit
62a323bdf9
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
.env.*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
stats.html
|
10
front/App.tsx
Normal file
10
front/App.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import './assets/global.scss'
|
||||
import {AppRouter} from "./pages/Router.tsx";
|
||||
|
||||
|
||||
export function App() {
|
||||
return (<div>
|
||||
<AppRouter/>
|
||||
</div>)
|
||||
}
|
138
front/assets/global.scss
Normal file
138
front/assets/global.scss
Normal file
@ -0,0 +1,138 @@
|
||||
:root {
|
||||
--max-width: 1100px;
|
||||
--border-radius: 12px;
|
||||
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
|
||||
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
|
||||
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
||||
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
|
||||
--primary-glow: conic-gradient(
|
||||
from 180deg at 50% 50%,
|
||||
#16abff33 0deg,
|
||||
#0885ff33 55deg,
|
||||
#54d6ff33 120deg,
|
||||
#0071ff33 160deg,
|
||||
transparent 360deg
|
||||
);
|
||||
--secondary-glow: radial-gradient(
|
||||
rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 239, 245, 249;
|
||||
--tile-end-rgb: 228, 232, 233;
|
||||
--tile-border: conic-gradient(
|
||||
#00000080,
|
||||
#00000040,
|
||||
#00000030,
|
||||
#00000020,
|
||||
#00000010,
|
||||
#00000010,
|
||||
#00000080
|
||||
);
|
||||
|
||||
--callout-rgb: 238, 240, 241;
|
||||
--callout-border-rgb: 172, 175, 176;
|
||||
--card-rgb: 180, 185, 188;
|
||||
--card-border-rgb: 131, 134, 135;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
|
||||
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
|
||||
--secondary-glow: linear-gradient(
|
||||
to bottom right,
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0),
|
||||
rgba(1, 65, 255, 0.3)
|
||||
);
|
||||
|
||||
--tile-start-rgb: 2, 13, 46;
|
||||
--tile-end-rgb: 2, 5, 19;
|
||||
--tile-border: conic-gradient(
|
||||
#ffffff80,
|
||||
#ffffff40,
|
||||
#ffffff30,
|
||||
#ffffff20,
|
||||
#ffffff10,
|
||||
#ffffff10,
|
||||
#ffffff80
|
||||
);
|
||||
|
||||
--callout-rgb: 20, 20, 20;
|
||||
--callout-border-rgb: 108, 108, 108;
|
||||
--card-rgb: 100, 100, 100;
|
||||
--card-border-rgb: 200, 200, 200;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
//color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.space-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.container {
|
||||
|
||||
width: 1000px;
|
||||
max-width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0;
|
||||
}
|
91
front/components/LoginComponent.tsx
Normal file
91
front/components/LoginComponent.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import {Button, Form, Divider, Modal, Row, Col, Space, Notification} from '@douyinfe/semi-ui'
|
||||
import {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useUserinfoStore} from "../store/userinfoStore.ts";
|
||||
|
||||
type FieldType = {
|
||||
account: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const {Input, Checkbox} = Form
|
||||
|
||||
export const LoginComponent = () => {
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const {login} = useUserinfoStore()
|
||||
|
||||
const onFinish = async (values: FieldType) => {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const user = await login(values)
|
||||
setVisible(true);
|
||||
navigate('/dashboard')
|
||||
} catch (err) {
|
||||
// 登录失败
|
||||
console.log(err)
|
||||
// 登录失败
|
||||
Notification.error({
|
||||
content: '登录失败,请检查用户名和密码'
|
||||
})
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const hideModal = () => setVisible(false)
|
||||
|
||||
return (<div>
|
||||
<Modal
|
||||
centered
|
||||
visible={visible}
|
||||
width={450}
|
||||
footer={null}
|
||||
maskClosable={false}
|
||||
title='登录'
|
||||
onCancel={hideModal}
|
||||
>
|
||||
<Form
|
||||
onSubmit={onFinish}
|
||||
>
|
||||
<Form.Input
|
||||
field="username"
|
||||
placeholder="请输入用户名"
|
||||
noLabel
|
||||
rules={[{required: true, message: '用户名不可空'}]}
|
||||
/>
|
||||
<Form.Input
|
||||
type="password"
|
||||
field="password"
|
||||
placeholder="请输入登录密码"
|
||||
noLabel
|
||||
rules={[{required: true, message: '密码不可空'}]}
|
||||
/>
|
||||
<div className='flex align-center space-between'>
|
||||
<Form.Checkbox
|
||||
field="remember"
|
||||
value={false}
|
||||
noLabel
|
||||
>下次自动登录</Form.Checkbox>
|
||||
<Space>
|
||||
<Button theme='light' onClick={hideModal}>取消</Button>
|
||||
<Button type='primary' theme='solid' loading={loading} htmlType="submit">登录</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
{/*<div style={{ padding: '30px 0 20px' }}>*/}
|
||||
{/* <Divider>OR</Divider>*/}
|
||||
{/* <div className="text-center" style={{ marginTop: 20 }}>*/}
|
||||
{/* <Space>*/}
|
||||
{/* <Button icon={<GoogleOutlined />}>GOOGLE</Button>*/}
|
||||
{/* <Button theme='solid' type="primary" icon={<GithubOutlined />}>GITHUB</Button>*/}
|
||||
{/* </Space>*/}
|
||||
{/* </div>*/}
|
||||
{/*</div>*/}
|
||||
</Modal>
|
||||
{visible?'xxx':'0000'}
|
||||
<Button type="primary" onClick={() => setVisible(true)}>点击登录</Button>
|
||||
</div>)
|
||||
}
|
13
front/components/Result.tsx
Normal file
13
front/components/Result.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
export type ResultProps = {
|
||||
extra: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
status: number | string;
|
||||
};
|
||||
export const Result: React.FC<ResultProps> = (props) => {
|
||||
return <div>
|
||||
<div className="title">{props.title}</div>
|
||||
<div className="extra">{props.extra}</div>
|
||||
</div>
|
||||
}
|
25
front/components/panel/index.tsx
Normal file
25
front/components/panel/index.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { cx, css } from '@emotion/css';
|
||||
import styles from './panel.module.scss'
|
||||
import React from 'react'
|
||||
|
||||
// 定义panel组件的属性类型
|
||||
export interface PanelProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
title?: string;
|
||||
noPadding?: boolean;
|
||||
}
|
||||
|
||||
export const Panel: React.FC<PanelProps> = (props) => {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.panelHeader}>
|
||||
<h3 className={styles.panelTitle}>{props.title}</h3>
|
||||
{props.extra && <div className={styles.panelExtra}>{props.extra}</div>}
|
||||
</div>
|
||||
<div className={cx(styles.panelBody, props.noPadding ? styles.noPadding : '')}>{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
21
front/components/panel/panel.module.scss
Normal file
21
front/components/panel/panel.module.scss
Normal file
@ -0,0 +1,21 @@
|
||||
.panel{
|
||||
margin-top: 20px;
|
||||
}
|
||||
.panelHeader{
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.panelTitle{
|
||||
font-size: 14px;
|
||||
}
|
||||
.panelExtra{
|
||||
|
||||
}
|
||||
.panelBody{
|
||||
background-color: #fff;
|
||||
padding:10px;
|
||||
border-radius: 4px;;
|
||||
}
|
||||
.noPadding{padding:0;}
|
5
front/config.ts
Normal file
5
front/config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const APP_CONFIG = {
|
||||
LOGIN_TOKEN_KEY: 'login_token',
|
||||
API_PREFIX: '',
|
||||
ROUTER_MODE: 'hash'
|
||||
}
|
10
front/main.tsx
Normal file
10
front/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import {App} from './App'
|
||||
|
||||
ReactDOM.createRoot(document.querySelector('#root')!).render(
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
)
|
32
front/pages/Router.tsx
Normal file
32
front/pages/Router.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import {BrowserRouter, HashRouter, Navigate, Route, Routes, useNavigate} from "react-router-dom";
|
||||
import {APP_CONFIG} from "../config.ts";
|
||||
import {Button} from "@douyinfe/semi-ui";
|
||||
import DefaultPage from "./index";
|
||||
import {Result} from "../components/Result.tsx";
|
||||
|
||||
const routerMode: 'browser' | 'hash' | string = APP_CONFIG.ROUTER_MODE;
|
||||
|
||||
const WebRouter: React.FC<{
|
||||
children?: React.ReactNode
|
||||
}> = (prop) => (
|
||||
routerMode == 'hash' ? <HashRouter>{prop.children}</HashRouter> : <BrowserRouter>{prop.children}</BrowserRouter>
|
||||
)
|
||||
|
||||
const NotFound: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
return <Result
|
||||
status="404"
|
||||
title="页面不存在或无法找到所请求的资源"
|
||||
// subTitle="Sorry, the page you visited does not exist."
|
||||
extra={<Button onClick={() => navigate('/')} type="primary">返回首页</Button>}/>
|
||||
}
|
||||
export const AppRouter = () => (<WebRouter>
|
||||
<div className="page-body-content">
|
||||
<Routes>
|
||||
<Route path="/" element={<DefaultPage/>}/>
|
||||
|
||||
<Route path="*" element={<NotFound/>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
</WebRouter>)
|
9
front/pages/index/index.module.scss
Normal file
9
front/pages/index/index.module.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.index{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.modal{
|
||||
|
||||
}
|
10
front/pages/index/index.tsx
Normal file
10
front/pages/index/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import css from './index.module.scss'
|
||||
import {LoginComponent} from "../../components/LoginComponent.tsx";
|
||||
|
||||
const DefaultPage: React.FC = () => {
|
||||
return (<div className={css.index}>
|
||||
<LoginComponent/>
|
||||
</div>);
|
||||
}
|
||||
export default DefaultPage;
|
112
front/service/request.ts
Normal file
112
front/service/request.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import axios from 'axios';
|
||||
import {BizError} from "./types.ts";
|
||||
import Storage from "./storage.ts";
|
||||
import {useUserinfoStore} from "../store/userinfoStore.ts";
|
||||
import {APP_CONFIG} from "../config.ts";
|
||||
|
||||
export interface APIResponse<T> {
|
||||
/**
|
||||
* 错误码,0:成功,其他失败
|
||||
*/
|
||||
code: number;
|
||||
data?: T;
|
||||
/**
|
||||
* 非0情况下,提示信息
|
||||
*/
|
||||
msg: string;
|
||||
}
|
||||
|
||||
// const baseURL = APP_CONFIG.API_PREFIX;
|
||||
// const FORM_FORMAT = 'application/x-www-form-urlencoded';
|
||||
const JSON_FORMAT = 'application/json';
|
||||
export type RequestMethod = 'get' | 'post' | 'put' | 'delete'
|
||||
|
||||
const Axios = axios.create({
|
||||
baseURL: APP_CONFIG.API_PREFIX,
|
||||
timeout: 120000, // 超时时长120s
|
||||
headers: {
|
||||
'Content-Type': JSON_FORMAT
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 请求前拦截
|
||||
Axios.interceptors.request.use(config => {
|
||||
const token = Storage.get<string>(APP_CONFIG.LOGIN_TOKEN_KEY)
|
||||
// const {token} = useUserinfoStore()
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
if (config.data && config.data instanceof FormData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data';
|
||||
}
|
||||
return config
|
||||
}, err => {
|
||||
return Promise.reject(err)
|
||||
})
|
||||
//
|
||||
// // 返回后拦截
|
||||
Axios.interceptors.response.use(res => {
|
||||
return res
|
||||
}, err => {
|
||||
err.message = '服务异常,请稍后再试';
|
||||
if (err.message === 'Network Error') {
|
||||
err.message = '网络连接异常!';
|
||||
} else if (err.code === 'ECONNABORTED') {
|
||||
err.message = '请求超时,请稍后再试';
|
||||
}
|
||||
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
export function request<T>(url: string, method: RequestMethod, data: any = null, getOriginResult = false) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
Axios.request<APIResponse<T>>({
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
}).then(res => {
|
||||
if (res.status != 200) {
|
||||
reject(new BizError("服务异常,请稍后再试", res.status))
|
||||
return;
|
||||
}
|
||||
const {code, msg, data} = res.data
|
||||
if (code == 0) {
|
||||
if (getOriginResult) {
|
||||
resolve(res.data as any)
|
||||
return;
|
||||
}
|
||||
resolve(data as any as T)
|
||||
} else {
|
||||
if (code == 403) {
|
||||
const state = useUserinfoStore.getState()
|
||||
// 未登录 显示登录modal
|
||||
state.showLogin(true);
|
||||
}
|
||||
reject(new BizError(msg, code))
|
||||
}
|
||||
}).catch(e => {
|
||||
console.log(e)
|
||||
reject(new BizError(e.message, 500))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function uploadFile<T>(url: string, file: File, data: any = {}, returnOrigin = false) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (data) {
|
||||
for (const key in data) {
|
||||
formData.append(key, data[key])
|
||||
}
|
||||
}
|
||||
return request<T>(url, 'post', formData, returnOrigin)
|
||||
}
|
||||
|
||||
export function post<T>(url: string, data: any = {}) {
|
||||
return request<T>(url, 'post', data)
|
||||
}
|
||||
|
||||
export function get<T>(url: string, data: any = {}) {
|
||||
return request<T>(url, 'get', data)
|
||||
}
|
34
front/service/storage.ts
Normal file
34
front/service/storage.ts
Normal file
@ -0,0 +1,34 @@
|
||||
const _store = window.localStorage
|
||||
const Storage = {
|
||||
put(key: string, value: any, expire = -1) {
|
||||
expire = expire == -1 ? -1 : Date.now() + expire * 1000;
|
||||
_store.setItem(key, JSON.stringify({
|
||||
value,
|
||||
expire
|
||||
}))
|
||||
},
|
||||
get<T>(key: string, defaultValue: null | T = null) {
|
||||
const data = _store.getItem(key);
|
||||
if (data == null) {
|
||||
return data;
|
||||
}
|
||||
try {
|
||||
const _data = JSON.parse(data);
|
||||
if (_data.expire) {
|
||||
if (_data.expire != -1 && _data.expire < Date.now()) {
|
||||
Storage.remove(key);
|
||||
return defaultValue;
|
||||
}
|
||||
return _data.value as T;
|
||||
}
|
||||
return _data as T;
|
||||
} catch (_) {
|
||||
console.log(_)
|
||||
}
|
||||
return data as T;
|
||||
},
|
||||
remove(key: string) {
|
||||
_store.removeItem(key);
|
||||
}
|
||||
}
|
||||
export default Storage
|
11
front/service/types.ts
Normal file
11
front/service/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export class BizError extends Error {
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
code = 1;
|
||||
|
||||
constructor(message: string, code = 1) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
13
front/service/user.ts
Normal file
13
front/service/user.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {get, post} from "./request.ts";
|
||||
import {UserModel} from "../../model";
|
||||
|
||||
|
||||
export function login(user: string, password: string) {
|
||||
return post<UserModel>('/api/user/info', {user, password})
|
||||
}
|
||||
|
||||
export function getInfo() {
|
||||
return get<UserModel>('/api/user/info')
|
||||
}
|
||||
|
||||
// login()
|
92
front/store/userinfoStore.ts
Normal file
92
front/store/userinfoStore.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import {create} from "zustand";
|
||||
import Storage from "../service/storage.ts";
|
||||
import {getInfo, login} from './../service/user.ts'
|
||||
import {APP_CONFIG} from "../config.ts";
|
||||
import {UserModel} from "../../model";
|
||||
|
||||
type LoginDataType = { account: string; code?: string; password: string }
|
||||
|
||||
const LOGIN_TOKEN_KEY = APP_CONFIG.LOGIN_TOKEN_KEY, CACHE = {
|
||||
init: false
|
||||
};
|
||||
|
||||
|
||||
export const useUserinfoStore = create<{
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
userinfo: UserModel;
|
||||
/**
|
||||
* 用户登录凭证
|
||||
*/
|
||||
token?: string;
|
||||
/**
|
||||
* 登录
|
||||
* @param data
|
||||
* @param type
|
||||
*/
|
||||
login: (data: LoginDataType) => Promise<void>;
|
||||
/**
|
||||
* 注销用户登录信息
|
||||
*/
|
||||
logout: () => Promise<void>;
|
||||
/**
|
||||
* 初始化用户数据
|
||||
*/
|
||||
init: () => Promise<void>;
|
||||
}>((set, _, _state) => {
|
||||
return {
|
||||
userinfo: {
|
||||
id: 0
|
||||
},
|
||||
loginModalVisible: false,
|
||||
init: () => {
|
||||
const state = _state.getState()
|
||||
if (state.token) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
const token = Storage.get<string>(LOGIN_TOKEN_KEY);
|
||||
return new Promise<void>((resolve) => {
|
||||
if (token) {
|
||||
getInfo().then((info) => {
|
||||
set({
|
||||
userinfo: info,
|
||||
token
|
||||
})
|
||||
})
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
logout: () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
set({
|
||||
userinfo: {
|
||||
id: 0
|
||||
},
|
||||
token: undefined
|
||||
})
|
||||
Storage.remove(LOGIN_TOKEN_KEY)
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
login: (data) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const promise = login(data.account, data.password);
|
||||
promise.then((ret) => {
|
||||
const token = ret.token;
|
||||
Storage.put(LOGIN_TOKEN_KEY, token)
|
||||
getInfo().then(userinfo => {
|
||||
set({
|
||||
userinfo,
|
||||
token: userinfo.token
|
||||
})
|
||||
resolve()
|
||||
}).catch(reject)
|
||||
}).catch(reject)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
1
front/vite-env.d.ts
vendored
Normal file
1
front/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
12
index.html
Normal file
12
index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Web Report FrontEnd</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/front/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
59
model/index.ts
Normal file
59
model/index.ts
Normal file
@ -0,0 +1,59 @@
|
||||
export type PvUvModel = {
|
||||
id: number;
|
||||
uid: number;
|
||||
uuid: number;
|
||||
type: 'pv' | 'uv' | string;
|
||||
location: string;
|
||||
resolution: string;
|
||||
browser: string;
|
||||
path: string;
|
||||
referrer: string;
|
||||
data: any;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
export type EventDataModel = {
|
||||
id: number;
|
||||
uid: number;
|
||||
uuid: number;
|
||||
type: 'pv' | 'uv' | string;
|
||||
location: string;
|
||||
resolution: string;
|
||||
browser: string;
|
||||
path: string;
|
||||
referrer: string;
|
||||
data: any;
|
||||
create_time: string;
|
||||
}
|
||||
type BaseModel = {
|
||||
id: number;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用
|
||||
*/
|
||||
export type AppModel = {
|
||||
uid: number;
|
||||
title: number;
|
||||
} & BaseModel;
|
||||
|
||||
/**
|
||||
* 事件类型
|
||||
*/
|
||||
export type EventModel = {
|
||||
app_id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
description: string;
|
||||
} & BaseModel;
|
||||
|
||||
export type UserModel = {
|
||||
id: number;
|
||||
account?: string;
|
||||
name?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
}
|
45
package.json
Normal file
45
package.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "app-report-all",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "nodemon service/index.ts",
|
||||
"start": "ts-node service/index.ts",
|
||||
"preview": "vite preview",
|
||||
"dev-front": "vite --mode development --host 0.0.0.0",
|
||||
"build-front": "tsc && vite build --mode production",
|
||||
"build": "tsc",
|
||||
"test": "mocha -r ts-node/register test/**/**.test.ts",
|
||||
"test-db": "mocha -r ts-node/register test/db.test.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@douyinfe/semi-ui": "^2.41.3",
|
||||
"@emotion/css": "^11.11.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/mysql": "^2.15.21",
|
||||
"@types/node": "^20.4.9",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"axios": "^1.4.0",
|
||||
"dayjs": "^1.11.9",
|
||||
"mocha": "^10.2.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"sass": "^1.66.1",
|
||||
"supertest": "^6.3.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.4.9",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mysql": "^2.18.1",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
}
|
12
service/config.ts
Normal file
12
service/config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const PORT = 3001
|
||||
|
||||
export const DB_CONFIG = {
|
||||
// 地址
|
||||
host: 'localhost',
|
||||
// 用户名
|
||||
user: 'root',
|
||||
// 密码
|
||||
password: '123456',
|
||||
// 数据库名
|
||||
database: 'app_report',
|
||||
}
|
19
service/core/server.ts
Normal file
19
service/core/server.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { InitServerOption } from "./types";
|
||||
// import { createServer as createServerOrigin } from "http";
|
||||
import express = require("express");
|
||||
|
||||
export function createServer(options: Partial<InitServerOption>, callback?: () => void) {
|
||||
// 创建express服务器
|
||||
const app = express();
|
||||
// 将请求体转换为JSON格式
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
// 监听端口
|
||||
app.listen(options.port, callback);
|
||||
app.get('/ping', (_req, res) => {
|
||||
res.appendHeader('app-ping', 'pong')
|
||||
res.send('pong')
|
||||
})
|
||||
return app;
|
||||
}
|
||||
|
18
service/core/types.ts
Normal file
18
service/core/types.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Response, Request } from 'express'
|
||||
export type InitServerOption = {
|
||||
port: number;
|
||||
host: string;
|
||||
https: boolean;
|
||||
}
|
||||
export type HttpMethod = 'all' | 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head' | string;
|
||||
export type RouteHandleFunctionParam = {
|
||||
path: string;
|
||||
param: Record<string, any>;
|
||||
query: Record<string, any>;
|
||||
body: Record<string, any>;
|
||||
method: HttpMethod;
|
||||
headers: Record<string, string>;
|
||||
res: Response<any, Record<string, any>>
|
||||
req: Request
|
||||
}
|
||||
export type RouteHandleFunction = (params: RouteHandleFunctionParam) => void | Promise<void>;
|
8
service/index.ts
Normal file
8
service/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {createServer} from "./core/server";
|
||||
import {initRoutes} from "./routes";
|
||||
import {PORT} from './config'
|
||||
|
||||
// 创建应用并初始化路由
|
||||
initRoutes(createServer({
|
||||
port: PORT
|
||||
}, () => console.log(`server is running at port ${PORT} open http://localhost:${PORT}`)))
|
5
service/routes/home.ts
Normal file
5
service/routes/home.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { RouteHandleFunction } from "../core/types";
|
||||
|
||||
export const home: RouteHandleFunction = ({ res }) => {
|
||||
res.send('home')
|
||||
}
|
32
service/routes/index.ts
Normal file
32
service/routes/index.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import {Application, Request, Response} from "express";
|
||||
import {RouteHandleFunction, RouteHandleFunctionParam} from "../core/types";
|
||||
import {home} from "./home";
|
||||
import {appList, reportToServer, appEvent, eventData} from "./reportor";
|
||||
import {loginHandler} from "./user.ts";
|
||||
|
||||
//
|
||||
function createRoute(handler: RouteHandleFunction) {
|
||||
return (req: Request, res: Response<any, Record<string, any>>) => {
|
||||
// console.log('params', req.params, req.query, req.body)
|
||||
handler({
|
||||
path: req.path,
|
||||
param: req.params,
|
||||
query: req.query,
|
||||
body: req.body,
|
||||
method: req.method,
|
||||
headers: {},
|
||||
res,
|
||||
req
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化路由
|
||||
export function initRoutes(app: Application) {
|
||||
app.get('/home', createRoute(home))
|
||||
app.all('/api/report', createRoute(reportToServer))
|
||||
app.all('/api/app/list', createRoute(appList))
|
||||
app.all('/api/app/event', createRoute(appEvent))
|
||||
app.all('/api/app/event-data', createRoute(eventData))
|
||||
app.all('/api/login', createRoute(loginHandler))
|
||||
}
|
37
service/routes/reportor.ts
Normal file
37
service/routes/reportor.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { RouteHandleFunction } from "../core/types";
|
||||
import { listAppByUID } from "../service/app";
|
||||
import { reportEvent } from "../service/report-service";
|
||||
|
||||
|
||||
export const reportToServer: RouteHandleFunction = ({
|
||||
param, res
|
||||
}) => {
|
||||
reportEvent(param).then(() => {
|
||||
res.send('got and saved!')
|
||||
}).catch((e: Error) => {
|
||||
console.log(e)
|
||||
res.send('got but not saved!')
|
||||
});
|
||||
}
|
||||
|
||||
export const appList: RouteHandleFunction = async ({
|
||||
param, res
|
||||
}) => {
|
||||
const apps = await listAppByUID(1);
|
||||
res.send({ code: 0, data: apps })
|
||||
}
|
||||
|
||||
export const appEvent: RouteHandleFunction = async ({
|
||||
query, res, param, body
|
||||
}) => {
|
||||
|
||||
res.send({
|
||||
query, param, body
|
||||
})
|
||||
}
|
||||
|
||||
export const eventData: RouteHandleFunction = ({
|
||||
param, res
|
||||
}) => {
|
||||
res.send(JSON.stringify(param))
|
||||
}
|
45
service/routes/user.ts
Normal file
45
service/routes/user.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import {RouteHandleFunction} from "../core/types.ts";
|
||||
import {login} from "../service/app.ts";
|
||||
import {UserModel} from "../../model";
|
||||
|
||||
export function encodeUserToken(user: UserModel) {
|
||||
if (user == null) throw new Error('user is null')
|
||||
const token = JSON.stringify(user)
|
||||
return btoa(encodeURIComponent(token));
|
||||
}
|
||||
|
||||
export function decodeUserToken(token: string) {
|
||||
// 将token转回UserModel对象数据
|
||||
const user = decodeURIComponent(atob(token));
|
||||
return JSON.parse(user) as UserModel
|
||||
}
|
||||
|
||||
|
||||
export const loginHandler: RouteHandleFunction
|
||||
= async ({
|
||||
body, res
|
||||
}) => {
|
||||
if (!body.username || !body.password) {
|
||||
// 没有用户名或者密码
|
||||
res.send({code: 2, message: '用户名和密码不能为空'})
|
||||
return;
|
||||
}
|
||||
const userinfo = await login(body.username, body.password);
|
||||
if (!userinfo) {
|
||||
res.send({code: 1, message: '用户名或密码错误'})
|
||||
return;
|
||||
}
|
||||
res.send({
|
||||
code: 0, data: {
|
||||
...userinfo,
|
||||
token: encodeUserToken(userinfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getUserInfo: RouteHandleFunction =
|
||||
({headers, res}) => {
|
||||
const token = headers.Authorization
|
||||
const user = decodeUserToken(token)
|
||||
res.send({code: 0, message: '登录成功', data: user})
|
||||
}
|
26
service/service/app.ts
Normal file
26
service/service/app.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {AppModel, EventModel, UserModel} from "../../model";
|
||||
import {selectArray, selectOne} from "./mysql";
|
||||
|
||||
export function listAppByUID(uid: number) {
|
||||
return selectArray<AppModel>('select * from apps where uid=?', [uid])
|
||||
}
|
||||
|
||||
export function getAppInfo(id: number) {
|
||||
// 查询app信息
|
||||
return selectOne<AppModel>('select * from apps where id=?', [id])
|
||||
}
|
||||
|
||||
// 查询应用的事件
|
||||
export function listAppEvent(id: number) {
|
||||
return selectArray<EventModel>('select * from events where app_id=0 or app_id=?', [id])
|
||||
}
|
||||
|
||||
// 查询应用所有的事件数据
|
||||
export function listAppEventData(id: number, page = 1, pageSize = 10) {
|
||||
return selectArray<EventModel>('select * from events_data where app_id=? limit ?,?',
|
||||
[id, (page - 1) * pageSize, pageSize])
|
||||
}
|
||||
|
||||
export function login(username: string, password: string) {
|
||||
return selectOne<UserModel>('select * from user where account=? and password=?', [username, password]);
|
||||
}
|
91
service/service/mysql.ts
Normal file
91
service/service/mysql.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { createPool } from 'mysql'
|
||||
import { DB_CONFIG } from './../config';
|
||||
|
||||
export const pool = createPool({
|
||||
...DB_CONFIG,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
// 查询对象数组
|
||||
export async function selectArray<T>(sql: string, params: any = null) {
|
||||
// 执行查询sql并返回查询结果
|
||||
return await executeSQL<T[]>(sql, params);
|
||||
// return new Promise<T[]>((resolve, reject) => {
|
||||
// pool.getConnection((err, connection) => {
|
||||
// if (err) {
|
||||
// reject(err)
|
||||
// return;
|
||||
// }
|
||||
// connection.query("").
|
||||
// connection.query(sql, params, (e, rows) => {
|
||||
// connection.release();
|
||||
// if (e) {
|
||||
// reject(e)
|
||||
// return;
|
||||
// }
|
||||
// resolve(rows);
|
||||
// })
|
||||
// })
|
||||
// });
|
||||
}
|
||||
// 查询单个对象
|
||||
export async function selectOne<T>(sql: string, params: any = null) {
|
||||
// 执行查询sql并返回查询结果
|
||||
const arr = await selectArray<T>(sql, params);
|
||||
if (arr.length != 1) throw new Error("查询结果数量不等于1");
|
||||
return arr[0];
|
||||
}
|
||||
// 统计数据
|
||||
export async function queryCount(sql: string, params: any = null) {
|
||||
const obj = await selectOne<{ [key: string]: number }>(sql, params);
|
||||
const keys = Object.keys(obj);
|
||||
return obj[keys[0]];
|
||||
}
|
||||
// 根据表名和条件判断数据是否存在
|
||||
export async function isExistByTable(tableName: string, condition: any = {}) {
|
||||
let sql = `select count(*) as count from ${tableName} where 1=1`;
|
||||
const params = [];
|
||||
for (let key in condition) {
|
||||
sql += ` and ${key}=?`;
|
||||
params.push(condition[key]);
|
||||
}
|
||||
const count = await queryCount(sql, params);
|
||||
return count > 0;
|
||||
}
|
||||
export async function isExist(sql: string, params: any) {
|
||||
const count = await queryCount(sql, params);
|
||||
return count > 0;
|
||||
}
|
||||
// 查询多条数据
|
||||
|
||||
function executeSQL<T>(sql: string, params: any) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
pool.query(sql, params, (err, ret) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(ret)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function execute(sql: string, params: any = null) {
|
||||
const ret = await executeSQL<{ affectedRows: number }>(sql, params)
|
||||
return ret.affectedRows
|
||||
}
|
||||
export async function insertAndGetInsertId(tableName: string, data: any = null) {
|
||||
const ret = await executeSQL<{ insertId: number }>(`insert into \`${tableName}\` set ?`, data)
|
||||
return ret.insertId
|
||||
}
|
||||
export default {
|
||||
selectArray,
|
||||
executeSQL,
|
||||
execute,
|
||||
insertAndGetInsertId,
|
||||
isExist,
|
||||
isExistByTable,
|
||||
pool
|
||||
}
|
19
service/service/report-service.ts
Normal file
19
service/service/report-service.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { PvUvModel } from "../../model";
|
||||
import { isExist, insertAndGetInsertId } from './mysql'
|
||||
|
||||
const table = {
|
||||
pv_uv: 'pv_uv'
|
||||
}
|
||||
export async function reportEvent(data: Partial<PvUvModel>) {
|
||||
//pv:用户每次打开一个页面便记录1次PV,多次打开同一页面则浏览量累计。
|
||||
//uv:1天内同一访客的多次访问只记录为一个访客。通过IP和cookie是判断UV值的两种方式。
|
||||
const { type } = data;
|
||||
if (type == 'uv') {
|
||||
// 判断今日的数据是否已经存在
|
||||
const existsToday = await isExist('select count(*) _ from pv_uv where DATE(created_at) = CURDATE() and uuid=?', [data.uuid])
|
||||
if (existsToday) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await insertAndGetInsertId('pv_uv', data)
|
||||
}
|
28
test/db.test.ts
Normal file
28
test/db.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { execute, selectArray, pool, queryCount } from './../src/service/mysql'
|
||||
|
||||
// 使用execute执行更新数据
|
||||
describe('Database', () => {
|
||||
it('excute update', () => {
|
||||
execute('update user set name = ? where id = ?', ['张三', '1']).then(ret => {
|
||||
console.log(ret)
|
||||
})
|
||||
})
|
||||
it('excute insert', () => {
|
||||
execute('insert into user set ?', {
|
||||
name: '张三111'
|
||||
}).then(ret => {
|
||||
console.log(ret)
|
||||
})
|
||||
})
|
||||
it('query array', async () => {
|
||||
const ret = await selectArray<{ id: number; name: string }>('select * from user')
|
||||
ret.forEach(it => {
|
||||
console.log(it.id, '=>', it.name)
|
||||
})
|
||||
// console.log(ret)
|
||||
})
|
||||
it('query count', async () => {
|
||||
const count = await queryCount('select count(*) from user');
|
||||
console.log('total:', count);
|
||||
})
|
||||
})
|
28
test/index.test.ts
Normal file
28
test/index.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { equal } from 'assert';
|
||||
import { get } from './server';
|
||||
|
||||
describe('Array', function () {
|
||||
describe('#indexOf()', function () {
|
||||
it('should return -1 when the value is not present', function () {
|
||||
equal([1, 2, 3].indexOf(4), -1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 使用mocha对路径 "/ping" 进行单元测试
|
||||
describe('Web', function () {
|
||||
it('/exece should return 404', () => {
|
||||
get('/exece').expect(404)
|
||||
.end((err) => {
|
||||
if (err) throw err
|
||||
})
|
||||
})
|
||||
it('/ping should return "pong"', function (done) {
|
||||
get('/ping')
|
||||
.expect('app-ping', 'pong')
|
||||
.expect('pong', done);
|
||||
});
|
||||
it('/home', function (done) {
|
||||
get('/home').expect('home', done);
|
||||
});
|
||||
});
|
15
test/server.ts
Normal file
15
test/server.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Test } from 'supertest';
|
||||
import { PORT } from './../config'
|
||||
const request = require("supertest");
|
||||
|
||||
// import { createServer } from './../src/core/server'
|
||||
// import { initRoutes } from './../src/routes'
|
||||
|
||||
// 创建应用并初始化路由
|
||||
// const app = createServer({
|
||||
// port: PORT
|
||||
// }, () => console.log('server created'))
|
||||
// initRoutes(app)
|
||||
|
||||
export const createRequest = () => request(`http://localhost:${PORT}`)
|
||||
export const get = (path: string): Test => createRequest().get(path)
|
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "CommonJS",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
},
|
||||
"include": [
|
||||
"front","service","test"
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
12
tsconfig.node.json
Normal file
12
tsconfig.node.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
23
utils/base64.ts
Normal file
23
utils/base64.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export function base64ToBuffer(data: string) {
|
||||
//去掉url的头,并转换为byte
|
||||
const split = data.split(',');
|
||||
const bytes = window.atob(split[1]);
|
||||
//处理异常,将ascii码小于0的转换为大于0
|
||||
const ab = new ArrayBuffer(bytes.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
ia[i] = bytes.charCodeAt(i);
|
||||
}
|
||||
const match = split[0].match(/^data:(.*);base64/)
|
||||
const contentType = match && match.length == 2 ? match[1] : ''
|
||||
//return new Blob([ab], {type: split[0]});
|
||||
return {
|
||||
buffer: [ab],
|
||||
contentType
|
||||
}
|
||||
}
|
||||
|
||||
export function base64ToFile(data: string, fileName: string) {
|
||||
const _data = base64ToBuffer(data);
|
||||
return new File(_data.buffer, fileName, {type:_data.contentType})
|
||||
}
|
7
utils/date.ts
Normal file
7
utils/date.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as dayjs from "dayjs";
|
||||
|
||||
|
||||
export function formatDate(time?: string | number | Date | dayjs.Dayjs | null, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!time) return '';
|
||||
return dayjs(time).format(format)
|
||||
}
|
30
utils/storage.ts
Normal file
30
utils/storage.ts
Normal file
@ -0,0 +1,30 @@
|
||||
const ArticleCache: {
|
||||
title: string;
|
||||
content?: string;
|
||||
key?: string;
|
||||
init: boolean;
|
||||
} = {
|
||||
title: '',
|
||||
content: '',
|
||||
key: '',
|
||||
init: false
|
||||
}
|
||||
export const storage = {
|
||||
saveTempContent: (content: string, title: string,key?:string) => {
|
||||
ArticleCache.content = content;
|
||||
ArticleCache.title = title;
|
||||
ArticleCache.key = key;
|
||||
},
|
||||
getTempContent() {
|
||||
return ArticleCache;
|
||||
},
|
||||
hasInit(){
|
||||
return ArticleCache.init;
|
||||
},
|
||||
// 是否初始化
|
||||
init(){
|
||||
// 可以完成一些初始化
|
||||
ArticleCache.init = true;
|
||||
}
|
||||
|
||||
}
|
35
utils/strings.ts
Normal file
35
utils/strings.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export function countString(str: string) {
|
||||
str = str.replace(/\n/, '')
|
||||
const chinese = Array.from(str)
|
||||
.filter(ch => /[\u4e00-\u9fa5]/.test(ch))
|
||||
.length
|
||||
const english = Array.from(str)
|
||||
.map(ch => /[a-zA-Z0-9\s]/.test(ch) ? ch : '')
|
||||
.join('').length//.split(/\s+/).filter(s => s)
|
||||
|
||||
return chinese + english
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes: number) {
|
||||
if (bytes <= 0) return '0KB';
|
||||
const units = ['KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0;
|
||||
while (bytes >= 1024 && i < units.length - 1) {
|
||||
bytes /= 1024;
|
||||
i++;
|
||||
}
|
||||
return bytes.toFixed(2) + units[i];
|
||||
}
|
||||
|
||||
const REGEX = {
|
||||
phone: /^(\d{3})\d{4}(\d{4})$/
|
||||
}
|
||||
|
||||
export function hidePhone(phone?: string | null) {
|
||||
if (!phone || phone.length < 11) return phone;
|
||||
return phone.replace(REGEX.phone, '$1****$2')
|
||||
}
|
||||
|
||||
export function isPhone(phone?: string) {
|
||||
return phone && REGEX.phone.test(phone)
|
||||
}
|
46
vite.config.ts
Normal file
46
vite.config.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import {defineConfig, PluginOption} from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig(({mode}) => {
|
||||
console.log('mode',mode)
|
||||
return {
|
||||
plugins: [
|
||||
react()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@/': '/front/'
|
||||
}
|
||||
},
|
||||
define: {
|
||||
buildVersion: JSON.stringify((new Date()).toLocaleString()),
|
||||
mode:JSON.stringify(mode),
|
||||
},
|
||||
build: {
|
||||
// 小于10kb直接base64
|
||||
assetsInlineLimit: 10240,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('react')) {
|
||||
return 'react-libs'
|
||||
}
|
||||
// if (id.includes('node_modules')) {
|
||||
// return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
base: "/",
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: "http://localhost:3001",
|
||||
changeOrigin: true,
|
||||
// rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user