├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── logo.png │ └── style │ │ ├── common.less │ │ └── reset.less ├── components │ ├── Headerbar.vue │ └── composables │ │ └── index.ts ├── main.ts ├── mock │ ├── data │ │ ├── about.ts │ │ └── user.ts │ ├── index.ts │ └── service.ts ├── router │ └── index.ts ├── shims-vue.d.ts ├── source.d.ts ├── store │ └── index.ts ├── types │ └── index.d.ts └── views │ ├── About.vue │ ├── Home.vue │ └── List.vue └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | '@vue/typescript/recommended', 9 | 'plugin:import/errors', 10 | 'plugin:import/warnings', 11 | 'plugin:import/typescript', 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | }, 16 | rules: { 17 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 19 | 'arrow-body-style': ['error', 'always'], 20 | 'max-len': ['error', { code: 160 }], 21 | 'import/no-extraneous-dependencies': ['warn', { devDependencies: false, optionalDependencies: false, peerDependencies: false }], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # admin 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.0", 12 | "axios-mock-adapter": "^1.19.0", 13 | "core-js": "^3.6.5", 14 | "vue": "^3.0.0", 15 | "vue-router": "^4.0.0-0", 16 | "vuex": "^4.0.0-0" 17 | }, 18 | "devDependencies": { 19 | "@typescript-eslint/eslint-plugin": "^2.33.0", 20 | "@typescript-eslint/parser": "^2.33.0", 21 | "@vue/cli-plugin-babel": "~4.5.0", 22 | "@vue/cli-plugin-eslint": "~4.5.0", 23 | "@vue/cli-plugin-router": "~4.5.0", 24 | "@vue/cli-plugin-typescript": "~4.5.0", 25 | "@vue/cli-plugin-vuex": "~4.5.0", 26 | "@vue/cli-service": "~4.5.0", 27 | "@vue/compiler-sfc": "^3.0.0", 28 | "@vue/eslint-config-airbnb": "^5.0.2", 29 | "@vue/eslint-config-typescript": "^5.0.2", 30 | "eslint": "^6.7.2", 31 | "eslint-plugin-import": "^2.20.2", 32 | "eslint-plugin-vue": "^7.0.0-0", 33 | "less": "^3.0.4", 34 | "less-loader": "^5.0.0", 35 | "mockjs": "^1.1.0", 36 | "typescript": "~3.9.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuteSunLee/vue3_ts_admin/cfd3f2eafa4b7e4c1d448497cb17102c593f0b58/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuteSunLee/vue3_ts_admin/cfd3f2eafa4b7e4c1d448497cb17102c593f0b58/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/style/common.less: -------------------------------------------------------------------------------- 1 | .content_page { 2 | width: 100%; 3 | height: 100%; 4 | background: #fff; 5 | .content_title { 6 | height: 56px; 7 | line-height: 56px; 8 | background: #f5f8fa; 9 | font-size: 14px; 10 | border-bottom: 1px solid #e4eaee; 11 | border-right: 1px solid #e4eaee; 12 | padding-left: 20px; 13 | font-weight: bold; 14 | } 15 | .content_body { 16 | height: calc(100% - 56px); 17 | background: #fff; 18 | .content_button { 19 | box-sizing: border-box; 20 | padding: 12px 20px; 21 | height: 52px; 22 | background: #fff; 23 | border-bottom: 1px solid #e4eaee; 24 | } 25 | .content_table { 26 | padding: 20px; 27 | height: calc(100% - 52px); 28 | overflow: auto; 29 | input { 30 | font-size: 12px; 31 | color: #666; 32 | } 33 | span { 34 | padding-left: 0px; 35 | padding-right: 10px; 36 | } 37 | table { 38 | border-collapse: collapse; 39 | width: 100%; 40 | table-layout: fixed; 41 | thead { 42 | font-weight: 700; 43 | word-wrap: normal; 44 | text-overflow: ellipsis; 45 | line-height: 30px; 46 | vertical-align: middle; 47 | } 48 | tbody { 49 | text-align: center; 50 | } 51 | tfoot { 52 | th { 53 | height: 0; 54 | } 55 | } 56 | th, 57 | td { 58 | border: 1px solid #e5e5e5; 59 | vertical-align: middle; 60 | box-sizing: border-box; 61 | padding: 0 5px; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | overflow: hidden; 65 | height: 35px; 66 | } 67 | th { 68 | background-color: #f6f6f6; 69 | } 70 | tr:nth-child(even) { 71 | background: rgba(246, 246, 246, 0.50); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | button { 78 | min-width: 74px; 79 | height: 28px; 80 | line-height: 26px; 81 | padding: 0 12px; 82 | font-size: 12px; 83 | border: 1px solid #d9d9d9; 84 | background-color: #FFF; 85 | transition-property: border-color,background-color,color,opacity; 86 | transition-duration: .2s; 87 | border-radius: 4px; 88 | cursor: pointer; 89 | &:hover { 90 | background-color: #f8f8f8; 91 | } 92 | &:focus { 93 | outline: none; 94 | } 95 | &.primary { 96 | border-color: transparent; 97 | background-color: #3998fc; 98 | color: #fff; 99 | &:hover { 100 | background-color: #3389e3; 101 | } 102 | } 103 | } 104 | input { 105 | min-width: 120px; 106 | height: 28px; 107 | line-height: 26px; 108 | border-radius: 4px; 109 | padding: 0 9px; 110 | vertical-align: middle; 111 | border: 1px solid #d9d9d9; 112 | background-color: #fff; 113 | color: #666; 114 | transition: border-color .2s,color .2s; 115 | font-size: 12px; 116 | &:focus { 117 | outline: none; 118 | border-color: #3998fc; 119 | } 120 | } -------------------------------------------------------------------------------- /src/assets/style/reset.less: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | body, 3 | html { 4 | width: 100%; 5 | height: 100%; 6 | line-height: 1; 7 | font-size: 12px; 8 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, "sans-serif"; 9 | background-color: #f4f8f9; 10 | color: #666; 11 | overflow: hidden; 12 | } 13 | 14 | * { 15 | margin: 0; 16 | padding: 0; 17 | -webkit-tap-highlight-color: rgba(0,0,0,0); 18 | -webkit-touch-callout: none; 19 | box-sizing: border-box; 20 | } -------------------------------------------------------------------------------- /src/components/Headerbar.vue: -------------------------------------------------------------------------------- 1 | 10 | 17 | 18 | 33 | -------------------------------------------------------------------------------- /src/components/composables/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted } from 'vue'; 2 | import {ItemType, FetchType, DeleteType, AddType, EditType} from '../../types/index'; 3 | 4 | export const compositionApi = ( 5 | fetchApi: FetchType, 6 | deleteApi: DeleteType, 7 | confirmAddApi: AddType, 8 | confirmEditApi: EditType, 9 | itemData: ItemType, 10 | ) => { 11 | const currentIndex = ref(null); 12 | const list = ref([{}]); 13 | const getList = () => { 14 | fetchApi().then((res: any) => { 15 | list.value = res.data.list; 16 | }); 17 | }; 18 | const addItem = () => { 19 | list.value.unshift(itemData); 20 | currentIndex.value = 0; 21 | }; 22 | const editItem = (index: number) => { 23 | currentIndex.value = index; 24 | }; 25 | const deleteItem = (index: number, item: ItemType) => { 26 | deleteApi(item).then(() => { 27 | list.value.splice(index, 1); 28 | // getList(); 29 | }); 30 | }; 31 | const cancel = (item: ItemType) => { 32 | currentIndex.value = null; 33 | if (!item.id) { 34 | list.value.splice(0, 1); 35 | } 36 | }; 37 | const confirm = (item: ItemType) => { 38 | const api = item.id ? confirmEditApi : confirmAddApi; 39 | api(item).then(() => { 40 | getList(); 41 | cancel(item); 42 | }); 43 | }; 44 | onMounted(() => { 45 | getList(); 46 | }); 47 | return { 48 | list, 49 | currentIndex, 50 | getList, 51 | addItem, 52 | editItem, 53 | deleteItem, 54 | cancel, 55 | confirm, 56 | }; 57 | }; 58 | 59 | export default compositionApi; 60 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import store from './store'; 5 | import './assets/style/reset.less'; 6 | import './assets/style/common.less'; 7 | import mock from './mock/index'; 8 | 9 | mock.bootstrap(); 10 | 11 | const app = createApp(App); 12 | 13 | app.use(store).use(router).mount('#app'); 14 | -------------------------------------------------------------------------------- /src/mock/data/about.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | import {ItemType} from '../../types/index'; 3 | const About: ItemType[] = []; 4 | 5 | for (let i = 0; i < 10; i++) { 6 | About.push(Mock.mock({ 7 | id: Mock.Random.guid(), 8 | users: Mock.Random.cname(), 9 | date: Mock.Random.date(), 10 | })); 11 | } 12 | export { About }; 13 | -------------------------------------------------------------------------------- /src/mock/data/user.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | import {ItemType} from '../../types/index'; 3 | const Users: ItemType[] = []; 4 | 5 | for (let i = 0; i < 10; i++) { 6 | Users.push(Mock.mock({ 7 | id: Mock.Random.guid(), 8 | name: Mock.Random.cname(), 9 | birth: Mock.Random.date(), 10 | sex: Mock.Random.integer(0, 1), 11 | address: Mock.mock('@county(true)'), 12 | 'age|18-30': 1, 13 | })); 14 | } 15 | export { Users }; 16 | -------------------------------------------------------------------------------- /src/mock/index.ts: -------------------------------------------------------------------------------- 1 | import mock from './service'; 2 | 3 | export default mock; 4 | -------------------------------------------------------------------------------- /src/mock/service.ts: -------------------------------------------------------------------------------- 1 | import MockAdapter from 'axios-mock-adapter'; 2 | import axios from 'axios'; 3 | import { Users } from './data/user'; 4 | import { About } from './data/about'; 5 | 6 | 7 | export default { 8 | bootstrap() { 9 | const mock = new MockAdapter(axios); 10 | mock.onGet('/users').reply(200, { 11 | list: Users, 12 | }); 13 | mock.onGet('/about').reply(200, { 14 | list: About, 15 | }); 16 | mock.onPost('/users/delete').reply((config) => { 17 | console.log(config); 18 | return new Promise((resolve) => { 19 | resolve([200, { code: 200, msg: '删除成功' }]); 20 | }); 21 | }); 22 | mock.onPost('/users/add').reply((config) => { 23 | console.log(config); 24 | return new Promise((resolve) => { 25 | resolve([200, { code: 200, msg: '添加成功' }]); 26 | }); 27 | }); 28 | mock.onPost('/users/edit').reply((config) => { 29 | console.log(config); 30 | return new Promise((resolve) => { 31 | resolve([200, { code: 200, msg: '编辑成功' }]); 32 | }); 33 | }); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; 2 | import Home from '../views/Home.vue'; 3 | 4 | const routes: Array = [ 5 | { 6 | path: '/', 7 | name: 'Home', 8 | component: Home, 9 | }, 10 | { 11 | path: '/about', 12 | name: 'About', 13 | // route level code-splitting 14 | // this generates a separate chunk (about.[hash].js) for this route 15 | // which is lazy-loaded when the route is visited. 16 | component: () => { return import(/* webpackChunkName: "about" */ '../views/About.vue'); }, 17 | }, 18 | { 19 | path: '/list', 20 | name: 'List', 21 | component: () => { return import(/* webpackChunkName: "list" */ '../views/List.vue'); }, 22 | }, 23 | ]; 24 | 25 | const router = createRouter({ 26 | history: createWebHashHistory(), 27 | routes, 28 | }); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | 4 | const component: DefineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /src/source.d.ts: -------------------------------------------------------------------------------- 1 | declare const Vue: string; 2 | declare module '*.json'; 3 | declare module '*.png'; 4 | declare module '*.jpg'; 5 | declare module 'api'; 6 | declare module 'axios'; 7 | declare module 'mockjs'; 8 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex'; 2 | 3 | export default createStore({ 4 | state: { 5 | }, 6 | mutations: { 7 | }, 8 | actions: { 9 | }, 10 | modules: { 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface ItemType { 2 | id?: number | string; 3 | name?: string; 4 | birth?: string; 5 | sex?: number; 6 | address?: string; 7 | users?: string; 8 | date?: string; 9 | [propName: string]: any; 10 | } 11 | 12 | export interface FetchType { 13 | () : Promsise 14 | } 15 | 16 | export interface DeleteType { 17 | (item: ItemType) : Promise 18 | } 19 | 20 | export interface AddType { 21 | (item: ItemType) : Promise 22 | } 23 | 24 | export interface EditType { 25 | (item: ItemType) : Promise 26 | } -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 61 | 116 | 126 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 19 | -------------------------------------------------------------------------------- /src/views/List.vue: -------------------------------------------------------------------------------- 1 | 78 | 139 | 149 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | --------------------------------------------------------------------------------