├── . gitattributes
├── README.md
├── react
├── .env
├── .gitignore
├── README.md
├── color.js
├── config-overrides.js
├── package.json
├── public
│ ├── color.less
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── assets
│ │ ├── iconfont
│ │ │ ├── demo.css
│ │ │ ├── demo_index.html
│ │ │ ├── iconfont.css
│ │ │ ├── iconfont.eot
│ │ │ ├── iconfont.js
│ │ │ ├── iconfont.svg
│ │ │ ├── iconfont.ttf
│ │ │ ├── iconfont.woff
│ │ │ └── iconfont.woff2
│ │ └── images
│ │ │ ├── antd.svg
│ │ │ ├── bg1.jpg
│ │ │ ├── bg2.jpg
│ │ │ ├── bg3.jpg
│ │ │ ├── dark.svg
│ │ │ └── light.svg
│ ├── components
│ │ ├── AnimatedBook
│ │ │ ├── index.js
│ │ │ └── style.less
│ │ ├── Background
│ │ │ └── index.js
│ │ ├── ColorPicker
│ │ │ └── index.js
│ │ ├── Loading
│ │ │ ├── index.js
│ │ │ └── style.css
│ │ ├── MyIcon
│ │ │ └── index.js
│ │ ├── Phone
│ │ │ ├── index.js
│ │ │ └── index.less
│ │ ├── PrivateRoute
│ │ │ └── index.js
│ │ ├── PromptBox
│ │ │ └── index.js
│ │ └── Typing
│ │ │ └── index.js
│ ├── config
│ │ └── secret.js
│ ├── index.css
│ ├── index.js
│ ├── pages
│ │ ├── About
│ │ │ └── index.js
│ │ ├── ButtonDemo
│ │ │ └── index.js
│ │ ├── Chat
│ │ │ ├── imgs
│ │ │ │ ├── administrator.png
│ │ │ │ ├── header1.png
│ │ │ │ ├── header2.png
│ │ │ │ ├── react.png
│ │ │ │ └── zanwu.png
│ │ │ ├── index.js
│ │ │ └── style.less
│ │ ├── Collection
│ │ │ ├── CreateModal.js
│ │ │ ├── index.js
│ │ │ └── style.less
│ │ ├── FeedbackDemo
│ │ │ └── index.js
│ │ ├── IconDemo
│ │ │ ├── icons.js
│ │ │ ├── index.js
│ │ │ └── style.less
│ │ ├── Index
│ │ │ ├── EditInfoModal.js
│ │ │ ├── EditPasswordModal.js
│ │ │ ├── MyContent.js
│ │ │ ├── MyHeader.js
│ │ │ ├── MySider.js
│ │ │ ├── index.js
│ │ │ └── style.less
│ │ ├── Login
│ │ │ ├── LoginForm.js
│ │ │ ├── RegisterForm.js
│ │ │ ├── example.html
│ │ │ ├── index.js
│ │ │ └── style.less
│ │ ├── MessageBoard
│ │ │ ├── Score.js
│ │ │ ├── index.js
│ │ │ └── style.less
│ │ ├── MobileUI
│ │ │ ├── config.js
│ │ │ ├── index.js
│ │ │ └── index.less
│ │ ├── Users
│ │ │ ├── CreateUserModal.js
│ │ │ ├── InfoModal.js
│ │ │ └── index.js
│ │ └── tabs.js
│ ├── serviceWorker.js
│ ├── store
│ │ ├── actions.js
│ │ ├── index.js
│ │ └── reducers.js
│ ├── styles
│ │ ├── main.less
│ │ └── vars.less
│ └── utils
│ │ ├── LoadableComponent.js
│ │ ├── ajax.js
│ │ ├── asyncComponent.js
│ │ ├── history.js
│ │ ├── session.js
│ │ └── util.js
└── yarn.lock
└── server
├── .gitignore
├── app.js
├── bin
└── www
├── chat.js
├── config
├── db.js
└── secret.js
├── controller
├── chat.js
├── message.js
├── score.js
├── user.js
└── works.js
├── db
└── mysql.js
├── middlewares
└── errorHandle.js
├── model
└── resModel.js
├── package.json
├── public
├── images
│ ├── avatar.png
│ ├── bg1.jpg
│ ├── bg2.jpg
│ ├── bg3.jpg
│ ├── default.png
│ ├── login_bg1.jpg
│ ├── login_bg2.jpg
│ └── login_bg3.jpg
├── stylesheets
│ └── style.css
└── upload-files
│ ├── chat
│ ├── 01.jpeg
│ ├── 1590399528000-ZZrRkP.jpeg
│ └── c6e0629a36c94106be63f8fc72dd977a.jpeg
│ └── myUpload
│ ├── 01.jpeg
│ ├── 217ad66f504747ca9b613036d3dcd453.jpg
│ ├── 67be71bd1a20486289f45005773386f2.jpg
│ ├── ece8c6fae0012978195c0ff364bc4c81.jpg
│ └── 浪子康,杨洪才 - Xun(易硕成)-可惜我不是她(咚鼓版)(浪子康 / 杨洪才 remix).mp3
├── routes
├── index.js
├── message.js
├── score.js
├── user.js
└── works.js
├── sql
└── admin.sql
├── utils
├── upload.js
└── util.js
├── views
├── error.pug
├── index.pug
└── layout.pug
└── yarn.lock
/. gitattributes:
--------------------------------------------------------------------------------
1 | *.js linguist-language=JavaScript
2 | *.css linguist-language=JavaScript
3 | *.html linguist-language=JavaScript
--------------------------------------------------------------------------------
/react/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_BASE_URL = http://localhost:8888
--------------------------------------------------------------------------------
/react/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | #忽略vscode的配置
9 | jsconfig.json
10 |
11 | # testing
12 | /coverage
13 |
14 | # production
15 | /build
16 |
17 | # misc
18 | .DS_Store
19 | .env.local
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
--------------------------------------------------------------------------------
/react/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/react/color.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { generateTheme, getLessVars } = require('antd-theme-generator');
3 |
4 | const options = {
5 | stylesDir: path.join(__dirname, './src/styles'),
6 | antDir: path.join(__dirname, './node_modules/antd'),
7 | varFile: path.join(__dirname, './src/styles/vars.less'),
8 | mainLessFile: path.join(__dirname, './src/styles/main.less'),
9 | themeVariables: [
10 | '@primary-color',
11 | ],
12 | indexFileName: 'index.html',
13 | outputFilePath: path.join(__dirname, './public/color.less'),
14 | }
15 |
16 | generateTheme(options).then(less => {
17 | console.log('Theme generated successfully');
18 | })
19 | .catch(error => {
20 | console.log('Error', error);
21 | });
22 |
--------------------------------------------------------------------------------
/react/config-overrides.js:
--------------------------------------------------------------------------------
1 | /* config-overrides.js */
2 | const path = require('path');
3 | const { injectBabelPlugin } = require('react-app-rewired');
4 | const rewireLess = require('react-app-rewire-less');
5 | const { getLessVars } = require('antd-theme-generator');
6 |
7 | function resolve (dir) {
8 | return path.join(__dirname, '.', dir)
9 | }
10 |
11 | module.exports = function override(config, env) {
12 | //do stuff with the webpack config...
13 |
14 | //按需加载
15 | config = injectBabelPlugin(
16 | ['import', { libraryName: 'antd', style: true }],
17 | config
18 | );
19 |
20 | //配置antd主题
21 | config = rewireLess.withLoaderOptions({
22 | modifyVars: getLessVars(path.join(__dirname, './src/styles/vars.less')),
23 | javascriptEnabled: true
24 | })(config, env);
25 |
26 | //配置别名
27 | config.resolve.alias = {
28 | '@': resolve('src')
29 | }
30 |
31 | config.devtool = false; // 关掉 sourceMap
32 |
33 | //启用ES7的修改器语法(babel 7)
34 | config = injectBabelPlugin(['@babel/plugin-proposal-decorators', { "legacy": true }], config)
35 |
36 | return config;
37 | }
38 |
--------------------------------------------------------------------------------
/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "t",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "antd": "^3.20.0",
7 | "antd-theme-generator": "^1.1.6",
8 | "babel-plugin-import": "^1.11.0",
9 | "braft-editor": "^2.3.7",
10 | "braft-extensions": "^0.0.18",
11 | "crypto-js": "^3.1.9-1",
12 | "draft-js-prism": "^1.0.6",
13 | "gsap": "^2.1.2",
14 | "prismjs": "^1.16.0",
15 | "react": "^16.8.6",
16 | "react-app-rewire-less": "^2.1.3",
17 | "react-app-rewired": "2.0.2-next.0",
18 | "react-color": "^2.17.3",
19 | "react-dom": "^16.8.6",
20 | "react-loadable": "^5.5.0",
21 | "react-redux": "^7.1.0",
22 | "react-router-dom": "^5.0.0",
23 | "react-scripts": "3.0.0",
24 | "react-virtualized": "^9.21.2",
25 | "redux": "^4.0.1",
26 | "redux-thunk": "^2.3.0",
27 | "screenfull": "^4.2.0"
28 | },
29 | "scripts": {
30 | "start": "npm run color && react-app-rewired start",
31 | "build": "react-app-rewired build",
32 | "color": "node color.js",
33 | "test": "react-app-rewired test",
34 | "eject": "react-scripts eject"
35 | },
36 | "eslintConfig": {
37 | "extends": "react-app"
38 | },
39 | "browserslist": {
40 | "production": [
41 | ">0.2%",
42 | "not dead",
43 | "not op_mini all"
44 | ],
45 | "development": [
46 | "last 1 chrome version",
47 | "last 1 firefox version",
48 | "last 1 safari version"
49 | ]
50 | },
51 | "devDependencies": {
52 | "@babel/plugin-proposal-decorators": "^7.4.4"
53 | },
54 | "homepage": "."
55 | }
56 |
--------------------------------------------------------------------------------
/react/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/public/favicon.ico
--------------------------------------------------------------------------------
/react/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | 后台管理系统
23 |
24 |
25 |
26 |
32 |
33 |
34 |
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/react/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/react/src/App.css:
--------------------------------------------------------------------------------
1 | .markdown {
2 | color: #314659;
3 | font-size: 14px;
4 | line-height: 2;
5 | }
6 |
7 | .markdown ol {
8 | list-style: circle inside;
9 | }
10 |
11 | .markdown code {
12 | margin: 0 1px;
13 | padding: .2em .4em;
14 | font-size: .9em;
15 | background: #f2f4f5;
16 | border: 1px solid #eee;
17 | border-radius: 3px;
18 | }
19 |
20 | .markdown h2,
21 | .markdown h3,
22 | .markdown h4,
23 | .markdown h5,
24 | .markdown h6 {
25 | clear: both;
26 | /* margin: 1.6em 0 .6em; */
27 | color: #0d1a26;
28 | font-weight: 500;
29 | font-family: Avenir, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif;
30 | }
31 |
32 | .markdown h3 {
33 | font-size: 18px;
34 | }
35 |
36 | .primary-color {
37 | color: var(--primaryColor);
38 | }
39 |
40 | .my-a {
41 | color: var(--primaryColor);
42 | cursor: pointer;
43 | }
44 |
45 | .ellipsis {
46 | overflow: hidden;
47 | text-overflow: ellipsis;
48 | white-space: nowrap;
49 | }
--------------------------------------------------------------------------------
/react/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './App.css'
3 | import './index.css'
4 | import { Switch, Route, withRouter } from 'react-router-dom'
5 | import PrivateRoute from './components/PrivateRoute'
6 | import './assets/iconfont/iconfont.css'
7 | import LoadableComponent from '@/utils/LoadableComponent'
8 |
9 | const Index = LoadableComponent(import('./pages/Index'))
10 | const Login = LoadableComponent(import('./pages/Login'))
11 |
12 | @withRouter
13 | class App extends React.Component {
14 | render() {
15 | return (
16 |
17 |
18 |
19 |
20 | )
21 | }
22 | }
23 |
24 | export default App
25 |
--------------------------------------------------------------------------------
/react/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/react/src/assets/iconfont/iconfont.css:
--------------------------------------------------------------------------------
1 | @font-face {font-family: "iconfont";
2 | src: url('iconfont.eot?t=1562140191837'); /* IE9 */
3 | src: url('iconfont.eot?t=1562140191837#iefix') format('embedded-opentype'), /* IE6-IE8 */
4 | url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAlgAAsAAAAAETAAAAkSAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCFDAqSLI5HATYCJANACyIABCAFhG0HgU4bXw4jETaMkyIj+6sE7jD9GmyUb+R5Ft2iTaWc0E1vsHM4FJe0ySDc9D4f0hBICdQcasIKc+pIJ66sM2Wdi9GJSJg7CRBgNuhmEehbqLxA2p7q9GarOfIiMp04WWENpD1xnd48wI8eA5trxSRM1LtFefbJs15IgbKTpkcM2GDYzv+ttXreeYSiH512oUEts+djqHo2CYnQTCzuhYZY79iQLW77CrAa9SPicCAABEQiFcRqr1oXPFgwJSjXtVOHVuBzIWAbHAh4f07ZlAWZCgV4ZhzjBjAl/HzyBpmEBxgoKPhINdvb2iJXhqc/dXkNNKXD4dpcFIDeXAAFkAqA7Ymzasxx0JikhkzQqrOYDSAUUulMMmSVLMo6OUyOk8vIVnmQJ9FT4Onv9aIrBT+l1nAUQagow32LjgGUF6CqL1RgQMH3aqohQoAGPgf7z+PATA3tyVwNIMNFoARkAQEBZBUCDpBFBL6ALFE0cS2CwIDrEAQKHoYg8OBxCIICvAyCoAa3gmKzfBAQCIAnEYEG8BQg8AE8/c2vApP8TaIBpADMCYDOA4vBIeGE0YeAMcijIFCkgYzICiXjzV2XTnokEt8Q37QpNb4dIJHP2VQUDITFSKYU35SxLYaeRqwhPAjDZr8xID4PkyBKjtBiKOK6pqZlygfNZWPFWsOXIt2VRZ5G5rr2hKfbte5p0GpVhuyu6F11YW8vg+y7LmBq2RzfPnA09GopR6CD/cm8+1W7cY9AEzlN0Ye0J5mQ7pMdP3Aam/rk+3ZSut07mALg7OeMdW1f9kZmvqUHmj/uCioE6tx/aF04UAGB52mMKdzV9HIa37KyOzS3lim0rt4zV/GatWIsXw2CLv/oKze7Kvzay9nMnr/j0od0Tpwpvj/ANX/dBbmlrNosukcuwmVF3ljJj/StAUGkQ03owWANLln0TjiDABf+8NFO5ETcwyC5EDf6pbdsHxmOJVfybVf1/SjoW0sFNQR6XrSkyGbKEZ65t+XoX18THmlnVerrxYQpNaDMYDBoegB38a7G1j6Ilry/x7X8woNUtN6tjHTQXV4xGPOwb7kvB7sKh/zo0lhq16pEfc3zFO9n3FnOTo7st93ino6ivbnDLZ5Xc8dBGlSIdIt7rKwJVz3FtMfmrhECoeOAG7ivhwsYxEykC5553SASHO8J3cC0hvzRHFNivxBJcMRVxXAcREcxH+FGRVnoL0hzGoeitqzqtG8gWXOxc8z+GJ+8X4qmCo2DMAhWklYtVNM4WuBp4tRr5jElsns45q/Fuf9V/6/ue0i5R7rR+vbWNjQjqw/pzECzdFenomgNAQGvIcnstzMwjyBR7SWpeqWKRNIl18qKV50yI4w5Vwc2r2/RmKJMGovkzyVV3G75aIo0fbQ8ptDHfypMtXM/onUqzK04r8K8in6P6hoyNxfopymzebJXVFtWxaS0VjEr1zecGmPWm3VmrVHbFKTAHYEZUeNV9OYYs9aswx3NTgeq2tWTVO8DH6qSAidrd8b4BiYJD3O7EZJQ9X4hzm9d7ejDqwd2jB29JtpF/jHD6I35A9V8n9WO0YZj+X3WBKvHdsSwZKkfca03aihYjbGrgILuml20yK0N8I4oLk7AeuDfc0pd0qhvv0aejXQxFqmbYvxAV9MPxr82J2ZgrLdQKjev4er6VKpUVOTemQ+MR1OCjVYrvac2onDusOrvhwUe/e+qPk9PYVTfo1arP40N0RZuN8r0MUsdNfdx8OO5UeqsjyZx/bBqmTC3HJD6c2Gd1H3fSnSh6TWjFgaRwhYtCj15kFYtUBigGxyFa9m8LvlbbBNjyR8/VrTsZkbpB5D+ZLSewe6JIlBuBr0sbuVWNeD69eNWNtjGHTvLN53nEdvv2GFwiNNTn72UdWP/xKLajr0r+uY0KD5xtvZCCHUq4uABi8OatSPLrmoJ1a/71BptiFT8clTNCyhwdl3V8StHGqIXfxrQL15ew+lcDGePxU7nEnoVTFpw9bjGoa/L5joNu6aWMoeNCaqc1vxkkqH2CimSn1Vq6i6DM5fV13Vojl+tFvqrnjj5LOHgDCjX+mdMfpqtq/zlV65+lSZn2gU/O1sQNNGV3+QVwj3jQSEwnXcrydQS5fg0sWtXMUNw+7uFiTbhgjIoXVkylSjdfGC6cAgVip8sV2SPatpH12dUE0XO8m2X1cGSO35K+spaRWbH2NZVQwT7gWZHLjB9pXKLkjtYffn2l5de6wkV58wqXhda5uPWOQM6XX06YXnLishc9qhMN6bDmLEdiN1k7FimQ7fSQQ8V2xUPg2qHlRdbu5GCxk2sBNi3X1IAS/7qhCd9mvk2CWtkqGLJMlYrzG8WUd/QjO/1NAit0NQpni6TOIwbiNB/iV+4qcwSLD1ZIXHf9jN9N+08RNZGuyfPClPdk8YzbkxbWWAfK/4WIVpd4bZbFAqvy8dn1tQrrAp9TTfVede85QW5BYb95QTTI98vyTgeppjrBPMZvDc20BTKOVmHWvLql87/7YzXeSLdSV0x/9o0OS+kz2mPGBcdrcW46CnkHW9Fc23d4QBAc5lt/4+L4WSNqbktNJZvvOqnOuFO/rCoMMXrYrF4DA+mTpqY/g2DWTLrxE4tHCqxY4pGa9pernFjrhLywWUxAAQZBPwTJujwcJciYGHTX7AISIGBDyKAgkesktWmggIqmIEDjxwQkALH1ir4oScoCKsEkIxRBAQStgADXxwBCgklGBjaB6BAIN4BB4lQELDYX8+7vPaSvXplHARnGHdgJ0rKCZerqPwX6hIGzlpCpz/kbNOhq9ti/icm5E2skde6F1GgmCJ80NEwBIKFyeEktRFZVk2jtDXrieLBlVcMDcT3OgOjnb41IYkaWlyVPv4L0opgwA3NTvT/IJbZ2YNOrV1A+cnSQs0eStVsTeuJiVLArctIBD5MFAqSQ8CiL+SgiaiZFbIWK43pTi3K6t2LeM/PABDwI0odiZKsqJpumJbtuF6PzsYU0J4HVbuzbKjs8O4cbxoxCRUJNuFxLnSu7q87KRn5FqPdDWlvMOk4nBajupPf0Vsuw+CsUNLdc1MRsUOaTAU52cTW3V3GqbCV3SvNWI3n2wk+OAAA') format('woff2'),
5 | url('iconfont.woff?t=1562140191837') format('woff'),
6 | url('iconfont.ttf?t=1562140191837') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
7 | url('iconfont.svg?t=1562140191837#iconfont') format('svg'); /* iOS 4.1- */
8 | }
9 |
10 | .iconfont {
11 | font-family: "iconfont" !important;
12 | font-size: 16px;
13 | font-style: normal;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | .icon-fenlei:before {
19 | content: "\e60a";
20 | }
21 |
22 | .icon-fenlei1:before {
23 | content: "\e611";
24 | }
25 |
26 | .icon-shouye:before {
27 | content: "\e612";
28 | }
29 |
30 | .icon-commentoutline:before {
31 | content: "\e778";
32 | }
33 |
34 | .icon-suo:before {
35 | content: "\e625";
36 | }
37 |
38 | .icon-shouye1:before {
39 | content: "\e60b";
40 | }
41 |
42 | .icon-user:before {
43 | content: "\e743";
44 | }
45 |
46 | .icon-emiyanzhengma:before {
47 | content: "\e61b";
48 | }
49 |
50 | .icon-user1:before {
51 | content: "\e600";
52 | }
53 |
54 | .icon-User:before {
55 | content: "\e67b";
56 | }
57 |
58 | .icon-lajitong1:before {
59 | content: "\e610";
60 | }
61 |
62 | .icon-yanzhengmatianchong:before {
63 | content: "\e636";
64 | }
65 |
66 | .icon-suo1:before {
67 | content: "\e644";
68 | }
69 |
70 | .icon-securityCode-b:before {
71 | content: "\e60d";
72 | }
73 |
74 | .icon-comment:before {
75 | content: "\e728";
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/react/src/assets/iconfont/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/assets/iconfont/iconfont.eot
--------------------------------------------------------------------------------
/react/src/assets/iconfont/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/assets/iconfont/iconfont.ttf
--------------------------------------------------------------------------------
/react/src/assets/iconfont/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/assets/iconfont/iconfont.woff
--------------------------------------------------------------------------------
/react/src/assets/iconfont/iconfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/assets/iconfont/iconfont.woff2
--------------------------------------------------------------------------------
/react/src/assets/images/antd.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/react/src/assets/images/bg1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/assets/images/bg1.jpg
--------------------------------------------------------------------------------
/react/src/assets/images/bg2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/assets/images/bg2.jpg
--------------------------------------------------------------------------------
/react/src/assets/images/bg3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/assets/images/bg3.jpg
--------------------------------------------------------------------------------
/react/src/assets/images/dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/react/src/assets/images/light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/react/src/components/AnimatedBook/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.less'
3 | import PropTypes from 'prop-types';
4 |
5 | class AnimatedBooks extends React.Component {
6 | static propTypes = {
7 | content: PropTypes.any, //内容
8 | cover: PropTypes.any, //封面
9 | }
10 | static defaultProps = {
11 | content: '',
12 | cover: ''
13 | }
14 |
15 | render() {
16 | const { cover, content, className = '', style = {} } = this.props
17 | return (
18 |
19 |
20 | {/* 封面 */}
21 |
25 | {/* 书页 */}
26 |
27 |
28 | - {content}
29 |
30 |
31 |
32 |
33 | {/* 背面 */}
34 |
38 |
39 |
40 | )
41 | }
42 | }
43 |
44 | export default AnimatedBooks
--------------------------------------------------------------------------------
/react/src/components/AnimatedBook/style.less:
--------------------------------------------------------------------------------
1 | .book-container {
2 | padding-left: 200px;
3 |
4 | .book {
5 | position: relative;
6 | width: 160px;
7 | height: 220px;
8 | perspective: 1000px;
9 | transform-style: preserve-3d;
10 |
11 | &:hover {
12 | .hardcover_front {
13 | transform: rotateY(-145deg) translateZ(0);
14 | z-index: 0;
15 | }
16 |
17 | .page {
18 | &>li:nth-child(1) {
19 | transform: rotateY(-30deg);
20 | transition-duration: 1.5s;
21 | }
22 |
23 | &>li:nth-child(2) {
24 | transform: rotateY(-35deg);
25 | transition-duration: 1.8s;
26 | }
27 |
28 | &>li:nth-child(3) {
29 | transform: rotateY(-118deg);
30 | transition-duration: 1.6s;
31 | }
32 |
33 | &>li:nth-child(4) {
34 | transform: rotateY(-130deg);
35 | transition-duration: 1.4s;
36 | }
37 |
38 | &>li:nth-child(5) {
39 | transform: rotateY(-140deg);
40 | transition-duration: 1.2s;
41 | }
42 | }
43 |
44 | }
45 |
46 | //公共样式
47 | li::before,
48 | li::after {
49 | content: "";
50 | }
51 | ul{
52 | margin-bottom: 0;
53 | }
54 |
55 | .hardcover_front,
56 | .hardcover_back,
57 | .book_spine,
58 | .hardcover_front>li,
59 | .hardcover_back>li,
60 | .book_spine>li,
61 | .page>li {
62 | position: absolute;
63 | top: 0;
64 | left: 0;
65 | width: 100%;
66 | height: 100%;
67 | transform-style: preserve-3d;
68 | }
69 |
70 | .hardcover_front>li:first-child:after,
71 | .hardcover_front>li:first-child:before,
72 | .hardcover_front>li:last-child:after,
73 | .hardcover_front>li:last-child:before,
74 | .hardcover_back>li:first-child:after,
75 | .hardcover_back>li:first-child:before,
76 | .hardcover_back>li:last-child:after,
77 | .hardcover_back>li:last-child:before,
78 | .book_spine>li:first-child:after,
79 | .book_spine>li:first-child:before,
80 | .book_spine>li:last-child:after,
81 | .book_spine>li:last-child:before {
82 | position: absolute;
83 | top: 0;
84 | left: 0;
85 | }
86 | }
87 |
88 | // 封面
89 | .hardcover_front {
90 | transition: all 0.8s ease;
91 | transform-origin: 0% 100%;
92 | transform: rotateY(-34deg) translateZ(8px);
93 | z-index: 100;
94 |
95 | &>li:first-child {
96 | transform: translateZ(2px);
97 | background-color: #eee;
98 |
99 | &::before,
100 | &::after {
101 | width: 3px;
102 | height: 100%;
103 | background: #999;
104 | }
105 |
106 | &::before {
107 | transform: rotateY(90deg) translateZ(-2px) translateX(2px);
108 | }
109 |
110 | &::after {
111 | transform: rotateY(90deg) translateZ(158px) translateX(2px);
112 | }
113 | }
114 |
115 | &>li:last-child {
116 | transform: rotateY(180deg) translateZ(2px);
117 | background: #fffbec;
118 |
119 | &::before {
120 | box-shadow: 0px 0px 30px 5px #333;
121 | transform: rotateX(90deg) rotateZ(90deg) translateZ(-140px) translateX(-2px) translateY(-78px);
122 | width: 4px;
123 | height: 160px;
124 | }
125 | }
126 | }
127 |
128 | // 书页
129 | .page {
130 | position: relative;
131 | width: 100%;
132 | height: 98%;
133 | top: 1%;
134 | left: 3%;
135 | z-index: 10;
136 | transform-style: preserve-3d; //父元素加了这个属性,子元素尽管用position属性设置z-index也不管用
137 |
138 | &>li {
139 | transform-origin: left center;
140 | transition-property: transform;
141 | transition-timing-function: ease;
142 | border-radius: 0px 5px 5px 0px;
143 | background: -webkit-linear-gradient(left, #e1ddd8 0%, #fffbf6 100%);
144 | box-shadow: inset 0px -1px 2px rgba(50, 50, 50, 0.1), inset -1px 0px 1px rgba(150, 150, 150, 0.2);
145 | }
146 |
147 | &>li:nth-child(1) {
148 | transition-duration: 0.6s;
149 | transform: rotateY(-28deg);
150 | }
151 |
152 | &>li:nth-child(2) {
153 | transition-duration: 0.6s;
154 | transform: rotateY(-30deg);
155 | }
156 |
157 | &>li:nth-child(3) {
158 | transition-duration: 0.4s;
159 | transform: rotateY(-32deg);
160 | }
161 |
162 | &>li:nth-child(4) {
163 | transition-duration: 0.5s;
164 | transform: rotateY(-34deg);
165 | }
166 |
167 | &>li:nth-child(5) {
168 | transition-duration: 0.6s;
169 | transform: rotateY(-36deg);
170 | }
171 |
172 | }
173 |
174 | // 背面
175 | .hardcover_back {
176 | transform-origin: 0% 100%;
177 | transform: rotateY(-15deg) translateZ(-8px);
178 |
179 | li:first-child {
180 | transform: translateZ(2px);
181 | background: #fffbec;
182 |
183 | &::before,
184 | &::after {
185 | width: 4px;
186 | height: 100%;
187 | background: #999;
188 | }
189 |
190 | &::before {
191 | transform: rotateY(90deg) translateZ(-2px) translateX(2px);
192 | }
193 |
194 | &::after {
195 | transform: rotateY(90deg) translateZ(158px) translateX(2px);
196 | }
197 | }
198 |
199 | li:last-child {
200 | transform: translateZ(-2px);
201 | background: #fffbec;
202 |
203 | &::before,
204 | &::after {
205 | width: 4px;
206 | height: 160px; //书的宽度
207 | background: #999;
208 | }
209 |
210 | &::before {
211 | transform: rotateX(90deg) rotateZ(90deg) translateZ(-140px) translateX(2px) translateY(-78px);
212 | box-shadow: 10px -1px 80px 20px #666;
213 | }
214 | }
215 | }
216 | }
--------------------------------------------------------------------------------
/react/src/components/Background/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { TweenLite, Circ } from "gsap/all";
4 | import { throttle } from '@/utils/util'
5 | import Loading from '@/components/Loading'
6 |
7 | class Background extends React.Component {
8 | static propTypes = {
9 | url: PropTypes.string
10 | }
11 | static defaultProps = {
12 | url: 'https://github.com/zhangZhiHao1996/image-store/blob/master/react-admin-master/bg1.jpg?raw=true',
13 | }
14 |
15 | constructor(props) {
16 | super(props)
17 | this.points = [] //背景粒子
18 | this.width = window.innerWidth
19 | this.height = window.innerHeight
20 | this.canvas = null
21 | this.ctx = null
22 | this.target = {} //当前鼠标在屏幕的位置
23 |
24 | this.state = {
25 | loading: false //背景图太大,所以加一个loading
26 | }
27 | }
28 | componentDidMount() {
29 | this.setState({
30 | loading: true
31 | })
32 | //当图片载入完成后再显示背景
33 | this.loadImageAsync(this.props.url).then(() => {
34 | this.setState({
35 | loading: false
36 | })
37 | }).then(() => {
38 | this.canvas && this.initPage()
39 | })
40 | }
41 | componentWillUnmount() {
42 | this.destory()
43 | }
44 | /**
45 | * 登录背景图太大,所以加了一个loading效果
46 | */
47 | loadImageAsync = (url) => {
48 | return new Promise((resolve, reject) => {
49 | const image = new Image();
50 | image.onload = function () {
51 | resolve(url);
52 | };
53 | image.onerror = function () {
54 | console.log('图片载入错误')
55 | };
56 | image.src = url;
57 | })
58 | }
59 | /**
60 | * 创建背景粒子
61 | */
62 | _createPoints() {
63 | const { width, height } = this
64 | //创建粒子和粒子的起始位置
65 | for (let x = 0; x < width; x = x + width / 20) {
66 | for (let y = 0; y < height; y = y + height / 20) {
67 | let px = x + Math.random() * width / 20;
68 | let py = y + Math.random() * height / 20;
69 | let p = { x: px, originX: px, y: py, originY: py };
70 | this.points.push(p);
71 | }
72 | }
73 | //给每个粒子添加新属性closest、radius
74 | for (let i = 0; i < this.points.length; i++) {
75 | let closest = [];
76 | let p1 = this.points[i];
77 | for (let j = i + 1; j < this.points.length; j++) {
78 | let p2 = this.points[j]
79 | let placed = false;
80 | for (let k = 0; k < 5; k++) {
81 | if (!placed) {
82 | if (closest[k] === undefined) {
83 | closest[k] = p2;
84 | placed = true;
85 | }
86 | }
87 | }
88 | for (let k = 0; k < 5; k++) {
89 | if (!placed) {
90 | if (this._getDistance(p1, p2) < this._getDistance(p1, closest[k])) {
91 | closest[k] = p2;
92 | placed = true;
93 | }
94 | }
95 | }
96 | }
97 | p1.closest = closest;
98 | p1.radius = 2 + Math.random() * 2
99 | //给粒子添加抖动
100 | this._shakePoint(p1);
101 | }
102 | }
103 | /**
104 | * 粒子抖动
105 | * @param {object} point
106 | */
107 | _shakePoint(point) {
108 | TweenLite.to(point, 1 + 1 * Math.random(), {
109 | x: point.originX - 50 + Math.random() * 100,
110 | y: point.originY - 50 + Math.random() * 100, ease: Circ.easeInOut,
111 | onComplete: () => {
112 | this._shakePoint(point);
113 | }
114 | });
115 | }
116 | /**
117 | * 绘制单个粒子
118 | * @param {*} point
119 | * @param {*} ctx
120 | */
121 | _drawPoint(point, ctx) {
122 | if (!point.pointActive) return;
123 | ctx.beginPath();
124 | ctx.arc(point.x, point.y, point.radius, 0, 2 * Math.PI, false);
125 | ctx.fillStyle = 'rgba(156,217,249,' + point.pointActive + ')';
126 | ctx.fill();
127 | }
128 | /**
129 | * 绘制线条
130 | */
131 | _drawLines(point, ctx) {
132 | if (!point.lineActive) return;
133 | for (let item of point.closest) {
134 | ctx.beginPath();
135 | ctx.moveTo(point.x, point.y);
136 | ctx.lineTo(item.x, item.y);
137 | ctx.strokeStyle = 'rgba(156,217,249,' + point.lineActive + ')';
138 | ctx.stroke();
139 | }
140 | }
141 | /**
142 | * 获取两个粒子之间的距离
143 | * @param {object} p1
144 | * @param {object} p2
145 | */
146 | _getDistance(p1, p2) {
147 | return Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2);
148 | }
149 | /**
150 | * 鼠标移动事件处理
151 | * @param {*} e
152 | */
153 | handleMouseMove = (e) => {
154 | let posx = 0, posy = 0;
155 | if (e.pageX || e.pageY) {
156 | posx = e.pageX;
157 | posy = e.pageY;
158 | }
159 | else if (e.clientX || e.clientY) {
160 | posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
161 | posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
162 | }
163 | this.target.x = posx;
164 | this.target.y = posy;
165 | }
166 | /**
167 | * 开始函数
168 | */
169 | start = () => {
170 | const { width, height, points, ctx, target } = this
171 | ctx.clearRect(0, 0, width, height);
172 | for (let point of points) {
173 | if (Math.abs(this._getDistance(target, point)) < 4000) {
174 | point.lineActive = 0.3;
175 | point.pointActive = 0.6;
176 | } else if (Math.abs(this._getDistance(target, point)) < 20000) {
177 | point.lineActive = 0.1;
178 | point.pointActive = 0.3;
179 | } else if (Math.abs(this._getDistance(target, point)) < 40000) {
180 | point.lineActive = 0.02;
181 | point.pointActive = 0.1;
182 | } else {
183 | point.lineActive = 0;
184 | point.pointActive = 0;
185 | }
186 | this._drawLines(point, ctx)
187 | this._drawPoint(point, ctx);
188 | }
189 | this.myReq = window.requestAnimationFrame(() => this.start());
190 | }
191 | initPage = () => {
192 | this.ctx = this.canvas.getContext('2d')
193 | this._createPoints()
194 | this.start()
195 | window.onmousemove = throttle(this.handleMouseMove, 50) //函数节流优化
196 | }
197 | destory = () => {
198 | window.cancelAnimationFrame(this.myReq)
199 | window.onmousemove = null
200 | }
201 |
202 | render() {
203 | const { url } = this.props
204 | const { loading } = this.state
205 | return (
206 |
207 | {
208 | loading ? (
209 |
210 |
211 |
212 | ) : (
213 |
214 |
221 | )
222 | }
223 |
224 | )
225 | }
226 | }
227 |
228 | const styles = {
229 | backgroundBox: {
230 | position: 'fixed',
231 | top: '0',
232 | left: '0',
233 | width: '100vw',
234 | height: '100vh',
235 | backgroundSize: '100% 100%',
236 | },
237 | canvasStyle: {
238 | display: 'block', //防止全屏的canvas出现滚动条
239 | position: 'fixed',
240 | top: '0',
241 | left: '0',
242 | }
243 | }
244 |
245 | export default Background
--------------------------------------------------------------------------------
/react/src/components/ColorPicker/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { SketchPicker } from 'react-color'
4 |
5 | class ColorPicker extends React.Component {
6 | static propTypes = {
7 | color: PropTypes.string,
8 | onChange: PropTypes.func,
9 | presetColors: PropTypes.array,
10 | disableAlpha:PropTypes.bool,
11 | }
12 | static defaultProps = {
13 | color: '',
14 | onChange: () => { },
15 | presetColors: [
16 | '#F5222D',
17 | '#FA541C',
18 | '#FA8C16',
19 | '#FAAD14',
20 | '#FADB14',
21 | '#A0D911',
22 | '#52C41A',
23 | '#13C2C2',
24 | '#1890FF',
25 | '#2F54EB',
26 | '#722ED1',
27 | '#EB2F96',
28 | ],
29 | disableAlpha:true //禁用rgba
30 | }
31 | static getDerivedStateFromProps(nextProps, prevState) {
32 | if (nextProps.color && nextProps.color !== prevState.color) {
33 | return {
34 | color: nextProps.color
35 | }
36 | }
37 | return null
38 | }
39 | state = {
40 | displayColorPicker: false,
41 | color: ''
42 | };
43 |
44 | handleClick = () => {
45 | this.setState({ displayColorPicker: !this.state.displayColorPicker })
46 | };
47 |
48 | handleClose = () => {
49 | this.setState({ displayColorPicker: false })
50 | };
51 |
52 | handleChange = (color) => {
53 | this.props.onChange(color.hex)
54 | this.setState({ color: color.hex })
55 | };
56 |
57 | render() {
58 | const styles = {
59 | color: {
60 | width: '36px',
61 | height: '14px',
62 | borderRadius: '2px',
63 | background: this.state.color,
64 | },
65 | swatch: {
66 | padding: '5px',
67 | background: '#fff',
68 | borderRadius: '1px',
69 | boxShadow: '0 0 0 1px rgba(0,0,0,.1)',
70 | display: 'inline-block',
71 | cursor: 'pointer',
72 | },
73 | popover: {
74 | position: 'absolute',
75 | zIndex: '2',
76 | },
77 | cover: {
78 | position: 'fixed',
79 | top: '0',
80 | right: '0',
81 | bottom: '0',
82 | left: '0',
83 | },
84 | };
85 |
86 | return (
87 |
88 |
91 | {this.state.displayColorPicker ?
: null}
95 |
96 | )
97 | }
98 | }
99 |
100 | export default ColorPicker
--------------------------------------------------------------------------------
/react/src/components/Loading/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.css'
3 |
4 | //此效果来源于https://codepen.io/MarioDesigns/pen/LLrVLK
5 |
6 | class Loading extends React.Component {
7 | render() {
8 | const { className = '', style = {} } = this.props
9 | return (
10 |
14 | )
15 | }
16 | }
17 |
18 | export default Loading
--------------------------------------------------------------------------------
/react/src/components/MyIcon/index.js:
--------------------------------------------------------------------------------
1 | import { Icon } from 'antd'
2 |
3 | // 刚开始我还没发现antd的Icon可以自定义图标,后来发现可以这样使用,非常方便,使用方法和Icon一致,svg也是未来图标的主流
4 | //
5 |
6 | const MyIcon = Icon.createFromIconfontCN({
7 | scriptUrl: '//at.alicdn.com/t/font_731989_0s6wteco74wa.js', // 在 iconfont.cn 上生成
8 | });
9 |
10 | export default MyIcon
--------------------------------------------------------------------------------
/react/src/components/Phone/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from 'react'
2 | import PropTypes from 'prop-types';
3 | import './index.less'
4 |
5 | function Phone(props) {
6 | const [time, setTime] = useState(handleTime)
7 | const timer = useRef()
8 |
9 | useEffect(() => {
10 | timer.current = setInterval(() => {
11 | setTime(handleTime)
12 | }, 1000);
13 | return () => {
14 | clearInterval(timer.current)
15 | }
16 | })
17 |
18 | function handleTime() {
19 | const date = new Date()
20 | const hour = String(date.getHours())
21 | const minute = String(date.getMinutes())
22 | return `${hour.padStart(2, 0)}:${minute.padStart(2, 0)}`
23 | }
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | T-Mobile
36 |
37 |
{time}
38 |
39 |
40 |
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | Phone.propTypes = {
53 | title: PropTypes.string,
54 | src: PropTypes.string
55 | }
56 |
57 | Phone.defaultProps = {
58 | title: 'UI',
59 | src: ''
60 | }
61 |
62 |
63 | export default Phone
64 |
--------------------------------------------------------------------------------
/react/src/components/Phone/index.less:
--------------------------------------------------------------------------------
1 | /* phone */
2 | #phone {
3 | position: relative;
4 | width: 310px;
5 | // height: 640px;
6 | height: 600px;
7 | border: 2px solid #ccc;
8 | border-radius: 30px;
9 | background: #fff;
10 | color: #595959;
11 |
12 | .screen {
13 | position: absolute;
14 | width: 290px;
15 | // height: 520px;
16 | height: 480px;
17 | border: 1px solid #ccc;
18 | left: 50%;
19 | top: 50%;
20 | transform: translate3d(-50%, -50%, 0);
21 | box-sizing: border-box;
22 | overflow: hidden;
23 |
24 | .status-bar {
25 | text-align: center;
26 | padding: 5px 10px 5px;
27 | font-size: 12px;
28 | line-height: 1;
29 |
30 | .signal {
31 | position: absolute;
32 | left: 10px;
33 | top: 5px;
34 |
35 | span {
36 | width: 7px;
37 | height: 7px;
38 | border: 1px solid #595959;
39 | display: inline-block;
40 | margin: 0 2px 0 0;
41 | border-radius: 50%;
42 | box-sizing: border-box;
43 | }
44 |
45 | .full {
46 | background-color: #595959;
47 | }
48 | }
49 |
50 |
51 |
52 | .battery {
53 | position: absolute;
54 | right: 10px;
55 | top: 6px;
56 | width: 22px;
57 | height: 10px;
58 | border: 1px solid #000;
59 | display: inline-block;
60 | border-radius: 2px;
61 | background-clip: padding-box;
62 | box-sizing: border-box;
63 |
64 | &::before {
65 | content: '';
66 | position: absolute;
67 | right: -3px;
68 | width: 3px;
69 | height: 4px;
70 | top: 2px;
71 | background: #000;
72 | display: inline-block;
73 | border-radius: 0 1px 1px 0;
74 | }
75 |
76 | &::after {
77 | content: '';
78 | position: absolute;
79 | top: 0;
80 | left: 0;
81 | width: 7px;
82 | height: 8px;
83 | background: #000;
84 | }
85 | }
86 | }
87 | }
88 |
89 | .home-btn {
90 | width: 36px;
91 | height: 36px;
92 | border: 1px solid #ccc;
93 | position: absolute;
94 | bottom: 10px;
95 | left: 50%;
96 | margin: 0 -18px;
97 | border-radius: 50%;
98 | }
99 |
100 | .speaker {
101 | width: 50px;
102 | height: 6px;
103 | border: 1px solid #ccc;
104 | border-radius: 6px;
105 | position: absolute;
106 | left: 50%;
107 | top: 25px;
108 | margin: 0 -25px;
109 | }
110 | }
--------------------------------------------------------------------------------
/react/src/components/PrivateRoute/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route, Redirect, } from 'react-router-dom'
3 | import { isAuthenticated } from '@/utils/session'
4 |
5 | const PrivateRoute = ({ component: Component, ...rest }) => (
6 | (
7 | !!isAuthenticated()
8 | ?
9 | :
13 | )} />
14 | )
15 |
16 | export default PrivateRoute
--------------------------------------------------------------------------------
/react/src/components/PromptBox/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class PromptBox extends React.Component {
5 | static propTypes = {
6 | info: PropTypes.string
7 | }
8 | static defaultProps = {
9 | info: ''
10 | }
11 | componentDidUpdate(prevProps) {
12 | if (this.props.info && this.props.info !== prevProps.info) {
13 | this._renderContent()
14 | }
15 | }
16 | _renderContent = () => {
17 | const ctx = this.canvas.getContext('2d')
18 | const width = this._calcWidth()
19 | ctx.strokeStyle = '#fff'
20 | ctx.shadowOffsetX = -2
21 | ctx.shadowOffsetY = 2
22 | ctx.shadowBlur = 2
23 | ctx.shadowColor = 'rgba(0,0,0,.3)'
24 | ctx.beginPath()
25 | ctx.moveTo(0, 20)
26 | ctx.lineTo(8, 16)
27 | ctx.lineTo(8, 1)
28 | ctx.lineTo(width - 1, 1)
29 | ctx.lineTo(width - 1, 39)
30 | ctx.lineTo(8, 39)
31 | ctx.lineTo(8, 23)
32 | ctx.closePath()
33 | ctx.stroke();
34 | ctx.fillStyle = '#D3D7F7'
35 | ctx.textBaseline = 'middle'
36 | ctx.font = '14px sans-serif'
37 | ctx.beginPath()
38 | ctx.fillText(this.props.info, 20, 20);
39 | }
40 | _calcWidth = () => {
41 | return 30 + this.props.info.length * 15
42 | }
43 | render() {
44 | const { className = '', style = {} } = this.props
45 | return (
46 |
47 |
49 | )
50 | }
51 | }
52 |
53 | export default PromptBox
--------------------------------------------------------------------------------
/react/src/components/Typing/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | // 如何将react节点转换为dom,比如拥有className、style、onClick的react元素,我们生成的dom如何保留这些。
5 | // 本来想直接用React.createElement来代替document.createElement。但是ReactDOM.render()方法插入的位置节点必须是dom所以此方法不行
6 | function isObject(obj){
7 | return Object.prototype.toString.call(obj) === '[object Object]'
8 | }
9 |
10 | class Typing extends React.Component {
11 | static propTypes = {
12 | delay: PropTypes.number, //设置打印延时,单位为毫秒
13 | frequency: PropTypes.number, //设置打印频率
14 | done: PropTypes.func //打印结束的函数
15 | }
16 | static defaultProps = {
17 | delay: 0,
18 | frequency: 30,
19 | done: () => { }
20 | }
21 |
22 | componentDidMount() {
23 | this.chain = { //此变量就是将要打印的对象
24 | parent: null,
25 | dom: this.wrapper,
26 | val: []
27 | };
28 | this.chain.val = this._convert(this.props.children, this.chain.val)
29 | setTimeout(() => {
30 | this._play(this.chain)
31 | }, this.props.delay)
32 | }
33 | /**
34 | * children转换为符合打印的数组
35 | * @param {*} children Object、Array、String、undefined、Null
36 | * @param {array} arr 保存打印的数组
37 | */
38 | _convert(children, arr = []) {
39 | let list = arr.slice()
40 | if(Array.isArray(children)){
41 | for(let item of children){
42 | list = list.concat(this._convert(item))
43 | }
44 | }
45 | if(isObject(children)){
46 | const dom = this._createDom({
47 | ...children.props,
48 | type:children.type
49 | })
50 | const val = this._convert(children.props.children,[])
51 | list.push({
52 | dom,
53 | val
54 | })
55 | }
56 | if(typeof children === 'string'){
57 | list = list.concat(children.split(''))
58 | }
59 | return list
60 | }
61 | /**
62 | * 打印字符
63 | * @param {*} dom 父节点
64 | * @param {*} val 打印内容
65 | * @param {*} callback 打印完成的回调
66 | */
67 | _print(dom, val, callback) {
68 | setTimeout(function () {
69 | dom.appendChild(document.createTextNode(val));
70 | callback();
71 | }, this.props.frequency);
72 | }
73 | /**
74 | * 打印节点
75 | * @param {*} node
76 | */
77 | _play = (node) => {
78 | //当打印最后一个字符时,动画完毕,执行done
79 | if (!node.val.length) {
80 | if (node.parent) this._play(node.parent);
81 | else this.props.done();
82 | return;
83 | }
84 | let current = node.val.shift() //获取第一个元素,并从打印列表中删除
85 | if (typeof current === 'string') {
86 | this._print(node.dom, current, () => {
87 | this._play(node)
88 | })
89 | } else {
90 | let dom = current.dom
91 | node.dom.appendChild(dom)
92 | this._play({
93 | parent: node,
94 | dom,
95 | val: current.val
96 | })
97 | }
98 | }
99 | /**
100 | * 根据信息生成dom节点
101 | * @param {object} info
102 | */
103 | _createDom(info) {
104 | info = { ...info }
105 | let dom = document.createElement(info.type)
106 |
107 | delete info.children
108 |
109 | for (let [key, value] of Object.entries(info)) {
110 | if (key === 'className') {
111 | key = 'class'
112 | }
113 | dom.setAttribute(key, value)
114 | }
115 | if (info.style) {
116 | let cssText = ''
117 | for (let [key, value] of Object.entries(info.style)) {
118 | cssText += `${key}:${value};`
119 | }
120 | dom.style.cssText = cssText
121 | }
122 |
123 | return dom
124 | }
125 |
126 | render() {
127 | const { className = '', style = {} } = this.props
128 | return (
129 | this.wrapper = el} className={className} style={style}>
130 |
131 |
132 | )
133 | }
134 | }
135 |
136 | export default Typing
137 |
--------------------------------------------------------------------------------
/react/src/config/secret.js:
--------------------------------------------------------------------------------
1 |
2 | //前台加密主要是为了防止post请求明文传递密码
3 |
4 |
5 | //使用例子
6 | // var CryptoJS = require("crypto-js");
7 |
8 | // var data = [{id: 1}, {id: 2}]
9 |
10 | // // Encrypt
11 | // var ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), 'secret key 123').toString();
12 |
13 | // // Decrypt
14 | // var bytes = CryptoJS.AES.decrypt(ciphertext, 'secret key 123');
15 | // var decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
16 |
17 | // console.log(decryptedData); // [{id: 1}, {id: 2}]
18 |
19 | export const SECRETKEY = 'front_666666'
--------------------------------------------------------------------------------
/react/src/index.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html, body, div, span, applet, object, iframe,
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 | a, abbr, acronym, address, big, cite, code,
9 | del, dfn, em, img, ins, kbd, q, s, samp,
10 | small, strike, strong, sub, sup, tt, var,
11 | b, u, i, center,
12 | dl, dt, dd, ol, ul, li,
13 | fieldset, form, label, legend,
14 | table, caption, tbody, tfoot, thead, tr, th, td,
15 | article, aside, canvas, details, embed,
16 | figure, figcaption, footer, header, hgroup,
17 | menu, nav, output, ruby, section, summary,
18 | time, mark, audio, video {
19 | margin: 0;
20 | padding: 0;
21 | border: 0;
22 | font-size: 100%;
23 | font: inherit;
24 | vertical-align: baseline;
25 | }
26 | /* HTML5 display-role reset for older browsers */
27 | article, aside, details, figcaption, figure,
28 | footer, header, hgroup, menu, nav, section {
29 | display: block;
30 | }
31 | body {
32 | line-height: 1;
33 | }
34 | ol, ul {
35 | list-style: none;
36 | }
37 | blockquote, q {
38 | quotes: none;
39 | }
40 | blockquote:before, blockquote:after,
41 | q:before, q:after {
42 | content: '';
43 | content: none;
44 | }
45 | table {
46 | border-collapse: collapse;
47 | border-spacing: 0;
48 | }
49 |
--------------------------------------------------------------------------------
/react/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 | import { Router } from 'react-router-dom'
7 | import { LocaleProvider } from 'antd'
8 | import zh_CN from 'antd/lib/locale-provider/zh_CN';
9 | import moment from 'moment';
10 | import 'moment/locale/zh-cn';
11 | import history from './utils/history'
12 | import { Provider } from 'react-redux'
13 | import store from './store'
14 |
15 | moment.locale('zh-cn');
16 |
17 | ReactDOM.render(
18 |
19 |
20 |
21 |
22 |
23 |
24 | ,
25 | document.getElementById('root'));
26 |
27 | // If you want your app to work offline and load faster, you can change
28 | // unregister() to register() below. Note this comes with some pitfalls.
29 | // Learn more about service workers: https://bit.ly/CRA-PWA
30 | serviceWorker.unregister();
31 |
--------------------------------------------------------------------------------
/react/src/pages/About/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import {Card} from 'antd'
3 | import Typing from '../../components/Typing/index'
4 |
5 | class About extends Component {
6 | state = { }
7 | render() {
8 | return (
9 |
10 |
11 |
12 | 关于
13 | 这个人很懒,什么也没留下...
14 |
15 |
16 |
17 | );
18 | }
19 | }
20 |
21 | export default About;
--------------------------------------------------------------------------------
/react/src/pages/ButtonDemo/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Button, Card, Row, Col, Menu, Dropdown, Icon } from 'antd'
3 | import Typing from '../../components/Typing/index'
4 |
5 |
6 | class ButtonDemo extends Component {
7 | state = {}
8 | render() {
9 | const menu = (
10 |
15 | );
16 | return (
17 |
18 |
19 |
20 | 何时使用
21 | 标记了一个(或封装一组)操作命令,响应用户点击行为,触发相应的业务逻辑。
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
75 |
76 |
77 |
78 |
79 |
80 | );
81 | }
82 | }
83 |
84 | export default ButtonDemo;
--------------------------------------------------------------------------------
/react/src/pages/Chat/imgs/administrator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/pages/Chat/imgs/administrator.png
--------------------------------------------------------------------------------
/react/src/pages/Chat/imgs/header1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/pages/Chat/imgs/header1.png
--------------------------------------------------------------------------------
/react/src/pages/Chat/imgs/header2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/pages/Chat/imgs/header2.png
--------------------------------------------------------------------------------
/react/src/pages/Chat/imgs/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/pages/Chat/imgs/react.png
--------------------------------------------------------------------------------
/react/src/pages/Chat/imgs/zanwu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/pages/Chat/imgs/zanwu.png
--------------------------------------------------------------------------------
/react/src/pages/Chat/style.less:
--------------------------------------------------------------------------------
1 | .chat-container {
2 | .chat-box {
3 | position: absolute;
4 | top: 20px;
5 | left: 25%;
6 | display: flex;
7 | flex-direction: column;
8 | width: 780px;
9 | height: 520px;
10 | border-radius: 5px;
11 | border: 1px solid #d1d1d1;
12 | background: #fff;
13 | box-shadow: 0 15px 40px rgba(0, 0, 0, .2);
14 |
15 | .chat-header {
16 | position: relative;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | width: 100%;
21 | height: 60px;
22 | background: #e6e6e6;
23 | flex-shrink: 0;
24 |
25 | .header-left {
26 | position: absolute;
27 | left: 0;
28 | height: 80%;
29 |
30 | img {
31 | height: 100%;
32 | }
33 | }
34 |
35 | .header-center {
36 | height: 80%;
37 |
38 | img {
39 | height: 100%;
40 | }
41 | }
42 |
43 | .header-right {
44 | position: absolute;
45 | right: 30px;
46 | }
47 | }
48 |
49 | .chat-body {
50 | display: flex;
51 | flex-grow: 1;
52 | display: flex;
53 |
54 | .left {
55 | flex-shrink: 0;
56 | width: 200px;
57 | height: 100%;
58 | border-right: 1px solid #f3f3f3;
59 | }
60 |
61 | .main {
62 | flex-grow: 1;
63 | height: 100%;
64 |
65 | .chat-list {
66 | height: 328px;
67 | overflow: auto;
68 | }
69 |
70 | .chat-editor-wrapper {
71 | height: 130px;
72 | flex-shrink: 0;
73 | border-top: 1px solid #f3f3f3;
74 | }
75 | }
76 |
77 | .right {
78 | display: flex;
79 | flex-direction: column;
80 | flex-shrink: 0;
81 | width: 135px;
82 | height: 100%;
83 | background: #f6f7f9;
84 | border-left: 1px solid #f3f3f3;
85 |
86 | .member {
87 | padding: 8px 10px 3px;
88 | }
89 |
90 | .user-item {
91 | display: flex;
92 | align-items: center;
93 | color: #787677;
94 | font-size: 12px;
95 | padding: 5px 10px;
96 |
97 | .avatar-box {
98 | position: relative;
99 | flex-shrink: 0;
100 | width: 24px;
101 | height: 24px;
102 | border-radius: 50%;
103 | overflow: hidden;
104 |
105 | &.mask {
106 | img {
107 | opacity: .7;
108 | }
109 |
110 | div {
111 | position: absolute;
112 | top: 0;
113 | left: 0;
114 | width: 100%;
115 | height: 100%;
116 | background: rgba(0, 0, 0, 0.2);
117 | }
118 | }
119 | }
120 | }
121 | }
122 | }
123 | }
124 | }
125 |
126 |
127 | //避免css嵌套太深就从上面的提出来了,css没有模块化,所以要注意此样式会影响其他页面相同样式名的样式
128 |
129 |
130 | .left-item {
131 | display: flex;
132 | align-items: center;
133 | padding: 10px;
134 |
135 | &:hover {
136 | background: #f3f3f3;
137 | }
138 |
139 | .left-item-text {
140 | flex-grow: 1;
141 | overflow: hidden;
142 | margin-left: 10px;
143 |
144 | .group-name {
145 | display: flex;
146 | justify-content: space-between;
147 | align-items: center;
148 |
149 | span:first-child {
150 | color: rgba(0, 0, 0, 0.75);
151 | font-size: 15px;
152 | line-height: 1.5;
153 | }
154 |
155 | span:last-child {
156 | font-size: 12px;
157 | color: #abaaab;
158 | }
159 | }
160 |
161 | .group-message {
162 | display: flex;
163 | color: #777777;
164 | font-size: 13px;
165 | height: 21px;
166 | overflow: hidden;
167 |
168 | &:last-child {
169 | flex-grow: 1;
170 | overflow: hidden;
171 | }
172 |
173 | p {
174 | margin-bottom: 0;
175 | overflow: hidden;
176 | text-overflow: ellipsis;
177 | white-space: nowrap;
178 | }
179 | }
180 | }
181 | }
182 |
183 | .chat-item {
184 | padding: 15px;
185 |
186 | .time {
187 | margin: 10px 0;
188 | text-align: center;
189 | font-size: 12px;
190 | color: #d0cfcf;
191 | }
192 |
193 | .chat-item-info {
194 | display: flex;
195 | justify-content: flex-start;
196 |
197 | &.chat-right {
198 | flex-direction: row-reverse;
199 |
200 | .chat-main {
201 | text-align: right;
202 | margin-right: 12px;
203 |
204 | .username {
205 | color: #9f9f9f;
206 | }
207 |
208 | .chat-content {
209 | background: #d9f4fe;
210 | text-align: left;
211 |
212 | &::before{
213 | display: none;
214 | }
215 |
216 | &::after {
217 | content: '';
218 | position: absolute;
219 | top: 8px;
220 | left: 100%;
221 | width: 0;
222 | height: 0;
223 | border-top: 3px solid transparent;
224 | border-bottom: 6px solid transparent;
225 | border-left: 6px solid #d9f4fe;
226 | }
227 | }
228 | }
229 | }
230 |
231 | .chat-main {
232 | flex-grow: 1;
233 | margin-left: 12px;
234 |
235 | .username {
236 | color: #a0abca;
237 | font-size: 14px;
238 | }
239 |
240 | .chat-content {
241 | position: relative;
242 | display: inline-block;
243 | max-width: 80%;
244 | padding: 12px;
245 | border-radius: 5px;
246 | background: #f3f3f3;
247 | word-wrap: break-word;
248 | word-break: break-all;
249 |
250 | &::before {
251 | content: '';
252 | position: absolute;
253 | top: 8px;
254 | right: 100%;
255 | width: 0;
256 | height: 0;
257 | border-top: 3px solid transparent;
258 | border-bottom: 6px solid transparent;
259 | border-right: 6px solid #f3f3f3;
260 | }
261 |
262 | img {
263 | max-width: 100% !important;
264 | height: 200px !important;
265 | object-fit: contain;
266 | }
267 |
268 | p {
269 | margin-bottom: 0;
270 | }
271 | }
272 | }
273 | }
274 | }
--------------------------------------------------------------------------------
/react/src/pages/Collection/CreateModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Form, Input, Modal, Icon, } from 'antd'
3 | import { json } from '../../utils/ajax'
4 | import { connect } from 'react-redux'
5 |
6 | const { TextArea } = Input;
7 |
8 | const store = connect(
9 | (state) => ({ user: state.user })
10 | )
11 |
12 | @store @Form.create()
13 | class CreateModal extends Component {
14 | state = {}
15 | onCancel = () => {
16 | this.props.form.resetFields()
17 | this.props.toggleVisible(false)
18 | }
19 | onOk = () => {
20 | this.props.form.validateFieldsAndScroll((errors, values) => {
21 | if (!errors) {
22 | this.onCreate(values)
23 | }
24 | })
25 | }
26 | onCreate = async (values) => {
27 | const res = await json.post('/works/create', values)
28 | if (res.status === 0) {
29 | this.props.onCreated() //更新外面的数据
30 | this.onCancel()
31 | }
32 | }
33 | render() {
34 | const { getFieldDecorator } = this.props.form
35 | const formItemLayout = {
36 | labelCol: { span: 6 },
37 | wrapperCol: { span: 14 },
38 | }
39 | return (
40 |
48 |
50 | {getFieldDecorator('title', {
51 | rules: [
52 | { required: true, message: '请输入项目名称' },
53 | ]
54 | })(
55 |
56 | )}
57 |
58 |
59 | {getFieldDecorator('description', {
60 | rules: [
61 | { required: true, message: '请输入项目描述' },
62 | ]
63 | })(
64 |
65 | )}
66 |
67 |
68 | {getFieldDecorator('url', {
69 | rules: [
70 | { required: true, message: '请输入项目预览地址' },
71 | { type: 'url', message: '请输入正确的网址' }
72 | ]
73 | })(
74 |
75 | )}
76 |
77 | 地址}>
78 | {getFieldDecorator('githubUrl', {
79 | rules: [
80 | { required: true, message: '请输入项目github地址' },
81 | { type: 'url', message: '请输入正确的网址' }
82 | ]
83 | })(
84 |
85 | )}
86 |
87 |
88 |
89 |
90 | );
91 | }
92 | }
93 |
94 | export default CreateModal;
--------------------------------------------------------------------------------
/react/src/pages/Collection/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import AnimatedBook from '../../components/AnimatedBook'
3 | import { Card, Icon, Button, Empty, Modal, Checkbox, message, Spin } from 'antd'
4 | import './style.less'
5 | import { connect } from 'react-redux'
6 | import CreateModal from './CreateModal'
7 | import { json } from '../../utils/ajax'
8 |
9 |
10 | const store = connect(
11 | (state) => ({ user: state.user })
12 | )
13 |
14 | @store
15 | class Collection extends Component {
16 | state = {
17 | collections: [], //作品列表
18 | isShowCreateModal: false,
19 | loading: false
20 | }
21 | componentDidMount() {
22 | this.getCollections()
23 | }
24 | /**
25 | * 获得作品集数据
26 | */
27 | getCollections = async () => {
28 | this.setState({
29 | loading: true
30 | })
31 | const res = await json.get('/works/list')
32 | this.setState({
33 | collections: res.data || [],
34 | loading: false
35 | })
36 | }
37 | /**
38 | * 打开/关闭创建模态框
39 | */
40 | toggleShowCreateModal = (visible) => {
41 | this.setState({
42 | isShowCreateModal: visible
43 | })
44 | }
45 | openCreateModal = () => {
46 | this.toggleShowCreateModal(true)
47 | }
48 | openDeleteModal = () => {
49 | let ids = []
50 | Modal.confirm({
51 | title: '请在下面勾选要删除的项目(仅管理员)',
52 | content: (
53 |
54 |
ids = values}>
55 | {
56 | this.state.collections.map(item => (
57 | {item.title}
58 | ))
59 | }
60 |
61 |
62 |
63 | ),
64 | maskClosable: true,
65 | okButtonProps: {
66 | disabled: !this.props.user.isAdmin
67 | },
68 | onOk: async () => {
69 | const res = await json.post('/works/delete', { ids })
70 | if (res.status === 0) {
71 | message.success('删除成功')
72 | this.getCollections()
73 | }
74 | }
75 | })
76 | }
77 |
78 |
79 | render() {
80 | const { collections, isShowCreateModal, loading } = this.state
81 | const colors = ['#f3b47e', '#83d3d3', '#8bc2e8', '#a3c7a3']
82 | return (
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | {collections && collections.map((item, index) => (
92 |
96 | {item.title}
97 | {item.description}
98 |
99 | )}
100 | content={(
101 |
107 | )}
108 | style={{ marginBottom: 100 }} />
109 | ))}
110 |
111 | {
112 | !collections.length &&
113 | }
114 |
115 |
119 |
120 |
121 | );
122 | }
123 | }
124 |
125 | const styles = {
126 | box: {
127 | display: 'flex',
128 | width: '100%',
129 | flexWrap: 'wrap',
130 | }
131 | }
132 |
133 |
134 | export default Collection;
--------------------------------------------------------------------------------
/react/src/pages/Collection/style.less:
--------------------------------------------------------------------------------
1 | .cover-box {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | width: 100%;
7 | height: 100%;
8 | background: #6fb6e5;
9 | color: #fff;
10 | padding: 0 10px;
11 | font-size: 12px;
12 | text-align: center;
13 |
14 | .title {
15 | width: 100%;
16 | color: #fff;
17 | font-size: 16px;
18 | margin-bottom: 0;
19 | }
20 |
21 | p {
22 | width: 100%;
23 | }
24 | }
25 |
26 | .content-box {
27 | display: flex;
28 | justify-content: center;
29 | align-items: center;
30 | width: 100%;
31 | height: 100%;
32 |
33 | .btn {
34 | display: inline-block;
35 | border: 2px solid #2c3e50;
36 | color: #2c3e50;
37 | padding: 5px;
38 | cursor: pointer;
39 | transition: all .3s;
40 |
41 | a {
42 | color: #2c3e50;
43 | }
44 |
45 | &:hover {
46 | a {
47 | color: var(--primaryColor);
48 | }
49 | border: 2px solid var(--primaryColor);
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/react/src/pages/IconDemo/icons.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export const categories = {
4 | direction: {
5 | title: '方向性图标',
6 | list: [
7 | 'step-backward',
8 | 'step-forward',
9 | 'fast-backward',
10 | 'fast-forward',
11 | 'shrink',
12 | 'arrows-alt',
13 | 'down',
14 | 'up',
15 | 'left',
16 | 'right',
17 | 'caret-up',
18 | 'caret-down',
19 | 'caret-left',
20 | 'caret-right',
21 | 'up-circle',
22 | 'down-circle',
23 | 'left-circle',
24 | 'right-circle',
25 | 'double-right',
26 | 'double-left',
27 | 'vertical-left',
28 | 'vertical-right',
29 | 'vertical-align-top',
30 | 'vertical-align-middle',
31 | 'vertical-align-bottom',
32 | 'forward',
33 | 'backward',
34 | 'rollback',
35 | 'enter',
36 | 'retweet',
37 | 'swap',
38 | 'swap-left',
39 | 'swap-right',
40 | 'arrow-up',
41 | 'arrow-down',
42 | 'arrow-left',
43 | 'arrow-right',
44 | 'play-circle',
45 | 'up-square',
46 | 'down-square',
47 | 'left-square',
48 | 'right-square',
49 | 'login',
50 | 'logout',
51 | 'menu-fold',
52 | 'menu-unfold',
53 | 'border-bottom',
54 | 'border-horizontal',
55 | 'border-inner',
56 | 'border-outer',
57 | 'border-left',
58 | 'border-right',
59 | 'border-top',
60 | 'border-verticle',
61 | 'pic-center',
62 | 'pic-left',
63 | 'pic-right',
64 | 'radius-bottomleft',
65 | 'radius-bottomright',
66 | 'radius-upleft',
67 | 'radius-upright',
68 | 'fullscreen',
69 | 'fullscreen-exit',
70 | ]
71 | },
72 | suggestion: {
73 | title: '提示建议性图标',
74 | list: [
75 | 'question',
76 | 'question-circle',
77 | 'plus',
78 | 'plus-circle',
79 | 'pause',
80 | 'pause-circle',
81 | 'minus',
82 | 'minus-circle',
83 | 'plus-square',
84 | 'minus-square',
85 | 'info',
86 | 'info-circle',
87 | 'exclamation',
88 | 'exclamation-circle',
89 | 'close',
90 | 'close-circle',
91 | 'close-square',
92 | 'check',
93 | 'check-circle',
94 | 'check-square',
95 | 'clock-circle',
96 | 'warning',
97 | 'issues-close',
98 | 'stop',
99 | ],
100 | },
101 |
102 | editor: {
103 | title: '编辑类图标',
104 | list: [
105 | 'edit',
106 | 'form',
107 | 'copy',
108 | 'scissor',
109 | 'delete',
110 | 'snippets',
111 | 'diff',
112 | 'highlight',
113 | 'align-center',
114 | 'align-left',
115 | 'align-right',
116 | 'bg-colors',
117 | 'bold',
118 | 'italic',
119 | 'underline',
120 | 'strikethrough',
121 | 'redo',
122 | 'undo',
123 | 'zoom-in',
124 | 'zoom-out',
125 | 'font-colors',
126 | 'font-size',
127 | 'line-height',
128 | 'colum-height',
129 | 'colum-width',
130 | 'dash',
131 | 'small-dash',
132 | 'sort-ascending',
133 | 'sort-descending',
134 | 'drag',
135 | 'ordered-list',
136 | 'unordered-list',
137 | 'radius-setting',
138 | 'column-width',
139 | ]
140 | },
141 | data: {
142 | title: '数据类图标',
143 | list: [
144 | 'area-chart',
145 | 'pie-chart',
146 | 'bar-chart',
147 | 'dot-chart',
148 | 'line-chart',
149 | 'radar-chart',
150 | 'heat-map',
151 | 'fall',
152 | 'rise',
153 | 'stock',
154 | 'box-plot',
155 | 'fund',
156 | 'sliders',
157 | ]
158 | },
159 | logo: {
160 | title: '品牌和标识',
161 | list: [
162 | 'android',
163 | 'apple',
164 | 'windows',
165 | 'ie',
166 | 'chrome',
167 | 'github',
168 | 'aliwangwang',
169 | 'dingding',
170 | 'weibo-square',
171 | 'weibo-circle',
172 | 'taobao-circle',
173 | 'html5',
174 | 'weibo',
175 | 'twitter',
176 | 'wechat',
177 | 'youtube',
178 | 'alipay-circle',
179 | 'taobao',
180 | 'skype',
181 | 'qq',
182 | 'medium-workmark',
183 | 'gitlab',
184 | 'medium',
185 | 'linkedin',
186 | 'google-plus',
187 | 'dropbox',
188 | 'facebook',
189 | 'codepen',
190 | 'code-sandbox',
191 | 'code-sandbox-circle',
192 | 'amazon',
193 | 'google',
194 | 'codepen-circle',
195 | 'alipay',
196 | 'ant-design',
197 | 'ant-cloud',
198 | 'aliyun',
199 | 'zhihu',
200 | 'slack',
201 | 'slack-square',
202 | 'behance',
203 | 'behance-square',
204 | 'dribbble',
205 | 'dribbble-square',
206 | 'instagram',
207 | 'yuque',
208 | 'alibaba',
209 | 'yahoo',
210 | 'reddit',
211 | 'sketch',
212 | ]
213 | }
214 | };
215 |
216 |
217 |
218 |
--------------------------------------------------------------------------------
/react/src/pages/IconDemo/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Card, Icon } from 'antd'
3 | import Typing from '../../components/Typing'
4 | import { categories } from './icons'
5 | import './style.less'
6 |
7 | class IconDemo extends Component {
8 |
9 | render() {
10 | return (
11 |
12 |
13 |
14 | SVG图标
15 | 在3.9.0
之后,我们使用了 SVG 图标替换了原先的 font 图标,从而带来了以下优势:
16 |
17 | - 完全离线化使用,不需要从 CDN 下载字体文件,图标不会因为网络问题呈现方块,也无需字体文件本地部署。
18 | - 在低端设备上 SVG 有更好的清晰度。
19 | - 支持多色图标。
20 | - 对于内建图标的更换可以提供更多 API,而不需要进行样式覆盖。
21 |
22 |
23 |
24 | {
25 | Object.entries(categories).map(item => (
26 |
27 | {
28 | item[1].list.map(icon => (
29 |
30 |
34 |
35 | ))
36 | }
37 |
38 | ))
39 | }
40 |
41 | );
42 | }
43 | }
44 |
45 | export default IconDemo;
--------------------------------------------------------------------------------
/react/src/pages/IconDemo/style.less:
--------------------------------------------------------------------------------
1 | .gridStyle{
2 | width: 25%;
3 | text-align: center;
4 |
5 | &:hover{
6 | .icon{
7 | transform: scale(1.4)
8 | }
9 | }
10 |
11 | .icon{
12 | font-size: 36px;
13 | transition: all 0.3s;
14 | line-height: 1.5;
15 | }
16 | }
--------------------------------------------------------------------------------
/react/src/pages/Index/EditInfoModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Modal, Form, Upload, Icon, message, Input, Radio, DatePicker, Alert } from 'antd'
3 | import { isAuthenticated, authenticateSuccess } from '../../utils/session'
4 | import moment from 'moment'
5 | import { json } from '../../utils/ajax'
6 | import { setUser, initWebSocket } from '../../store/actions'
7 | import { connect, } from 'react-redux'
8 | import { bindActionCreators } from 'redux'
9 | import { createFormField } from '../../utils/util'
10 |
11 |
12 | const RadioGroup = Radio.Group;
13 |
14 | const store = connect(
15 | (state) => ({ user: state.user, websocket: state.websocket }),
16 | (dispatch) => bindActionCreators({ setUser, initWebSocket }, dispatch)
17 | )
18 | const form = Form.create({
19 | /**
20 | * 表单回显
21 | * @param {*} props
22 | */
23 | mapPropsToFields(props) {
24 | const user = props.user
25 | return createFormField({
26 | ...user,
27 | birth: user.birth ? moment(user.birth) : null
28 | })
29 | }
30 | })
31 |
32 | @store @form
33 | class EditInfoModal extends React.Component {
34 | state = {
35 | uploading: false
36 | }
37 | /**
38 | * 关闭模态框
39 | */
40 | handleCancel = () => {
41 | this.props.form.resetFields()
42 | this.props.toggleVisible(false)
43 | }
44 | /**
45 | * 模态框的确定按钮
46 | */
47 | handleOk = () => {
48 | this.props.form.validateFieldsAndScroll((err, values) => {
49 | if (!err) {
50 | this.onUpdate(values)
51 | }
52 | });
53 | }
54 | /**
55 | * 更新用户信息
56 | */
57 | onUpdate = async (values) => {
58 | const param = {
59 | ...values,
60 | birth: values.birth && moment(values.birth).valueOf()
61 | }
62 | const res = await json.post('/user/update', param)
63 | if (res.status === 0) {
64 | //修改localStorage,为什么我们在redux中保存了用户信息还要在localStorage中保存?redux刷新就重置了,我们需要username重新去后台获取
65 | localStorage.setItem('username', values.username)
66 | //修改cookie
67 | authenticateSuccess(res.data.token)
68 | //修改redux中的user信息
69 | this.props.setUser(res.data)
70 | //修改websocket中的user信息
71 | if (this.props.websocket.readyState !== 1) {
72 | this.props.initWebSocket(res.data)
73 | } else {
74 | this.props.websocket.send(JSON.stringify({
75 | id: res.data.id,
76 | username: res.data.username,
77 | avatar: res.data.avatar
78 | }))
79 | }
80 | message.success('修改信息成功')
81 | this.handleCancel()
82 | }
83 | }
84 | /**
85 | * 转换上传组件表单的值
86 | */
87 | _normFile = (e) => {
88 | if (e.file.response && e.file.response.data) {
89 | return e.file.response.data.url
90 | } else {
91 | return ''
92 | }
93 | }
94 | render() {
95 | const { uploading } = this.state
96 | const { visible } = this.props
97 | const { getFieldDecorator, getFieldValue } = this.props.form
98 |
99 | const avatar = getFieldValue('avatar')
100 |
101 | const formItemLayout = {
102 | labelCol: { span: 6 },
103 | wrapperCol: { span: 14 },
104 | }
105 |
106 | const uploadProps = {
107 | name: "avatar",
108 | listType: "picture-card",
109 | headers: {
110 | Authorization: `Bearer ${isAuthenticated()}`,
111 | },
112 | action: `${process.env.REACT_APP_BASE_URL}/upload?isImg=1`,
113 | showUploadList: false,
114 | accept: "image/*",
115 | onChange: (info) => {
116 | if (info.file.status !== 'uploading') {
117 | this.setState({
118 | uploading: true
119 | })
120 | }
121 | if (info.file.status === 'done') {
122 | this.setState({
123 | uploading: false
124 | })
125 | message.success('上传头像成功')
126 | } else if (info.file.status === 'error') {
127 | this.setState({
128 | uploading: false
129 | })
130 | message.error(info.file.response.message || '上传头像失败')
131 | }
132 | }
133 | }
134 | return (
135 |
141 |
142 |
144 | {getFieldDecorator('avatar', {
145 | rules: [{ required: true, message: '请上传用户头像' }],
146 | getValueFromEvent: this._normFile, //将上传的结果作为表单项的值(用normalize报错了,所以用的这个属性)
147 | })(
148 |
149 | {avatar ?
: }
150 |
151 | )}
152 |
153 |
154 | {getFieldDecorator('username', {
155 | validateFirst: true,
156 | rules: [
157 | { required: true, message: '用户名不能为空' },
158 | { pattern: /^[^\s']+$/, message: '不能输入特殊字符' },
159 | { min: 3, message: '用户名至少为3位' }
160 | ]
161 | })(
162 |
163 | )}
164 |
165 |
166 | {getFieldDecorator('birth', {
167 | // rules: [{ required: true, message: '请选择出生年月日' }],
168 | })(
169 |
170 | )}
171 |
172 |
173 | {getFieldDecorator('phone', {
174 | // rules: [{ required: true, message: '请输入电话号码' }, { pattern: /^[0-9]*$/, message: '请输入正确的电话号码' }],
175 | })(
176 |
177 | )}
178 |
179 |
180 | {getFieldDecorator('location', {
181 | validateFirst: true,
182 | // rules: [{ required: true, message: '请输入目前所在地' }],
183 | })(
184 |
185 | )}
186 |
187 |
188 | {getFieldDecorator('gender', {
189 | initialValue: '男',
190 | // rules: [{ required: true, message: '请选择性别' }],
191 | })(
192 |
193 | 男
194 | 女
195 |
196 | )}
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 | )
205 | }
206 | }
207 |
208 | const styles = {
209 | avatarUploader: {
210 | display: 'flex',
211 | justifyContent: 'center',
212 | alignItems: 'center',
213 | width: 150,
214 | height: 150,
215 | backgroundColor: '#fff'
216 | },
217 | icon: {
218 | fontSize: 28,
219 | color: '#999'
220 | },
221 | avatar: {
222 | maxWidth: '100%',
223 | maxHeight: '100%',
224 | },
225 | }
226 |
227 |
228 | export default EditInfoModal
--------------------------------------------------------------------------------
/react/src/pages/Index/EditPasswordModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Modal, Input, Form, message } from 'antd'
3 | import { connect } from 'react-redux'
4 | import { createFormField, encrypt } from '../../utils/util'
5 | import { json } from '../../utils/ajax'
6 |
7 | const store = connect(
8 | (state) => ({ user: state.user }),
9 | )
10 | const form = Form.create({
11 | //表单回显
12 | mapPropsToFields(props) {
13 | const user = props.user
14 | return createFormField({
15 | username: user.username
16 | })
17 | }
18 | })
19 |
20 | @store @form
21 | class EditPasswordModal extends React.Component {
22 | handleCancel = () => {
23 | this.props.form.resetFields()
24 | this.props.toggleVisible(false)
25 | }
26 | /**
27 | * 模态框的确定按钮
28 | */
29 | handleOk = () => {
30 | this.props.form.validateFields((err, values) => {
31 | if (!err) {
32 | this.onSubmit(values)
33 | }
34 | });
35 | }
36 | /**
37 | * 提交修改密码
38 | */
39 | onSubmit = async (values) => {
40 | //加密密码
41 | const ciphertext = encrypt(values.oldPassword)
42 | const res = await json.post('/user/login', {
43 | username: values.username,
44 | password: ciphertext
45 | })
46 | if (res.status === 0) {
47 | const ciphertext2 = encrypt(values.password)
48 | const res2 = await json.post('/user/update', {
49 | username: values.username,
50 | password: ciphertext2
51 | })
52 | if (res2.status === 0) {
53 | message.success('修改密码成功')
54 | this.handleCancel()
55 | }
56 | }
57 | }
58 |
59 | render() {
60 | const { visible } = this.props
61 | const { getFieldDecorator, getFieldValue } = this.props.form
62 |
63 | const formItemLayout = {
64 | labelCol: { span: 6 },
65 | wrapperCol: { span: 14 },
66 | }
67 | return (
68 |
74 |
76 | {getFieldDecorator('username', {})(
77 |
78 | )}
79 |
80 |
81 | {getFieldDecorator('oldPassword', {
82 | rules: [{ required: true, message: '请输入旧密码' }],
83 | })(
84 |
88 | )}
89 |
90 |
91 | {getFieldDecorator('password', {
92 | validateFirst: true,
93 | rules: [
94 | { required: true, message: '密码不能为空' },
95 | { pattern: '^[^ ]+$', message: '密码不能有空格' },
96 | { min: 3, message: '密码至少为3位' },
97 | ]
98 | })(
99 |
103 | )}
104 |
105 |
106 | {getFieldDecorator('confirmPassword', {
107 | validateFirst: true,
108 | rules: [
109 | { required: true, message: '请确认密码' },
110 | {
111 | validator: (rule, value, callback) => {
112 | if (value !== getFieldValue('password')) {
113 | callback('两次输入不一致!')
114 | }
115 | callback()
116 | }
117 | },
118 | ]
119 | })(
120 |
125 | )}
126 |
127 |
128 |
129 | )
130 | }
131 | }
132 |
133 | export default EditPasswordModal
--------------------------------------------------------------------------------
/react/src/pages/Index/MyContent.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Tabs, Carousel, Layout, Icon } from 'antd'
3 | import './style.less'
4 |
5 | const Footer = Layout.Footer
6 | const TabPane = Tabs.TabPane;
7 | const imgs = [
8 | `${process.env.REACT_APP_BASE_URL}/public/images/bg1.jpg`,
9 | `${process.env.REACT_APP_BASE_URL}/public/images/bg2.jpg`,
10 | `${process.env.REACT_APP_BASE_URL}/public/images/bg3.jpg`,
11 | ]
12 |
13 | class MyContent extends React.Component {
14 | /**
15 | * 标签页的改变触发的函数
16 | */
17 | onChange = (activeKey) => {
18 | this.props.onChangeState({
19 | activeMenu: activeKey
20 | })
21 | }
22 | onEdit = (targetKey, action) => {
23 | if (action === 'remove') {
24 | this.remove(targetKey)
25 | }
26 | }
27 | /**
28 | * 关闭标签页
29 | */
30 | remove = (targetKey) => {
31 | let activeMenu = this.props.activeMenu
32 | let panes = this.props.panes.slice()
33 | let preIndex = panes.findIndex(item => item.key === targetKey) - 1
34 | preIndex = Math.max(preIndex, 0)
35 |
36 | panes = panes.filter(item => item.key !== targetKey)
37 |
38 | if (targetKey === activeMenu) {
39 | activeMenu = panes[preIndex] ? panes[preIndex].key : ''
40 | }
41 | this.props.onChangeState({
42 | activeMenu,
43 | panes
44 | })
45 | }
46 | render() {
47 | const { panes, activeMenu } = this.props
48 | return (
49 |
50 | {
51 | panes.length ? (
52 |
60 | {
61 | panes.map(item => (
62 |
63 | {item.content}
64 |
65 |
68 | ))
69 | }
70 |
71 | ) : (
72 |
73 |
74 | {imgs.map(item => (
75 |
76 |

77 |
78 | ))}
79 |
80 |
81 | )
82 | }
83 |
84 | )
85 | }
86 | }
87 |
88 |
89 |
90 | export default MyContent
--------------------------------------------------------------------------------
/react/src/pages/Index/MyHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import screenfull from 'screenfull'
3 | import { Icon, message, Menu, Avatar } from 'antd'
4 | import ColorPicker from '@/components/ColorPicker/index'
5 | import { logout } from '@/utils/session'
6 | import { withRouter } from 'react-router-dom'
7 | import { connect } from 'react-redux'
8 | import LoadableComponent from '@/utils/LoadableComponent'
9 | import MyIcon from '../../components/MyIcon'
10 |
11 | const SubMenu = Menu.SubMenu;
12 | const MenuItemGroup = Menu.ItemGroup;
13 |
14 | const EditInfoModal = LoadableComponent(import('./EditInfoModal'))
15 | const EditPasswordModal = LoadableComponent(import('./EditPasswordModal'))
16 |
17 | const store = connect(
18 | (state) => ({ user: state.user })
19 | )
20 |
21 | @withRouter @store
22 | class MyHeader extends React.Component {
23 | constructor(props) {
24 | super(props);
25 | const userTheme = JSON.parse(localStorage.getItem('user-theme'))
26 | let color = '#13C2C2'
27 | if (userTheme) {
28 | window.less.modifyVars(userTheme)
29 | color = userTheme['@primary-color']
30 | }
31 | this.state = {
32 | isFullscreen: false, //控制页面全屏
33 | color: color,
34 | infoVisible: false, //控制修改用户信息的模态框
35 | passwordVisible: false //控制修改密码的模态框
36 | }
37 | }
38 | /**
39 | * 切换侧边栏的折叠和展开
40 | */
41 | toggleCollapsed = () => {
42 | this.props.onChangeState({
43 | collapsed: !this.props.collapsed
44 | })
45 | }
46 | /**
47 | * 切换全屏
48 | */
49 | toggleFullscreen = () => {
50 | if (screenfull.enabled) {
51 | screenfull.toggle().then(() => {
52 | this.setState({
53 | isFullscreen: screenfull.isFullscreen
54 | })
55 | });
56 | }
57 | }
58 | /**
59 | * 切换主题
60 | */
61 | changeColor = (color) => {
62 | window.less.modifyVars({
63 | '@primary-color': color,
64 | }).then(() => {
65 | localStorage.setItem('user-theme', JSON.stringify({ '@primary-color': color }))
66 | this.setState({
67 | color
68 | })
69 | message.success('更换主题颜色成功')
70 | })
71 | }
72 | /**
73 | * 重置主题
74 | */
75 | resetColor = () => {
76 | this.changeColor('#13C2C2')
77 | }
78 | /**
79 | * 展开/关闭修改信息模态框
80 | */
81 | toggleInfoVisible = (visible) => {
82 | this.setState({
83 | infoVisible: visible
84 | })
85 | }
86 | /**
87 | * 展开/关闭修改密码模态框
88 | */
89 | togglePasswordVisible = (visible) => {
90 | this.setState({
91 | passwordVisible: visible
92 | })
93 | }
94 | /**
95 | * 退出登录
96 | */
97 | onLogout = () => {
98 | logout() //清空cookie
99 | this.props.history.push('/login')
100 | }
101 | changeTheme = () => {
102 | const theme = this.props.theme === 'dark' ? 'light' : 'dark'
103 | localStorage.setItem('theme', theme)
104 | this.props.onChangeState({
105 | theme
106 | })
107 | }
108 |
109 | render() {
110 | const { isFullscreen, color, infoVisible, passwordVisible } = this.state
111 | const { user, theme } = this.props
112 | return (
113 |
114 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | {/*
*/}
126 |
127 |
128 |
}>
130 |
131 | this.toggleInfoVisible(true)}>编辑个人信息
132 | this.togglePasswordVisible(true)}>修改密码
133 | 退出登录
134 |
135 |
136 | 切换全屏
137 | 恢复默认主题
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | )
147 | }
148 | }
149 |
150 | const styles = {
151 | headerRight: {
152 | float: 'right',
153 | display: 'flex',
154 | height: 64,
155 | marginRight: 50
156 | },
157 | headerItem: {
158 | display: 'flex',
159 | alignItems: 'center',
160 | padding: '0 20px'
161 | },
162 | avatarBox: {
163 | display: 'flex',
164 | alignItems: 'center',
165 | }
166 | }
167 |
168 | export default MyHeader
--------------------------------------------------------------------------------
/react/src/pages/Index/MySider.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Menu, Icon } from 'antd'
3 | import { tabs, menu } from '../tabs'
4 |
5 | class MySider extends React.Component {
6 | /**
7 | * 生成侧边栏菜单
8 | */
9 | renderMenu = (menu) => {
10 | if (Array.isArray(menu)) {
11 | return menu.map(item => {
12 | if (!item.children || !item.children.length) {
13 | return (
14 |
15 | this.addPane(item)}>{item.icon && }{item.name}
16 |
17 | )
18 | } else {
19 | return (
20 | {item.icon && }{item.name}}>
21 | {this.renderMenu(item.children)}
22 |
23 | )
24 | }
25 | })
26 | }
27 | }
28 | /**
29 | * 点击侧边栏菜单添加标签页
30 | */
31 | addPane = (item) => {
32 | const panes = this.props.panes.slice()
33 | const activeMenu = item.key
34 | //如果标签页不存在就添加一个
35 | if (!panes.find(i => i.key === activeMenu)) {
36 | panes.push({
37 | name: item.name,
38 | key: item.key,
39 | content: tabs[item.key] || item.name
40 | })
41 | }
42 | this.props.onChangeState({
43 | panes,
44 | activeMenu
45 | })
46 | }
47 | render() {
48 | const { activeMenu, theme } = this.props
49 | return (
50 |
51 |
57 |
60 |
61 | )
62 | }
63 | }
64 |
65 |
66 |
67 | export default MySider
--------------------------------------------------------------------------------
/react/src/pages/Index/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Layout } from 'antd'
3 | import MySider from './MySider'
4 | import MyHeader from './MyHeader'
5 | import MyContent from './MyContent'
6 | import { getUser, initWebSocket } from '@/store/actions'
7 | import { connect, } from 'react-redux'
8 | import { bindActionCreators } from 'redux'
9 |
10 | const { Header, Sider, Content } = Layout;
11 |
12 | const store = connect(
13 | (state) => ({ user: state.user, websocket: state.websocket }),
14 | (dispatch) => bindActionCreators({ getUser, initWebSocket }, dispatch)
15 | )
16 |
17 | @store
18 | class Index extends React.Component {
19 | //因为这些状态在不同组件中使用了,所以这里使用了状态提升(这里也可以用状态管理,为了学习这里就使用状态提升)
20 | state = {
21 | collapsed: false, //侧边栏的折叠和展开
22 | panes: [], //网站打开的标签页列表
23 | activeMenu: '', //网站活动的菜单
24 | theme: localStorage.getItem('theme') || 'light', //侧边栏主题
25 | };
26 | componentDidMount() {
27 | this.init()
28 | }
29 | componentWillUnmount() {
30 | const websocket = this.props.websocket
31 | websocket && websocket.close()
32 | }
33 | /**
34 | * 初始化用户信息和建立websocket连接
35 | */
36 | init = async () => {
37 | const username = localStorage.getItem('username')
38 | await this.props.getUser({ username })
39 | this.props.initWebSocket(this.props.user)
40 | }
41 | _setState = (obj) => {
42 | this.setState(obj)
43 | }
44 | render() {
45 | const { collapsed, panes, activeMenu, theme } = this.state
46 | return (
47 |
48 |
49 |
54 |
55 |
56 |
62 |
63 |
67 |
68 |
69 |
70 | )
71 | }
72 | }
73 |
74 | export default Index
--------------------------------------------------------------------------------
/react/src/pages/Index/style.less:
--------------------------------------------------------------------------------
1 | .content-container {
2 | position: relative;
3 | height: 100%;
4 | background-color: #fff;
5 |
6 | .ant-tabs-tabpane {
7 | position: relative;
8 | height: calc(100vh - 104px);
9 | overflow: auto;
10 | }
11 |
12 | .bg-box {
13 | position: 'absolute';
14 | top: 0;
15 | left: 0;
16 | width: 100%;
17 | height: 100%;
18 | overflow: hidden;
19 | background: #eee;
20 | }
21 |
22 | .bg-size {
23 | width: 100%;
24 | height: calc(100vh - 64px);
25 | overflow: hidden;
26 | }
27 |
28 | .tabpane-box {
29 | min-height: calc(100vh - 173px);
30 | }
31 | }
32 |
33 | .my-sider {
34 | position: relative;
35 | z-index: 10;
36 | height: 100%;
37 | box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
38 | &.light{
39 | box-shadow: 2px 0 8px 0 rgba(29,35,41,.05);
40 | }
41 |
42 | .ant-menu-light{
43 | border-right-color: transparent;
44 | }
45 |
46 |
47 | .sider-menu-logo {
48 | position: relative;
49 | height: 64px;
50 | padding-left: 24px;
51 | overflow: hidden;
52 | line-height: 64px;
53 | background: #001529;
54 | transition: all .3s;
55 |
56 | &.light {
57 | background: #fff;
58 | box-shadow: 1px 1px 0 0 #e8e8e8;
59 |
60 | h1 {
61 | color: #1890ff;
62 | }
63 | }
64 |
65 | img {
66 | display: inline-block;
67 | height: 32px;
68 | vertical-align: middle;
69 | }
70 |
71 | h1 {
72 | display: inline-block;
73 | margin: 0 0 0 12px;
74 | color: #fff;
75 | font-weight: 600;
76 | font-size: 20px;
77 | font-family: Avenir,Helvetica Neue,Arial,Helvetica,sans-serif;
78 | vertical-align: middle;
79 | }
80 | }
81 | }
82 |
83 |
84 |
--------------------------------------------------------------------------------
/react/src/pages/Login/RegisterForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Form, Input, message } from 'antd'
3 | import Promptbox from '@/components/PromptBox/index'
4 | import { debounce, encrypt } from '@/utils/util'
5 | import { get, post } from '@/utils/ajax'
6 |
7 | @Form.create()
8 | class RegisterForm extends React.Component {
9 | state = {
10 | focusItem: -1, //当前焦点聚焦在哪一项上
11 | loading: false //注册的loding
12 | }
13 | /**
14 | * 返回登录面板
15 | */
16 | backLogin = () => {
17 | this.props.form.resetFields()
18 | this.props.toggleShow()
19 | }
20 | onSubmit = () => {
21 | this.props.form.validateFields((errors, values) => {
22 | if (!errors) {
23 | this.onRegister(values)
24 | }
25 | });
26 | }
27 | /**
28 | * 注册函数
29 | */
30 | onRegister = async (values) => {
31 | //如果正在注册,则return,防止重复注册
32 | if (this.state.loading) {
33 | return
34 | }
35 | this.setState({
36 | loading: true
37 | })
38 | const hide = message.loading('注册中...', 0)
39 | //加密密码
40 | const ciphertext = encrypt(values.registerPassword)
41 | const res = await post('/user/register', {
42 | username: values.registerUsername,
43 | password: ciphertext,
44 | })
45 | this.setState({
46 | loading: false
47 | })
48 | hide()
49 | if (res.status === 0) {
50 | message.success('注册成功')
51 | }
52 | }
53 |
54 | /**
55 | * @description: 检查用户名是否重复,这里用了函数防抖(函数防抖的典型应用),防抖函数要注意this和事件对象
56 | * 也可以在input的失去焦点的时候验证,这时候不用函数防抖
57 | * @param {type} 事件对象
58 | * @return:
59 | */
60 | checkName = debounce(async (value) => {
61 | if (value) {
62 | const res = await get(`/user/checkName?username=${value}`)
63 | if (res.status === 0 && res.data.num) {
64 | this.props.form.setFields({
65 | registerUsername: {
66 | value,
67 | errors: [new Error('用户名已存在')]
68 | }
69 | })
70 | }
71 | }
72 | })
73 | render() {
74 | const { getFieldDecorator, getFieldValue, getFieldError } = this.props.form
75 | const { focusItem } = this.state
76 | return (
77 |
78 |
管理员注册
79 |
}
82 | style={{ marginBottom: 10 }}
83 | wrapperCol={{ span: 20, pull: focusItem === 0 ? 1 : 0 }}
84 | labelCol={{ span: 3, pull: focusItem === 0 ? 1 : 0 }}
85 | label={
}
86 | colon={false}>
87 | {getFieldDecorator('registerUsername', {
88 | validateFirst: true,
89 | rules: [
90 | { required: true, message: '用户名不能为空' },
91 | { pattern: /^[^\s']+$/, message: '不能输入特殊字符' },
92 | { min: 3, message: '用户名至少为3位' }
93 | ]
94 | })(
95 |
this.setState({ focusItem: 0 })}
100 | onBlur={() => this.setState({ focusItem: -1 })}
101 | onPressEnter={this.onSubmit}
102 | onChange={(e) => this.checkName(e.target.value)}
103 | placeholder="用户名"
104 | />
105 | )}
106 |
107 |
}
109 | style={{ marginBottom: 10 }}
110 | wrapperCol={{ span: 20, pull: focusItem === 1 ? 1 : 0 }}
111 | labelCol={{ span: 3, pull: focusItem === 1 ? 1 : 0 }}
112 | label={
}
113 | colon={false}>
114 | {getFieldDecorator('registerPassword', {
115 | validateFirst: true,
116 | rules: [
117 | { required: true, message: '密码不能为空' },
118 | { pattern: '^[^ ]+$', message: '密码不能有空格' },
119 | { min: 3, message: '密码至少为3位' },
120 | ]
121 |
122 | })(
123 |
this.setState({ focusItem: 1 })}
128 | onBlur={() => this.setState({ focusItem: -1 })}
129 | onPressEnter={this.onSubmit}
130 | placeholder="密码"
131 | />
132 | )}
133 |
134 |
}
136 | style={{ marginBottom: 35 }}
137 | wrapperCol={{ span: 20, pull: focusItem === 2 ? 1 : 0 }}
138 | labelCol={{ span: 3, pull: focusItem === 2 ? 1 : 0 }}
139 | label={
}
140 | colon={false}>
141 | {getFieldDecorator('confirmPassword', {
142 | rules: [
143 | { required: true, message: '请确认密码' },
144 | {
145 | validator: (rule, value, callback) => {
146 | if (value && value !== getFieldValue('registerPassword')) {
147 | callback('两次输入不一致!')
148 | }
149 | callback()
150 | }
151 | },
152 | ]
153 |
154 | })(
155 |
this.setState({ focusItem: 2 })}
159 | onBlur={() => this.setState({ focusItem: -1 })}
160 | onPressEnter={this.onSubmit}
161 | placeholder="确认密码"
162 | />
163 | )}
164 |
165 |
166 |
167 |
注册
168 |
返回登录
169 |
170 |
171 |
172 |
欢迎登陆后台管理系统
173 |
174 | )
175 | }
176 | }
177 |
178 | export default RegisterForm
--------------------------------------------------------------------------------
/react/src/pages/Login/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 翻转动画例子原理
8 |
41 |
42 |
43 |
47 |
48 |
--------------------------------------------------------------------------------
/react/src/pages/Login/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './style.less'
3 | import { isAuthenticated, logout } from '../../utils/session'
4 | import { withRouter } from 'react-router-dom'
5 | import LoadableComponent from '@/utils/LoadableComponent'
6 | import { preloadingImages } from '../../utils/util'
7 |
8 | const LoginForm = LoadableComponent(import('./LoginForm'))
9 | const RegisterForm = LoadableComponent(import('./RegisterForm'))
10 | const Background = LoadableComponent(import('@/components/Background'))
11 |
12 | const imgs = [
13 | `${process.env.REACT_APP_BASE_URL}/public/images/bg1.jpg`,
14 | `${process.env.REACT_APP_BASE_URL}/public/images/bg2.jpg`,
15 | `${process.env.REACT_APP_BASE_URL}/public/images/bg3.jpg`,
16 | ]
17 |
18 |
19 | @withRouter
20 | class Login extends React.Component {
21 | state = {
22 | show: 'login' //当前展示的是登录框还是注册框
23 | }
24 |
25 | componentDidMount() {
26 | // 防止用户通过浏览器的前进后退按钮来进行路由跳转
27 | // 当用户登陆后再通过浏览器的后退按钮回到登录页时,再点击前进按钮可以直接回到首页
28 | if (!!isAuthenticated()) {
29 | this.props.history.go(1) //不然他后退或者后退了直接登出
30 | // logout()
31 | }
32 | preloadingImages(imgs)
33 | }
34 | /**
35 | * 切换登录和注册的面板
36 | */
37 | toggleShow = () => {
38 | this.setState({
39 | show: this.state.show === 'login' ? 'register' : 'login'
40 | })
41 | }
42 | render() {
43 | const { show } = this.state
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | )
56 | }
57 | }
58 |
59 | export default Login
--------------------------------------------------------------------------------
/react/src/pages/Login/style.less:
--------------------------------------------------------------------------------
1 | .login-container {
2 | position: fixed;
3 | top: 50%;
4 | left: 50%;
5 | transform: translate(-50%, -50%);
6 | width: 240px;
7 | height: 300px;
8 | padding: 100px 40px 40px;
9 | box-sizing: content-box;
10 |
11 | .box {
12 | position: absolute;
13 | top: 0;
14 | left: 0;
15 | width: 100%;
16 | height: 100%;
17 | transition: all 1s;
18 | backface-visibility: hidden;
19 | color: #d3d7f7;
20 | padding: 90px 40px 40px;
21 | box-shadow: -15px 15px 15px rgba(0, 0, 0, .4);
22 | transform: rotateY(180deg);
23 |
24 | &:before {
25 | content: '';
26 | position: absolute;
27 | top: 0;
28 | left: 0;
29 | width: 100%;
30 | height: 100%;
31 | background: linear-gradient(230deg, rgba(53, 57, 74, 0), #000);
32 | // opacity: .8;
33 | z-index: -1;
34 | }
35 |
36 | &.active {
37 | transform: rotateY(0deg);
38 | }
39 |
40 | .title {
41 | height: 60px;
42 | color: #d3d7f7;
43 | font-size: 16px;
44 | margin-bottom: 0;
45 | font-weight: 500;
46 | }
47 |
48 | .iconfont {
49 | color: #fff;
50 | opacity: .6;
51 | }
52 |
53 | .myInput.ant-input {
54 | background-color: transparent;
55 | color: #61BFFF;
56 | outline: none;
57 | border: none;
58 | box-shadow: none;
59 | padding-right: 0;
60 | }
61 |
62 | .btn-box {
63 | display: flex;
64 | height: 42px;
65 | justify-content: space-between;
66 | align-items: center;
67 |
68 | .registerBtn {
69 | &:hover {
70 | color: #4FA1D9;
71 | }
72 |
73 | cursor: pointer;
74 | color: #D3D7F7;
75 | }
76 |
77 | .loginBtn {
78 | &:hover {
79 | color: white;
80 | background: #4FA1D9;
81 |
82 | }
83 |
84 | cursor: pointer;
85 | padding: 10px 50px;
86 | border: 2px solid #4FA1D9;
87 | border-radius: 50px;
88 | background: transparent;
89 | font-size: 11px;
90 | color: #4FA1D9;
91 | transition: all .2s;
92 | height: 100%;
93 | line-height: 1.5;
94 | }
95 |
96 | }
97 |
98 | .footer {
99 | position: absolute;
100 | bottom: 20px;
101 | left: 35px;
102 | width: 250px;
103 | color: #d3d7f7;
104 | font-size: 10px;
105 | font-weight: 600;
106 | }
107 |
108 | //覆盖antd样式,注意只覆盖此页面antd的样式,而其他页面的antd不会被覆盖
109 | .ant-col {
110 | right: 0%; //过渡要有初始值,否则无效
111 | transition: all .3s;
112 | }
113 |
114 | .ant-col-pull-1 {
115 | right: 4.16666667%; //为什么这里又写一遍?因为上面的优先级会覆盖antd,所以这里在写一遍提高优先级
116 | }
117 |
118 | .ant-form-explain {
119 | position: absolute;
120 | z-index: 99;
121 | left: 110%;
122 | top: 0;
123 | height: 41px;
124 | }
125 |
126 | /*更改谷歌浏览器input背景*/
127 | input:-webkit-autofill,
128 | input:-webkit-autofill:hover,
129 | input:-webkit-autofill:focus,
130 | input:-webkit-autofill:active {
131 | /*延迟背景颜色载入*/
132 | -webkit-transition-delay: 99999s;
133 | -webkit-transition: color 99999s ease-out, background-color 99999s ease-out;
134 | }
135 | }
136 | }
--------------------------------------------------------------------------------
/react/src/pages/MessageBoard/Score.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Rate, Progress, Icon, Modal } from 'antd'
3 | import { json } from '../../utils/ajax'
4 | import { connect } from 'react-redux'
5 |
6 | const store = connect(
7 | (state) => ({ user: state.user })
8 | )
9 |
10 | @store
11 | class Score extends Component {
12 | state = {
13 | isScored: false, //是否已经评过分
14 | scores: [], //所有评分列表
15 | userScore: 4, //当前用户的评分值(默认4星)
16 | average: 0, //平均分
17 | rankList: [], //1-5星的占比
18 | visible: true,
19 | }
20 | componentDidMount() {
21 | this.getScores()
22 | }
23 | /**
24 | * 获取评分列表
25 | */
26 | getScores = async () => {
27 | const res = await json.get('/score/list')
28 | const list = res.data || []
29 | const total = list.reduce((total, current) => {
30 | return total + current.score
31 | }, 0)
32 | const average = total * 2 / list.length
33 | let rankList = []
34 | // 因为有四舍五入的处理,所以我想最后一个数用100相减,但是js的浮点数计算是不准确的(0.1+0.3!=0.3),就没多此一举了
35 | for (let i = 0; i < 5; i++) {
36 | const num = list.filter(item => item.score === 5 - i).length / list.length
37 | rankList[i] = Number((num * 100).toFixed(1)) //注意toFixed方法返回的是字符串
38 | }
39 | this.setState({
40 | isScored: !!list.find(item => item.userId === this.props.user.id),
41 | scores: list,
42 | average: average.toFixed(1), //保留一位小数,这里不要用Number否则7.0会显示7
43 | rankList
44 | })
45 | }
46 | /**
47 | * 评分
48 | */
49 | createScore = async (num) => {
50 | Modal.confirm({
51 | title: '提示',
52 | content: 确定当前评分
,
53 | onOk: async () => {
54 | const res = await json.post('/score/create', {
55 | score: num,
56 | userId: this.props.user.id
57 | })
58 | if (res.status === 0) {
59 | this.getScores()
60 | this.setState({
61 | userScore: num
62 | })
63 | }
64 | }
65 | })
66 | }
67 | /**
68 | * 计算显示平均分的星星,满分10分,一分为半星,大于0小于0.5不算星,大于等于0.5小于1算半星
69 | */
70 | handleScore = (score) => {
71 | score = Number(score)
72 | const integer = Math.floor(score) //取整数部分
73 | let decimal = score - integer //取小数部分
74 | if (decimal >= 0.5) {
75 | decimal = 1
76 | } else {
77 | decimal = 0
78 | }
79 | return (integer + decimal) / 2
80 | }
81 | render() {
82 | const { isScored, userScore, scores, average, rankList, visible } = this.state
83 | const desc = ['有bug', '再接再厉', '有待提高', '不错', '666']
84 |
85 | const NotScore = () => (
86 |
87 |
92 | {desc[userScore - 1]}
93 |
94 | )
95 | const ScoreInfo = () => (
96 |
97 |
98 |
{average}
99 |
100 |
101 |
{scores.length}人评价
102 |
103 |
this.setState({ visible: false })}>
104 |
105 |
106 | {rankList.map((item, index) => (
107 |
108 |
{5 - index}星
109 |
110 |
111 | ))}
112 |
113 |
114 | )
115 | return (
116 | {isScored ? : }
117 | );
118 | }
119 | }
120 |
121 |
122 | export default Score;
--------------------------------------------------------------------------------
/react/src/pages/MessageBoard/style.less:
--------------------------------------------------------------------------------
1 | .editor-wrapper {
2 | width: 800px;
3 | border: 1px solid #d1d1d1;
4 | border-radius: 5px;
5 | margin-bottom: 10px;
6 | }
7 |
8 | .message-list-box {
9 | width: 800px;
10 |
11 | .avatar-img {
12 | width: 64px;
13 | height: 64px;
14 | object-fit: cover;
15 | }
16 |
17 | .avatar-img-small {
18 | width: 56px;
19 | height: 56px;
20 | object-fit: cover;
21 | }
22 |
23 | .my-iconfont {
24 | display: inline-block;
25 | transform: translateY(1px);
26 | }
27 |
28 | .info-box {
29 | img {
30 | max-width: 80% !important;
31 | max-height: 500px !important;
32 | object-fit: contain;
33 | }
34 | video {
35 | max-width: 80% !important;
36 | }
37 |
38 | }
39 |
40 | .toggle-reply-box {
41 | color: #79a5e5;
42 | cursor: pointer;
43 | margin-bottom: 20px;
44 | }
45 | }
46 |
47 | .score-box {
48 | position: fixed;
49 | right: 50px;
50 | top: 220px;
51 | width: 250px;
52 |
53 | .info {
54 | position: relative;
55 | display: flex;
56 | align-items: center;
57 |
58 | .average-num {
59 | font-size: 36px;
60 | color: #494949;
61 | margin-right: 10px;
62 | font-family: Helvetica;
63 | }
64 |
65 | .people-num {
66 | font-size: 12px;
67 | color: #37a;
68 | }
69 |
70 | .close-box{
71 | position: absolute;
72 | top: 0;
73 | right: 10px;
74 | font-size: 12px;
75 | color: #999;
76 | }
77 | }
78 |
79 | .star-item{
80 | display: flex;
81 | justify-items: center;
82 | margin-top: 5px;
83 |
84 | .star-label{
85 | flex-shrink: 0;
86 | font-size: 13px;
87 | margin-right: 10px;
88 | color: rgba(0, 0, 0, 0.45);
89 | transform: translateY(3px);
90 | // color: var(--primaryColor);
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/react/src/pages/MobileUI/config.js:
--------------------------------------------------------------------------------
1 | export const BASE_URL = 'http://47.99.130.140/project/sty-ui/#/'
2 |
3 | export const nav = [
4 | {
5 | title: '通用组件',
6 | items: [
7 | { path: 'button', title: 'Button 按钮' },
8 | { path: 'cell', title: 'Cell 单元格' },
9 | { path: 'icon', title: 'Icon 图标' },
10 | { path: 'image', title: 'Image 图片' },
11 | { path: 'popup', title: 'Popup 弹出层' },
12 | { path: 'ripple', title: 'Ripple 波纹' },
13 | { path: 'timeline', title: 'Timeline 时间线' },
14 | { path: 'swipe', title: 'Swipe 轮播' }
15 | ]
16 | },
17 | {
18 | title: '表单组件',
19 | items: [
20 | { path: 'switch', title: 'Switch 开关' },
21 | { path: 'radio', title: 'Radio 单选框' },
22 | { path: 'checkbox', title: 'Checkbox 复选框' },
23 | { path: 'select', title: 'Select 选择框' },
24 | { path: 'picker', title: 'Picker 选择器' },
25 | { path: 'date-picker', title: 'DatePicker 日期选择器' }
26 | ]
27 | },
28 | {
29 | title: '反馈组件',
30 | items: [
31 | { path: 'action-sheet', title: 'ActionSheet 动作面板' },
32 | { path: 'dialog', title: 'Dialog 弹出框' },
33 | { path: 'loading', title: 'Loading 加载' },
34 | { path: 'pull-refresh', title: 'PullRefresh 下拉刷新' },
35 | { path: 'toast', title: 'Toast 提示' }
36 | ]
37 | },
38 | {
39 | title: '导航组件',
40 | items: [
41 | { path: 'nav-bar', title: 'NavBar 导航栏' },
42 | { path: 'tabs', title: 'Tabs 标签页' }
43 | ]
44 | }
45 | ];
46 | export default nav;
47 |
--------------------------------------------------------------------------------
/react/src/pages/MobileUI/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Card, Row, Col } from 'antd'
3 | import Typing from '../../components/Typing/index'
4 | import Phone from '../../components/Phone'
5 | import { nav, BASE_URL } from './config'
6 | import './index.less'
7 |
8 | function MobileUI() {
9 | const [path, setPath] = useState('')
10 |
11 | function Item(props) {
12 | const item = props.item
13 | return (
14 |
15 | {item.title}
16 |
17 | {item.items.map(sub => - setPath(sub.path)} key={sub.path}>{sub.title}
)}
18 |
19 |
20 | )
21 | }
22 |
23 | return (
24 |
25 |
26 |
27 |
28 | 代码和样式参考了
29 | antd、
30 | vant、
31 | react-components
32 | 项目地址
33 | 预览地址
34 |
35 |
36 | {nav.slice(0, 2).map(item => )}
37 |
38 |
39 | {nav.slice(2, 4).map(item => )}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | )
48 | }
49 |
50 | export default MobileUI
--------------------------------------------------------------------------------
/react/src/pages/MobileUI/index.less:
--------------------------------------------------------------------------------
1 | .nav-item{
2 | cursor: pointer;
3 | color: #0366d6;
4 | }
--------------------------------------------------------------------------------
/react/src/pages/Users/CreateUserModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Modal, Form, Input, message } from 'antd'
3 | import { encrypt } from '../../utils/util'
4 | import { post } from '../../utils/ajax'
5 |
6 | @Form.create()
7 | class CreateUserModal extends Component {
8 | state = {}
9 | onCancel = () => {
10 | this.props.form.resetFields()
11 | this.props.toggleVisible(false)
12 | }
13 | handleOk = () => {
14 | this.props.form.validateFields((errors, values) => {
15 | if (!errors) {
16 | this.onRegister(values)
17 | }
18 | })
19 | }
20 | onRegister = async (values) => {
21 | //加密密码
22 | const ciphertext = encrypt(values.password)
23 | const res = await post('/user/register', {
24 | username: values.username,
25 | password: ciphertext,
26 | })
27 | if (res.status === 0) {
28 | message.success('注册成功')
29 | this.props.onRegister() //注册成功后,要刷新外面的数据
30 | this.onCancel()
31 | }
32 | }
33 | render() {
34 | const { visible } = this.props
35 | const { getFieldDecorator, getFieldValue } = this.props.form
36 | const formItemLayout = {
37 | labelCol: { span: 6 },
38 | wrapperCol: { span: 14 },
39 | }
40 | return (
41 |
48 |
50 | {getFieldDecorator('username', {
51 | validateFirst: true,
52 | rules: [
53 | { required: true, message: '用户名不能为空' },
54 | { pattern: /^[^\s']+$/, message: '不能输入特殊字符' },
55 | { min: 3, message: '用户名至少为3位' }
56 | ]
57 | })(
58 |
61 | )}
62 |
63 |
64 |
65 | {getFieldDecorator('password', {
66 | validateFirst: true,
67 | rules: [
68 | { required: true, message: '密码不能为空' },
69 | { pattern: '^[^ ]+$', message: '密码不能有空格' },
70 | { min: 3, message: '密码至少为3位' },
71 | ]
72 | })(
73 |
78 | )}
79 |
80 |
81 | {getFieldDecorator('confirmPassword', {
82 | validateFirst: true,
83 | rules: [
84 | { required: true, message: '请确认密码' },
85 | {
86 | validator: (rule, value, callback) => {
87 | if (value !== getFieldValue('password')) {
88 | callback('两次输入不一致!')
89 | }
90 | callback()
91 | }
92 | },
93 | ]
94 | })(
95 |
100 | )}
101 |
102 |
103 |
104 | );
105 | }
106 | }
107 |
108 | export default CreateUserModal;
--------------------------------------------------------------------------------
/react/src/pages/Users/InfoModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Modal, Row, Col, Form, Input, Button } from 'antd'
3 | import { createFormField } from '../../utils/util'
4 |
5 |
6 | const form = Form.create({
7 | //表单回显
8 | mapPropsToFields(props) {
9 | return createFormField(props.userInfo)
10 | }
11 | })
12 |
13 | @form
14 | class InfoModal extends Component {
15 | state = {}
16 | render() {
17 | const { getFieldDecorator } = this.props.form
18 | const formItemLayout = {
19 | labelCol: {
20 | span: 10
21 | },
22 | wrapperCol: {
23 | span: 14
24 | },
25 | };
26 | return (
27 | 确定}
33 | title='用户注册信息'>
34 |
83 |
84 |
85 | );
86 | }
87 | }
88 |
89 | export default InfoModal;
--------------------------------------------------------------------------------
/react/src/pages/tabs.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import LoadableComponent from '../utils/LoadableComponent'
3 | //const Test = React.lazy(() => import('./Test')); //报错,就没用React.lazy了
4 | const ButtonDemo = LoadableComponent(import('./ButtonDemo/index'), true);
5 | const IconDemo = LoadableComponent(import('./IconDemo/index'), true);
6 | const FeedbackDemo = LoadableComponent(import('./FeedbackDemo/index'), true);
7 | const Users = LoadableComponent(import('./Users/index'), true);
8 | const Collection = LoadableComponent(import('./Collection/index'), true);
9 | const MessageBoard = LoadableComponent(import('./MessageBoard/index'), true);
10 | const Chat = LoadableComponent(import('./Chat/index'), true);
11 | const About = LoadableComponent(import('./About/index'), true);
12 | const MobileUI = LoadableComponent(import('./MobileUI'), true);
13 |
14 |
15 | const menu = [
16 | {
17 | name: 'antd',
18 | icon: 'ant-design',
19 | key: 'antd',
20 | children: [
21 | {
22 | name: '按钮',
23 | icon: '',
24 | key: 'ButtonDemo',
25 | },
26 | {
27 | name: '图标',
28 | icon: '',
29 | key: 'IconDemo',
30 | },
31 | {
32 | name: '反馈',
33 | icon: '',
34 | key: 'FeedbackDemo',
35 | },
36 | ]
37 | },
38 | {
39 | name: '用户管理',
40 | icon: 'user',
41 | key: 'Users'
42 | },
43 | {
44 | name: '移动端UI',
45 | icon: 'bulb',
46 | key: 'MobileUI'
47 | },
48 | {
49 | name: '作品集',
50 | icon: 'bulb',
51 | key: 'Collection'
52 | },
53 | {
54 | name: '留言板',
55 | icon: 'message',
56 | key: 'MessageBoard'
57 | },
58 | {
59 | name: '聊天室',
60 | icon: 'qq',
61 | key: 'Chat'
62 | },
63 | {
64 | name: '关于',
65 | icon: 'info-circle',
66 | key: 'About'
67 | }
68 | ]
69 |
70 | const tabs = {
71 | ButtonDemo: ,
72 | IconDemo: ,
73 | FeedbackDemo: ,
74 | Users: ,
75 | MobileUI: ,
76 | Collection: ,
77 | MessageBoard: ,
78 | Chat: ,
79 | About: ,
80 |
81 | }
82 |
83 | export {
84 | menu,
85 | tabs
86 | }
--------------------------------------------------------------------------------
/react/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/react/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { json } from '../utils/ajax'
3 | import { notification, Avatar } from 'antd'
4 | import { replaceImg } from '../utils/util'
5 |
6 | // 虽然用户信息放在localStorage也可以全局使用,但是如果放在localStorage中,用户信息改变时页面不会实时更新
7 | export const SET_USER = 'SET_USER'
8 | export function setUser(user) {
9 | return {
10 | type: SET_USER,
11 | user
12 | }
13 | }
14 |
15 | //异步action,从后台获取用户信息
16 | export function getUser(param) {
17 | return async function (dispatch) {
18 | const res = await json.get('/user/getUser', param)
19 | dispatch(setUser(res.data || {}))
20 | }
21 | }
22 |
23 | export const SET_WEBSOCKET = 'SET_WEBSOCKET' //设置websocket对象
24 | export function setWebsocket(websocket) {
25 | return {
26 | type: SET_WEBSOCKET,
27 | websocket
28 | }
29 | }
30 |
31 |
32 | export function initWebSocket(user) { //初始化websocket对象
33 | return async function (dispatch) {
34 | const websocket = new WebSocket("ws://" + window.location.hostname + ":8081")
35 | //建立连接时触发
36 | websocket.onopen = function () {
37 | const data = {
38 | id: user.id,
39 | username: user.username,
40 | avatar: user.avatar
41 | }
42 | //当用户第一次建立websocket链接时,发送用户信息到后台,告诉它是谁建立的链接
43 | websocket.send(JSON.stringify(data))
44 | }
45 | //监听服务端的消息事件
46 | websocket.onmessage = function (event) {
47 | const data = JSON.parse(event.data)
48 | //在线人数变化的消息
49 | if (data.type === 0) {
50 | dispatch(setOnlinelist(data.msg.onlineList))
51 | data.msg.text && notification.info({
52 | message: '提示',
53 | description: data.msg.text
54 | })
55 | }
56 | //聊天的消息
57 | if (data.type === 1) {
58 | dispatch(addChat(data.msg))
59 | notification.open({
60 | message: data.msg.username,
61 | description: ,
62 | icon:
63 | })
64 | }
65 | console.log(11, data)
66 | }
67 | dispatch(setWebsocket(websocket))
68 | dispatch(initChatList())
69 | }
70 | }
71 |
72 | export const SET_ONLINELIST = 'SET_ONLINELIST' //设置在线列表
73 | export function setOnlinelist(onlineList) {
74 | return {
75 | type: SET_ONLINELIST,
76 | onlineList
77 | }
78 | }
79 |
80 | //异步action,初始化聊天记录列表
81 | export function initChatList() {
82 | return async function (dispatch) {
83 | const res = await json.get('/chat/list')
84 | dispatch(setChatList(res.data || []))
85 | }
86 | }
87 |
88 | export const SET_CHATLIST = 'SET_CHATLIST'
89 | export function setChatList(chatList) {
90 | return {
91 | type: SET_CHATLIST,
92 | chatList
93 | }
94 | }
95 |
96 | export const ADD_CHAT = 'ADD_CHAT'
97 | export function addChat(chat) {
98 | return {
99 | type: ADD_CHAT,
100 | chat
101 | }
102 | }
--------------------------------------------------------------------------------
/react/src/store/index.js:
--------------------------------------------------------------------------------
1 | import {createStore,applyMiddleware} from 'redux'
2 | import rootReducer from './reducers'
3 | import thunk from 'redux-thunk'
4 |
5 | const rootStore = createStore(rootReducer,applyMiddleware(thunk))
6 |
7 | export default rootStore
--------------------------------------------------------------------------------
/react/src/store/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { SET_USER, SET_WEBSOCKET, SET_ONLINELIST, SET_CHATLIST, ADD_CHAT } from './actions'
3 |
4 | /**
5 | * 用户信息
6 | * @param {*} state
7 | * @param {*} action
8 | */
9 | function user(state = {}, action) {
10 | switch (action.type) {
11 | case SET_USER: {
12 | return action.user
13 | }
14 | default:
15 | return state
16 | }
17 | }
18 |
19 | /**
20 | * websocket对象
21 | * @param {*} state
22 | * @param {*} action
23 | */
24 | function websocket(state = null, action) {
25 | switch (action.type) {
26 | case SET_WEBSOCKET: {
27 | return action.websocket
28 | }
29 | default:
30 | return state
31 | }
32 | }
33 |
34 | /**
35 | * 在线列表
36 | * @param {*} state
37 | * @param {*} action
38 | */
39 | function onlineList(state = [], action) {
40 | switch (action.type) {
41 | case SET_ONLINELIST: {
42 | return action.onlineList
43 | }
44 | default:
45 | return state
46 | }
47 | }
48 |
49 | /**
50 | * 聊天记录
51 | * @param {*} state
52 | * @param {*} action
53 | */
54 | function chatList(state = [], action) {
55 | switch (action.type) {
56 | case SET_CHATLIST: {
57 | return action.chatList
58 | }
59 | case ADD_CHAT: {
60 | return [...state, action.chat]
61 | }
62 | default:
63 | return state
64 | }
65 | }
66 |
67 |
68 | const rootReducer = combineReducers({
69 | user,
70 | websocket,
71 | onlineList,
72 | chatList
73 | })
74 |
75 | export default rootReducer
--------------------------------------------------------------------------------
/react/src/styles/main.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/react/src/styles/main.less
--------------------------------------------------------------------------------
/react/src/styles/vars.less:
--------------------------------------------------------------------------------
1 | // This file will contain all varibales, our custom varibales and
2 | //those from Ant Design which we want to override.
3 | @import "~antd/lib/style/themes/default.less";
4 | @primary-color: #13C2C2;
5 |
6 |
7 | /* 非antd组件如何使用@primary-color ?
8 | 1.在用的地方@import "vars.less" 然后直接使用@primary-color,然而颜色不会实时切换。
9 | 2.可以在vars.less文件或main.less定义样式,如:
10 | .test{
11 | color:@primary-color;
12 | }
13 | 然后直接使用类名即可(不需要再import,不过每次都要单独定义样式很麻烦)
14 | 3.定义css变量,在使用的地方直接使用css变量(推荐这种方法,首次设置需要重启项目)
15 | */
16 |
17 | :root {
18 | --primaryColor: @primary-color;
19 | }
--------------------------------------------------------------------------------
/react/src/utils/LoadableComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Loadable from 'react-loadable'
3 | import Loading from '../components/Loading'
4 |
5 |
6 | /**
7 | *
8 | * @param {*} component
9 | * @param {*} haveLoading 组件加载时是否有loading效果
10 | */
11 | const LoadableComponent = (component, haveLoading = false) => {
12 | return Loadable({
13 | loader: () => component,
14 | loading: () => {
15 | if (haveLoading) {
16 | return
17 | }
18 | return null
19 | }
20 | })
21 | }
22 |
23 | export default LoadableComponent
--------------------------------------------------------------------------------
/react/src/utils/ajax.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch'
2 | import { message } from 'antd'
3 | import { logout } from '../utils/session'
4 | import history from './history'
5 |
6 | const BASE_URL = process.env.REACT_APP_BASE_URL || ''
7 |
8 | /**
9 | * 处理url
10 | * @param url
11 | * @param param
12 | * @returns {*}
13 | */
14 | function handleURL(url, param) {
15 | let completeUrl = ''
16 | if (url.match(/^(https?:\/\/)([0-9a-z.]+)(:[0-9]+)?([/0-9a-z.]+)?(\?[0-9a-z&=]+)?(#[0-9-a-z]+)?/i)) {
17 | completeUrl = url
18 | } else {
19 | completeUrl = BASE_URL + url
20 | }
21 | if (param) {
22 | if (completeUrl.indexOf('?') === -1) {
23 | completeUrl = `${completeUrl}?${ObjToURLString(param)}`
24 | } else {
25 | completeUrl = `${completeUrl}&${ObjToURLString(param)}`
26 | }
27 | }
28 | return completeUrl
29 | }
30 |
31 | /**
32 | * 将参数对象转化为'test=1&test2=2'这种字符串形式
33 | * @param param
34 | * @returns {string}
35 | * @constructor
36 | */
37 | function ObjToURLString(param) {
38 | let str = ''
39 | if (Object.prototype.toString.call(param) === '[object Object]') {
40 | const list = Object.entries(param).map(item => {
41 | return `${item[0]}=${item[1]}`
42 | })
43 | str = list.join('&')
44 | }
45 | return str
46 | }
47 |
48 | export async function get(url, param) {
49 | const completeUrl = handleURL(url, param)
50 | const response = await fetch(completeUrl, {
51 | credentials: 'include',
52 | xhrFields: {
53 | withCredentials: true //跨域
54 | },
55 | })
56 | const reslut = await response.json()
57 | if (!response.ok) {
58 | if(response.status === 401){
59 | logout()
60 | history.push('/login')
61 | }
62 | message.error(reslut.message || '网络错误')
63 | }
64 | return reslut
65 |
66 | }
67 |
68 | export async function post(url, parma) {
69 | let completeUrl = ''
70 | if (url.match(/^(https?:\/\/)([0-9a-z.]+)(:[0-9]+)?([/0-9a-z.]+)?(\?[0-9a-z&=]+)?(#[0-9-a-z]+)?/i)) {
71 | completeUrl = url
72 | } else {
73 | completeUrl = BASE_URL + url
74 | }
75 | const response = await fetch(completeUrl, {
76 | credentials: 'include',
77 | method: 'POST',
78 | xhrFields: {
79 | withCredentials: true
80 | },
81 | headers: {
82 | 'Content-Type': 'application/json',
83 | },
84 | body: JSON.stringify(parma),
85 | })
86 | const reslut = await response.json()
87 | if (!response.ok) {
88 | if(response.status === 401){
89 | logout()
90 | history.push('/login')
91 | }
92 | message.error(reslut.message || '网络错误')
93 | }
94 | return reslut
95 | }
96 |
97 | export const json = {
98 | get,
99 | post
100 | }
101 |
--------------------------------------------------------------------------------
/react/src/utils/asyncComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | export default function asyncComponent(importComponent) {
4 | class AsyncComponent extends Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | this.state = {
9 | component: null
10 | };
11 | }
12 |
13 | async componentDidMount() {
14 | const { default: component } = await importComponent();
15 |
16 | this.setState({
17 | component: component
18 | });
19 | }
20 |
21 | render() {
22 | const C = this.state.component;
23 |
24 | return C ? : null;
25 | }
26 | }
27 |
28 | return AsyncComponent;
29 | }
--------------------------------------------------------------------------------
/react/src/utils/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history'
2 |
3 |
4 | const env = process.env.NODE_ENV // 环境参数
5 | let options = {}
6 | if (env === 'production') {
7 | options.basename = '/admin'
8 | }
9 |
10 |
11 | export default createBrowserHistory(options)
--------------------------------------------------------------------------------
/react/src/utils/session.js:
--------------------------------------------------------------------------------
1 | const LOGIN_COOKIE_NAME = 'sessionId'
2 |
3 | export function isAuthenticated() {
4 | return _getCookie(LOGIN_COOKIE_NAME)
5 | }
6 |
7 | export function authenticateSuccess(token) {
8 | _setCookie(LOGIN_COOKIE_NAME, token)
9 | }
10 |
11 | export function logout() {
12 | _setCookie(LOGIN_COOKIE_NAME, '', 0)
13 | }
14 |
15 | function _getCookie(name) {
16 | let start, end
17 | if (document.cookie.length > 0) {
18 | start = document.cookie.indexOf(name + '=')
19 | if (start !== -1) {
20 | start = start + name.length + 1
21 | end = document.cookie.indexOf(';', start)
22 | if (end === -1) {
23 | end = document.cookie.length
24 | }
25 | return unescape(document.cookie.substring(start, end))
26 | }
27 | }
28 | return ''
29 | }
30 |
31 | function _setCookie(name, value, expire) {
32 | let date = new Date()
33 | date.setDate(date.getDate() + expire)
34 | document.cookie = name + '=' + escape(value) + '; path=/' +
35 | (expire ? ';expires=' + date.toGMTString() : '')
36 | }
--------------------------------------------------------------------------------
/react/src/utils/util.js:
--------------------------------------------------------------------------------
1 | import CryptoJS from 'crypto-js'
2 | import { SECRETKEY } from '../config/secret'
3 | import { Form } from 'antd'
4 |
5 | /**
6 | * 防抖函数
7 | * @param {*} func
8 | * @param {*} wait
9 | */
10 | export function debounce(func, wait = 500) {
11 | let timeout; // 定时器变量
12 | return function (event) {
13 | clearTimeout(timeout); // 每次触发时先清除上一次的定时器,然后重新计时
14 | event.persist && event.persist() //保留对事件的引用
15 | timeout = setTimeout(() => {
16 | func(event)
17 | }, wait); // 指定 xx ms 后触发真正想进行的操作 handler
18 | };
19 | }
20 |
21 | /**
22 | * 节流函数
23 | * @param {*} func
24 | * @param {*} interval
25 | */
26 | export function throttle(func, interval = 100) {
27 | let timeout;
28 | let startTime = new Date();
29 | return function (event) {
30 | event.persist && event.persist() //保留对事件的引用
31 | clearTimeout(timeout);
32 | let curTime = new Date();
33 | if (curTime - startTime <= interval) {
34 | //小于规定时间间隔时,用setTimeout在指定时间后再执行
35 | timeout = setTimeout(() => {
36 | func(event);
37 | }, interval)
38 | } else {
39 | //重新计时并执行函数
40 | startTime = curTime;
41 | func(event)
42 | }
43 | }
44 | }
45 |
46 | /**
47 | * 生成指定区间的随机整数
48 | * @param min
49 | * @param max
50 | * @returns {number}
51 | */
52 | export function randomNum(min, max) {
53 | return Math.floor(Math.random() * (max - min) + min);
54 | }
55 |
56 | /**
57 | * 加密函数,加密同一个字符串生成的都不相同
58 | * @param {*} str
59 | */
60 | export function encrypt(str) {
61 | return CryptoJS.AES.encrypt(JSON.stringify(str), SECRETKEY).toString();
62 | }
63 |
64 | /**
65 | * 解密函数
66 | * @param {*} str
67 | */
68 | export function decrypt(str) {
69 | const bytes = CryptoJS.AES.decrypt(str, SECRETKEY);
70 | return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
71 | }
72 |
73 | /**
74 | * 判断是否是对象
75 | * @param {*} obj
76 | */
77 | export function isObject(obj) {
78 | return Object.prototype.toString.call(obj) === '[object Object]'
79 | }
80 |
81 | /**
82 | * 创建表单回显的对象
83 | * @param {*} obj
84 | */
85 | export function createFormField(obj) {
86 | let target = {}
87 | if (isObject(obj)) {
88 | for (let [key, value] of Object.entries(obj)) {
89 | target[key] = Form.createFormField({
90 | value
91 | })
92 | }
93 | }
94 | return target
95 | }
96 |
97 | /**
98 | * 将img标签转换为【图片】
99 | * @param {string} str
100 | */
101 | export function replaceImg(str){
102 | if(typeof str === 'string'){
103 | str = str.replace(/
/g, "[图片]")
104 | }
105 | return str
106 | }
107 |
108 | /**
109 | * 图片预加载
110 | * @param arr
111 | * @constructor
112 | */
113 | export function preloadingImages(arr) {
114 | if(Array.isArray(arr)){
115 | arr.forEach(item=>{
116 | const img = new Image()
117 | img.src = item
118 | })
119 | }
120 | }
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa')
2 | const app = new Koa()
3 | const views = require('koa-views')
4 | const json = require('koa-json')
5 | const onerror = require('koa-onerror')
6 | const bodyparser = require('koa-bodyparser')
7 | const logger = require('koa-logger')
8 | const cors = require('koa2-cors');
9 | const historyApiFallback = require('koa-history-api-fallback')
10 | const { TOKEN_SECRETKEY } = require('./config/secret')
11 | const jwt = require('koa-jwt')
12 | const errorHandle = require('./middlewares/errorHandle')
13 | const koaStatic = require('koa-static')
14 | const chat = require('./chat')
15 |
16 | const index = require('./routes/index')
17 | const user = require('./routes/user')
18 | const works = require('./routes/works')
19 | const message = require('./routes/message')
20 | const score = require('./routes/score')
21 |
22 | // error handler
23 | onerror(app)
24 |
25 | // middlewares
26 | app.use(bodyparser({
27 | enableTypes: ['json', 'form', 'text']
28 | }))
29 | app.use(json())
30 | app.use(logger())
31 |
32 | app.use(views(__dirname + '/public/build'))
33 |
34 | // logger
35 | app.use(async (ctx, next) => {
36 | const start = new Date()
37 | await next()
38 | const ms = new Date() - start
39 | console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
40 | })
41 |
42 | app.use(errorHandle)
43 |
44 | app.use(cors({ credentials: true })); //前端调试时解决跨域,上线不用跨域
45 |
46 | //验证token登陆,unless是不需要验证的路由,每一项是匹配路由的正则
47 | const unPath = [/^\/$/, /public/, /checkName/, /register/, /getIpInfo/, /login/]
48 | const buildFiles = [/\.js$/, /\.css$/, /\.less$/, /\.ico/, /\.json$/, /static/] //前端打包后不需要验证的资源
49 | app.use(jwt({ secret: TOKEN_SECRETKEY, cookie: 'sessionId' }).unless({ path: unPath.concat(buildFiles) }));
50 |
51 |
52 | // routes
53 | app.use(index.routes(), index.allowedMethods())
54 | app.use(user.routes(), user.allowedMethods())
55 | app.use(works.routes(), works.allowedMethods())
56 | app.use(message.routes(), message.allowedMethods())
57 | app.use(score.routes(), score.allowedMethods())
58 |
59 | //一定要写在路由后面,写在前面就不会返回接口内容,而是直接返回首页了
60 | app.use(historyApiFallback()); // 在这个地方加入。一定要加在静态文件的serve之前,否则会失效。
61 | app.use(koaStatic(__dirname, { maxage: 604800000 })) //一周的缓存时间,单位ms
62 | app.use(koaStatic(__dirname + '/public/build', { maxage: 604800000 }))
63 | app.use(koaStatic(__dirname + '/public/upload-files', { maxage: 604800000 }))
64 |
65 | // error-handling
66 | app.on('error', (err, ctx) => {
67 | console.error('server error', err, ctx)
68 | });
69 |
70 | module.exports = app
71 |
--------------------------------------------------------------------------------
/server/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('demo:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '8888');
16 | // app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app.callback());
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/server/chat.js:
--------------------------------------------------------------------------------
1 | const ws = require('nodejs-websocket')
2 | const { addChat } = require('./controller/chat')
3 |
4 | //当前在线列表
5 | let onlineList = []
6 | //信息类型
7 | const msgType = {
8 | onlineInfo: 0, //关于在线列表
9 | chatInfo: 1 //关于聊天内容
10 | }
11 |
12 | //对象数组去重
13 | function unique(arr) {
14 | const obj = {}
15 | const result = arr.reduce((total, cur) => {
16 | if (!obj[cur.id]) {
17 | obj[cur.id] = total.push(cur)
18 | }
19 | return total
20 | }, [])
21 | return result
22 | }
23 |
24 | const server = ws.createServer(function (connection) {
25 | connection.user = {}
26 |
27 | connection.on('text', function (str) {
28 | const info = JSON.parse(str)
29 | if (!connection.user.id) {
30 | connection.user = info
31 | //防止同一个账号在同一个浏览器中的不同窗口重复上线
32 | const isExist = onlineList.find(item => item.id === connection.user.id)
33 | onlineList.push(info)
34 | const data = {
35 | onlineList: unique(onlineList),
36 | text: isExist ? '' : `用户${connection.user.username}已上线`
37 | }
38 | broadcast(data, msgType.onlineInfo)
39 | } else {
40 | //当用户修改头像或昵称时,修改connection.user,onlineList不用修改,因为userid不会变
41 | if (info.id) {
42 | connection.user = info
43 | return
44 | }
45 | const data = {
46 | userId: connection.user.id,
47 | username: connection.user.username,
48 | userAvatar: connection.user.avatar,
49 | createTime: Date.now(),
50 | content: info.content
51 | }
52 | addChat(data)
53 | broadcast(data)
54 | }
55 | })
56 | // 断开连接
57 | connection.on('close', function (code, reason) {
58 | //当同一个账号在同个浏览器的多个窗口打开时,会有多个userId相同的连接,如果用filter就全部下线了,我们应该只删除当前窗口的连接
59 | // onlineList = onlineList.filter(item => item.id !== connection.user.id)
60 | const index = onlineList.findIndex(item => item.id === connection.user.id)
61 | onlineList.splice(index, 1) //只删除一个连接
62 | const isExist = onlineList.find(item => item.id === connection.user.id)
63 | const data = {
64 | onlineList: unique(onlineList),
65 | text: isExist ? '' : `用户${connection.user.username}已下线`
66 | }
67 | broadcast(data, msgType.onlineInfo)
68 | })
69 |
70 | // 连接错误,一定要有这个错误事件,当前台刷新页面时,后台会报错,并被这里捕获,这样不会导致后台错误
71 | connection.on('error', function (error) {
72 | console.log(error)
73 | })
74 |
75 | })
76 |
77 |
78 | //广播
79 | function broadcast(msg, type = msgType.chatInfo) {
80 | server.connections.forEach(function (connection) {
81 | connection.sendText(JSON.stringify({
82 | type,
83 | msg
84 | }))
85 | })
86 | }
87 |
88 | server.listen(8081)
89 |
--------------------------------------------------------------------------------
/server/config/db.js:
--------------------------------------------------------------------------------
1 | const env = process.env.NODE_ENV // 环境参数
2 | let MYSQL_CONF = null
3 |
4 |
5 | if (env === 'dev') {
6 | MYSQL_CONF = {
7 | host: 'localhost',
8 | user: 'root',
9 | password: '1234567890',
10 | port: '3306',
11 | database: 'admin',
12 | charset:'utf8mb4' //字符集一定要写,否则表情包存储不了
13 | }
14 | }
15 | if (env === 'production') {
16 | MYSQL_CONF = {
17 | host: 'localhost',
18 | user: 'root',
19 | password: '1234567890',
20 | port: '3306',
21 | database: 'admin',
22 | charset:'utf8mb4' //字符集一定要写,否则表情包存储不了
23 | }
24 | }
25 | module.exports = {
26 | MYSQL_CONF
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/server/config/secret.js:
--------------------------------------------------------------------------------
1 | const FRONT_SECRETKEY = 'front_666666'
2 | const BACKEND_SECRETKEY = 'backend_666666'
3 | const TOKEN_SECRETKEY = 'token_666666'
4 |
5 | module.exports = {
6 | FRONT_SECRETKEY,
7 | BACKEND_SECRETKEY,
8 | TOKEN_SECRETKEY
9 | }
--------------------------------------------------------------------------------
/server/controller/chat.js:
--------------------------------------------------------------------------------
1 | const { exec } = require('../db/mysql')
2 | const { SuccessModel, ErrorModel } = require('../model/resModel')
3 |
4 | /**
5 | * 添加聊天记录
6 | * @param {*} param
7 | */
8 | const addChat = async (param) => {
9 | const { userId, username, userAvatar, createTime, content } = param
10 | const sql = `insert into chats (userId,username,userAvatar,createTime,content) values
11 | (${userId},'${username}','${userAvatar}',${createTime},'${content}')
12 | `
13 | const res = await exec(sql)
14 | return new SuccessModel({
15 | data: {
16 | id: res.insertId
17 | }
18 | })
19 | }
20 |
21 | /**
22 | * 获取最新聊天记录前100条
23 | */
24 | const getChatList = async ()=>{
25 | const sql = `select * from chats order by createTime DESC limit 100`
26 | const res = await exec(sql)
27 | return new SuccessModel({
28 | data:res.reverse()
29 | })
30 | }
31 |
32 | module.exports = {
33 | addChat,
34 | getChatList
35 | }
--------------------------------------------------------------------------------
/server/controller/message.js:
--------------------------------------------------------------------------------
1 | const { exec } = require('../db/mysql')
2 | const jwt = require('jsonwebtoken');
3 | const { TOKEN_SECRETKEY } = require('../config/secret')
4 | const { SuccessModel, ErrorModel } = require('../model/resModel')
5 | const { getUser } = require('./user')
6 |
7 | /**
8 | * 创建留言
9 | * @param {*} param
10 | */
11 | const createMessage = async (param, sessionId) => {
12 | const loginName = jwt.verify(sessionId, TOKEN_SECRETKEY).username
13 | const userRes = await getUser({ username: loginName })
14 | const user = userRes.data || {}
15 |
16 | let insertObj = {
17 | type: param.type || 0,
18 | pid: param.pid || -1,
19 | createTime: Date.now(),
20 | content: `'${param.content}'` || '',
21 | userId: user.id,
22 | userIsAdmin: user.isAdmin,
23 | userName: `'${user.username}'`,
24 | userAvatar: `'${user.avatar}'`
25 | }
26 | if (param.type === 1) {
27 | const targetUserRes = await getUser({ id: param.targetUserId })
28 | const targetUser = targetUserRes.data || {}
29 | insertObj = {
30 | ...insertObj,
31 | targetUserId: targetUser.id,
32 | targetUserIsAdmin: targetUser.isAdmin,
33 | targetUserName: `'${targetUser.username}'`,
34 | targetUserAvatar: `'${targetUser.avatar}'`,
35 | }
36 | }
37 | const sql = `insert into messages (${Object.keys(insertObj).join(',')}) values (${Object.values(insertObj).join(',')})`
38 | const res = await exec(sql)
39 | if (res.affectedRows) {
40 | return new SuccessModel({
41 | data: {
42 | id: res.insertId
43 | },
44 | message: '新增成功'
45 | })
46 | } else {
47 | return new ErrorModel({
48 | message: '新增失败'
49 | })
50 | }
51 | }
52 |
53 | /**
54 | * 获取留言列表
55 | */
56 | const getMessages = async (query) => {
57 | const { current = 0, pageSize = 10 } = query
58 | // 获取留言信息
59 | const sqlMsg = `select SQL_CALC_FOUND_ROWS * from messages where pid=-1 order by createTime DESC limit ${current * pageSize},${pageSize}`
60 | const resMsg = await exec(sqlMsg)
61 |
62 | // 获取总留言数
63 | const sqlTotal = 'select found_rows() as total'
64 | const resTotal = await exec(sqlTotal)
65 |
66 | // 获取对应页的回复数据
67 | const pids = Array.isArray(resMsg) ? resMsg.map(i => i.id) : []
68 | let resReply = []
69 | if (pids.length) {
70 | const sqlReply = `select * from messages where pid in (${pids.join(',')}) order by createTime`
71 | resReply = await exec(sqlReply)
72 | }
73 |
74 | const list = resMsg.map(item => {
75 | const children = resReply.filter(i => i.pid === item.id)
76 | return {
77 | ...item,
78 | children
79 | }
80 | })
81 |
82 | return new SuccessModel({
83 | data: {
84 | list,
85 | current: parseInt(current),
86 | pageSize: parseInt(pageSize),
87 | total: resTotal[0].total,
88 | }
89 | })
90 | }
91 |
92 | /**
93 | * 删除留言
94 | * @param {*} param
95 | * @param {*} sessionId
96 | */
97 | const deleteMessage = async (param, sessionId) => {
98 | const loginName = jwt.verify(sessionId, TOKEN_SECRETKEY).username
99 | const userRes = await getUser({ username: loginName })
100 | const user = userRes.data || {}
101 | const sql = `select userId from messages where id=${param.id}`
102 | const res = await exec(sql)
103 | if (user.id !== res[0].userId && !user.isAdmin) {
104 | return new ErrorModel({
105 | message: '暂无权限'
106 | })
107 | }
108 | const sql2 = `delete from messages where id=${param.id} or pid=${param.id}`
109 | const res2 = await exec(sql2)
110 | return new SuccessModel({
111 | message: `成功删除${res2.affectedRows}条数据`,
112 | })
113 | }
114 |
115 | /**
116 | * 当更新用户名或用户头像时,更新他留言的用户名和头像
117 | * @param {*} user
118 | */
119 | const updateUserMessage = (user) => {
120 | const sql = `update messages set userIsAdmin=${user.isAdmin},userName='${user.username}',userAvatar='${user.avatar}' where userId=${user.id}`
121 | const sql2 = `update messages set targetUserIsAdmin=${user.isAdmin},targetUserName='${user.username}',targetUserAvatar='${user.avatar}' where targetUserId=${user.id}`
122 | Promise.all([exec(sql), exec(sql2)]).then(([res, res2]) => {
123 | console.log(444, res)
124 | console.log(555, res2)
125 | })
126 | }
127 |
128 | module.exports = {
129 | createMessage,
130 | getMessages,
131 | deleteMessage,
132 | updateUserMessage
133 | }
--------------------------------------------------------------------------------
/server/controller/score.js:
--------------------------------------------------------------------------------
1 | const { exec } = require('../db/mysql')
2 | const { SuccessModel, ErrorModel } = require('../model/resModel')
3 |
4 | /**
5 | * 获取用户评分列表
6 | */
7 | const getScores = async () => {
8 | const sql = 'select * from scores'
9 | const res = await exec(sql)
10 | return new SuccessModel({
11 | data: res
12 | })
13 | }
14 |
15 | /**
16 | * 用户评分
17 | * @param {*} param
18 | */
19 | const createScore = async (param) => {
20 | const { userId, score } = param
21 | if (userId === undefined || score === undefined) {
22 | return new ErrorModel({
23 | message: '参数异常'
24 | })
25 | }
26 | const res = await exec(`select userId from scores where userId=${userId}`)
27 | if (res.length) {
28 | return new ErrorModel({
29 | message: '用户已经评过分'
30 | })
31 | }
32 | const sql = `insert into scores (createTime,userId,score) values (${Date.now()},${userId},${score})`
33 | const res2 = await exec(sql)
34 | if (res2.affectedRows) {
35 | return new SuccessModel({
36 | message: '评分成功'
37 | })
38 | } else {
39 | return new ErrorModel({
40 | message: '评分失败'
41 | })
42 | }
43 | }
44 |
45 | module.exports = {
46 | getScores,
47 | createScore
48 | }
--------------------------------------------------------------------------------
/server/controller/works.js:
--------------------------------------------------------------------------------
1 |
2 | const { exec } = require('../db/mysql')
3 | const jwt = require('jsonwebtoken');
4 | const { TOKEN_SECRETKEY } = require('../config/secret')
5 | const { SuccessModel, ErrorModel } = require('../model/resModel')
6 | const { getUser } = require('./user')
7 |
8 |
9 | /**
10 | * 判断是否是管理员
11 | * @param {*} loginName
12 | */
13 | async function isAdmin(loginName) {
14 | const userRes = await getUser({ username: loginName })
15 | if (userRes.status === 0) {
16 | return userRes.data.isAdmin
17 | }
18 | return false
19 | }
20 |
21 | /**
22 | * 创建作品
23 | * @param {*} param
24 | * @param {*} token
25 | */
26 | const createWorks = async (param, sessionId) => {
27 | const { title, description, url, githubUrl } = param
28 | const loginName = jwt.verify(sessionId, TOKEN_SECRETKEY).username
29 | const isAdminRes = await isAdmin(loginName)
30 | if (!isAdminRes) {
31 | return new ErrorModel({
32 | message: '暂无权限'
33 | })
34 | }
35 | const sql = `insert into works (author,createTime,title,description, url, githubUrl) values
36 | ('${loginName}',${Date.now()},'${title}','${description}','${url}','${githubUrl}')`
37 | const res = await exec(sql)
38 | if (res.affectedRows) {
39 | return new SuccessModel({
40 | data: { id: res.insertId },
41 | message: '创建成功'
42 | })
43 | } else {
44 | return new ErrorModel({
45 | message: '创建失败',
46 | httpCode: 500
47 | })
48 | }
49 |
50 | }
51 | /**
52 | * 获取作品列表
53 | */
54 | const getWorks = async () => {
55 | const sql = `select * from works order by id DESC`
56 | const res = await exec(sql)
57 | return new SuccessModel({
58 | data: res
59 | })
60 | }
61 |
62 | const deleteWorks = async (param, sessionId) => {
63 | const loginName = jwt.verify(sessionId, TOKEN_SECRETKEY).username
64 | const isAdminRes = await isAdmin(loginName)
65 | if (!isAdminRes) {
66 | return new ErrorModel({
67 | message: '暂无权限'
68 | })
69 | }
70 | const ids = param.ids
71 | if (!Array.isArray(ids)) {
72 | return new ErrorModel({
73 | message: '参数异常',
74 | httpCode: 400
75 | })
76 | }
77 | const sql = `delete from works where id in (${ids.join(',')})`
78 | const res = await exec(sql)
79 | return new SuccessModel({
80 | message: `成功删除${res.affectedRows}条数据`
81 | })
82 | }
83 |
84 | module.exports = {
85 | createWorks,
86 | getWorks,
87 | deleteWorks
88 | }
89 |
--------------------------------------------------------------------------------
/server/db/mysql.js:
--------------------------------------------------------------------------------
1 | const mysql = require('mysql')
2 | const { MYSQL_CONF } = require('../config/db')
3 |
4 | // 创建链接对象
5 | const con = mysql.createConnection(MYSQL_CONF)
6 |
7 | // 开始链接
8 | con.connect()
9 |
10 | // 统一执行 sql 的函数
11 | function exec(sql) {
12 | const promise = new Promise((resolve, reject) => {
13 | con.query(sql, (err, result) => {
14 | if (err) {
15 | reject(err)
16 | return
17 | }
18 | resolve(result)
19 | })
20 | })
21 | return promise
22 | }
23 |
24 | module.exports = {
25 | exec,
26 | }
--------------------------------------------------------------------------------
/server/middlewares/errorHandle.js:
--------------------------------------------------------------------------------
1 | const { ErrorModel } = require('../model/resModel')
2 | module.exports = errorHandle = (ctx, next) => {
3 | return next().catch((err) => {
4 | if (err.status === 401) {
5 | ctx.status = 401;
6 | ctx.body = new ErrorModel({
7 | httpCode: 401,
8 | message: '登陆异常'
9 | })
10 | } else {
11 | throw err;
12 | }
13 | });
14 | }
--------------------------------------------------------------------------------
/server/model/resModel.js:
--------------------------------------------------------------------------------
1 | class SuccessModel{
2 | constructor({data,message}){
3 | this.status = 0
4 | this.httpCode = 200
5 | this.message = message || '请求成功'
6 | this.data = data || {}
7 | }
8 | }
9 | class ErrorModel{
10 | constructor({message,httpCode}){
11 | this.status = 1
12 | this.httpCode = httpCode || 500
13 | this.message = message || '网络错误'
14 | }
15 | }
16 |
17 |
18 | module.exports = {
19 | SuccessModel,
20 | ErrorModel
21 | }
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www",
7 | "dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www",
8 | "prd": "cross-env NODE_ENV=production pm2 start --name admin bin/www",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "dependencies": {
12 | "axios": "^0.19.0",
13 | "busboy": "^0.3.1",
14 | "crypto-js": "^3.1.9-1",
15 | "debug": "^2.6.3",
16 | "koa": "^2.2.0",
17 | "koa-bodyparser": "^3.2.0",
18 | "koa-convert": "^1.2.0",
19 | "koa-history-api-fallback": "^0.2.0",
20 | "koa-json": "^2.0.2",
21 | "koa-jwt": "^3.5.1",
22 | "koa-logger": "^2.0.1",
23 | "koa-onerror": "^1.2.1",
24 | "koa-router": "^7.1.1",
25 | "koa-static": "^3.0.0",
26 | "koa-views": "^5.2.1",
27 | "koa2-cors": "^2.0.6",
28 | "mysql": "^2.17.1",
29 | "nodejs-websocket": "^1.7.2",
30 | "pug": "^2.0.0-rc.1"
31 | },
32 | "devDependencies": {
33 | "cross-env": "^5.2.0",
34 | "nodemon": "^1.8.1"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/server/public/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/images/avatar.png
--------------------------------------------------------------------------------
/server/public/images/bg1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/images/bg1.jpg
--------------------------------------------------------------------------------
/server/public/images/bg2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/images/bg2.jpg
--------------------------------------------------------------------------------
/server/public/images/bg3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/images/bg3.jpg
--------------------------------------------------------------------------------
/server/public/images/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/images/default.png
--------------------------------------------------------------------------------
/server/public/images/login_bg1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/images/login_bg1.jpg
--------------------------------------------------------------------------------
/server/public/images/login_bg2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/images/login_bg2.jpg
--------------------------------------------------------------------------------
/server/public/images/login_bg3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/images/login_bg3.jpg
--------------------------------------------------------------------------------
/server/public/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4 | }
5 |
6 | a {
7 | color: #00B7FF;
8 | }
9 |
--------------------------------------------------------------------------------
/server/public/upload-files/chat/01.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/upload-files/chat/01.jpeg
--------------------------------------------------------------------------------
/server/public/upload-files/chat/1590399528000-ZZrRkP.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/upload-files/chat/1590399528000-ZZrRkP.jpeg
--------------------------------------------------------------------------------
/server/public/upload-files/chat/c6e0629a36c94106be63f8fc72dd977a.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/upload-files/chat/c6e0629a36c94106be63f8fc72dd977a.jpeg
--------------------------------------------------------------------------------
/server/public/upload-files/myUpload/01.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/upload-files/myUpload/01.jpeg
--------------------------------------------------------------------------------
/server/public/upload-files/myUpload/217ad66f504747ca9b613036d3dcd453.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/upload-files/myUpload/217ad66f504747ca9b613036d3dcd453.jpg
--------------------------------------------------------------------------------
/server/public/upload-files/myUpload/67be71bd1a20486289f45005773386f2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/upload-files/myUpload/67be71bd1a20486289f45005773386f2.jpg
--------------------------------------------------------------------------------
/server/public/upload-files/myUpload/ece8c6fae0012978195c0ff364bc4c81.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/upload-files/myUpload/ece8c6fae0012978195c0ff364bc4c81.jpg
--------------------------------------------------------------------------------
/server/public/upload-files/myUpload/浪子康,杨洪才 - Xun(易硕成)-可惜我不是她(咚鼓版)(浪子康 / 杨洪才 remix).mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/z-9527/admin/72aaf2dd05cf4ec2e98f390668b41e128eec5ad2/server/public/upload-files/myUpload/浪子康,杨洪才 - Xun(易硕成)-可惜我不是她(咚鼓版)(浪子康 / 杨洪才 remix).mp3
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')()
2 | const uploadFile = require('../utils/upload')
3 | const path = require('path')
4 | const { getChatList } = require('../controller/chat')
5 |
6 | function handleRes(ctx, next, res) {
7 | if (res.status === 0) {
8 | ctx.body = res
9 | } else {
10 | ctx.status = res.httpCode
11 | ctx.body = res
12 | // ctx.message = res.message //本来想直接设置fetch的statusText,但是加了这句话请求就出错
13 | }
14 | }
15 |
16 | router.get('/', async (ctx, next) => {
17 | await ctx.render('index.html')
18 | })
19 |
20 | //上传接口
21 | router.post('/upload', async (ctx, next) => {
22 | const { isImg, fileType } = ctx.query
23 | const serverFilePath = path.join(__dirname, '../public/upload-files')
24 | const res = await uploadFile(ctx, {
25 | fileType: fileType || 'myUpload', // common or album
26 | path: serverFilePath,
27 | isImg: !!isImg
28 | })
29 | handleRes(ctx, next, res)
30 | })
31 |
32 | router.get('/chat/list', async (ctx, next) => {
33 | const res = await getChatList()
34 | handleRes(ctx, next, res)
35 | })
36 |
37 | router.get('/json', async (ctx, next) => {
38 | console.log(ctx.cookies.get('sessionId'))
39 | ctx.body = {
40 | title: 'koa2 json'
41 | }
42 | })
43 |
44 | module.exports = router
45 |
--------------------------------------------------------------------------------
/server/routes/message.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')()
2 | const { } = require('../controller/message')
3 | const { createMessage, getMessages, deleteMessage } = require('../controller/message')
4 |
5 | router.prefix('/message')
6 |
7 | function handleRes(ctx, next, res) {
8 | if (res.status === 0) {
9 | ctx.body = res
10 | } else {
11 | ctx.status = res.httpCode
12 | ctx.body = res
13 | // ctx.message = res.message //本来想直接设置fetch的statusText,但是加了这句话请求就出错
14 | }
15 | }
16 |
17 | router.post('/create', async function (ctx, next) {
18 | const sessionId = ctx.cookies.get('sessionId')
19 | const res = await createMessage(ctx.request.body, sessionId)
20 | handleRes(ctx, next, res)
21 | })
22 |
23 | router.get('/list', async function (ctx, next) {
24 | const res = await getMessages(ctx.query)
25 | handleRes(ctx, next, res)
26 | })
27 |
28 | router.post('/delete', async function (ctx, next) {
29 | const sessionId = ctx.cookies.get('sessionId')
30 | const res = await deleteMessage(ctx.request.body, sessionId)
31 | handleRes(ctx, next, res)
32 | })
33 |
34 |
35 |
36 |
37 | module.exports = router
38 |
--------------------------------------------------------------------------------
/server/routes/score.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')()
2 | const { getScores, createScore } = require('../controller/score')
3 |
4 | router.prefix('/score')
5 |
6 | function handleRes(ctx, next, res) {
7 | if (res.status === 0) {
8 | ctx.body = res
9 | } else {
10 | ctx.status = res.httpCode
11 | ctx.body = res
12 | // ctx.message = res.message //本来想直接设置fetch的statusText,但是加了这句话请求就出错
13 | }
14 | }
15 |
16 | router.get('/list', async function (ctx, next) {
17 | const res = await getScores()
18 | handleRes(ctx, next, res)
19 | })
20 |
21 | router.post('/create', async function (ctx, next) {
22 | const res = await createScore(ctx.request.body)
23 | handleRes(ctx, next, res)
24 | })
25 |
26 |
27 | module.exports = router
28 |
--------------------------------------------------------------------------------
/server/routes/user.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')()
2 | const { register, checkName, getIpInfo, login, getUsers, getUser, updateUser, deleteUsers, getAllUsers } = require('../controller/user')
3 |
4 | router.prefix('/user')
5 |
6 | function handleRes(ctx, next, res) {
7 | if (res.status === 0) {
8 | ctx.body = res
9 | } else {
10 | ctx.status = res.httpCode
11 | ctx.body = res
12 | // ctx.message = res.message //本来想直接设置fetch的statusText,但是加了这句话请求就出错
13 | }
14 | }
15 |
16 | router.post('/register', async function (ctx, next) {
17 | const { username, password } = ctx.request.body
18 | const res = await register(username, password, ctx)
19 | handleRes(ctx, next, res)
20 | })
21 |
22 | router.post('/login', async function (ctx, next) {
23 | const { username, password } = ctx.request.body
24 | const res = await login(username, password, ctx)
25 | handleRes(ctx, next, res)
26 | })
27 |
28 | router.get('/checkName', async function (ctx, next) {
29 | const { username } = ctx.query
30 | const res = await checkName(username)
31 | handleRes(ctx, next, res)
32 | })
33 |
34 | router.get('/getIpInfo', async function (ctx, next) {
35 | const res = await getIpInfo(ctx)
36 | handleRes(ctx, next, res)
37 | })
38 |
39 | router.get('/getUsers', async function (ctx, next) {
40 | const res = await getUsers(ctx.query)
41 | handleRes(ctx, next, res)
42 | })
43 |
44 | router.get('/getUser', async function (ctx, next) {
45 | const res = await getUser(ctx.query)
46 | handleRes(ctx, next, res)
47 | })
48 |
49 | router.post('/update', async function (ctx, next) {
50 | const sessionId = ctx.cookies.get('sessionId')
51 | const res = await updateUser(ctx.request.body, sessionId)
52 | handleRes(ctx, next, res)
53 | })
54 |
55 | router.post('/delete', async function (ctx, next) {
56 | const res = await deleteUsers(ctx.request.body)
57 | handleRes(ctx, next, res)
58 | })
59 |
60 | router.get('/getAllUsers', async function (ctx, next) {
61 | const res = await getAllUsers()
62 | handleRes(ctx, next, res)
63 | })
64 |
65 | module.exports = router
66 |
--------------------------------------------------------------------------------
/server/routes/works.js:
--------------------------------------------------------------------------------
1 | const router = require('koa-router')()
2 | const { createWorks, getWorks, deleteWorks } = require('../controller/works')
3 |
4 | router.prefix('/works')
5 |
6 | function handleRes(ctx, next, res) {
7 | if (res.status === 0) {
8 | ctx.body = res
9 | } else {
10 | ctx.status = res.httpCode
11 | ctx.body = res
12 | // ctx.message = res.message //本来想直接设置fetch的statusText,但是加了这句话请求就出错
13 | }
14 | }
15 |
16 | router.post('/create', async function (ctx, next) {
17 | const sessionId = ctx.cookies.get('sessionId')
18 | const res = await createWorks(ctx.request.body, sessionId)
19 | handleRes(ctx, next, res)
20 | })
21 |
22 | router.get('/list', async function (ctx, next) {
23 | const res = await getWorks()
24 | handleRes(ctx, next, res)
25 | })
26 |
27 | router.post('/delete', async function (ctx, next) {
28 | const sessionId = ctx.cookies.get('sessionId')
29 | const res = await deleteWorks(ctx.request.body, sessionId)
30 | handleRes(ctx, next, res)
31 | })
32 |
33 |
34 |
35 |
36 | module.exports = router
37 |
--------------------------------------------------------------------------------
/server/sql/admin.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Navicat Premium Data Transfer
3 |
4 | Source Server : admin-project
5 | Source Server Type : MySQL
6 | Source Server Version : 80016
7 | Source Host : localhost:3306
8 | Source Schema : admin
9 |
10 | Target Server Type : MySQL
11 | Target Server Version : 80016
12 | File Encoding : 65001
13 |
14 | Date: 09/07/2019 16:38:19
15 | */
16 |
17 | SET NAMES utf8mb4;
18 | SET FOREIGN_KEY_CHECKS = 0;
19 |
20 | -- ----------------------------
21 | -- Table structure for chats
22 | -- ----------------------------
23 | DROP TABLE IF EXISTS `chats`;
24 | CREATE TABLE `chats` (
25 | `id` int(11) NOT NULL AUTO_INCREMENT,
26 | `userId` int(11) DEFAULT NULL COMMENT '用户id',
27 | `username` varchar(32) DEFAULT NULL COMMENT '用户名',
28 | `userAvatar` varchar(128) DEFAULT NULL COMMENT '用户头像',
29 | `createTime` bigint(20) DEFAULT NULL COMMENT '创建时间',
30 | `content` varchar(512) DEFAULT NULL COMMENT '聊天内容',
31 | PRIMARY KEY (`id`)
32 | ) ENGINE=InnoDB AUTO_INCREMENT=73 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
33 |
34 | -- ----------------------------
35 | -- Table structure for messages
36 | -- ----------------------------
37 | DROP TABLE IF EXISTS `messages`;
38 | CREATE TABLE `messages` (
39 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
40 | `type` int(8) DEFAULT NULL COMMENT '0(留言)、1(回复)',
41 | `createTime` bigint(20) DEFAULT NULL COMMENT '创建信息时间',
42 | `content` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '信息内容',
43 | `userId` int(11) DEFAULT NULL COMMENT '回复人id',
44 | `userIsAdmin` int(8) DEFAULT NULL COMMENT '回复人是否是管理员',
45 | `userName` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '回复人用户名',
46 | `userAvatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '回复人头像',
47 | `targetUserId` int(11) DEFAULT NULL COMMENT '被回复人id',
48 | `targetUserIsAdmin` int(8) DEFAULT NULL COMMENT '被回复人是否是管理员',
49 | `targetUserName` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '被回复人用户名',
50 | `targetUserAvatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '被回复人头像',
51 | `pid` int(11) DEFAULT '-1' COMMENT '父id',
52 | `likeNum` int(11) DEFAULT '0' COMMENT '赞的数量',
53 | PRIMARY KEY (`id`)
54 | ) ENGINE=InnoDB AUTO_INCREMENT=182 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
55 |
56 | -- ----------------------------
57 | -- Table structure for scores
58 | -- ----------------------------
59 | DROP TABLE IF EXISTS `scores`;
60 | CREATE TABLE `scores` (
61 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
62 | `userId` int(11) DEFAULT NULL COMMENT '用户id',
63 | `createTime` bigint(20) DEFAULT NULL COMMENT '创建时间',
64 | `score` int(8) DEFAULT NULL COMMENT '分数',
65 | PRIMARY KEY (`id`)
66 | ) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
67 |
68 | -- ----------------------------
69 | -- Table structure for users
70 | -- ----------------------------
71 | DROP TABLE IF EXISTS `users`;
72 | CREATE TABLE `users` (
73 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
74 | `username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
75 | `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户密码',
76 | `registrationAddress` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '注册地址信息',
77 | `registrationTime` bigint(20) DEFAULT NULL COMMENT '注册时间',
78 | `lastLoginAddress` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '最后登录地址信息',
79 | `lastLoginTime` bigint(20) DEFAULT NULL COMMENT '最后登录时间',
80 | `isAdmin` int(8) unsigned DEFAULT '0' COMMENT '是否是管理员',
81 | `avatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT 'http://localhost:8888/public/images/default.png' COMMENT '用户头像',
82 | `birth` bigint(20) DEFAULT NULL COMMENT '出生日期',
83 | `phone` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '电话',
84 | `location` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '所在地',
85 | `gender` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '性别',
86 | PRIMARY KEY (`id`),
87 | UNIQUE KEY `username` (`username`) COMMENT '唯一索引'
88 | ) ENGINE=InnoDB AUTO_INCREMENT=92 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
89 |
90 | -- ----------------------------
91 | -- Table structure for works
92 | -- ----------------------------
93 | DROP TABLE IF EXISTS `works`;
94 | CREATE TABLE `works` (
95 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
96 | `title` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '作品标题',
97 | `description` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '作品描述',
98 | `url` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '预览地址',
99 | `githubUrl` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'github地址',
100 | `createTime` bigint(20) DEFAULT NULL COMMENT '创建时间',
101 | `author` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '作者',
102 | PRIMARY KEY (`id`)
103 | ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
104 |
105 | SET FOREIGN_KEY_CHECKS = 1;
106 |
--------------------------------------------------------------------------------
/server/utils/upload.js:
--------------------------------------------------------------------------------
1 | const inspect = require('util').inspect
2 | const path = require('path')
3 | const fs = require('fs')
4 | const Busboy = require('busboy')
5 | const { SuccessModel, ErrorModel } = require('../model/resModel')
6 |
7 | /**
8 | * 同步创建文件目录
9 | * @param {string} dirname 目录绝对地址
10 | * @return {boolean} 创建目录结果
11 | */
12 | function mkdirsSync(dirname) {
13 | if (fs.existsSync(dirname)) {
14 | return true
15 | } else {
16 | if (mkdirsSync(path.dirname(dirname))) {
17 | fs.mkdirSync(dirname)
18 | return true
19 | }
20 | }
21 | }
22 |
23 | /**
24 | * 获取上传文件的后缀名
25 | * @param {string} fileName 获取上传文件的后缀名
26 | * @return {string} 文件后缀名
27 | */
28 | function getSuffixName(fileName) {
29 | let nameList = fileName.split('.')
30 | return nameList[nameList.length - 1]
31 | }
32 |
33 | /**
34 | * 上传文件
35 | * @param {object} ctx koa上下文
36 | * @param {object} options 文件上传参数 fileType文件类型, path文件存放路径
37 | * @return {promise}
38 | */
39 | function uploadFile(ctx, options) {
40 | let req = ctx.req
41 | let res = ctx.res
42 | let busboy = new Busboy({ headers: req.headers })
43 |
44 | // 获取类型
45 | let fileType = options.fileType || 'common'
46 | let filePath = path.join(options.path, fileType)
47 | let mkdirResult = mkdirsSync(filePath) //创建上传文件的目录
48 |
49 | return new Promise((resolve, reject) => {
50 | console.log('文件上传中...')
51 | let result = {}
52 |
53 | // 解析请求文件事件
54 | busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
55 | const patt = /\.(jpg|jpeg|png|bmp|BMP|JPG|PNG|JPEG)$/
56 | const isPic = patt.test(filename)
57 |
58 | if (options.isImg) {
59 | if (!isPic) {
60 | resolve(new ErrorModel({
61 | message: '文件格式非图片类型'
62 | }))
63 | return
64 | }
65 | }
66 |
67 | let fileName = filename
68 | let _uploadFilePath = path.join(filePath, fileName)
69 | let saveTo = path.join(_uploadFilePath)
70 |
71 | // 文件保存到制定路径
72 | file.pipe(fs.createWriteStream(saveTo))
73 |
74 | // 文件写入事件结束
75 | file.on('end', function () {
76 | result = new SuccessModel({
77 | message: '文件上传成功',
78 | data: {
79 | url: `${ctx.origin}/${fileType}/${fileName}`
80 | }
81 | })
82 | console.log('文件上传成功!')
83 | })
84 | })
85 |
86 | // 解析结束事件
87 | busboy.on('finish', function () {
88 | console.log('文件上结束')
89 | resolve(result)
90 | })
91 |
92 | // 解析错误事件
93 | busboy.on('error', function (err) {
94 | console.log('文件上出错')
95 | resolve(new ErrorModel({
96 | message: '文件上传出错'
97 | }))
98 | })
99 |
100 | req.pipe(busboy)
101 | })
102 |
103 | }
104 |
105 |
106 | module.exports = uploadFile
--------------------------------------------------------------------------------
/server/utils/util.js:
--------------------------------------------------------------------------------
1 | const CryptoJS = require('crypto-js')
2 | const crypto = require('crypto') //这是node自带的
3 | const {FRONT_SECRETKEY,BACKEND_SECRETKEY} = require('../config/secret')
4 |
5 | /**
6 | * 前端加密函数,加密同一个字符串生成的都不相同,加密/解密秘钥必须和前端的相同
7 | * @param {*} str
8 | */
9 | function encrypt(str){
10 | return CryptoJS.AES.encrypt(JSON.stringify(str), FRONT_SECRETKEY).toString();
11 | }
12 |
13 | /**
14 | * 前端解密函数
15 | * @param {*} str
16 | */
17 | function decrypt(str){
18 | const bytes = CryptoJS.AES.decrypt(str, FRONT_SECRETKEY);
19 | return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
20 | }
21 |
22 | /**
23 | * md5 加密
24 | * @param {*} content
25 | */
26 | function md5(content) {
27 | let md5 = crypto.createHash('md5')
28 | return md5.update(content).digest('hex')
29 | }
30 |
31 | /**
32 | * 后端及加密函数,加密同一个字符串每次结果相同,可存数据库
33 | * @param {*} password
34 | */
35 | function genPassword(password) {
36 | const str = `password=${password}&key=${BACKEND_SECRETKEY}`
37 | return md5(str)
38 | }
39 |
40 | module.exports = {
41 | encrypt,
42 | decrypt,
43 | genPassword
44 | }
45 |
--------------------------------------------------------------------------------
/server/views/error.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/server/views/index.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= title
5 | p Welcome to #{title}
6 |
--------------------------------------------------------------------------------
/server/views/layout.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= title
5 | link(rel='stylesheet', href='/stylesheets/style.css')
6 | body
7 | block content
8 |
--------------------------------------------------------------------------------