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