init add first commit

This commit is contained in:
LittleBoy 2023-08-21 15:36:10 +08:00
commit 62a323bdf9
43 changed files with 3877 additions and 0 deletions

27
.gitignore vendored Normal file
View 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
View 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
View 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;
}

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

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

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

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

View File

@ -0,0 +1,9 @@
.index{
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
.modal{
}

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

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

@ -0,0 +1 @@
/// <reference types="vite/client" />

12
index.html Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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))
}

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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/, ""),
}
}
}
}
})

2544
yarn.lock Normal file

File diff suppressed because it is too large Load Diff