Project Background
In the front-end development process, multimedia materials such as pictures and videos are inevitably used. Common processing solutions usually separate dynamic and static, and place pictures and other resources on the map bed. In addition to using common map bed resources in the industry, such as: Qiniuyun, Weibo map beds, etc., in addition to using third-party map beds, we can also build our own map beds to provide better basic services for team business development and improve development experience and efficiency. The purpose of this article is to review and summarize the implementation plan of the front-end part of the self-built map bed, hoping to give some reference and solutions to students with similar needs.
plan
The front-end part of the architecture selection, considering that Vue3 is about to become the main version, as an application on the front-end infrastructure side, consider that you want to use the Vue3 family bucket for the related implementation of the front-end side. Here, the usage plan of vite(vue-template-ts)+vue3+vuex@next+vue-router@next
is used, which is also for the packaging and construction of vite One-step technical pre-research (cai) study (keng). (ps: vite is really fast, but it still needs to be considered when going directly to the industrial environment, and there are still many pitfalls. I personally think that cross-language front-end engineering may be the development direction of subsequent front-end engineering)
content
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 image bed involves permission verification. No authentication confirmation is required for obtaining pictures, but login authentication is required for uploading and deleting pictures.
source code
In vue3, it can be written through two schemes: class and template. Using the composition-api scheme, I personally suggest that it is more comfortable to use class-component, and it is more like the writing method of react. Here, the use of composition-api and options-api is mixed. , Vue is currently compatible. For students who come from Vue2, they can gradually adapt to the writing method of composition-api, and then gradually implement the front-end business 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: {
// 键必须以斜线开始和结束
'/@/': path.resolve(__dirname, './src'),
},
/**
* 在生产中服务时的基本公共路径。
* @default '/'
*/
base: './',
/**
* 与“根”相关的目录,构建输出将放在其中。如果目录存在,它将在构建之前被删除。
* @default 'dist'
*/
outDir: 'dist',
port: 3000,
// 是否自动在浏览器打开
open: false,
// 是否开启 https
https: false,
// 服务端渲染
ssr: false,
// 引入第三方的配置
// optimizeDeps: {
// include: ["moment", "echarts", "axios", "mockjs"],
// },
proxy: {
// 如果是 /bff 打头,则访问地址如下
'/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 requires only 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" >新建文件夹</el-button> -->
<el-button class="upload" :icon="Upload" type="success" @click="handleImage">上传图片</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()}图片删除成功`);
setTimeout(() => {
this.$router.go(0)
}, 100)
} else {
this.$message.error(`${objectName.pop()}图片删除失败,失败原因:${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 => {
// 当前目录下
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
Perform basic login/registration implementation, pop-up windows and embedded packages can be performed on the outside, and business logic and presentation form can be separated
<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">修改密码</span>
<span class="go" @click="handleGo(form)">{{ form == 'login' ? ' 去注册 >>' : ' 去登录 >>' }}</span>
</section>
<section class="login-button">
<template v-if="form == 'login'">
<el-button @click="handleLogin">登录</el-button>
</template>
<template v-else-if="form == 'register'">
<el-button @click="handleRegister">注册</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">取消</el-button>
<el-button class="confirm" @click="handleConfirm" type="primary">确认</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: "手机号",
prop: "phone",
placeholder: '请输入手机号'
},
{
label: "密码",
prop: "upwd",
placeholder: '',
type: 'password'
}
],
registerForm: {
name: '',
tfs: '',
email: '',
phone: '',
upwd: '',
rpwd: ''
},
registerFormItems: [
{
label: "姓名",
prop: "name",
placeholder: ''
},
{
label: "TFS账号",
prop: "tfs",
placeholder: ''
},
{
label: "邮箱",
prop: "email",
placeholder: ''
},
{
label: "手机号",
prop: "phone",
placeholder: ''
},
{
label: "请输入密码",
prop: "upwd",
placeholder: '',
type: 'password'
},
{
label: "请确认密码",
prop: "rpwd",
placeholder: '',
type: 'password'
}
],
registerRules: {
name: [
{
validator: validateName,
trigger: 'blur',
}
],
tfs: [
{
required: true,
message: '请按要求输入tfs账号',
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('输入的密码不同'))
}
},
trigger: 'blur',
}
],
},
changeForm: {
phone: '',
opwd: '',
npwd: '',
rpwd: ''
},
changeFormItems: [
{
label: "手机号",
prop: "phone",
placeholder: '请输入手机号'
},
{
label: "请输入原始密码",
prop: "opwd",
placeholder: '',
type: 'password'
},
{
label: "请输入新密码",
prop: "npwd",
placeholder: '',
type: 'password'
},
{
label: "请重复新密码",
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('输入的密码不同'))
}
},
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('登录成功');
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('注册成功');
} 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('修改密码成功');
} 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
The dynamic routing scheme in vue-router@next is slightly different, and there is a ranking mechanism similar to rank. For details, please refer to the official document of vue-router@next
import { WrapperLayouts } from '../components';
import menuMap from './menuMap'
// 1. 定义路由组件, 注意,这里一定要使用 文件的全名(包含文件后缀名)
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';
// Vue-router新版本中,需要使用createRouter来创建路由
export default createRouter({
// 指定路由的模式,此处使用的是hash模式
history: createWebHashHistory(),
routes // short for `routes: routes`
})
Aside.vue
Combine routing to perform routing jump and display in the left sidebar
<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)
//引用全局变量
const { proxy } = getCurrentInstance();
const menuMap = props.menuMap;
let menuLists = reactive([]);
//dom挂载后
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>
Summarize
As an important development tool on the front-end infrastructure side, the front-end map bed can not only provide business developers with a better development experience, but also save the efficiency reduction caused by the business development process, thereby improving development efficiency and reducing cost losses. There are many different solutions for the implementation of front-end display. For the front-end image bed implementation with higher requirements, higher-level display and improvement can also be performed based on the requirements.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。