Code Repository Previous Gitee code templates Gitee example for this article, please download branch 5_other
Creating a Project 1 2 3 4 5 6 # I personally prefer using pnpm npm create vite pnpm create vite custom-vue-starter # After running the command , options will pop up. Select Vue and then JavaScript cd custom-vue-starter pnpm i
Directory Structure A complete frontend project requires:
State Management Maintain common state (data) globally to share data between page components. We use Pinia.
Routing Routing allows navigation between pages. We use Vue Router.
Styling Styling makes the page more visually appealing. We use TailwindCSS.
Network Requests The frontend interacts with the backend through network requests to implement functionality. We use Axios.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ├── dev-dist/ # Development environment build output directory ├── node_modules/ # Node.js dependencies directory ├── public/ # Static resources directory, not processed by the build tool ├── src/ ├── admin/ # Contains backend management pages ├── api/ # API request logic ├── assets/ # Static resources (images, fonts, etc.) ├── components/ # Common components ├── includes/ # Include files, for external libraries ├── lib/ # Contains some common resources within the project ├── locales/ # Internationalization language packages ├── mocks/ # Mock data ├── pages/ # Contains regular page components ├── router/ # Router configuration ├── store/ # State management (Pinia) ├── styles/ # Global styles or CSS files ├── utils/ # Utility functions ├── App.vue # Root component ├── main.js # Project entry file ├── middleware.js # Middleware logic (e.g., route guards) └── settings.js # Project settings or configuration files ├── .gitignore # Git ignore file configuration ├── index.html # Project entry HTML file ├── package.json # Project configuration and dependencies declaration ├── postcss.config.js # PostCSS configuration file ├── README.md # Project documentation ├── tailwind.config.js # Tailwind CSS configuration file ├── vite.config.js # Vite build tool configuration file
Configuration Path Aliases Configure path aliases to use @/
to represent the src/
directory.
1 2 # Node type declarations to prevent errors after using Node dependencies pnpm i @types/node --save-dev
1 2 3 4 5 6 7 8 9 10 11 12 13 import {defineConfig} from 'vite' import {join} from 'path' ;import vue from '@vitejs/plugin-vue' export default defineConfig ({ plugins : [vue ()], resolve : { alias : { '@' : join (__dirname, 'src' ), } } })
Project Settings 1 2 3 4 5 export default { routeMode : 'history' , BaseURL : 'http://localhost:4000' , timeout : 5000 , }
PWA Configuration 1 2 # v2 pnpm i vite-plugin-pwa --save-dev
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import {defineConfig} from 'vite' import {join} from 'path' ;import vue from '@vitejs/plugin-vue' import {VitePWA } from "vite-plugin-pwa" ;export default defineConfig ({ plugins : [ vue (), VitePWA ({ registerType : 'autoUpdate' , devOptions : { enabled : true }, manifest : { name : "vue-quick-start" , theme_color : '#ff5e3a' , icons : [ { src : 'assets/logo.png' , size : '192x192' , type : 'image/png' } ] }, workbox : { globPatterns : ['**/*.{js,css,html,png,jpg}' ] } }) ], resolve : { alias : { '@' : join (__dirname, 'src' ), } } })
Loading Progress Bar
src/main.js
1 2 3 4 5 import 'nprogress/nprogress.css' import progressBar from "./includes/progress-bar" ;progressBar (router)app.use (router)
includes/progress-bar
1 2 3 4 5 6 7 8 9 import NProgress from "nprogress" ;export default (router) => { router.beforeEach ((to, from , next ) => { NProgress .start (); next (); }); router.afterEach (NProgress .done ); };
Tailwind Using Tailwind 3 (if you want to use version 4, there are some installation differences)
1 2 pnpm install -D tailwindcss@3 postcss autoprefixer pnpm dlx tailwindcss@3 init -p
tailwind.config.js
1 2 3 4 5 6 7 8 9 10 11 export default { content : [ "./index.html" , "./src/**/*.{vue,js,ts,jsx,tsx}" , ], theme : { extend : {}, }, plugins : [], }
styles/base.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 @tailwind base;@layer base { h1 { @apply text-2 xl; } h2 { @apply text-xl; } h3 { @apply text-lg; } h4 { @apply text-base; } h5 { @apply text-sm; } h6 { @apply text-xs; } } @tailwind components;@tailwind utilities;body { @apply h-full w-full p-0 m-0 ; } @media (min-width : 1536px ) { .container { max-width : 100% ; } } .container { width : 100% ; height : 100% ; background-color : #f5f7fb ; padding : 20px ; .container-wrapper { width : 100% ; height : 100% ; padding : 15px 30px 0 ; background-color : #fff ; border-radius : 8px ; display : flex; flex-direction : column; overflow : hidden; } } .plugin-download { width : 500px !important ; a :hover { text-decoration : underline; } }
Remember to import in src/main.js
1 2 3 4 5 6 import {createApp} from 'vue' import '@/styles/base.css' import App from './App.vue' createApp (App ) .mount ('#app' )
Router Using Vue Router for route navigation, and we will implement file routing, automatically scanning the page.vue
files in the directory and registering them as routes. There are two page directories: one is admin
, and the other ispages
. The admin
directory is for backend management pages, and the pages
directory is for regular page components.
1 2 # v4 pnpm i vue-router@4
File Routing File routing automatically scans and registers routes based on the directory structure, eliminating the need to manually declare and register each one. The key is:
This is a method provided by Vite to scan and retrieve files. Webpack has a similar method:
We are using Vite, so we use import.meta.glob
. Now, let’s implement the file scanning functionality. pages/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 const pages = import .meta .glob ('./**/page.vue' )const routes = Object .keys (pages).map (key => { const path = key .replace (/^\.\// , '' ) .replace (/\/?page\.vue$/ , '' ); return { path : `/${path} ` , name : path, component : () => pages[key](), meta : { title : path.split ('/' ).pop () || 'Home' }, hidden : false , whiteList : true }; }); const allPaths = routes.map (route => route.path );const parentRoutes = new Set ();allPaths.forEach (path => { let currentPath = path.substring (0 , path.lastIndexOf ('/' )); console .log ('currentPath' , currentPath) while (currentPath) { if (!parentRoutes.has (currentPath)) { routes.push ({ path : currentPath, name : currentPath.replace (/\//g , '-' ), meta : {title : currentPath.split ('/' ).pop ()} }); parentRoutes.add (currentPath); } currentPath = currentPath.substring (0 , currentPath.lastIndexOf ('/' )); } }); console .log ('scan routes' , routes)export default routes
app.vue renders child routes
1 2 3 4 <template > <router-view /> </template >
admin/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 const pages = import .meta .glob ('./**/page.vue' )const routes = Object .keys (pages).map (key => { const path = key .replace (/^\.\// , '' ) .replace (/\/?page\.vue$/ , '' ); return { path : path, name : path, component : () => pages[key](), meta : { title : path.split ('/' ).pop () || 'Home' }, hidden : false , whiteList : true }; }); const allPaths = routes.map (route => route.path );const parentRoutes = new Set ();allPaths.forEach (path => { let currentPath = path.substring (0 , path.lastIndexOf ('/' )); console .log ('currentPath' , currentPath) while (currentPath) { if (!parentRoutes.has (currentPath)) { routes.push ({ path : currentPath, name : currentPath.replace (/\//g , '-' ), meta : {title : currentPath.split ('/' ).pop ()} }); parentRoutes.add (currentPath); } currentPath = currentPath.substring (0 , currentPath.lastIndexOf ('/' )); } }); console .log ('scan admin routes' , routes)export default routes
admin/base.vue, first simply render child routes, and then improve the management system skeleton in the component section
1 2 3 4 <template > <router-view /> </template >
These codes are similar, the principle is to scan the directory and then process the nested structure. After scanning and processing, put them into a route array, which is the definition of the route, and can be uniformly registered to the global route. Now, let’s look at the route code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import {createRouter, createWebHashHistory, createWebHistory} from 'vue-router' import routes from "@/pages/index.js" ;import adminRoutes from '@/admin/index.js' import settings from "../settings.js" ;const router = createRouter ({ history : settings.routeMode === 'history' ? createWebHistory (import .meta .env .BASE_URL ) : createWebHashHistory (import .meta .env .BASE_URL ), routes : [ { path : '/about' , name : 'about' , component : () => import ('@/pages/about/page.vue' ) }, { path : '/login' , name : 'login' , component : () => import ('@/pages/login/page.vue' ) }, { path : '/404' , name : '404' , component : () => import ('@/pages/404.vue' ), beforeEnter : (to, from , next ) => { console .log ('Router Guard' ) console .log (to, from ) next () } }, ...routes, { name : 'admin' , path : '/admin' , component : () => import ('@/admin/base.vue' ), children : [ ...adminRoutes ] }, { path : '/:catchAll(.*)*' , redirect : {name : '404' } } ] }) router.beforeEach ((to, from , next ) => { console .log ('Global Guard' ) console .log (to, from ) if (to.meta && to.meta .title ) { document .title = to.meta .title } next () }) export default router
Remember to import and use in src/main.js
1 2 3 4 5 6 7 8 import {createApp} from 'vue' import '@/styles/base.css' import App from './App.vue' import router from '@/router/index.js' createApp (App ) .use (router) .mount ('#app' )
At this point, you can create test files in pages and admin, such as pages/a/b/c/page.vue, and then start the project to access it.
Components We place components in the components directory, and automatically scan and register them into the Vue instance through the index.js in this directory, so they can be used globally.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function toPascalCase (str ) { return str .split ('-' ) .map (p => p.charAt (0 ).toUpperCase () + p.slice (1 )) .join ('' ) } export default { install : (app ) => { const components = import .meta .glob ('./**/*.vue' , {eager : true }) for (const path in components) { const componentName = path .replace (/^\.\// , '' ) .replace (/\.\w+$/ , '' ) .split ('/' ) .map (toPascalCase) .join ('' ) const component = components[path].default app.component (componentName, component) } } }
Here, install
is the Vue plugin logic, and the install
method will be automatically called when the Vue instance uses use
. Use in src/main.js
1 2 3 4 5 6 7 8 9 10 import {createApp} from 'vue' import '@/styles/base.css' import App from './App.vue' import router from '@/router/index.js' import components from "@/components/index.js" ;createApp (App ) .use (router) .use (components) .mount ('#app' )
Testing: components/hello-world.vue
1 2 3 4 <template > hello </template >
components/mal/red.vue
1 2 3 4 <template > malred </template >
pages/page.vue
1 2 3 4 5 <template > <hello-world /> <mal-red /> </template >
Element Plus Earlier, admin/base.vue only simply rendered the content of child routes. Now, we need to add some skeletons, such as the left menu, top navigation, and bottom copyright; these skeletons are components, and we use the Element Plus component library to simplify development.
1 2 # v2 pnpm i element-plus @element-plus/icons-vue
Use in src/main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import {createApp} from 'vue' import '@/styles/base.css' import App from './App.vue' import router from '@/router/index.js' import components from "@/components/index.js" ;import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' const app = createApp (App )app.use (router) import * as ElementPlusIconsVue from '@element-plus/icons-vue' for (const [key, component] of Object .entries (ElementPlusIconsVue )) { app.component (key, component) } app.use (ElementPlus ) app.use (components) app.mount ('#app' )
Layout Components 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template > <div :key ="item.path" > <el-menu-item v-if ="!item.children || !item.children.length" :index ="item.path" > {{ item.meta.title || item.name }} </el-menu-item > <el-sub-menu v-else :index ="item.path" :key ="item.path" > <template #title > {{ item.meta.title || item.name }}</template > <MenuItem v-for ="child in item.children" :key ="child.path" :item ="child" /> </el-sub-menu > </div > </template > <script > export default { name : 'MenuItem' , props : ['item' ] }; </script > <style scoped > </style >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 <template > <div class ="aside-container" > <div class ="logo-section" > <img src ="../../assets/logo.png" alt ="Logo" class ="logo-img" /> <span class ="logo-title" > Malred</span > </div > <el-menu :default-active ="$route.path" class ="el-menu-vertical-demo" background-color ="#409EFF" text-color ="white" active-text-color ="#ffd04b" :router ="true" > <menu-item v-for ="item in menuTree" :key ="item.path" :item ="item" /> </el-menu > </div > </template > <script > import routes from '@/admin/index' ; import {buildMenuTree} from '@/utils/menu' ; import MenuItem from "./aside/menu-item.vue" ; export default { components : { MenuItem }, data ( ) { return { menuTree : buildMenuTree (routes.map (item => ({ ...item, path : '/admin/' + item.path }))) }; }, mounted ( ) { console .log (this .menuTree ) } }; </script > <style scoped > .aside-container { height : 100% ; } .logo-section { background-color : #4fa3fa ; display : flex; align-items : center; padding : 14px ; color : white; } .logo-img { width : 32px ; height : 32px ; margin-right : 10px ; } .logo-title { font-size : 18px ; font-weight : bold; } </style >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 <template > <div class ="header-container" > <el-menu mode ="horizontal" background-color ="#4fa3fa" text-color ="white" active-text-color ="#ffd04b" :router ="true" > <el-menu-item index ="/" > Home</el-menu-item > <el-sub-menu index ="/about" popper-append-to-body > <template #title > About</template > <el-menu-item index ="/about/team" > Team Introduction</el-menu-item > <el-menu-item index ="/about/contact" > Contact Us</el-menu-item > </el-sub-menu > <el-menu-item index ="/services" > Services</el-menu-item > </el-menu > <div class ="right-tools" > <translation-btn style ="margin-right: 16px;" /> <el-button circle style ="margin-right: 8px;" > <el-icon > <bell /> </el-icon > </el-button > <el-button circle > <el-icon > <user /> </el-icon > </el-button > </div > </div > </template > <script setup > </script > <style scoped > .header-container { width : 100% ; display : flex; align-items : center; } .right-tools { display : flex; align-items : center; margin-left : auto; } </style >
admin/base.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <template > <el-container style ="height: 100vh; width: 100vw;" > <el-aside width ="200px" style ="background-color: #409EFF; height: 100vh;" > <layout-aside /> </el-aside > <el-container style ="flex: 1; min-width: 0;" > <el-header style ="background-color: #4fa3fa; height: 60px; display: flex; align-items: center;" > <layout-header /> </el-header > <el-main style ="margin: 0; padding: 0; height: calc(100vh - 60px);" > <router-view /> </el-main > <el-footer style ="display: flex; justify-content: center; align-items: center;" > Copyright © Malred </el-footer > </el-container > </el-container > </template > <script setup > </script > <style scoped > .el-main { --el-main-padding : 0px ; } </style >
Menu Tree Building Utility
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export function buildMenuTree (routes ) { console .log ('build menu routes' , routes) const tree = []; const map = {}; routes.forEach (route => { map[route.path ] = {...route, children : []}; }); routes.forEach (route => { const path = route.path ; const parentPath = path.substring (0 , path.lastIndexOf ('/' )); if (parentPath && map[parentPath]) { map[parentPath].children .push (map[path]); } else { tree.push (map[path]); } }); return tree; }
Base Components There are also some components for managing pages that we’ll write as well.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template > <el-form-item :label ="label" > <el-input :modelValue ="value" @update:modelValue ="$emit('update:value', $event)" /> </el-form-item > </template > <script setup > import {defineProps, defineEmits} from 'vue' ; defineProps ({ label : String , value : [String , Number ] }); const emit = defineEmits (['update:value' ]); </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <template > <el-dialog v-model ="visible" :title ="title" width ="50%" > <el-form :model ="formData" label-width ="120px" > <slot > </slot > </el-form > <template #footer > <el-button @click ="visible = false" > Cancel</el-button > <el-button type ="primary" @click ="submit" > Submit</el-button > </template > </el-dialog > </template > <script setup > import {ref} from 'vue' ; const emit = defineEmits (['submit' ]); const visible = ref (false ); const show = ( ) => { visible.value = true ; }; const submit = ( ) => { emit ('submit' ); visible.value = false ; }; defineExpose ({show}); import {defineProps} from 'vue' ; defineProps ({ title : String , formData : { type : Object , default : () => ({}) } }); </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <template > <div class ="search-item" > <label v-if ="label" > {{ label }}</label > <el-date-picker v-model ="value" type ="date" placeholder ="Select date" value-format ="YYYY-MM-DD" /> </div > </template > <script setup > import {defineProps, defineModel} from 'vue' ; defineProps ({ label : String }); const value = defineModel (); </script > <style scoped > .search-item { display : flex; flex-direction : row; align-items : center; gap : 8px ; } </style >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <template > <div class ="search-item" > <label v-if ="label" > {{ label }}</label > <el-input style ="width: 120px;" v-model ="value" placeholder ="Please enter" /> </div > </template > <script setup > import {defineProps, defineModel} from 'vue' ; const props = defineProps ({ label : String }); const value = defineModel (); </script > <style scoped > .search-item { display : flex; flex-direction : row; align-items : center; gap : 8px ; } </style >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <template > <div class ="search-item" > <label v-if ="label" > {{ label }}</label > <el-select v-model ="value" placeholder ="Please select" > <el-option v-for ="item in options" :key ="item.value" :label ="item.label" :value ="item.value" /> </el-select > </div > </template > <script setup > import {defineProps, defineModel} from 'vue' ; defineProps ({ label : String , options : { type : Array , required : true , default : () => [] } }); const value = defineModel (); </script > <style scoped > .search-item { display : flex; flex-direction : row; align-items : center; width : 120px ; label { width : 56px ; } } </style >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 <template > <div class ="table-header" > <div class ="search-container" > <slot name ="search" > </slot > </div > <div class ="actions" > <slot name ="action" > <el-button type ="primary" @click ="$emit('add')" > New</el-button > <el-button type ="danger" @click ="onDeleteSelected" :disabled ="deleteDisabled" > Delete Selected </el-button > </slot > </div > </div > </template > <script setup > defineProps ({ deleteDisabled : Boolean , onDeleteSelected : { type : Function , default : null } }); defineEmits (['add' ]); </script > <style scoped > .table-header { display : flex; justify-content : space-between; align-items : center; margin-bottom : 12px ; margin-top : 8px ; flex-wrap : wrap; gap : 12px ; } .search-container { display : flex; align-items : center; flex-grow : 1 ; min-width : 200px ; gap : 8px ; } .actions { display : flex; gap : 8px ; } </style >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 <template > <el-table :data ="data" border style ="width: 100%" @selection-change ="handleSelectionChange" > <el-table-column type ="selection" width ="55" > </el-table-column > <el-table-column v-for ="(col, index) in columns" :key ="index" :prop ="col.prop" :label ="col.label" :width ="col.width" > <template #default ="scope" > <slot :name ="'col-' + col.prop" :row ="scope.row" > {{ scope.row[col.prop] }} </slot > </template > </el-table-column > <el-table-column label ="Actions" width ="150" > <template #default ="scope" > <slot name ="operation" :row ="scope.row" > <el-button link type ="primary" size ="small" @click ="handleEdit(scope.row)" > Edit</el-button > <el-button link type ="danger" size ="small" @click ="handleDelete(scope.row)" > Delete</el-button > </slot > </template > </el-table-column > </el-table > </template > <script setup > import {defineProps, defineEmits, ref} from 'vue' ; const props = defineProps ({ data : { type : Array , required : true }, columns : { type : Array , required : true }, onDelete : { type : Function , default : null } }); const emit = defineEmits (['edit' , 'delete' , 'selection-change' ]); const selectedRows = ref ([]); const handleSelectionChange = (rows ) => { selectedRows.value = rows; emit ('selection-change' , rows); }; const handleEdit = (row ) => { emit ('edit' , row); }; const handleDelete = (row ) => { if (props.onDelete ) { props.onDelete ([row]); } else { emit ('delete' , [row]); } }; </script >
Testing: admin/test/page.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 <template > <div class ="container" > <div class ="container-wrapper space-y-4" > <base-table-header :delete-disabled ="selectedRows.length === 0" @add ="openAddDialog" :onDeleteSelected ="handleDeleteSelected" > <template #search > <base-table-search-item label ="Username" v-model ="searchForm.username" /> <base-table-search-item label ="Email" v-model ="searchForm.email" /> <base-table-search-item label ="ID" v-model ="searchForm.id" /> <base-table-search-select label ="Status" v-model ="searchForm.status" :options ="[ { label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' } ]" /> <base-table-search-date label ="Creation Time" v-model ="searchForm.createTime" /> <el-button type ="info" size ="small" @click ="resetSearchForm" > Reset</el-button > </template > </base-table-header > <base-table-body :data ="tableData" :columns ="columns" @selection-change ="handleSelectionChange" @edit ="openEditDialog" @delete ="handleDelete" :onDelete ="handleDelete" > <template #operation ="props" > <el-button link type ="primary" size ="small" @click ="openEditDialog(props.row)" > Edit</el-button > <el-button link type ="danger" size ="small" @click ="handleDelete([props.row])" > Delete</el-button > </template > </base-table-body > <base-operation ref ="operationDialog" title ="User Information" @submit ="submitForm" > <base-operation-item label ="Username" :value ="formData.username" @update:value ="val => formData.username = val" /> <base-operation-item label ="Email" :value ="formData.email" @update:value ="val => formData.email = val" /> </base-operation > </div > </div > </template > <script setup > import {ref, watch} from 'vue' ; const originalData = [ {id : 1 , username : 'admin' , email : 'admin@example.com' , status : 'active' , createTime : '2025-06-01' }, {id : 2 , username : 'user1' , email : 'user1@example.com' , status : 'inactive' , createTime : '2025-06-02' }, {id : 3 , username : 'test' , email : 'test@example.com' , status : 'active' , createTime : '2025-06-03' } ]; const tableData = ref ([...originalData]); const columns = [ {prop : 'id' , label : 'ID' }, {prop : 'username' , label : 'Username' }, {prop : 'email' , label : 'Email' }, {prop : 'status' , label : 'Status' }, {prop : 'createTime' , label : 'Creation Time' }, ]; const searchForm = ref ({ username : '' , email : '' , id : '' , status : '' , createTime : '' }); const resetSearchForm = ( ) => { searchForm.value = { username : '' , email : '' , id : '' , status : '' , createTime : '' }; }; const formData = ref ({ username : '' , email : '' , }); const operationDialog = ref (null ); const openAddDialog = ( ) => { formData.value = {username : '' , email : '' }; operationDialog.value .show (); }; const openEditDialog = (row ) => { formData.value = {...row}; operationDialog.value .show (); }; const submitForm = ( ) => { if (!formData.value .id ) { const newRow = { id : Date .now (), username : formData.value .username , email : formData.value .email }; tableData.value .unshift (newRow); } else { const index = tableData.value .findIndex (item => item.id === formData.value .id ); if (index > -1 ) { tableData.value .splice (index, 1 , {...formData.value }); } } }; const selectedRows = ref ([]); const handleSelectionChange = (rows ) => { selectedRows.value = rows; }; const handleDelete = (rows ) => { tableData.value = tableData.value .filter (row => !rows.some (r => r.id === row.id ) ); }; const handleDeleteSelected = ( ) => { if (selectedRows.value .length === 0 ) return ; handleDelete (selectedRows.value ); }; function applySearch (data, search ) { const {username, email, id, status, createTime} = search; return data.filter (item => { return ( (!username || item.username .toLowerCase ().includes (username.toLowerCase ())) && (!email || item.email .toLowerCase ().includes (email.toLowerCase ())) && (!id || item.id .toString ().includes (id.toString ())) && (!status || item.status === status) && (!createTime || item.createTime === createTime) ); }); } watch ( () => searchForm.value , (newVal ) => { tableData.value = applySearch (originalData, newVal); }, {deep : true } ); </script > <style scoped > .search-form { display : flex; align-items : center; gap : 12px ; } </style >
i18n is a commonly used library for internationalization.
src/includes/i18n.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import {createI18n} from "vue-i18n" ;export const messages = {};const modules = import .meta .glob ('../locales/*.json' ); for (const path in modules) { const key = path.match (/\.\.\/locales\/([^.]+)\.json$/ )?.[1 ]; if (key) { const module = await modules[path](); messages[key] = module .default ; } console .log (key) console .log (path) } console .log ('messages' , messages)export default createI18n ({ legacy : false , locale : 'cn' , fallbackLocale : 'cn' , messages, });
main.js
1 2 3 import i18n from "@/includes/i18n.js" ;app.use (i18n)
src/locales/cn.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "error" : { "nofound" : "Page is lost~!" } , "greet" : "Hello" , "info" : { "translate" : { "btn" : { "cn" : "Chinese" , "en" : "English" } } } }
src/locales/en.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "error" : { "nofound" : "404 NOT FOUND!" } , "greet" : "hello" , "info" : { "translate" : { "btn" : { "cn" : "Chinese" , "en" : "English" } } } }
Translation button component src/components/translation-btn.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <template > <el-dropdown trigger ="click" @command ="handleSelect" > <el-button circle style ="background-color: transparent;" > <img src ="../assets/icons/components/translation.svg" alt ="Language" style ="width: 20px; height: 20px;" /> </el-button > <template #dropdown > <el-dropdown-menu > <el-dropdown-item v-for ="option in options" :key ="option.key" :command ="option.key" > {{ option.label }} </el-dropdown-item > </el-dropdown-menu > </template > </el-dropdown > </template > <script setup > import {computed} from 'vue' import {useI18n} from 'vue-i18n' const {t, locale} = useI18n () const options = computed (() => [ { label : t ('info.translate.btn.en' ), key : 'en' }, { label : t ('info.translate.btn.cn' ), key : 'cn' } ]); const handleSelect = (key ) => { locale.value = String (key) } </script > <style scoped > .el-button { border : none; padding : 0 ; width : 36px ; height : 36px ; } </style >
Common Methods utils Place utility functions in the utils folder, and we will mount them to the Vue instance globally by scanning the JS files in this directory and then mounting them to the Vue instance.
1 2 3 4 5 6 7 8 9 10 11 12 13 const modules = import .meta .glob ('./*.js' ); const utils = {};for (const path in modules) { const moduleName = path.replace ('./' , '' ).replace ('.js' , '' ); const module = await modules[path](); utils[moduleName] = module .default ; } export default { install : (app ) => { app.config .globalProperties .$utils = utils; }, };
Getting the Vue 3 instance method
1 2 3 4 5 6 7 import {getCurrentInstance} from "vue" export const instance = ( ) => getCurrentInstance () .appContext .config .globalProperties export const api = ( ) => instance ().$api export const utils = ( ) => instance ().$utils
1 2 3 4 import utils from '@/utils/index.js' app.use (utils)
Testing
1 2 3 4 export default { test : () => console .log ('test' ) }
1 2 3 4 5 6 <script setup > import {utils} from "./utils/vue-instance.js" ; utils ().test .test () </script >
State Management store When learning Vue, you will find that native component value passing can be quite troublesome, especially for sibling/cross-level value passing. We can use a global state management solution to solve this. Here, we use Pinia.
1 2 3 4 5 6 7 8 9 10 import {createPinia} from 'pinia' import {useUserStore} from './user-store.js' export { useUserStore } export default createPinia ()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import {defineStore, acceptHMRUpdate} from 'pinia' export const useUserStore = defineStore ('user' , { state : () => ({ name : 'Zhang San' , age : 25 }), actions : { setName (newName ) { this .name = newName } } }) if (import .meta .hot ) { import .meta .hot .accept (acceptHMRUpdate (useUserStore, import .meta .hot )) }
1 2 3 4 import store from "@/store/index.js" ;app.use (store)
Testing
1 2 3 4 5 6 7 8 9 10 11 <template > <div > <div @click ="userStore.setName('malred')" > {{ userStore.name }}</div > </div > </template > <script setup > import {useUserStore} from "../store/index.js" ; const userStore = useUserStore () </script >
Network Request API Using Axios for network requests, in most cases, front-end and back-end development are done by different teams. The front-end request interface address is provided by the back-end. If the back-end does not have an interface yet, you need to use fake data or json-server for simulation testing.
Encapsulation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import axios from 'axios' import settings from "../settings.js" ;const request = axios.create ({ baseURL : settings.BaseURL , timeout : settings.timeout , }) request.interceptors .request .use ( (config ) => { const token = localStorage .getItem ('token' ) if (token) { config.headers ['Authorization' ] = `Bearer ${token} ` } return config }, (error ) => { return Promise .reject (error) } ) request.interceptors .response .use ( (response ) => { return response.data }, (error ) => { return Promise .reject (error) } ) export default request
Scan and register to Vue instance
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const modules = import .meta .glob ('./**/index.js' ) const requests = {}for (const path in modules) { if (path === './index.js' ) continue const modulePath = path .replace ('./' , '' ) .replace ('/index.js' , '' ) const segments = modulePath.split ('/' ) const moduleName = segments[segments.length - 1 ] console .log ('api' , moduleName) console .log ('api' , segments) let currentLevel = requests for (let i = 0 ; i < segments.length ; i++) { const segment = segments[i] currentLevel[segment] = currentLevel[segment] || {} currentLevel = currentLevel[segment] } await modules[path]().then (module => { console .log ('currentLevel' , currentLevel) console .log ('moduleName' , moduleName) Object .assign (currentLevel, module .default ) }) } export default { install : (app ) => { app.config .globalProperties .$api = requests }, }
Add to main.js
1 2 3 4 import api from '@/api/index.js' app.use (api)
Testing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import request from "../request.js" ;export default { queryAdmin (body ) { return request ({url : '/api/admin' , method : 'get' , data : '' , params : body}) }, queryAdminDetail (id ) { return request ({url : '/api/admin' + id, method : 'get' , data : '' , params : '' }) }, addAdmin (body ) { return request ({url : '/api/admin' , method : 'post' , data : body, params : '' }) }, editAdmin (body, id ) { return request ({url : '/api/admin' + id, method : 'put' , data : body, params : '' }) }, deleteAdmin (ids ) { return request ({url : '/api/admin' + ids.join (',' ), method : 'delete' , data : '' , params : '' }) }, }
1 2 3 4 5 6 <script setup > import {api} from "./utils/vue-instance.js" ; api ().admin .queryAdmin () </script >
Next Steps
json-server to simulate interfaces
Code generator, batch generate repetitive pages and code
You can contact me on these platforms: