├── .gitignore ├── README.md ├── backend ├── .huskyrc ├── .prettierrc ├── Dockerfile ├── README.md ├── backup │ └── sequlize │ │ ├── README.md │ │ ├── api.ts │ │ └── user.ts ├── db │ ├── Dockerfile │ └── init.sh ├── docker-compose.yml ├── jest.config.js ├── package.json ├── process.yml ├── src │ ├── __tests__ │ │ └── index.spec.js │ ├── config │ │ └── index.ts │ ├── controller │ │ └── user.ts │ ├── data │ │ └── user.json │ ├── lib │ │ ├── __tests__ │ │ │ └── index.spec.ts.bak │ │ ├── app.ts │ │ ├── decors.ts │ │ ├── index.ts │ │ ├── middleware │ │ │ ├── helper.ts │ │ │ └── restful │ │ │ │ ├── api.ts │ │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── initDb.ts │ │ │ └── mongodb.ts │ │ └── validate.ts │ ├── middleware │ │ └── auth.ts │ └── model │ │ ├── abc.json │ │ └── user.json ├── tsconfig.json ├── tslint.json └── yarn.lock ├── bin ├── smarty └── smarty-init ├── frontend ├── .env.development ├── .gitignore ├── README.md ├── index.html ├── mock │ ├── models.ts │ └── test.ts ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── HelloWorld.vue │ │ └── Pagination.vue │ ├── layouts │ │ ├── components │ │ │ ├── AppMain.vue │ │ │ ├── Breadcrumb.vue │ │ │ ├── Navbar.vue │ │ │ └── Sidebar │ │ │ │ ├── Item.vue │ │ │ │ ├── Link.vue │ │ │ │ ├── SidebarItem.vue │ │ │ │ └── index.vue │ │ └── index.vue │ ├── locales │ │ ├── en.json │ │ └── jp.json │ ├── main.ts │ ├── plugins │ │ └── element3.ts │ ├── router │ │ └── index.ts │ ├── routes-model.ts │ ├── shims-vue.d.ts │ ├── store │ │ ├── index.ts │ │ └── routes.ts │ ├── styles │ │ ├── index.scss │ │ ├── mixin.scss │ │ ├── sidebar.scss │ │ └── variables.module.scss │ ├── types.d.ts │ ├── utils │ │ ├── request.ts │ │ └── validate.js │ └── views │ │ ├── detail.vue │ │ ├── home.vue │ │ ├── models │ │ ├── DataList.vue │ │ ├── ModelEdit.vue │ │ └── ModelList.vue │ │ └── users │ │ ├── components │ │ └── detail.vue │ │ ├── create.vue │ │ ├── edit.vue │ │ ├── list.vue │ │ └── model │ │ └── userModel.js ├── tsconfig.json └── vite.config.ts ├── package.json ├── publish.sh ├── script └── start.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | /idea/ 3 | **/*/node_modules/ 4 | dist/ 5 | .DS_Store 6 | backend/yarn.lock 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smarty-end 2 | 3 | > Smarty是一款基于JS技术栈的低代码平台! 4 | 5 | - 前后端分离架构 6 | - Koa2,Typescript,Vite2.0,Vue3.0 , Element3 , 7 | - 通过数据模型定制器实现低代码开发 8 | - 支持多用户多角色权限分配 9 | - 支持插件扩展例如: blog插件电商插件、小程序插件 10 | ## 目录结构 11 | 12 | ```bash 13 | . 14 | ├── backend 后端NodeJS 15 | ├── frontend 前端Admin界面 16 | ``` 17 | 18 | ## Install 安装 19 | 20 | - [后端 Node](./backend/README.md) 21 | 22 | ## 欢迎志同道合的兄弟们一起交流 23 | 24 | ![二维码](assets/wx_qr.png) 25 | 26 | ## 里程碑 27 | 28 | - step01 添加模型 ,Admin 完成 CRUD 29 | - step02 表单定制器定制模型 30 | - step03 User 表登陆注册 31 | - step04 权限分配 Role&Api 32 | - step05 一对多和一对一 33 | 34 | - 插件功能: Blog插件、电商插件、CMS插件、小程序插件、公众号插件 35 | - SSO 单点登陆服务 36 | - 监控 37 | - Devops 38 | 39 | ## 参考资料 40 | 41 | - 测试 Jest And Supertest 42 | 测试用例:supertest 43 | http://www.voidcn.com/article/p-zselcjuo-bnw.html 44 | 45 | - Jeecg低代码平台 46 | - 在线Demo http://boot.jeecg.com/ 47 | 48 | - KeyStoreJS 49 | 50 | - Mongoose中文文档 51 | http://mongoosejs.net/docs/queries.html -------------------------------------------------------------------------------- /backend/.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | // "pre-commit": "npm run format && npm run tslint && npm run test" 4 | } 5 | } -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": true, 6 | "endOfLine": "lf", 7 | "printWidth": 120, 8 | "overrides": [ 9 | { 10 | "files": ["*.md", "*.json", "*.yml", "*.yaml"], 11 | "options": { 12 | "tabWidth": 2 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM keymetrics/pm2:latest-alpine 2 | WORKDIR /usr/src/app 3 | ADD . /usr/src/app 4 | # 下载依赖 5 | RUN npm config set registry https://registry.npm.taobao.org/ && \ 6 | npm i && \ 7 | npm run build 8 | 9 | EXPOSE 3000 10 | CMD ["pm2-runtime", "start", "process.yml"] 11 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Server端 2 | 3 | ## Install 4 | 5 | ### Node环境 6 | - NodeJS 8.0 need [nodejs.org/en](https://nodejs.org/en/) 7 | - mysql need 8 | - Clone or download this repository Enter your local directory, and 9 | - install dependencies: 10 | 11 | ``` 12 | npm install 13 | npm start 14 | ``` 15 | 16 | ## 数据库环境 17 | 需要Mongodb,可以自行安装 18 | 也可以在安装Docker后运行 一键启动MongoDB环境 19 | ``` 20 | yarn dockerd 21 | ``` 22 | 23 | ## 测试版API接口 24 | 1. 数据库配置 /config/index.ts 25 | ```json 26 | mongo: { 27 | url: 'mongodb://localhost:27017/smarty', 28 | options: { 29 | useNewUrlParser: true, 30 | useUnifiedTopology: true, 31 | }, 32 | forceUpate: true, // 是否强制更新数据库数据 33 | }, 34 | ``` 35 | 36 | 37 | 38 | 2. 设定测试数据 /data/user.json 39 | 40 | 示例数据 41 | 42 | ```json 43 | [ 44 | { 45 | "avatar": "213213", 46 | "mobile": "1361234121", 47 | "password": "王1", 48 | "realName": "123" 49 | }, 50 | ] 51 | 52 | ``` 53 | 54 | 3. 在 /model中添加模型文件后会自动加载对该资源的Restful接口 55 | 56 | 例: /model/user.ts 57 | 58 | ``` 59 | export default { 60 | schema: { 61 | mobile: { type: String, unique: true, required: true }, 62 | password: { type: String, required: true }, 63 | realName: { type: String, required: true }, 64 | avatar: { type: String, default: 'https://1.gravatar.com/avatar/a3e54af3cb6e157e496ae430aed4f4a3?s=96&d=mm' }, 65 | createdAt: { type: Date, default: Date.now }, 66 | }, 67 | } 68 | 69 | ``` 70 | 71 | 自动对应该资源的Restful接口 72 | 73 | ``` 74 | GET /api/resource/user/:id' 查询指定id的数据 75 | GET '/api/resource/user 获取数据列表 76 | POST '/api/resource/user 创建数据 77 | PUT '/api/resource/user/:id' 修改数据 78 | DELETE '/api/resource/user/:id' 删除数据 79 | ``` 80 | 81 | 82 | 83 | ## API接口列表 84 | ``` 85 | # 模型元数据 86 | GET /api/metadata 获取模型列表 87 | GET /api/metadata/user 获取用户模型 88 | 89 | # User数据 90 | GET /api/resource/user/:id' 查询指定id的数据 91 | GET /api/resource/user?pageNo=2&pageSize=5 92 | 带翻页的请求 93 | GET api/resource/user?pageNo=1&pageSize=5&sortField=mobile&sortOrder=desc 带排序的请求 94 | GET '/api/resource/user 获取数据列表 95 | POST '/api/resource/user 创建数据 96 | PUT '/api/resource/user/:id' 修改数据 97 | DELETE '/api/resource/user/:id' 删除数据 98 | ``` 99 | -------------------------------------------------------------------------------- /backend/backup/sequlize/README.md: -------------------------------------------------------------------------------- 1 | # Server端 2 | 3 | ## Install 4 | 5 | ### Node环境 6 | - NodeJS 8.0 need [nodejs.org/en](https://nodejs.org/en/) 7 | - mysql need 8 | - Clone or download this repository Enter your local directory, and 9 | - install dependencies: 10 | 11 | ``` 12 | npm install 13 | npm start 14 | ``` 15 | 16 | ## 数据库环境 17 | - Mysql 18 | ``` 19 | # 创建数据库 20 | # user: root 21 | # password: example 22 | mysql -h localhost -u root -pexample 23 | 24 | # database: 'smarty' 25 | CREATE DATABASE smarty; 26 | 27 | # 退出 28 | exit 29 | ``` 30 | 31 | ## Sample 32 | 33 | - 约定CRUD接口 34 | 35 | > 只需要添加模型就可以自动生成基础CRUD接口 36 | 37 | ```js 38 | // 添加模型model/user.js 39 | import { Table, Column, Model, DataType } from 'sequelize-typescript'; 40 | @Table({}) 41 | class User extends Model { 42 | @Column({ 43 | primaryKey: true, 44 | autoIncrement: true, 45 | type: DataType.INTEGER, 46 | }) 47 | public id: number; 48 | 49 | @Column(DataType.CHAR) 50 | public name: string; 51 | } 52 | export default User 53 | ``` 54 | 55 | - 自动生成如下接口 56 | - GET /api/user 57 | - POST /api/user/:id 58 | - PUT /api/user/:id 59 | - DELETE /api/user/:id 60 | 61 | - 装饰器路由 62 | 63 | ```js 64 | // router/api.js 65 | import { get, post } from '../framework/decors' 66 | export default class User { 67 | /** 68 | * 获取购物车 69 | * @param ctx 70 | */ 71 | @get('/carts') 72 | public async list(ctx) { 73 | ctx.body = { ok: 1, data: ['hello'] }; 74 | } 75 | 76 | /** 77 | * 创建购物车 78 | * @param ctx 79 | */ 80 | @post('/carts') 81 | public add(ctx) { 82 | ctx.body = { ok: 1 } 83 | } 84 | } 85 | ``` 86 | 87 | 88 | 89 | - 接口参数校验装饰器 90 | 91 | > 检验规则 https://www.npmjs.com/package/parameter 92 | 93 | ```js 94 | // Post方式 95 | @body({ 96 | name: { type: 'string', required: true, max: 200, }, 97 | }) 98 | // Get方式 99 | @querystring({ 100 | id: { type: 'string', required: false, max: 200 }, 101 | }) 102 | ``` 103 | 104 | ## Docker使用 105 | ### 获取最新版代码 106 | git pull 107 | 108 | #### 强制重新编译容器 109 | docker-compose down 110 | docker-compose up -d --force-recreate --build 111 | 112 | 113 | -------------------------------------------------------------------------------- /backend/backup/sequlize/api.ts: -------------------------------------------------------------------------------- 1 | export const api = { 2 | init(app) { 3 | return async (ctx, next) => { 4 | console.log('model:', app.$model, ctx.params.list) 5 | 6 | // const { default: model } = require(`../../model/${ctx.params.list}`) 7 | // console.log('model', model) 8 | 9 | const model = app.$model[ctx.params.list] 10 | if (model) { 11 | ctx.list = model 12 | await next() 13 | } else { 14 | ctx.body = 'no this model' 15 | } 16 | } 17 | }, 18 | 19 | async get(ctx) { 20 | ctx.body = await ctx.list.findByPk(ctx.params.id) 21 | }, 22 | 23 | async list(ctx) { 24 | ctx.body = await ctx.list.findAll({}) 25 | }, 26 | 27 | async create(ctx) { 28 | const res = await ctx.list.create(ctx.request.body) 29 | ctx.body = res 30 | }, 31 | async update(ctx) { 32 | const res = await ctx.list.update(ctx.request.body, { where: { id: ctx.params.id } }) 33 | ctx.body = res 34 | }, 35 | async del(ctx) { 36 | const res = await ctx.list.destroy({ where: { id: ctx.params.id } }) 37 | ctx.body = res 38 | }, 39 | async page(ctx) { 40 | console.log('page...', ctx.params.page) 41 | ctx.body = await ctx.list.findAll({}) /* */ 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /backend/backup/sequlize/user.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model, DataType, Default, Comment, Unique } from 'sequelize-typescript' 2 | @Table({}) 3 | class User extends Model { 4 | @Column({ 5 | primaryKey: true, 6 | autoIncrement: true, 7 | type: DataType.INTEGER, 8 | }) 9 | public id: number 10 | 11 | @Column({ 12 | type: DataType.CHAR, 13 | }) 14 | public mobile: string 15 | 16 | @Column({ 17 | type: DataType.CHAR, 18 | }) 19 | public password: string 20 | 21 | @Column({ 22 | type: DataType.CHAR, 23 | }) 24 | public realname: string 25 | 26 | @Column({ 27 | type: DataType.CHAR, 28 | }) 29 | public avatar: string 30 | } 31 | export default User 32 | -------------------------------------------------------------------------------- /backend/db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM keymetrics/pm2:latest-alpine 2 | WORKDIR /usr/src/app 3 | ADD . /usr/src/app 4 | # 下载依赖 5 | RUN npm config set registry https://registry.npm.taobao.org/ && \ 6 | npm i && \ 7 | npm run build 8 | 9 | EXPOSE 3000 10 | CMD ["pm2-runtime", "start", "process.yml"] 11 | -------------------------------------------------------------------------------- /backend/db/init.sh: -------------------------------------------------------------------------------- 1 | # 创建数据库 2 | # user: root 3 | # password: example 4 | mysql -h localhost -u root -pexample 5 | 6 | # database: 'smarty' 7 | CREATE DATABASE smarty; 8 | 9 | # 退出 10 | exit -------------------------------------------------------------------------------- /backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | mongo: 4 | image: mongo 5 | restart: always 6 | ports: 7 | - 27017:27017 8 | mongo-express: 9 | image: mongo-express 10 | restart: always 11 | ports: 12 | - 8081:8081 13 | # mysql: 14 | # image: mysql 15 | # command: --default-authentication-plugin=mysql_native_password 16 | # restart: always 17 | # environment: 18 | # MYSQL_ROOT_PASSWORD: example 19 | # ports: 20 | # - 3306:3306 21 | # adminer: 22 | # image: adminer 23 | # restart: always 24 | # ports: 25 | # - 8080:8080 26 | # app-pm2: 27 | # container_name: app-pm2 28 | # #构建容器 29 | # build: . 30 | # environment: 31 | # - DB_HOST=mysql 32 | # - DB_NAME=smarty 33 | # - DB_USER=root 34 | # - DB_PASSWORD=example 35 | # ports: 36 | # - "3000:3000" 37 | # depends_on: 38 | # - mysql 39 | 40 | #直接从git拉去 41 | # build: git@github.com:su37josephxia/docker_ci.git#:backend 42 | # 需要链接本地代码时 43 | # volumes: 44 | # - ./backend:/usr/src/app 45 | -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smarty-end", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "ts-node-dev ./src/lib/index.ts -P tsconfig.json --no-cache", 8 | "build": "tsc -P tsconfig.json", 9 | "tslint": "tslint --fix -p tsconfig.json", 10 | "db": "docker-compose up mysql adminer", 11 | "docker": "docker-compose up --force-recreate --build", 12 | "dockerd": "docker-compose up -d --force-recreate --build", 13 | "format": "prettier --write \"src/**/*.ts\"", 14 | "coverage": "jest --config jestconfig.json --coverage" 15 | }, 16 | "bin": { 17 | "smarty": "./bin/smarty" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "@types/bluebird": "^3.5.30", 24 | "@types/koa": "^2.11.3", 25 | "@types/node": "^13.11.1", 26 | "@types/validator": "^13.0.0", 27 | "nodemon": "^2.0.3", 28 | "ts-node-dev": "^1.0.0-pre.40", 29 | "tslint": "^5.20.1", 30 | "typescript": "^3.8.3" 31 | }, 32 | "dependencies": { 33 | "@types/jest": "^26.0.22", 34 | "chalk": "^4.1.1", 35 | "clear": "^0.1.0", 36 | "commander": "^3.0.1", 37 | "dotenv": "^8.2.0", 38 | "download-git-repo": "^2.0.0", 39 | "express": "^4.17.1", 40 | "figlet": "^1.5.0", 41 | "husky": "^4.2.5", 42 | "jest": "^26.6.3", 43 | "js-yaml": "^4.1.0", 44 | "koa": "^2.7.0", 45 | "koa-body": "^4.1.0", 46 | "koa-router": "^7.4.0", 47 | "koa-static": "^5.0.0", 48 | "koa-xtime": "^1.0.0", 49 | "mongoose": "^5.12.5", 50 | "mysql2": "^1.6.5", 51 | "ora": "^3.4.0", 52 | "parameter": "^3.6.0", 53 | "prettier": "^2.0.4", 54 | "reflect-metadata": "^0.1.13", 55 | "request": "^2.88.2", 56 | "supertest": "^6.1.3", 57 | "ts-jest": "^26.5.5", 58 | "tslint-config-prettier": "^1.18.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/process.yml: -------------------------------------------------------------------------------- 1 | apps: 2 | - script : dist/framework/index.js 3 | name : smarty 4 | instances: 2 5 | watch : true 6 | env : 7 | NODE_ENV: production -------------------------------------------------------------------------------- /backend/src/__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | function createServer(host) { 3 | const createRequest = (method) => (url) => { 4 | return new Promise((resolve, reject) => { 5 | request( 6 | { 7 | url: host + url, 8 | method, 9 | headers: { 10 | 'content-type': 'application/json', 11 | }, 12 | }, 13 | (err, req, body) => { 14 | if (!err) { 15 | resolve(JSON.parse(body)) 16 | } else { 17 | reject(err) 18 | } 19 | }, 20 | ) 21 | }) 22 | } 23 | 24 | return { 25 | get: createRequest('GET'), 26 | post: createRequest('POST'), 27 | } 28 | } 29 | const server = createServer('http://localhost:4000') 30 | 31 | it('Test /api/metadata', async () => { 32 | const body = await server.get('/api/metadata') 33 | expect(body.code).toBe(0) 34 | expect(body.data).toEqual([ 35 | { 36 | id: 'user', 37 | description: '用户管理', 38 | }, 39 | ]) 40 | }) 41 | 42 | it('Test /api/metadata/user', async () => { 43 | const body = await server.get('/api/metadata/user') 44 | expect(body.code).toBe(0) 45 | expect(body.data).toEqual({ 46 | description: '用户管理', 47 | schema: { 48 | mobile: { 49 | description: '手机号', 50 | type: 'String', 51 | unique: true, 52 | required: true, 53 | }, 54 | password: { 55 | description: '密码', 56 | type: 'String', 57 | required: true, 58 | }, 59 | realName: { 60 | description: '真实姓名', 61 | type: 'String', 62 | required: true, 63 | }, 64 | avatar: { 65 | description: '头像', 66 | type: 'String', 67 | default: 'https://1.gravatar.com/avatar/a3e54af3cb6e157e496ae430aed4f4a3?s=96&d=mm', 68 | }, 69 | }, 70 | }) 71 | }) 72 | 73 | it('Test /api/resource/user', async () => { 74 | const body = await server.get('/api/resource/user') 75 | expect(body.code).toBe(0) 76 | }) 77 | -------------------------------------------------------------------------------- /backend/src/config/index.ts: -------------------------------------------------------------------------------- 1 | // import { ISequelizeConfig } from "sequelize-typescript" 2 | export interface IConfig { 3 | // db?: ISequelizeConfig, 4 | mysql?: {} 5 | mongo?: { 6 | url: string 7 | options: { 8 | useNewUrlParser? 9 | useUnifiedTopology? 10 | } 11 | forceUpate: boolean 12 | } 13 | option?: { 14 | restful: boolean 15 | // 是否强制数据库同步 16 | forceSync: boolean 17 | } 18 | root: string 19 | } 20 | import { resolve } from 'path' 21 | const config: IConfig = { 22 | // mysql: { 23 | // dialect: 'mysql', 24 | // host: 'localhost', 25 | // database: 'smarty', 26 | // username: 'root', 27 | // password: 'example', 28 | // }, 29 | mongo: { 30 | url: 'mongodb://localhost:27017/smarty', 31 | options: { 32 | useNewUrlParser: true, 33 | useUnifiedTopology: true, 34 | }, 35 | forceUpate: true, // 是否强制更新数据库数据 36 | }, 37 | option: { 38 | restful: true, 39 | // 是否强制数据库同步 40 | forceSync: true, 41 | }, 42 | root: resolve('.'), 43 | } 44 | 45 | if (process.env.NODE_ENV === 'production') { 46 | // config.mysql = { 47 | // dialect: 'mysql', 48 | // host: process.env.DB_HOST, 49 | // database: process.env.DB_NAME, 50 | // username: process.env.DB_USER, 51 | // password: process.env.DB_PASSWORD, 52 | // } 53 | } 54 | 55 | export { config } 56 | 57 | // export config 58 | -------------------------------------------------------------------------------- /backend/src/controller/user.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa' 2 | import { get, post, middlewares } from '../lib/decors' 3 | import { querystring, body } from '../lib/validate' 4 | const users = [{ name: 'tom', age: 20 }] 5 | 6 | // import model from '../model/user' 7 | 8 | @middlewares([ 9 | async (ctx, next) => { 10 | console.log('class middlewares ....') 11 | await next() 12 | }, 13 | ]) 14 | export default class User { 15 | /** 16 | * 获取用户信息 17 | * @param ctx 18 | */ 19 | @get('/users', { 20 | middlewares: [ 21 | async (ctx, next) => { 22 | console.log('method middleware') 23 | await next() 24 | }, 25 | ], 26 | }) 27 | @querystring({ 28 | id: { type: 'string', required: false, max: 200 }, 29 | }) 30 | public async list(ctx) { 31 | // const records = await model.findAll() 32 | ctx.body = { ok: 1, data: users } 33 | } 34 | 35 | /** 36 | * 创建用户 37 | * @param ctx 38 | */ 39 | @body({ 40 | name: { type: 'string', required: true, max: 200 }, 41 | }) 42 | @post('/users', { 43 | middlewares: [ 44 | async (ctx, next) => { 45 | console.log('middleware go.....') 46 | await next() 47 | }, 48 | ], 49 | }) 50 | public add(ctx) { 51 | users.push(ctx.request.body) 52 | ctx.body = { ok: 1 } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /backend/src/data/user.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "avatar": "213213", 4 | "mobile": "1361234121", 5 | "password": "111111", 6 | "realName": "姓名1" 7 | }, 8 | { 9 | "avatar": "213213", 10 | "mobile": "1361234122", 11 | "password": "111111", 12 | "realName": "姓名2" 13 | }, 14 | { 15 | "avatar": "213213", 16 | "mobile": "1361234123", 17 | "password": "111111", 18 | "realName": "姓名3" 19 | }, 20 | { 21 | "avatar": "213213", 22 | "mobile": "1361234124", 23 | "password": "111111", 24 | "realName": "姓名4" 25 | }, 26 | { 27 | "avatar": "213213", 28 | "mobile": "1361234125", 29 | "password": "111111", 30 | "realName": "姓名5" 31 | }, 32 | { 33 | "avatar": "213213", 34 | "mobile": "1361234126", 35 | "password": "111111", 36 | "realName": "姓名6" 37 | }, 38 | { 39 | "avatar": "213213", 40 | "mobile": "1361234127", 41 | "password": "111111", 42 | "realName": "姓名7" 43 | }, 44 | { 45 | "avatar": "213213", 46 | "mobile": "1361234128", 47 | "password": "111111", 48 | "realName": "姓名8" 49 | }, 50 | { 51 | "avatar": "213213", 52 | "mobile": "1361234129", 53 | "password": "111111", 54 | "realName": "姓名9" 55 | }, 56 | { 57 | "avatar": "213213", 58 | "mobile": "1361234111", 59 | "password": "111111", 60 | "realName": "姓名10" 61 | }, 62 | { 63 | "avatar": "213213", 64 | "mobile": "1361234112", 65 | "password": "111111", 66 | "realName": "姓名11" 67 | }, 68 | { 69 | "avatar": "213213", 70 | "mobile": "1361234113", 71 | "password": "111111", 72 | "realName": "姓名12" 73 | }, 74 | { 75 | "avatar": "213213", 76 | "mobile": "1361234114", 77 | "password": "111111", 78 | "realName": "姓名13" 79 | }, 80 | { 81 | "avatar": "213213", 82 | "mobile": "13612342321", 83 | "password": "111111", 84 | "realName": "姓名14" 85 | }, 86 | { 87 | "avatar": "213213", 88 | "mobile": "1361234021", 89 | "password": "111111", 90 | "realName": "姓名15" 91 | }, 92 | { 93 | "avatar": "213213", 94 | "mobile": "1361234723", 95 | "password": "111111", 96 | "realName": "姓名16" 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /backend/src/lib/__tests__/index.spec.ts.bak: -------------------------------------------------------------------------------- 1 | // import request from 'supertest' 2 | // import Smarty from '../app' 3 | 4 | // // const request = require('supertest') 5 | // // const express = require('express') 6 | 7 | // // const app = express() 8 | // // app.get('/user', function (req, res) { 9 | // // res.status(200).json({ name: 'john' }) 10 | // // }) 11 | 12 | // const app = new Smarty() 13 | // const api = request(app) 14 | // beforeAll(done => { 15 | // setTimeout(() => { 16 | // done() 17 | // },20 *1000) 18 | // }) 19 | 20 | 21 | // test('hello world', async (done) => { 22 | // console.log('hello') 23 | // // api.get('/api/user') 24 | // // .expect('Content-Type', /json/) 25 | // // .expect('Content-Length', '15') 26 | // // .expect(200, { name: 'john' }, done) 27 | // }) 28 | -------------------------------------------------------------------------------- /backend/src/lib/app.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa' 2 | import * as bodify from 'koa-body' 3 | 4 | import { load } from './decors' 5 | import { addRestful, addModelList } from './middleware/restful/index' 6 | import { resolve } from 'path' 7 | import { config, IConfig } from '../config/index' 8 | import * as KoaRouter from 'koa-router' 9 | // import { createDataBase } from './utils/initDb' 10 | import { loadModel, initData } from './utils/mongodb' 11 | import addHelper from './middleware/helper' 12 | 13 | import * as figlet from 'figlet' 14 | import * as clear from 'clear' 15 | import * as chalk from 'chalk' 16 | const log = (content) => console.log(chalk.yellowBright(content)) 17 | 18 | export default class Smarty { 19 | app: Koa 20 | $router: KoaRouter 21 | $model: any 22 | rootPath: string 23 | config: IConfig 24 | helper: any 25 | constructor() { 26 | this.rootPath = resolve('./src') 27 | this.app = new Koa() 28 | this.app.use( 29 | bodify({ 30 | multipart: true, 31 | strict: false, 32 | }), 33 | ) 34 | this.config = config 35 | 36 | // 添加Help函数 37 | this.app.use(addHelper) 38 | 39 | this.app.use(async (ctx, next) => { 40 | try { 41 | await next() 42 | } catch (err) { 43 | 44 | ctx.body = { 45 | code: 500, // 服务端自身的处理逻辑错误(包含框架错误500 及 自定义业务逻辑错误533开始 ) 客户端请求参数导致的错误(4xx开始),设置不同的状态码 46 | error: err.message, 47 | } 48 | // throw err 49 | } 50 | }) 51 | 52 | // 加载数据库 53 | // if (config.mysql) { 54 | // // 初始化数据库 55 | // createDataBase(config.mysql) 56 | // const sequelize = new Sequelize(Object.assign(config.db, { modelPaths: [`${config.root}/src/model`] })) 57 | // // 数据库强制同步 58 | // sequelize.sync({ force: config.option.forceSync }) 59 | 60 | // // 加载数据Model 61 | // // 这个地方有点偷懒 赶进度先这样 应该是基于文件名加载 而不应该是直接小写字母 62 | // const models = sequelize.models 63 | // this.$model = {} 64 | // Object.keys(models).map((key) => { 65 | // console.log('keys:', key.toLowerCase()) 66 | // this.$model[key.toLowerCase()] = models[key] 67 | // }) 68 | // } 69 | // 加载Mongo 70 | if (config.mongo) { 71 | // 加载模型 72 | loadModel(this) 73 | 74 | // 初始化数据 75 | initData(this) 76 | } 77 | 78 | this.$router = new KoaRouter() 79 | 80 | // 加载restfu接口 81 | if (config.option.restful) { 82 | addRestful(this) 83 | 84 | addModelList(this) 85 | } 86 | 87 | // 路由加载器 88 | // load(resolve(__dirname, `${config.root}/src/controller`), {}, this) 89 | this.app.use(this.$router.routes()) 90 | } 91 | 92 | listen(port: number, listeningListener?: () => void) { 93 | this.app.listen( 94 | port, 95 | listeningListener || 96 | (async () => { 97 | clear() 98 | log( 99 | figlet.textSync('Smarty', { 100 | font: 'Ghost', 101 | horizontalLayout: 'default', 102 | verticalLayout: 'default', 103 | width: 80, 104 | whitespaceBreak: true, 105 | }), 106 | ) 107 | log(`===================`) 108 | log(`Smarty End Start at ${port}`) 109 | }), 110 | ) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /backend/src/lib/decors.ts: -------------------------------------------------------------------------------- 1 | import * as glob from 'glob' 2 | import * as Koa from 'koa' 3 | import * as KoaRouter from 'koa-router' 4 | import { resolve, join } from 'path' 5 | // import { router } from './restful/router'; 6 | 7 | type HTTPMethod = 'get' | 'put' | 'del' | 'post' | 'patch' 8 | 9 | interface ILoadOptions { 10 | extname?: string 11 | } 12 | 13 | interface IRouteOptions { 14 | prefix?: string 15 | middlewares?: Koa.Middleware[] 16 | } 17 | 18 | // const router = new KoaRouter() 19 | 20 | let router = null 21 | 22 | const decorate = (httpMethod: HTTPMethod, path: string, options: IRouteOptions = {}) => { 23 | return (target, property: string) => { 24 | process.nextTick(() => { 25 | if (!router) { 26 | return 27 | } 28 | 29 | // 添加中间件数组 30 | const mids = [] 31 | if (options.middlewares) { 32 | mids.push(...options.middlewares) 33 | } 34 | 35 | if (target.middlewares) { 36 | mids.push(...target.middlewares) 37 | } 38 | 39 | mids.push(target[property]) 40 | 41 | // url前缀 42 | const url = options.prefix ? options.prefix + path : path 43 | // router[method](url, target[property]) 44 | router[httpMethod](url, ...mids) 45 | }) 46 | } 47 | } 48 | 49 | const method = (methodName) => (path: string, options?: IRouteOptions) => decorate(methodName, path, options) 50 | export const get = method('get') 51 | export const post = method('post') 52 | export const put = method('put') 53 | export const del = method('del') 54 | 55 | export const load = (folder: string, options: ILoadOptions = {}, app) => { 56 | router = app.$router 57 | const extname = options.extname || '.{js,ts}' 58 | 59 | glob.sync(join(folder, `./**/*${extname}`)) 60 | .filter((v) => v.indexOf('.spec') === -1) // 排除测试代码 61 | .forEach((item) => require(item)) 62 | } 63 | 64 | export const middlewares = (mids: Koa.Middleware[]) => { 65 | return (target) => { 66 | target.prototype.middlewares = mids 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /backend/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import Smarty from './app' 2 | const app = new Smarty() 3 | app.listen(4000) 4 | -------------------------------------------------------------------------------- /backend/src/lib/middleware/helper.ts: -------------------------------------------------------------------------------- 1 | export default async function addHelper(ctx,next) { 2 | ctx.success = (res = null, msg = '请求成功') => { 3 | ctx.body = { 4 | code: 0, 5 | data: res, 6 | msg, 7 | } 8 | ctx.status = 200 9 | } 10 | await next() 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/lib/middleware/restful/api.ts: -------------------------------------------------------------------------------- 1 | export const api = { 2 | init(app) { 3 | return async (ctx, next) => { 4 | console.log('model:', app.$model, ctx.params.model) 5 | const model = app.$model[ctx.params.model] 6 | if (model) { 7 | ctx.model = model 8 | await next() 9 | } else { 10 | ctx.body = 'no this model' 11 | } 12 | } 13 | }, 14 | 15 | async list(ctx) { 16 | // sortOrder = 'asc' | 'desc' 17 | // 18 | const condition = { 19 | pageNo: 1, 20 | pageSize: 100, 21 | sortField: '_id', 22 | sortOrder: 'asc', 23 | } 24 | Object.assign(condition, ctx.query) 25 | 26 | const total = await ctx.model.find().count() 27 | const sort = {} 28 | sort[condition.sortField] = condition.sortOrder 29 | const list = await ctx.model 30 | // 1是升序,-1是降序 31 | .find({}, null, { sort }) // 增加 32 | // 对查询结果培训 33 | // .sort({ 'realName': 'desc' }) 34 | .skip((condition.pageNo - 1) * condition.pageSize) 35 | .limit(condition.pageSize - 0) 36 | 37 | ctx.success({ list, pagination: { total, pageNo: condition.pageNo - 0, pageSize: condition.pageSize - 0 } }) 38 | }, 39 | 40 | async get(ctx) { 41 | ctx.success(await ctx.model.findOne({ _id: ctx.params.id })) 42 | }, 43 | 44 | async create(ctx) { 45 | const res = await ctx.model.create(ctx.request.body) 46 | ctx.success(res) 47 | }, 48 | async update(ctx) { 49 | const res = await ctx.model.updateOne({ _id: ctx.params.id }, ctx.request.body) 50 | ctx.success(res) 51 | }, 52 | async del(ctx) { 53 | const res = await ctx.model.deleteOne({ _id: ctx.params.id }) 54 | ctx.success(res) 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /backend/src/lib/middleware/restful/index.ts: -------------------------------------------------------------------------------- 1 | import { api } from './api' 2 | import * as fs from 'fs' 3 | import { loadModel } from '../../utils/mongodb' 4 | export const addRestful = (app) => { 5 | const { init, get, list, create, update, del } = api 6 | const router = app.$router 7 | 8 | router.get('/api/resource/:model/:id', init(app), get) 9 | router.get('/api/resource/:model', init(app), list) 10 | router.post('/api/resource/:model', init(app), create) 11 | router.put('/api/resource/:model/:id', init(app), update) 12 | router.delete('/api/resource/:model/:id', init(app), del) 13 | } 14 | 15 | export const addModelList = (app) => { 16 | const router = app.$router 17 | 18 | // 获取模型列表 19 | router.get('/api/metadata/', (ctx) => { 20 | const list = fs.readdirSync(app.rootPath + '/model').map((id) => { 21 | id = id.replace('.json', '') 22 | const model = require(`${app.rootPath}/model/${id}`) 23 | return { 24 | id, 25 | description: model.description, 26 | } 27 | }) 28 | 29 | ctx.success(list) 30 | }) 31 | // 获取单个模型 32 | router.get('/api/metadata/:id', (ctx) => { 33 | const model = require(`${app.rootPath}/model/${ctx.params.id}`) 34 | console.log('model', model) 35 | ctx.success(model) 36 | }) 37 | 38 | // 创建模型 39 | router.post('/api/metadata/:id', (ctx) => { 40 | console.log('create model:', ctx.request.body) 41 | // 判断是否存在此模型 42 | const list = fs.readdirSync(`${app.rootPath}/model/`) 43 | console.log('list', list) 44 | if (list.find((v) => v.replace('.json', '') === ctx.params.id)) { 45 | ctx.throw('model is exited') 46 | } 47 | writeModel(ctx.params.id, ctx.request.body) 48 | // 重新加载模型 49 | // TODO 还没找到好的方法 目前只能通过重启来实现 50 | // loadModel(app) 51 | ctx.success(ctx.request.body) 52 | }) 53 | 54 | // 修改模型 55 | router.put('/api/metadata/:id', (ctx) => { 56 | console.log('update model:', ctx.request.body) 57 | writeModel(ctx.params.id, ctx.request.body) 58 | // 重新加载模型 59 | // TODO 还没找到好的方法 目前只能通过重启来实现 60 | // loadModel(app) 61 | ctx.success(ctx.request.body) 62 | }) 63 | 64 | /** 65 | * 写入模型 66 | * @param id 67 | * @param data 68 | */ 69 | function writeModel(id, data) { 70 | const model = JSON.parse(data) 71 | fs.writeFileSync(`${app.rootPath}/model/${id}.json`, JSON.stringify(model, null, '\t'), 'utf-8') 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /backend/src/lib/utils/initDb.ts: -------------------------------------------------------------------------------- 1 | import * as mysql from 'mysql2/promise' 2 | export async function createDataBase(config) { 3 | const connection = await mysql.createConnection({ 4 | host: config.host, 5 | user: config.username, 6 | password: config.password, 7 | }) 8 | // console.log('connec',connection) 9 | const res = await connection.query( 10 | `CREATE DATABASE IF NOT EXISTS ${config.database} default character set utf8 COLLATE utf8_general_ci;`, 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/lib/utils/mongodb.ts: -------------------------------------------------------------------------------- 1 | import Smarty from '../app' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const mongoose = require('mongoose') 6 | 7 | function load(dir, cb, ext = 'ts') { 8 | const files = fs.readdirSync(dir) 9 | files.forEach((filename) => { 10 | // 去掉后缀名 11 | filename = filename.replace('.' + ext, '') 12 | // 导入文件 13 | const file = require(dir + '/' + filename) 14 | // 处理逻辑 15 | cb(filename, file) 16 | }) 17 | } 18 | 19 | export const loadModel = (app: Smarty) => { 20 | const { url, options } = app.config.mongo 21 | mongoose.connect(url, options) 22 | const conn = mongoose.connection 23 | conn.on('error', () => console.error('连接数据库失败')) 24 | app.$model = {} 25 | const Schema = mongoose.Schema 26 | 27 | load( 28 | app.rootPath + '/model', 29 | (filename, config) => { 30 | console.log('load model: ' + filename, config.schema) 31 | app.$model[filename] = mongoose.model(filename, new Schema(config.schema)) 32 | 33 | }, 34 | 'json', 35 | ) 36 | } 37 | 38 | export const initData = (app: Smarty) => { 39 | load( 40 | app.rootPath + '/data', 41 | async (name, data) => { 42 | console.log('initData: ', name) 43 | const forceUpdate = app.config.mongo.forceUpate 44 | const model = app.$model[name] 45 | model.deleteMany({}) 46 | if (forceUpdate) await model.deleteMany() 47 | await model.insertMany(data) 48 | }, 49 | 'json', 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /backend/src/lib/validate.ts: -------------------------------------------------------------------------------- 1 | import * as Parameter from 'parameter' 2 | const validateRule = (paramPart) => (rule) => { 3 | return (target, name, descriptor) => { 4 | const oldValue = descriptor.value 5 | descriptor.value = (...args) => { 6 | const ctx = args[0] 7 | const p = new Parameter() 8 | const data = ctx[paramPart] 9 | if (data) { 10 | const errors = p.validate(rule, data) 11 | if (errors) { 12 | throw new Error(JSON.stringify(errors)) 13 | } 14 | } 15 | return oldValue.apply(null, args) 16 | } 17 | return descriptor 18 | } 19 | } 20 | 21 | export const querystring = validateRule('query') 22 | export const body = validateRule('body') 23 | -------------------------------------------------------------------------------- /backend/src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa' 2 | export const findByName = (name) => { 3 | return new Promise((resolve, reject) => { 4 | setTimeout(() => { 5 | if (name === 'xia') { 6 | reject('用户已存在') 7 | } else { 8 | resolve() 9 | } 10 | }, 500) 11 | }) 12 | } 13 | 14 | // export const guard = async function guard(ctx: Koa.Context, next: () => Promise) { 15 | 16 | // if (ctx.header.token) { 17 | // await next(); 18 | // } else { 19 | // throw "请登录"; 20 | // } 21 | // } 22 | // ]) 23 | -------------------------------------------------------------------------------- /backend/src/model/abc.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "用户管理", 3 | "schema": { 4 | "mobile": { 5 | "description": "手机号", 6 | "type": "String", 7 | "unique": true, 8 | "required": true 9 | }, 10 | "password": { 11 | "description": "密码", 12 | "type": "String", 13 | "required": true 14 | }, 15 | "realName": { 16 | "description": "真实姓名", 17 | "type": "String", 18 | "required": true 19 | }, 20 | "avatar": { 21 | "description": "头像", 22 | "type": "String", 23 | "default": "https://1.gravatar.com/avatar/a3e54af3cb6e157e496ae430aed4f4a3?s=96&d=mm" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /backend/src/model/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "用户管理", 3 | "schema": { 4 | "mobile": { "description": "手机号", "type": "String", "unique": true, "required": true }, 5 | "password": { "description": "密码", "type": "String", "required": true }, 6 | "realName": { "description": "真实姓名", "type": "String", "required": true }, 7 | "avatar": { 8 | "description": "头像", 9 | "type": "String", 10 | "default": "https://1.gravatar.com/avatar/a3e54af3cb6e157e496ae430aed4f4a3?s=96&d=mm" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "target": "es2017", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "emitDecoratorMetadata": true, 11 | "lib": ["es2015"], 12 | "typeRoots": ["./node_modules/@types"], 13 | }, 14 | "include": [ 15 | "src/**/*", 16 | ] 17 | } -------------------------------------------------------------------------------- /backend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "no-console": false, // 忽略console.log 5 | "object-literal-sort-keys": false, 6 | "member-access": false, 7 | "ordered-imports": false 8 | }, 9 | "linterOptions": { 10 | "exclude": ["**/*.json", "node_modules"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bin/smarty: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const program = require('commander') 3 | program.version(require('../package').version, '-v', '--version') 4 | .command('init ', 'init project') 5 | program.parse(process.argv) -------------------------------------------------------------------------------- /bin/smarty-init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const program = require('commander') 3 | const {clone} = require('../lib/download') 4 | program 5 | .action(async name => { 6 | console.log('🚀创建项目:' + name) 7 | // 从github克隆项目到指定文件夹 8 | await clone('github:su37josephxia/smarty-end',name) 9 | }) 10 | program.parse(process.argv) -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | VITE_BASE_API=/api -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-ssr 3 | node_modules 4 | 5 | *.local 6 | .DS_Store 7 | yarn.lock 8 | package-lock.json -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Vite2项目最佳实践 2 | 3 | ### 配套视频演示 4 | 5 | 我专门录了一套视频演示本文所做的所有操作,喜欢看视频学习的小伙伴移步: 6 | [「备战2021」Vite2 + Vue3项目最佳实践](https://www.bilibili.com/video/BV1vX4y1K7bQ) 7 | 8 | 制作不易,求`3连`,求`关注` 9 | 10 | ### vite2来了 11 | 12 | `Vite1`还没用上,`Vite2`已经更新了,全新插件架构,丝滑的开发体验,和`Vue3`的完美结合。 2021年第一弹,村长打算以Vite2+Vue3为主题开启大家的前端学习之旅。 13 | 14 | ### 2021先学学vite准没错 15 | 16 | ![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a26ab28cab8d45a981986b581ae71d04~tplv-k3u1fbpfcp-zoom-1.image) 17 | 18 | ### 本文目标 19 | 20 | - `vite2`变化分析 21 | - 项目中常见任务`vite2+vue3`实践 22 | 23 | 24 | ### 创建Vite2项目 25 | 26 | 闲言碎语不必说,下面我们表一表好汉`vite2` 27 | 28 | 使用npm: 29 | 30 | ```bash 31 | $ npm init @vitejs/app 32 | ``` 33 | 34 | > 按提示指定项目名称和模板,或直接指定 35 | > 36 | > ```bash 37 | > $ npm init @vitejs/app my-vue-app --template vue 38 | > ``` 39 | 40 | 41 | 42 | ### Vite2主要变化 43 | 44 | 对我们之前项目影响较大的我已经都标记出来了: 45 | 46 | - 配置选项变化:`vue特有选项`、创建选项、css选项、jsx选项等 47 | - `别名行为变化`:不再要求`/`开头或结尾 48 | - `Vue支持`:通过 [@vitejs/plugin-vue](https://github.com/vitejs/vite/tree/main/packages/plugin-vue)插件支持 49 | - React支持 50 | - HMR API变化 51 | - 清单格式变化 52 | - `插件API重新设计` 53 | 54 | 55 | 56 | #### Vue支持 57 | 58 | Vue的整合也通过插件实现,和其他框架一视同仁: 59 | 60 | 61 | 62 | 63 | 64 | SFC定义默认使用`setup script`,语法比较激进,但更简洁,好评! 65 | 66 | 67 | 68 | #### 别名定义 69 | 70 | 不再需要像`vite1`一样在别名前后加上`/`,这和`webpack`项目配置可以保持一致便于移植,好评! 71 | 72 | ```js 73 | import path from 'path' 74 | 75 | export default { 76 | alias: { 77 | "@": path.resolve(__dirname, "src"), 78 | "comps": path.resolve(__dirname, "src/components"), 79 | }, 80 | } 81 | ``` 82 | 83 | `App.vue`里面用一下试试 84 | 85 | ```vue 86 | 89 | ``` 90 | 91 | 92 | 93 | #### 插件API重新设计 94 | 95 | `Vite2`主要变化在插件体系,这样更标准化、易扩展。`Vite2`插件API扩展自`Rollup`插件体系,因此能兼容现存的`Rollup`插件,编写的Vite插件也可以同时运行于开发和创建,好评! 96 | 97 | > 插件编写我会另开专题讨论,欢迎大家关注我。 98 | 99 | 100 | 101 | ##### Vue3 Jsx支持 102 | 103 | `vue3`中`jsx`支持需要引入插件:`@vitejs/plugin-vue-jsx` 104 | 105 | ```bash 106 | $ npm i @vitejs/plugin-vue-jsx -D 107 | ``` 108 | 109 | 注册插件,`vite.config.js` 110 | 111 | ```js 112 | import vueJsx from "@vitejs/plugin-vue-jsx"; 113 | 114 | export default { 115 | plugins: [vue(), vueJsx()], 116 | } 117 | ``` 118 | 119 | 用法也有要求,改造一下`App.vue` 120 | 121 | ```vue 122 | 123 | 138 | ``` 139 | 140 | 141 | 142 | ##### Mock插件应用 143 | 144 | 之前给大家介绍的[vite-plugin-mock](https://github.com/vbenjs/vite-plugin-mock)已经重构支持了Vite2。 145 | 146 | 147 | 148 | 安装插件 149 | 150 | ```bash 151 | npm i mockjs -S 152 | ``` 153 | 154 | ```bash 155 | npm i vite-plugin-mock cross-env -D 156 | ``` 157 | 158 | 159 | 160 | 配置,`vite.config.js` 161 | 162 | ```js 163 | import { viteMockServe } from 'vite-plugin-mock' 164 | 165 | export default { 166 | plugins: [ viteMockServe({ supportTs: false }) ] 167 | } 168 | ``` 169 | 170 | 171 | 172 | 设置环境变量,`package.json` 173 | 174 | ```json 175 | { 176 | "scripts": { 177 | "dev": "cross-env NODE_ENV=development vite", 178 | "build": "vite build" 179 | }, 180 | } 181 | ``` 182 | 183 | 184 | 185 | 186 | 187 | ### 项目基础架构 188 | 189 | #### 路由 190 | 191 | 安装`vue-router 4.x` 192 | 193 | ```js 194 | npm i vue-router@next -S 195 | ``` 196 | 197 | 198 | 199 | 200 | 201 | 路由配置,`router/index.js` 202 | 203 | ```js 204 | import { createRouter, createWebHashHistory } from 'vue-router'; 205 | 206 | const router = createRouter({ 207 | history: createWebHashHistory(), 208 | routes: [ 209 | { path: '/', component: () => import('views/home.vue') } 210 | ] 211 | }); 212 | 213 | export default router 214 | ``` 215 | 216 | 217 | 218 | 引入,`main.js` 219 | 220 | ```js 221 | import router from "@/router"; 222 | createApp(App).use(router).mount("#app"); 223 | ``` 224 | 225 | > 别忘了创建`home.vue`并修改`App.vue` 226 | > 227 | > 路由用法略有变化,[村长的视频教程](https://www.bilibili.com/video/BV1Wh411X7Xp?p=19) 228 | 229 | 230 | 231 | #### 状态管理 232 | 233 | 安装`vuex 4.x` 234 | 235 | ```bash 236 | npm i vuex@next -S 237 | ``` 238 | 239 | image 240 | 241 | 242 | 243 | Store配置,`store/index.js` 244 | 245 | ```js 246 | import {createStore} from 'vuex'; 247 | 248 | export default createStore({ 249 | state: { 250 | couter: 0 251 | } 252 | }); 253 | ``` 254 | 255 | 256 | 257 | 引入,`main.js` 258 | 259 | ```js 260 | import store from "@/store"; 261 | createApp(App).use(store).mount("#app"); 262 | ``` 263 | 264 | > 用法和以前基本一样,[村长的视频教程](https://www.bilibili.com/video/BV1Wh411X7Xp?p=23) 265 | 266 | 267 | 268 | 269 | 270 | #### 样式组织 271 | 272 | 安装sass 273 | 274 | ```bash 275 | npm i sass -D 276 | ``` 277 | 278 | 279 | 280 | `styles`目录保存各种样式 281 | 282 | ![截屏2020-12-24 上午11.51.30](https://gitee.com/57code/picgo/raw/master/%E6%88%AA%E5%B1%8F2020-12-24%20%E4%B8%8A%E5%8D%8811.51.30.png) 283 | 284 | `index.scss`作为出口组织这些样式,同时编写一些全局样式 285 | 286 | ![image-20201224115414266](https://gitee.com/57code/picgo/raw/master/image-20201224115414266.png) 287 | 288 | 最后在`main.js`导入 289 | 290 | ```js 291 | import "styles/index.scss"; 292 | ``` 293 | 294 | > 注意在`vite.config.js`添加`styles`别名 295 | 296 | 297 | 298 | #### UI库 299 | 300 | 就用我们[花果山团队](https://www.yuque.com/hugsun)自家的[element3](https://github.com/hug-sun/element3)。 301 | 302 | > [中文文档](https://element3-ui.com/) 303 | 304 | 305 | 306 | 安装 307 | 308 | ```bash 309 | npm i element3 -S 310 | ``` 311 | 312 | 313 | 314 | 完整引入,`main.js` 315 | 316 | ```js 317 | import element3 from "element3"; 318 | import "element3/lib/theme-chalk/index.css"; 319 | 320 | createApp(App).use(element3) 321 | ``` 322 | 323 | 324 | 325 | 按需引入,`main.js` 326 | 327 | ```js 328 | import "element3/lib/theme-chalk/button.css"; 329 | import { ElButton } from "element3" 330 | createApp(App).use(ElButton) 331 | ``` 332 | 333 | 334 | 335 | 抽取成插件会更好,`plugins/element3.js` 336 | 337 | ```js 338 | // 完整引入 339 | import element3 from "element3"; 340 | import "element3/lib/theme-chalk/index.css"; 341 | 342 | // 按需引入 343 | // import { ElButton } from "element3"; 344 | // import "element3/lib/theme-chalk/button.css"; 345 | 346 | export default function (app) { 347 | // 完整引入 348 | app.use(element3) 349 | 350 | // 按需引入 351 | // app.use(ElButton); 352 | } 353 | ``` 354 | 355 | 356 | 357 | 测试 358 | 359 | ```html 360 | my button 361 | ``` 362 | 363 | 364 | 365 | #### 基础布局 366 | 367 | 我们应用需要一个基本布局页,类似下图,将来每个页面以布局页为父页面即可: 368 | 369 | ![image-20201223143247535](https://gitee.com/57code/picgo/raw/master/image-20201223143247535.png) 370 | 371 | 372 | 373 | 布局页面,`layout/index.vue` 374 | 375 | ```vue 376 | 389 | 390 | 394 | 395 | 405 | ``` 406 | 407 | > 别忘了创建`AppMain.vue`和`Navbar.vue` 408 | 409 | 410 | 411 | 路由配置,`router/index.js` 412 | 413 | ```js 414 | { 415 | path: "/", 416 | component: Layout, 417 | children: [ 418 | { 419 | path: "", 420 | component: () => import('views/home.vue'), 421 | name: "Home", 422 | meta: { title: "首页", icon: "el-icon-s-home" }, 423 | }, 424 | ], 425 | }, 426 | ``` 427 | 428 | 429 | 430 | #### 动态导航 431 | 432 | ##### 侧边导航 433 | 434 | 根据路由表动态生成侧边导航菜单。 435 | 436 | ![image-20201225180300250](https://gitee.com/57code/picgo/raw/master/image-20201225180300250.png) 437 | 438 | 439 | 440 | 首先创建侧边栏组件,递归输出`routes`中的配置为多级菜单,`layout/Sidebar/index.vue` 441 | 442 | ```vue 443 | 462 | 463 | 479 | 480 | ``` 481 | 482 | > 注意:`sass`文件导出变量解析需要用到`css module`,因此`variables`文件要加上`module`中缀。 483 | 484 | 485 | 486 | 添加相关样式: 487 | 488 | - `styles/variables.module.scss` 489 | - `styles/sidebar.scss` 490 | - `styles/index.scss`中引入 491 | 492 | 493 | 494 | 创建`SidebarItem.vue`组件,解析当前路由是导航链接还是父菜单: 495 | 496 | ![image-20201229123955087](https://gitee.com/57code/picgo/raw/master/image-20201229123955087.png) 497 | 498 | 499 | 500 | ##### 面包屑 501 | 502 | 通过路由匹配数组可以动态生成面包屑。 503 | 504 | 505 | 506 | 面包屑组件,`layouts/components/Breadcrumb.vue` 507 | 508 | ```vue 509 | 520 | 521 | 561 | 562 | 575 | ``` 576 | 577 | > 别忘了添加依赖:`path-to-regexp` 578 | > 579 | > 注意:`vue-router4`已经不再使用`path-to-regexp`解析动态`path`,因此这里后续还需要改进。 580 | 581 | 582 | 583 | #### 数据封装 584 | 585 | 统一封装数据请求服务,有利于解决一下问题: 586 | 587 | - 统一配置请求 588 | - 请求、响应统一处理 589 | 590 | 591 | 592 | 准备工作: 593 | 594 | - 安装`axios`: 595 | 596 | ```bash 597 | npm i axios -S 598 | ``` 599 | 600 | - 添加配置文件:`.env.development` 601 | 602 | ``` 603 | VITE_BASE_API=/api 604 | ``` 605 | 606 | 607 | 608 | 请求封装,`utils/request.js` 609 | 610 | ```js 611 | import axios from "axios"; 612 | import { Message, Msgbox } from "element3"; 613 | 614 | // 创建axios实例 615 | const service = axios.create({ 616 | // 在请求地址前面加上baseURL 617 | baseURL: import.meta.env.VITE_BASE_API, 618 | // 当发送跨域请求时携带cookie 619 | // withCredentials: true, 620 | timeout: 5000, 621 | }); 622 | 623 | // 请求拦截 624 | service.interceptors.request.use( 625 | (config) => { 626 | // 模拟指定请求令牌 627 | config.headers["X-Token"] = "my token"; 628 | return config; 629 | }, 630 | (error) => { 631 | // 请求错误的统一处理 632 | console.log(error); // for debug 633 | return Promise.reject(error); 634 | } 635 | ); 636 | 637 | // 响应拦截器 638 | service.interceptors.response.use( 639 | /** 640 | * 通过判断状态码统一处理响应,根据情况修改 641 | * 同时也可以通过HTTP状态码判断请求结果 642 | */ 643 | (response) => { 644 | const res = response.data; 645 | 646 | // 如果状态码不是20000则认为有错误 647 | if (res.code !== 20000) { 648 | Message.error({ 649 | message: res.message || "Error", 650 | duration: 5 * 1000, 651 | }); 652 | 653 | // 50008: 非法令牌; 50012: 其他客户端已登入; 50014: 令牌过期; 654 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 655 | // 重新登录 656 | Msgbox.confirm("您已登出, 请重新登录", "确认", { 657 | confirmButtonText: "重新登录", 658 | cancelButtonText: "取消", 659 | type: "warning", 660 | }).then(() => { 661 | store.dispatch("user/resetToken").then(() => { 662 | location.reload(); 663 | }); 664 | }); 665 | } 666 | return Promise.reject(new Error(res.message || "Error")); 667 | } else { 668 | return res; 669 | } 670 | }, 671 | (error) => { 672 | console.log("err" + error); // for debug 673 | Message({ 674 | message: error.message, 675 | type: "error", 676 | duration: 5 * 1000, 677 | }); 678 | return Promise.reject(error); 679 | } 680 | ); 681 | 682 | export default service; 683 | 684 | ``` 685 | 686 | 687 | 688 | #### 业务处理 689 | 690 | ##### 结构化数据展示 691 | 692 | 使用`el-table`展示结构化数据,配合`el-pagination`做数据分页。 693 | 694 | ![image-20210201110626262](https://gitee.com/57code/picgo/raw/master/image-20210201110626262.png) 695 | 696 | 文件组织结构如下:`list.vue`展示列表,`edit.vue`和`create.vue`编辑或创建,内部复用`detail.vue`处理,`model`中负责数据业务处理。 697 | 698 | ![image-20210201110542893](https://gitee.com/57code/picgo/raw/master/image-20210201110542893.png) 699 | 700 | `list.vue`中的数据展示 701 | 702 | ```vue 703 | 704 | 705 | 706 | 707 | 708 | ``` 709 | 710 | 711 | 712 | `list`和`loading`数据的获取逻辑,可以使用`compsition-api`提取到`userModel.js` 713 | 714 | ```js 715 | export function useList() { 716 | // 列表数据 717 | const state = reactive({ 718 | loading: true, // 加载状态 719 | list: [], // 列表数据 720 | }); 721 | 722 | // 获取列表 723 | function getList() { 724 | state.loading = true; 725 | return request({ 726 | url: "/getUsers", 727 | method: "get", 728 | }).then(({ data, total }) => { 729 | // 设置列表数据 730 | state.list = data; 731 | }).finally(() => { 732 | state.loading = false; 733 | }); 734 | } 735 | 736 | // 首次获取数据 737 | getList(); 738 | 739 | return { state, getList }; 740 | } 741 | ``` 742 | 743 | 744 | 745 | `list.vue`中使用 746 | 747 | ```js 748 | import { useList } from "./model/userModel"; 749 | ``` 750 | 751 | ```js 752 | const { state, getList } = useList(); 753 | ``` 754 | 755 | 756 | 757 | 分页处理,`list.vue` 758 | 759 | ```html 760 | 766 | ``` 767 | 768 | 数据也在`userModel`中处理 769 | 770 | ```js 771 | const state = reactive({ 772 | total: 0, // 总条数 773 | listQuery: {// 分页查询参数 774 | page: 1, // 当前页码 775 | limit: 5, // 每页条数 776 | }, 777 | }); 778 | ``` 779 | 780 | ```js 781 | request({ 782 | url: "/getUsers", 783 | method: "get", 784 | params: state.listQuery, // 在查询中加入分页参数 785 | }) 786 | ``` 787 | 788 | 789 | 790 | ##### 表单处理 791 | 792 | 用户数据新增、编辑使用`el-form`处理 793 | 794 | 795 | 796 | 可用一个组件`detail.vue`来处理,区别仅在于初始化时是否获取信息回填到表单。 797 | 798 | ```html 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 提交 808 | 809 | 810 | ``` 811 | 812 | 813 | 814 | 数据处理同样可以提取到`userModel`中处理。 815 | 816 | ```js 817 | export function useItem(isEdit, id) { 818 | const model = ref(Object.assign({}, defaultData)); 819 | 820 | // 初始化时,根据isEdit判定是否需要获取详情 821 | onMounted(() => { 822 | if (isEdit && id) { 823 | // 获取详情 824 | request({ 825 | url: "/getUser", 826 | method: "get", 827 | params: { id }, 828 | }).then(({ data }) => { 829 | model.value = data; 830 | }); 831 | } 832 | }); 833 | return { model }; 834 | } 835 | ``` 836 | 837 | ### 配套视频演示 838 | 839 | 我专门录了一套视频演示本文所做的所有操作,喜欢看视频学习的小伙伴移步: 840 | [「备战2021」Vite2 + Vue3项目最佳实践](https://www.bilibili.com/video/BV1vX4y1K7bQ) 841 | 842 | 制作不易,求`3连`,求`关注` 843 | 844 | 845 | ### 关注村长 846 | 847 | 欢迎关注我的公众号「村长学前端」跟我一起学习最新前端知识。 848 | 849 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/mock/models.ts: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | url: "/api/models/list", 4 | type: "get", 5 | response: () => { 6 | return { 7 | code: 20000, 8 | data: ['user', 'role'], 9 | }; 10 | }, 11 | }, 12 | ]; -------------------------------------------------------------------------------- /frontend/mock/test.ts: -------------------------------------------------------------------------------- 1 | const mockList = [ 2 | { id: 1, name: "tom", age: 18 }, 3 | { id: 2, name: "jerry", age: 18 }, 4 | { id: 3, name: "mike", age: 18 }, 5 | { id: 4, name: "jack", age: 18 }, 6 | { id: 5, name: "larry", age: 18 }, 7 | { id: 6, name: "white", age: 18 }, 8 | { id: 7, name: "peter", age: 18 }, 9 | { id: 8, name: "james", age: 18 }, 10 | ]; 11 | 12 | module.exports = [ 13 | { 14 | url: "/api/getUser", 15 | type: "get", 16 | response: () => { 17 | return { 18 | code: 20000, 19 | data: { id: 1, name: "tom", age: 18 }, 20 | }; 21 | }, 22 | }, 23 | { 24 | url: "/api/getUsers", 25 | type: "get", 26 | response: (config) => { 27 | // 从查询参数中获取分页、过滤关键词等参数 28 | const { page = 1, limit = 5 } = config.query; 29 | 30 | // 分页 31 | const data = mockList.filter( 32 | (item, index) => index < limit * page && index >= limit * (page - 1) 33 | ); 34 | 35 | return { 36 | code: 20000, 37 | data, 38 | total: mockList.length, 39 | }; 40 | }, 41 | }, 42 | { 43 | url: "/api/addUser", 44 | type: "post", 45 | response: () => { 46 | // 直接返回 47 | return { 48 | code: 20000, 49 | }; 50 | }, 51 | }, 52 | { 53 | url: "/api/updateUser", 54 | type: "post", 55 | response: () => { 56 | return { 57 | code: 20000, 58 | }; 59 | }, 60 | }, 61 | { 62 | url: "/api/deleteUser", 63 | type: "get", 64 | response: () => { 65 | return { 66 | code: 20000, 67 | }; 68 | }, 69 | }, 70 | ]; 71 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite2-in-action", 3 | "version": "0.0.0", 4 | "license": "ISC", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "ant-design-vue": "^2.1.2", 12 | "axios": "^0.21.1", 13 | "element3": "0.0.40", 14 | "js-yaml": "^4.0.0", 15 | "mockjs": "^1.1.0", 16 | "path-browserify": "^1.0.1", 17 | "path-to-regexp": "^6.2.0", 18 | "process": "^0.11.10", 19 | "vue": "^3.0.5", 20 | "vue-i18n": "^9.0.0-rc.7", 21 | "vue-router": "^4.0.4", 22 | "vuex": "^4.0.0" 23 | }, 24 | "devDependencies": { 25 | "@intlify/vite-plugin-vue-i18n": "^2.0.0-rc.2", 26 | "@vitejs/plugin-vue": "^1.1.4", 27 | "@vitejs/plugin-vue-jsx": "^1.1.0", 28 | "@vue/compiler-sfc": "^3.0.5", 29 | "cross-env": "^7.0.3", 30 | "sass": "^1.32.8", 31 | "vite": "^2.0.1", 32 | "vite-plugin-mock": "^2.1.4", 33 | "vue-tsc": "^0.0.24" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smarty-team/smarty-end/4dd58f826b32c590f9dd1907e9ceaa404f2b4c5b/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smarty-team/smarty-end/4dd58f826b32c590f9dd1907e9ceaa404f2b4c5b/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /frontend/src/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 81 | 82 | 91 | -------------------------------------------------------------------------------- /frontend/src/layouts/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /frontend/src/layouts/components/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 56 | 57 | 70 | -------------------------------------------------------------------------------- /frontend/src/layouts/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 25 | 28 | 93 | -------------------------------------------------------------------------------- /frontend/src/layouts/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 5 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /frontend/src/layouts/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | -------------------------------------------------------------------------------- /frontend/src/layouts/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 99 | -------------------------------------------------------------------------------- /frontend/src/layouts/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 40 | -------------------------------------------------------------------------------- /frontend/src/layouts/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 31 | -------------------------------------------------------------------------------- /frontend/src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Language", 3 | "hello": "hello, world!" 4 | } -------------------------------------------------------------------------------- /frontend/src/locales/jp.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "言語", 3 | "hello": "こんにちは、世界!" 4 | } -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | // 全局样式 5 | import "styles/index.scss"; 6 | 7 | // element3 8 | import element3 from "./plugins/element3"; 9 | 10 | // router 11 | import router from "./router"; 12 | 13 | // store 14 | import store from "./store"; 15 | 16 | // i18n 17 | // import { createI18n } from "vue-i18n"; 18 | // import messages from "@intlify/vite-plugin-vue-i18n/messages"; 19 | // const i18n = createI18n({ 20 | // legacy: false, 21 | // locale: "en", 22 | // messages, 23 | // }); 24 | 25 | // 模型相关动态路由获取 26 | import './routes-model' 27 | 28 | createApp(App) 29 | .use(element3) 30 | .use(router) 31 | .use(store) 32 | // .use(i18n) 33 | .mount("#app"); 34 | -------------------------------------------------------------------------------- /frontend/src/plugins/element3.ts: -------------------------------------------------------------------------------- 1 | // 完整引入 2 | import { App } from "vue"; 3 | import element3 from "element3"; 4 | import "element3/lib/theme-chalk/index.css"; 5 | 6 | // 按需引入 7 | // import "element3/lib/theme-chalk/button.css"; 8 | // import { 9 | // ElRow, 10 | // ElCol, 11 | // ElContainer, 12 | // ElHeader, 13 | // ElFooter, 14 | // ElAside, 15 | // ElMain, 16 | // ElIcon, 17 | // ElButton, 18 | // ElLink, 19 | // ElRadio, 20 | // ElRadioButton, 21 | // ElRadioGroup, 22 | // ElCheckbox, 23 | // ElCheckboxButton, 24 | // ElCheckboxGroup, 25 | // ElInput, 26 | // ElInputNumber, 27 | // ElSelect, 28 | // ElOption, 29 | // ElOptionGroup, 30 | // ElCascader, 31 | // ElCascaderPanel, 32 | // ElSwitch, 33 | // ElSlider, 34 | // ElTimePicker, 35 | // ElTimeSelect, 36 | // ElDatePicker, 37 | // ElUpload, 38 | // ElRate, 39 | // ElColorPicker, 40 | // ElTransfer, 41 | // ElForm, 42 | // ElFormItem, 43 | // ElTag, 44 | // ElProgress, 45 | // ElTree, 46 | // ElPagination, 47 | // ElBadge, 48 | // ElAvatar, 49 | // ElAlert, 50 | // ElLoading, 51 | // ElMenu, 52 | // ElMenuItem, 53 | // ElSubmenu, 54 | // ElMenuItemGroup, 55 | // ElTabs, 56 | // ElTabPane, 57 | // ElBreadcrumb, 58 | // ElBreadcrumbItem, 59 | // ElPageHeader, 60 | // ElDropdown, 61 | // ElDropdownItem, 62 | // ElDropdownMenu, 63 | // ElSteps, 64 | // ElStep, 65 | // ElDialog, 66 | // ElTooltip, 67 | // ElPopover, 68 | // ElPopconfirm, 69 | // ElCard, 70 | // ElCarousel, 71 | // ElCarouselItem, 72 | // ElCollapse, 73 | // ElCollapseItem, 74 | // ElTimeline, 75 | // ElTimelineItem, 76 | // ElDivider, 77 | // ElCalendar, 78 | // ElImage, 79 | // ElBacktop, 80 | // ElInfiniteScroll, 81 | // ElDrawer, 82 | // ElScrollbar, 83 | // } from "element3"; 84 | 85 | export default function (app: App) { 86 | // 完整引入 87 | app.use(element3) 88 | 89 | // 按需引入 90 | // app.use(ElButton); 91 | } 92 | -------------------------------------------------------------------------------- /frontend/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | import Layout from "layouts/index.vue"; 3 | 4 | /** 5 | * Note: 子菜单仅当路由的children.length >= 1时才出现 6 | * 7 | * hidden: true 设置为true时路由将显示在sidebar中(默认false) 8 | * alwaysShow: true 如果设置为true则总是显示在菜单根目录 9 | * 如果不设置alwaysShow, 当路由有超过一个子路由时, 10 | * 将会变为嵌套模式, 否则不会显示根菜单 11 | * redirect: noRedirect 如果设置noRedirect时,breadcrumb中点击将不会跳转 12 | * name:'router-name' name用于 (必须设置!!!) 13 | * meta : { 14 | roles: ['admin','editor'] 页面可访问角色设置 15 | title: 'title' sidebar和breadcrumb显示的标题 16 | icon: 'svg-name'/'el-icon-x' sidebar中显示的图标 17 | breadcrumb: false 设置为false,将不会出现在面包屑中 18 | activeMenu: '/example/list' 如果设置一个path, sidebar将会在高亮匹配项 19 | } 20 | */ 21 | export const routes = [ 22 | { 23 | path: "/", 24 | redirect: "/home", 25 | }, 26 | { 27 | path: "/home", 28 | component: Layout, 29 | children: [ 30 | { 31 | path: "", 32 | component: () => import("views/home.vue"), 33 | } 34 | ] 35 | }, 36 | { 37 | path: "/users", 38 | component: Layout, 39 | meta: { 40 | title: "用户管理", 41 | icon: "el-icon-user-solid", 42 | }, 43 | redirect: "/users/list", 44 | children: [ 45 | { 46 | path: "list", 47 | component: () => import("views/users/list.vue"), 48 | meta: { 49 | title: "用户列表", 50 | icon: "el-icon-document", 51 | }, 52 | }, 53 | { 54 | path: "create", 55 | component: () => import("views/users/create.vue"), 56 | hidden: true, 57 | meta: { 58 | title: "创建新用户", 59 | activeMenu: "/users/list", 60 | }, 61 | }, 62 | { 63 | path: "edit/:id(\\d+)", 64 | name: "userEdit", 65 | component: () => import("views/users/edit.vue"), 66 | hidden: true, 67 | meta: { 68 | title: "编辑用户信息", 69 | activeMenu: "/users/list", 70 | }, 71 | }, 72 | ], 73 | }, 74 | { 75 | path: "/models", 76 | name: "modelsMgt", 77 | component: Layout, 78 | meta: { title: "模型管理" }, 79 | children: [ 80 | { 81 | path: "list", 82 | component: () => import("views/models/ModelList.vue"), 83 | meta: { 84 | title: "模型列表", 85 | icon: "el-icon-document", 86 | }, 87 | }, 88 | ] 89 | } 90 | ]; 91 | 92 | const router = createRouter({ 93 | history: createWebHashHistory(), 94 | routes, 95 | }); 96 | 97 | export default router; 98 | -------------------------------------------------------------------------------- /frontend/src/routes-model.ts: -------------------------------------------------------------------------------- 1 | import Layout from "layouts/index.vue"; 2 | import axios from "./utils/request"; 3 | import store from "./store"; 4 | import router from "./router"; 5 | 6 | axios.get("/metadata").then((res) => { 7 | // 模型数据管理路由定义 8 | const dataListRoute = { 9 | path: "/list", 10 | component: Layout, 11 | meta: { title: "数据管理" }, 12 | alwaysShow: true, 13 | name: "dataMgt", 14 | children: res.data.map((model: Model) => ({ 15 | path: model.id, 16 | props: { model }, 17 | component: () => import("views/models/DataList.vue"), 18 | meta: { title: model.description }, 19 | })), 20 | }; 21 | router.addRoute(dataListRoute); 22 | store.commit("routes/addRoute", dataListRoute); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } -------------------------------------------------------------------------------- /frontend/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | import routes from "./routes"; 3 | 4 | const store = createStore({ 5 | modules: { 6 | routes, 7 | }, 8 | }); 9 | 10 | export default store; 11 | -------------------------------------------------------------------------------- /frontend/src/store/routes.ts: -------------------------------------------------------------------------------- 1 | import { routes } from "../router"; 2 | 3 | export default { 4 | namespaced: true, 5 | state: { 6 | routes, 7 | }, 8 | mutations: { 9 | addRoute(state, route) { 10 | state.routes.push(route); 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "./mixin.scss"; 2 | @import "./variables.module.scss"; 3 | @import "./sidebar.scss"; 4 | 5 | // 编写全局样式 6 | body { 7 | height: 100%; 8 | -moz-osx-font-smoothing: grayscale; 9 | -webkit-font-smoothing: antialiased; 10 | text-rendering: optimizeLegibility; 11 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, 12 | Microsoft YaHei, Arial, sans-serif; 13 | margin: 0; 14 | } 15 | 16 | label { 17 | font-weight: 700; 18 | } 19 | 20 | html { 21 | height: 100%; 22 | box-sizing: border-box; 23 | } 24 | 25 | #app { 26 | height: 100%; 27 | font-family: Avenir, Helvetica, Arial, sans-serif; 28 | -webkit-font-smoothing: antialiased; 29 | -moz-osx-font-smoothing: grayscale; 30 | text-align: center; 31 | color: #2c3e50; 32 | } 33 | 34 | *, 35 | *:before, 36 | *:after { 37 | box-sizing: inherit; 38 | } 39 | 40 | a:focus, 41 | a:active { 42 | outline: none; 43 | } 44 | 45 | a, 46 | a:focus, 47 | a:hover { 48 | cursor: pointer; 49 | color: inherit; 50 | text-decoration: none; 51 | } 52 | 53 | div:focus { 54 | outline: none; 55 | } 56 | 57 | .clearfix { 58 | &:after { 59 | visibility: hidden; 60 | display: block; 61 | font-size: 0; 62 | content: " "; 63 | clear: both; 64 | height: 0; 65 | } 66 | } 67 | 68 | // main-container global css 69 | .app-container { 70 | padding: 20px; 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: $sideBarWidth; 7 | position: relative; 8 | } 9 | 10 | .sidebar-container { 11 | transition: width 0.28s; 12 | width: $sideBarWidth !important; 13 | background-color: $menuBg; 14 | height: 100%; 15 | position: fixed; 16 | font-size: 0px; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | z-index: 1001; 21 | overflow: hidden; 22 | 23 | // reset element-ui css 24 | .horizontal-collapse-transition { 25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 26 | } 27 | 28 | .scrollbar-wrapper { 29 | overflow-x: hidden !important; 30 | } 31 | 32 | .el-scrollbar__bar.is-vertical { 33 | right: 0px; 34 | } 35 | 36 | .el-scrollbar { 37 | height: 100%; 38 | } 39 | 40 | &.has-logo { 41 | .el-scrollbar { 42 | height: calc(100% - 50px); 43 | } 44 | } 45 | 46 | .is-horizontal { 47 | display: none; 48 | } 49 | 50 | a { 51 | display: inline-block; 52 | width: 100%; 53 | overflow: hidden; 54 | } 55 | 56 | .svg-icon { 57 | margin-right: 16px; 58 | } 59 | 60 | .sub-el-icon { 61 | margin-right: 12px; 62 | margin-left: -2px; 63 | } 64 | 65 | .el-menu { 66 | border: none; 67 | height: 100%; 68 | width: 100% !important; 69 | } 70 | 71 | // menu hover 72 | .submenu-title-noDropdown, 73 | .el-submenu__title { 74 | &:hover { 75 | background-color: $menuHover !important; 76 | } 77 | } 78 | 79 | .is-active>.el-submenu__title { 80 | color: $subMenuActiveText !important; 81 | } 82 | 83 | & .nest-menu .el-submenu>.el-submenu__title, 84 | & .el-submenu .el-menu-item { 85 | min-width: $sideBarWidth !important; 86 | background-color: $subMenuBg !important; 87 | 88 | &:hover { 89 | background-color: $subMenuHover !important; 90 | } 91 | } 92 | } 93 | 94 | .hideSidebar { 95 | .sidebar-container { 96 | width: 50px !important; 97 | } 98 | 99 | .main-container { 100 | margin-left: 54px; 101 | } 102 | 103 | .submenu-title-noDropdown { 104 | padding: 0 !important; 105 | position: relative; 106 | 107 | .el-tooltip { 108 | padding: 0 !important; 109 | 110 | .svg-icon { 111 | margin-left: 20px; 112 | } 113 | 114 | .sub-el-icon { 115 | margin-left: 19px; 116 | } 117 | } 118 | } 119 | 120 | .el-submenu { 121 | overflow: hidden; 122 | 123 | &>.el-submenu__title { 124 | padding: 0 !important; 125 | 126 | .svg-icon { 127 | margin-left: 20px; 128 | } 129 | 130 | .sub-el-icon { 131 | margin-left: 19px; 132 | } 133 | 134 | .el-submenu__icon-arrow { 135 | display: none; 136 | } 137 | } 138 | } 139 | 140 | .el-menu--collapse { 141 | .el-submenu { 142 | &>.el-submenu__title { 143 | &>span { 144 | height: 0; 145 | width: 0; 146 | overflow: hidden; 147 | visibility: hidden; 148 | display: inline-block; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | .el-menu--collapse .el-menu .el-submenu { 156 | min-width: $sideBarWidth !important; 157 | } 158 | 159 | // mobile responsive 160 | .mobile { 161 | .main-container { 162 | margin-left: 0px; 163 | } 164 | 165 | .sidebar-container { 166 | transition: transform .28s; 167 | width: $sideBarWidth !important; 168 | } 169 | 170 | &.hideSidebar { 171 | .sidebar-container { 172 | pointer-events: none; 173 | transition-duration: 0.3s; 174 | transform: translate3d(-$sideBarWidth, 0, 0); 175 | } 176 | } 177 | } 178 | 179 | .withoutAnimation { 180 | 181 | .main-container, 182 | .sidebar-container { 183 | transition: none; 184 | } 185 | } 186 | } 187 | 188 | // when menu collapsed 189 | .el-menu--vertical { 190 | &>.el-menu { 191 | .svg-icon { 192 | margin-right: 16px; 193 | } 194 | .sub-el-icon { 195 | margin-right: 12px; 196 | margin-left: -2px; 197 | } 198 | } 199 | 200 | .nest-menu .el-submenu>.el-submenu__title, 201 | .el-menu-item { 202 | &:hover { 203 | // you can use $subMenuHover 204 | background-color: $menuHover !important; 205 | } 206 | } 207 | 208 | // the scroll bar appears when the subMenu is too long 209 | >.el-menu--popup { 210 | max-height: 100vh; 211 | overflow-y: auto; 212 | 213 | &::-webkit-scrollbar-track-piece { 214 | background: #d3dce6; 215 | } 216 | 217 | &::-webkit-scrollbar { 218 | width: 6px; 219 | } 220 | 221 | &::-webkit-scrollbar-thumb { 222 | background: #99a9bf; 223 | border-radius: 20px; 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /frontend/src/styles/variables.module.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText:#bfcbd9; 3 | $menuActiveText:#409EFF; 4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 5 | 6 | $menuBg:#304156; 7 | $menuHover:#263445; 8 | 9 | $subMenuBg:#1f2d3d; 10 | $subMenuHover:#001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | // the :export directive is the magic sauce for webpack 15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 16 | :export { 17 | menuText: $menuText; 18 | menuActiveText: $menuActiveText; 19 | subMenuActiveText: $subMenuActiveText; 20 | menuBg: $menuBg; 21 | menuHover: $menuHover; 22 | subMenuBg: $subMenuBg; 23 | subMenuHover: $subMenuHover; 24 | sideBarWidth: $sideBarWidth; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/types.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Model { 3 | id: string; 4 | description: string; 5 | } -------------------------------------------------------------------------------- /frontend/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { Message, Msgbox } from "element3"; 3 | import store from "../store"; 4 | 5 | // 创建axios实例 6 | const service = axios.create({ 7 | // 在请求地址前面加上baseURL 8 | baseURL: import.meta.env.VITE_BASE_API as string, 9 | // 当发送跨域请求时携带cookie 10 | // withCredentials: true, 11 | timeout: 5000, 12 | }); 13 | 14 | // 请求拦截 15 | service.interceptors.request.use( 16 | (config) => { 17 | // 指定请求令牌 18 | // if (store.getters.token) { 19 | // // 自定义令牌的字段名为X-Token,根据咱们后台再做修改 20 | // config.headers["X-Token"] = store.getters.token; 21 | // } 22 | config.headers["X-Token"] = "my token"; 23 | return config; 24 | }, 25 | (error) => { 26 | // 请求错误的统一处理 27 | console.log(error); // for debug 28 | return Promise.reject(error); 29 | } 30 | ); 31 | 32 | // 响应拦截器 33 | service.interceptors.response.use( 34 | /** 35 | * If you want to get http information such as headers or status 36 | * Please return response => response 37 | */ 38 | 39 | /** 40 | * 通过判断状态码统一处理响应,根据情况修改 41 | * 同时也可以通过HTTP状态码判断请求结果 42 | */ 43 | (response) => { 44 | const res = response.data; 45 | 46 | // 如果状态码不是20000则认为有错误 47 | if (res.code !== 0) { 48 | Message.error({ 49 | message: res.message || "Error", 50 | duration: 5 * 1000, 51 | }); 52 | 53 | // 50008: 非法令牌; 50012: 其他客户端已登入; 50014: 令牌过期; 54 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 55 | // 重新登录 56 | Msgbox.confirm("您已登出, 请重新登录", "确认", { 57 | confirmButtonText: "重新登录", 58 | cancelButtonText: "取消", 59 | type: "warning", 60 | }).then(() => { 61 | store.dispatch("user/resetToken").then(() => { 62 | location.reload(); 63 | }); 64 | }); 65 | } 66 | return Promise.reject(new Error(res.message || "Error")); 67 | } else { 68 | return res; 69 | } 70 | }, 71 | (error) => { 72 | console.log("err" + error); // for debug 73 | Message({ 74 | message: error.message, 75 | type: "error", 76 | duration: 5 * 1000, 77 | }); 78 | return Promise.reject(error); 79 | } 80 | ); 81 | 82 | export default service; 83 | -------------------------------------------------------------------------------- /frontend/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | export function isExternal(path) { 2 | return /^(https?:|mailto:|tel:)/.test(path); 3 | } -------------------------------------------------------------------------------- /frontend/src/views/detail.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/src/views/home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/views/models/DataList.vue: -------------------------------------------------------------------------------- 1 | 308 | 309 | 315 | -------------------------------------------------------------------------------- /frontend/src/views/models/ModelEdit.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /frontend/src/views/models/ModelList.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | 30 | 42 | -------------------------------------------------------------------------------- /frontend/src/views/users/components/detail.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 79 | 80 | 85 | 110 | -------------------------------------------------------------------------------- /frontend/src/views/users/create.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /frontend/src/views/users/edit.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /frontend/src/views/users/list.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 110 | 111 | 117 | -------------------------------------------------------------------------------- /frontend/src/views/users/model/userModel.js: -------------------------------------------------------------------------------- 1 | import { reactive, onMounted, ref } from "vue"; 2 | import request from "utils/request"; 3 | 4 | export function useList() { 5 | // 列表数据 6 | const state = reactive({ 7 | loading: true, // 加载状态 8 | list: [], // 列表数据 9 | total: 0, 10 | listQuery: { 11 | page: 1, 12 | limit: 5, 13 | }, 14 | }); 15 | 16 | // 获取列表 17 | function getList() { 18 | state.loading = true; 19 | 20 | return request({ 21 | url: "/getUsers", 22 | method: "get", 23 | params: state.listQuery, 24 | }) 25 | .then(({ data, total }) => { 26 | // 设置列表数据 27 | state.list = data; 28 | state.total = total; 29 | }) 30 | .finally(() => { 31 | state.loading = false; 32 | }); 33 | } 34 | 35 | // 删除项 36 | function delItem(id) { 37 | state.loading = true; 38 | 39 | return request({ 40 | url: "/deleteUser", 41 | method: "get", 42 | params: { id }, 43 | }).finally(() => { 44 | state.loading = false; 45 | }); 46 | } 47 | 48 | // 首次获取数据 49 | getList(); 50 | 51 | return { state, getList, delItem }; 52 | } 53 | 54 | const defaultData = { 55 | name: "", 56 | age: undefined, 57 | }; 58 | 59 | export function useItem(isEdit, id) { 60 | const model = ref(Object.assign({}, defaultData)); 61 | 62 | // 初始化时,根据isEdit判定是否需要获取玩家详情 63 | onMounted(() => { 64 | if (isEdit && id) { 65 | // 获取玩家详情 66 | request({ 67 | url: "/getUser", 68 | method: "get", 69 | params: { id }, 70 | }).then(({ data }) => { 71 | model.value = data; 72 | }); 73 | } 74 | }); 75 | 76 | const updateUser = () => { 77 | return request({ 78 | url: "/updateUser", 79 | method: "post", 80 | data: model.value, 81 | }); 82 | }; 83 | 84 | const addUser = () => { 85 | return request({ 86 | url: "/addUser", 87 | method: "post", 88 | data: model.value, 89 | }); 90 | }; 91 | 92 | return { model, updateUser, addUser }; 93 | } 94 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "noImplicitAny": false, 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"], 13 | "types": ["vite/client"] 14 | }, 15 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 16 | } 17 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import vueJsx from "@vitejs/plugin-vue-jsx"; 4 | import { viteMockServe } from "vite-plugin-mock"; 5 | import vueI18n from "@intlify/vite-plugin-vue-i18n"; 6 | import { defineConfig } from "vite"; 7 | 8 | export default defineConfig({ 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "src"), 12 | comps: path.resolve(__dirname, "src/components"), 13 | styles: path.resolve(__dirname, "src/styles"), 14 | plugins: path.resolve(__dirname, "src/plugins"), 15 | views: path.resolve(__dirname, "src/views"), 16 | layouts: path.resolve(__dirname, "src/layouts"), 17 | utils: path.resolve(__dirname, "src/utils"), 18 | apis: path.resolve(__dirname, "src/apis"), 19 | dirs: path.resolve(__dirname, "src/directives"), 20 | }, 21 | }, 22 | server: { 23 | proxy: { 24 | "/api": "http://localhost:4000" 25 | }, 26 | }, 27 | plugins: [ 28 | vue(), 29 | vueJsx(), 30 | // viteMockServe({ localEnabled: false }), 31 | vueI18n({ 32 | include: path.resolve(__dirname, "./src/locales/**"), 33 | }), 34 | ], 35 | }); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smarty-end", 3 | "version": "1.0.0", 4 | "description": "- 为全栈工程师定制的中后台框架 - 无代码和少代码化 - 基于 TypeScript", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "node script/start.js", 8 | "start": "npm run dev" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/su37josephxia/smarty-end.git" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/su37josephxia/smarty-end/issues" 19 | }, 20 | "homepage": "https://github.com/su37josephxia/smarty-end#readme" 21 | } 22 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | npm config get registry # 检查仓库镜像库 3 | npm config set registry=http://registry.npmjs.org 4 | echo '请进行登录相关操作:' 5 | npm login # 登陆 6 | echo "-------publishing-------" 7 | npm publish # 发布 8 | npm config set registry=https://registry.npm.taobao.org # 设置为淘宝镜像 9 | echo "发布完成" 10 | exit -------------------------------------------------------------------------------- /script/start.js: -------------------------------------------------------------------------------- 1 | async function spawn(...args) { 2 | const { spawn } = require("child_process"); 3 | const proc = spawn(...args); 4 | proc.stdout.pipe(process.stdout); 5 | proc.stderr.pipe(process.stderr); 6 | return proc; 7 | } 8 | 9 | // 启动npm调试模式 10 | const backend = spawn("yarn", ["run", "dev"], { cwd: `./backend` }); 11 | const frontend = spawn("yarn", ["run", "dev"], { cwd: `./frontend` }); 12 | 13 | process.on("exit", () => { 14 | backend.kill(); 15 | frontend.kill(); 16 | }); 17 | --------------------------------------------------------------------------------