1
头图

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.


维李设论
1.1k 声望4k 粉丝

专注大前端领域发展