Construction practice of front-end drawing bed (front-end chapter)

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.

Keywords: Front-end TypeScript Vue vite

Added by ranjuvs on Mon, 07 Feb 2022 12:59:05 +0200