Project background
It is inevitable to use multimedia materials such as pictures and videos in the front-end development process. The common processing schemes usually separate the dynamic and static, and place pictures and other resources on the drawing bed. In addition to using the drawing bed resources commonly used in the industry, such as qiniu cloud and microblog drawing bed, we can also build a drawing bed ourselves with the help of a third-party drawing bed, Provide better basic services for team business development and improve development experience and efficiency. This paper aims to review and summarize the implementation scheme of the front end of the self built drawing bed, hoping to give some reference and schemes to students with similar needs.
programme
For the architecture selection of the front-end part, considering that Vue3 is about to become the main version, as an application on the front-end infrastructure side, we want to use Vue3 to implement the front-end side. Vite (Vue template TS) + Vue3 is used here+ vuex@next+vue - router@next It also carries out one-step technical cai and keng for the packaging and construction of vite. (ps: vite is really fast, but it still needs to be considered directly in the industrial environment at present, and there are still many pits. Personally, I think cross language front-end engineering may be the development direction of subsequent front-end engineering)
catalogue
src
- assets
components
- index.ts
- Card.vue
- Login.vue
- Upload.vue
- WrapperLayouts.vue
- WrapperLogin.vue
- WrapperUpload.vue
config
- index.ts
- menuMap.ts
- routes.ts
layouts
- index.ts
- Aside.vue
- Layouts.vue
- Main.vue
- Nav.vue
route
- index.ts
store
- index.ts
utils
- index.ts
- reg.ts
- validate.ts
views
- Page.vue
- App.vue
- index.scss
- main.ts
- vue-app-env.d.ts
- index.html
- tsconfig.json
- vite.config.ts
practice
The front-end picture bed involves permission verification. Authentication and confirmation are not required for obtaining pictures, but login authentication is required for uploading and deleting pictures
Source code
vue3 can be written through two schemes: class and template. For the scheme of using composition API, I suggest using class component is more comfortable and more like the writing method of react. Here, the use of composition API and options API is mixed. At present, Vue is compatible. For students from vue2, they can gradually adapt to the writing method of composition API, Then, the front-end business is realized step by step according to the functional idea of hooks
vite.config.ts
Some configurations related to vite construction can be configured according to the project requirements
const path = require('path') // vite.config.js # or vite.config.ts console.log(path.resolve(__dirname, './src')) module.exports = { alias: { // The key must start and end with a slash '/@/': path.resolve(__dirname, './src'), }, /** * The basic common path when serving in production. * @default '/' */ base: './', /** * The directory associated with the "root" where the build output will be placed. If the directory exists, it will be deleted before building. * @default 'dist' */ outDir: 'dist', port: 3000, // Automatically open in browser open: false, // Enable https https: false, // Server rendering ssr: false, // Introduction of third-party configuration // optimizeDeps: { // include: ["moment", "echarts", "axios", "mockjs"], // }, proxy: { // If it starts with / bff, the access address is as follows '/bff/': { target: 'http://localhost:30096/',// 'http://10.186.2.55:8170/', changeOrigin: true, rewrite: (path) => path.replace(/^\/bff/, ''), } }, optimizeDeps: { include: ['element-plus/lib/locale/lang/zh-cn', 'axios'], }, }
Page.vue
The display of each sub project page only needs one component to render different data
<template> <div class="page-header"> <el-row> <el-col :span="12"> <el-page-header :content="$route.fullPath.split('/').slice(2).join(' > ')" @back="handleBack" /> </el-col> <el-col :span="12"> <section class="header-button"> <!-- <el-button class="folder-add" :icon="FolderAdd" @click="handleFolder" >New folder</el-button> --> <el-button class="upload" :icon="Upload" type="success" @click="handleImage">Upload pictures</el-button> </section> </el-col> </el-row> </div> <div class="page"> <el-row :gutter="10"> <el-col v-for="(item, index) in cards" :xs="12" :sm="8" :md="6" :lg="4" :xl="4"> <Card @next="handleRouteView(item.ext, item.name)" @delete="handleDelete" :name="item.name" :src="item.src" :ext="item.ext" :key="index" /> </el-col> </el-row> <el-pagination layout="sizes, prev, pager, next, total" @size-change="handleSizeChange" @current-change="handlePageChange" :current-page.sync="pageNum" :page-size="pageSize" :total="total" ></el-pagination> <router-view /> </div> <WrapperUpload ref="wrapper-upload" :headers="computedHeaders" /> <WrapperLogin ref="wrapper-login" /> </template> <script lang="ts"> import { defineComponent, } from 'vue'; import { useRoute } from 'vue-router' import { FolderAdd, Upload } from '@element-plus/icons-vue' import { Card, WrapperUpload, WrapperLogin } from '../components' export default defineComponent({ name: 'Page', components: { Card, WrapperUpload, WrapperLogin }, props: { }, setup() { return { FolderAdd, Upload } }, data() { return { cards: [], total: 30, pageSize: 30, pageNum: 1, bucketName: '', prefix: '', } }, watch: { $route: { immediate: true, handler(val) { console.log('val', val) if (val) { this.handleCards() } } } }, methods: { handleBack() { this.$router.go(-1) }, handleFolder() { }, handleDelete(useName) { console.log('useName', useName) const [bucketName, ...objectName] = useName.split('/'); console.log('bukcetName', bucketName); console.log('objectName', objectName.join('/')); if (sessionStorage.getItem('token')) { this.$http.post("/bff/imagepic/object/removeObject", { bucketName: bucketName, objectName: objectName.join('/') }, { headers: { 'Authorization': sessionStorage.getItem('token'), } }).then(res => { console.log('removeObject', res) if (res.data.success) { this.$message.success(`${objectName.pop()}Picture deleted successfully`); setTimeout(() => { this.$router.go(0) }, 100) } else { this.$message.error(`${objectName.pop()}Picture deletion failed. Reason for failure: ${res.data.data}`) } }) } else { this.$refs[`wrapper-login`].handleOpen() } }, handleImage() { sessionStorage.getItem('token') ? this.$refs[`wrapper-upload`].handleOpen() : this.$refs[`wrapper-login`].handleOpen() }, handleRouteView(ext, name) { // console.log('extsss', ext) if (ext == 'file') { console.log('$router', this.$router) console.log('$route.name', this.$route.name, this.$route.path) this.$router.addRoute(this.$route.name, { path: `:${name}`, name: name, component: () => import('./Page.vue') } ) console.log('$router.options.routes', this.$router.options.routes) this.$router.push({ path: `/page/${this.$route.params.id}/${name}` }) } else { } }, handlePageChange(val) { this.pageNum = val; this.handleCards(); }, handleSizeChange(val) { this.pageSize = val; this.handleCards(); }, handleCards() { this.cards = []; let [bucketName, prefix] = this.$route.path.split('/').splice(2); this.bucketName = bucketName; this.prefix = prefix; console.log('bucketName', bucketName, prefix) this.$http.post("/bff/imagepic/object/listObjects", { bucketName: bucketName, prefix: prefix ? prefix + '/' : '', pageSize: this.pageSize, pageNum: this.pageNum }).then(res => { console.log('listObjects', res.data) if (res.data.success) { this.total = res.data.data.total; if (prefix) { this.total -= 1; return res.data.data.lists.filter(f => f.name != prefix + '/') } return res.data.data.lists } }).then(data => { console.log('data', data) data.forEach(d => { // Current directory if (d.name) { this.$http.post('/bff/imagepic/object/presignedGetObject', { bucketName: bucketName, objectName: d.name }).then(url => { // console.log('url', url) if (url.data.success) { const ext = url.data.data.split('?')[0]; // console.log('ext', ext) let src = '', ext_type = ''; switch (true) { case /\.(png|jpg|jpeg|gif|svg|webp)$/.test(ext): src = url.data.data; ext_type = 'image'; break; case /\.(mp4)$/.test(ext): src = 'icon_mp4'; ext_type = 'mp4'; break; case /\.(xls)$/.test(ext): src = 'icon_xls'; ext_type = 'xls'; break; case /\.(xlsx)$/.test(ext): src = 'icon_xlsx'; ext_type = 'xlsx'; break; case /\.(pdf)$/.test(ext): src = 'icon_pdf'; ext_type = 'pdf'; break; default: src = 'icon_unknow'; ext_type = 'unknown'; break; } this.cards.push({ name: d.name, src: src, ext: ext_type }) } }) } else { if (d.prefix) { const src = 'icon_file', ext_type = 'file'; this.cards.push({ name: d.prefix.slice(0, -1), src: src, ext: ext_type }) } } }) }) } }, computed: { computedHeaders: function () { console.log('this.$route.fullPath', this.$route.fullPath) return { 'Authorization': sessionStorage.getItem('token'), 'bucket': this.bucketName, 'folder': this.$route.fullPath.split('/').slice(3).join('/') } } } }) </script> <style lang="scss"> @import "../index.scss"; .page-header { margin: 1rem; .header-info { display: flex; align-items: center; justify-content: space-between; } .header-button { display: flex; align-items: center; justify-content: right; .el-button.upload { background-color: $color-primary; } .el-button.upload:hover { background-color: lighten($color: $color-primary, $amount: 10%); } } } .page { margin: 1rem; height: 90vh; .el-row { height: calc(100% - 6rem); overflow-y: scroll; } .el-pagination { margin: 1rem 0; } } </style>
Login.vue
Basic login / registration can be realized. Pop ups and embedded packages can be carried out on the outside to separate business logic from presentation form
<template> <div :class="loginClass"> <section class="login-header"> <span class="title">{{ title }}</span> </section> <section class="login-form"> <template v-if="form == 'login'"> <el-form ref="login-form" label-width="70px" label-position="left" :model="loginForm" :rules="loginRules" > <el-form-item :key="item.prop" v-for="item in loginFormItems" :label="item.label" :prop="item.prop" > <el-input v-model="loginForm[`${item.prop}`]" :placeholder="item.placeholder" :type="item.type" ></el-input> </el-form-item> </el-form> </template> <template v-else-if="form == 'register'"> <el-form ref="register-form" label-width="100px" label-position="left" :model="registerForm" :rules="registerRules" > <el-form-item :key="item.prop" v-for="item in registerFormItems" :label="item.label" :prop="item.prop" > <el-input v-model="registerForm[`${item.prop}`]" :placeholder="item.placeholder" :type="item.type" ></el-input> </el-form-item> </el-form> </template> </section> <section class="login-select"> <span class="change" v-if="form == 'login'" @click="isShow = true">Change Password</span> <span class="go" @click="handleGo(form)">{{ form == 'login' ? ' Go register >>' : ' Go login >>' }}</span> </section> <section class="login-button"> <template v-if="form == 'login'"> <el-button @click="handleLogin">Sign in</el-button> </template> <template v-else-if="form == 'register'"> <el-button @click="handleRegister">register</el-button> </template> </section> </div> <el-dialog v-model="isShow"> <el-form ref="change-form" label-width="130px" label-position="left" :model="changeForm" :rules="changeRules" > <el-form-item :key="item.prop" v-for="item in changeFormItems" :label="item.label" :prop="item.prop" > <el-input v-model="changeForm[`${item.prop}`]" :placeholder="item.placeholder" :type="item.type" ></el-input> </el-form-item> </el-form> <div class="change-button"> <el-button class="cancel" @click="isShow = false">cancel</el-button> <el-button class="confirm" @click="handleConfirm" type="primary">confirm</el-button> </div> </el-dialog> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { validatePwd, validateEmail, validateName, validatePhone } from '../utils/index'; export default defineComponent({ name: 'Login', props: { title: { type: String, default: '' }, border: { type: Boolean, default: false } }, data() { return { form: 'login', isShow: false, loginForm: { phone: '', upwd: '' }, loginRules: { phone: [ { required: true, validator: validatePhone, trigger: 'blur', } ], upwd: [ { validator: validatePwd, required: true, trigger: 'blur', } ] }, loginFormItems: [ { label: "cell-phone number", prop: "phone", placeholder: 'Please enter your mobile number' }, { label: "password", prop: "upwd", placeholder: '', type: 'password' } ], registerForm: { name: '', tfs: '', email: '', phone: '', upwd: '', rpwd: '' }, registerFormItems: [ { label: "full name", prop: "name", placeholder: '' }, { label: "TFS account number", prop: "tfs", placeholder: '' }, { label: "mailbox", prop: "email", placeholder: '' }, { label: "cell-phone number", prop: "phone", placeholder: '' }, { label: "Please input a password", prop: "upwd", placeholder: '', type: 'password' }, { label: "Please confirm the password", prop: "rpwd", placeholder: '', type: 'password' } ], registerRules: { name: [ { validator: validateName, trigger: 'blur', } ], tfs: [ { required: true, message: 'Please enter as required tfs account number', trigger: 'blur', } ], email: [ { required: true, validator: validateEmail, trigger: 'blur', } ], phone: [ { required: true, validator: validatePhone, trigger: 'blur', } ], upwd: [ { required: true, validator: validatePwd, trigger: 'blur', } ], rpwd: [ { required: true, validator: validatePwd, trigger: 'blur', }, { validator(rule: any, value: any, callback: any) { if (value != this.registerForm.upwd) { callback(new Error('The passwords entered are different')) } }, trigger: 'blur', } ], }, changeForm: { phone: '', opwd: '', npwd: '', rpwd: '' }, changeFormItems: [ { label: "cell-phone number", prop: "phone", placeholder: 'Please enter your mobile number' }, { label: "Please enter the original password", prop: "opwd", placeholder: '', type: 'password' }, { label: "Please enter a new password", prop: "npwd", placeholder: '', type: 'password' }, { label: "Please repeat the new password", prop: "rpwd", placeholder: '', type: 'password' } ], changeRules: { phone: [ { required: true, validator: validatePhone, trigger: 'blur', } ], opwd: [ { required: true, validator: validatePwd, trigger: 'blur', } ], npwd: [ { required: true, validator: validatePwd, trigger: 'blur', } ], rpwd: [ { required: true, validator: validatePwd, trigger: 'blur', }, { validator(rule: any, value: any, callback: any) { if (value != this.changeForm.npwd) { callback(new Error('The passwords entered are different')) } }, trigger: 'blur', } ], } } }, computed: { loginClass() { return this.border ? 'login login-unwrapper' : 'login login-wrapper' } }, methods: { handleGo(form) { if (form == 'login') { this.form = 'register' } else if (form == 'register') { this.form = 'login' } }, handleLogin() { this.$http.post("/bff/imagepic/auth/login", { phone: this.loginForm.phone, upwd: this.loginForm.upwd }).then(res => { if (res.data.success) { this.$message.success('Login successful'); sessionStorage.setItem('token', res.data.data.token); this.$router.go(0); } else { this.$message.error(res.data.data.err); } }) }, handleRegister() { this.$http.post("/bff/imagepic/auth/register", { name: this.registerForm.name, tfs: this.registerForm.tfs, email: this.registerForm.email, phone: this.registerForm.phone, upwd: this.registerForm.upwd }).then(res => { if (res.data.success) { this.$message.success('login was successful'); } else { this.$message.error(res.data.data.err); } }) }, handleConfirm() { this.$http.post("/bff/imagepic/auth/change", { phone: this.changeForm.phone, opwd: this.changeForm.opwd, npwd: this.changeForm.npwd }).then(res => { if (res.data.success) { this.$message.success('Password modified successfully'); } else { this.$message.error(res.data.data.err); } }) } } }) </script> <style lang="scss"> @import "../index.scss"; .login-wrapper { } .login-unwrapper { border: 1px solid #ececec; border-radius: 4px; } .login { &-header { text-align: center; .title { font-size: 1.875rem; font-size: bold; color: #333; } } &-form { margin-top: 2rem; } &-select { display: flex; justify-content: right; align-items: center; cursor: pointer; .go { color: orange; text-decoration: underline; margin-left: 0.5rem; } .go:hover { color: orangered; } .change { color: skyblue; } .change:hover { color: rgb(135, 178, 235); } } &-button { margin-top: 2rem; .el-button { width: 100%; background-color: $color-primary; color: white; } } } .change-button { display: flex; justify-content: space-around; align-items: center; .confirm { background-color: $color-primary; } } </style>
routes.ts
vue-router@next The dynamic routing scheme in is slightly different. It has a ranking mechanism similar to rank. For details, please refer to vue-router@next Official documents
import { WrapperLayouts } from '../components'; import menuMap from './menuMap' // 1. Define the routing component. Note that the full name of the file (including the file suffix) must be used here const routes = [ { path: "/", component: WrapperLayouts, redirect: `/page/${Object.keys(menuMap)[0]}`, children: [ { path: '/page/:id', name: 'page', component: () => import('../views/Page.vue'), children: [ { path: '/page/:id(.*)*', // redirect: `/page/${Object.keys(menuMap)[0]}`, name: 'pageno', component: () => import('../views/Page.vue') } ] } ] }, ]; export default routes;
import {createRouter, createWebHashHistory} from 'vue-router'; import { routes } from '../config'; // In the new version of Vue router, you need to use createRouter to create a route export default createRouter({ // Specifies the routing mode. Here, hash mode is used history: createWebHashHistory(), routes // short for `routes: routes` })
Aside.vue
The side bar of route combination and route jump on the left
<template> <div class="aside"> <el-menu @select="handleSelect" :default-active="Array.isArray($route.params.id) ? $route.params.id[0] : $route.params.id"> <el-menu-item v-for="(menu, index) in menuLists" :index="menu.id" > <span>{{menu.label}}</span> </el-menu-item> </el-menu> </div> </template> <script lang="ts"> import { computed, defineComponent, getCurrentInstance, onMounted, reactive, ref, toRefs, } from 'vue'; export default defineComponent({ name: 'Aside', props: { menuMap: { type: Object, default: () => {} } }, components: { }, methods: { handleSelect(e) { console.log('$route', this.$route.params.id) console.log('select', e) this.$router.push(`/page/${e}`) } }, setup(props, context) { console.log('props', props.menuMap) //Reference global variable const { proxy } = getCurrentInstance(); const menuMap = props.menuMap; let menuLists = reactive([]); //After dom is mounted onMounted(() => { handleMenuLists(); }); function handleMenuLists() { (proxy as any).$http.get('/bff/imagepic/bucket/listBuckets').then(res => { console.log('listBuckets', res); if(res.data.success) { res.data.data.forEach(element => { menuMap[`${element.name}`] && menuLists.push({ id: element.name, label: menuMap[`${element.name}`] }) }) } }) } return { ...toRefs(menuLists), handleMenuLists, menuLists }; } }) </script> <style lang="scss"> .aside { height: 100%; background-color: #fff; width: 100%; border-right: 1px solid #d7d7d7; } </style>
summary
As an important development tool of the front-end infrastructure side, the front-end drawing bed can not only provide better development experience for business developers, but also save the efficiency reduction caused by the business development process, so as to improve the development efficiency and reduce the cost loss. There are many different schemes for the implementation of front-end display. For the implementation of front-end drawing bed with higher requirements, it can also be displayed and improved at a higher level based on requirements.