前言

1.简述后台管理系统

后台管理系统 (手动编程除外)是 内容管理系统 Content Manage System(简称CMS)的一个子集。
WMS是Web Management System 的简写,简单的说:WMS是一个网站管理系统。
一个网站管理系统是把一个网站的内容(文字,图片,等等)与网站的组件分离开来,可以将各个页面连接到一起,可以控制页面的显示。

2.主要实现功能

由于后台管理的功能实际上都差别不大,所以这次只总结若依后台管理的主要功能,分别是主题切换,i18n国际化,动态路由,excel导入导出等。

3.涉及到的前端技术

1.vue
一套用于构建用户界面的渐进式框架
2.vue-router
Vue.js官方的路由插件
3.vuex
Vuex是实现组件全局状态(数据)管理的一种机制,可以方便实现组件之间的数据的共享。
4.md5
md5的全称是md5信息摘要算法(英文:MD5 Message-Digest Algorithm ),一种被广泛使用的密码散列函数
5.i18n
使产品或软件具有不同国际市场的普遍适应性,从而无需重新设计就可适应多种语言
6.scss
层叠样式表
7.xlsx
用于解析xlsx,xls格式的文件
8.element-Plus
基于vue3的一款前端UI框架
9.axios
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中

主要功能

i18n语言切换

这一步的具体实现思路是,自己编写国际化的文本,如这个项目只需中英切换,所以只需创建两个js文件,并导出其中的文本对象,国际化的思路就是通过切换目标文本源来更改文本,

代码示例如下

			//中文文本
export default {
	  route: {
	    profile: '个人中心',
	    user: '用户',
    	excelImport: 'Excel导入',
    	userManage: '员工管理',
    	userInfo: '员工信息',
    	roleList: '角色列表',
    	permissionList: '权限列表',
    	article: '文章',
    	articleRanking: '文章排名',
    	articleCreate: '创建文章',
    	articleDetail: '文章详情',
    	articleEditor: '文章编辑'
  	},	
 }
			//英文文本
export default {
	  route: {
    	profile: 'Profile',
    	user: 'user',
    	excelImport: 'ExcelImport',
    	userManage: 'EmployeeManage',
    	userInfo: 'UserInfo',
   	 	roleList: 'RoleList',
    	permissionList: 'PermissionList',
    	article: 'article',
    	articleRanking: 'ArticleRanking',
    	articleCreate: 'ArticleCreate',
    	articleDetail: 'ArticleDetail',
    	articleEditor: 'ArticleEditor'
  	},
  }

然后在再建一个全局注入国际化文本的js文件
目录结构如下

在这里插入图片描述
注入文件代码如下

//引入i18n
import { createI18n } from 'vue-i18n'
//引入中文文本
import mZhLocale from './lang/zh'
//引入英文文本
import mEnLocale from './lang/en'
//引入vuex  用来存储切换后的文本文件
import store from '@/store'


const messages = {
  en: {
    msg: {
      ...mEnLocale
    }
  },
  zh: {
    msg: {
      ...mZhLocale
    }
  }
}
//返回当前语言的函数
function getLanguage() {
  return store && store.state && store.state.language
}

const i18n = createI18n({
  // 使用 Composition API 模式,则需要将其设置为false
  legacy: false,
  // 全局注入 $t 函数
  globalInjection: true,
  locale:  store.state.language,
  messages
})

export default i18n

全局注册完成之后就可以使用了

//使用前需要先引入
import { useI18n } from "vue-i18n";
//声明
const i18n = useI18n();
//script标签中使用
i18n.t("msg.elcel.importSuccess")
//dom可以直接使用$t
<el-button @click="closed">{{ $t("msg.excel.close") }}</el-button>

接下来完成语言切换功能

这里使用的也是element-plus的国际化组件
需要用这个组件包裹住整个router-view标签

  <el-config-provider :locale="$store.state.language == 'en' ? en : zhCn">
    <router-view />
  </el-config-provider>

切换语言是我使用element封装的一个组件,直接上代码

<template>
  <div class="lang">
    <el-col :span="8">
      <el-dropdown @command="handleCommand">
        <span class="el-dropdown-link">
          <div class="language">
            <div class="cn"></div>
            <div class="en">A</div>
          </div>
        </span>
        <template #dropdown>
        //通过下拉菜单来切换语言
          <el-dropdown-menu>
            <el-dropdown-item
              :disabled="$store.state.language === 'zh'"
              command="zh"
              >中文</el-dropdown-item
            >
            <el-dropdown-item
              :disabled="$store.state.language === 'en'"
              command="en"
              >English</el-dropdown-item
            >
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </el-col>
  </div>
</template>

<script setup>
const emit = defineEmits(["changeMenus"])
import { useI18n } from "vue-i18n";
import { useStore } from "vuex";
let store = useStore();
const i18n = useI18n();
const handleCommand = (e) => {
//先在这里修改当前语言
  i18n.locale.value = e;
  //再通过vuex存入本地  下次进入页面时会先读取本地语言
  store.commit("setLanguage", e);
  emit('changeMenus')
};
</script>

<style lang="less" scoped>
.block-col-2 .demonstration {
  display: block;
  color: var(--el-text-color-secondary);
  font-size: 14px;
  margin-bottom: 20px;
}
.language {
  width: 35px;
  height: 35px;
  background-color: #fff;
  border-radius: 5px;
  box-shadow: 1px 1px 5px 1px #000;
  position: relative;
  cursor: pointer;
  .cn {
    width: 18px;
    height: 18px;
    text-align: center;
    line-height: 18px;
    border-radius: 2px;
    font-size: 14px;
    font-weight: 700;
    border: 0.5px solid #999;
    position: absolute;
    top: 3px;
    left: 3px;
    z-index: 100;
    background-color: #fff;
  }
  .en {
    width: 18px;
    height: 18px;
    text-align: center;
    line-height: 18px;
    border-radius: 2px;
    font-size: 14px;
    font-weight: 900;
    background-color: rgb(82, 82, 82);
    color: white;
    position: absolute;
    right: 3px;
    bottom: 3px;
  }
}
</style>

组件效果图如下
在这里插入图片描述

主题切换

这里我只是简单完成了主题切换功能,并没有使用element-plus的主题色
首先需要通过element-plus的颜色选择器来获取将要修改的主题颜色
在这里插入图片描述
这个组件有一个回调函数可以获取到当前颜色的16进制的值,可以通过这个值存入vuex来进行主题的切换,比如

		  :background-color="$store.state.mainColor"
		  
          :style="$store.state.mainColor != ''? 'border-color:' + $store.state.mainColor: false "

切换/取消全屏显示

这里是使用H5新增的api来完成的 ,不同浏览器的api不相同,考虑到兼容的问题,所以都写上了

 if (document.documentElement.RequestFullScreen) {
      document.documentElement.RequestFullScreen();
    }
    //兼容火狐
    console.log(document.documentElement.mozRequestFullScreen);
    if (document.documentElement.mozRequestFullScreen) {
      document.documentElement.mozRequestFullScreen();
    }
    //兼容谷歌等可以webkitRequestFullScreen也可以webkitRequestFullscreen
    if (document.documentElement.webkitRequestFullScreen) {
      document.documentElement.webkitRequestFullScreen();
    }
    //兼容IE,只能写msRequestFullscreen
    if (document.documentElement.msRequestFullscreen) {
      document.documentElement.msRequestFullscreen();
    }
    f11Flag.value = true;
  } else {
    if (document.exitFullScreen) {
      document.exitFullscreen();
    }
    //兼容火狐
    console.log(document.mozExitFullScreen);
    if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen();
    }
    //兼容谷歌等
    if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen();
    }
    //兼容IE
    if (document.msExitFullscreen) {
      document.msExitFullscreen();
    }

excel导入导出

这里的导入有两种方式,分别为按钮上传和拖拽上传,这里需要使用xlsx插件,他的作用是用来解析xlsx文件
先分析一下两种上传文件的方式,上传之后的文件解析是完全一样的逻辑,所以放到最后说
先分析下点击上传

导入

按钮导入

		//上传的按钮
      <el-button type="primary" :loading="loading" @click="handleUpload">{{
        $t("msg.uploadExcel.upload")
      }}</el-button>
//上传文件的input
<input
		//type为文件上传类型
        type="file"
        //ref为后边触发点击事件做准备
        ref="excelUploadInput"
        class="excel-upload-input"
        //限制上传文件的格式
        accept=".xlsx,xlx"
        //选择文件后触发的函数
        @change="handelChange"
      />

在点击上传之后触发上传文本事件


const handleUpload = () => {
  excelUploadInput.value.click();
};

在选择文件之后就会触发input的change事件

//e就是当前上传的文件
const handelChange = (e) => {
//因为这里一次只上传一个文件  所以可以直接通过下标0来获取
  const rawFile = e.target.files[0];
  //如果该文件则return
  if (!rawFile) return;
  //如果有的话则触发upload函数
  uoload(rawFile);
};

拖拽导入

upload函数代码如下

  <div
        class="drop"
        @drop.stop.prevent="handleDorp"
        @dragover.stop.prevent="handelDragover"
        @dragenter.stop.prevent="handelDragover"
      >
        <i class="el-icon-upload"></i>
        <span>{{ $t("msg.uploadExcel.drop") }}</span>
      </div>

这里提一下拖拽事件

ondragenter 当被鼠标拖动的对象进入其容器范围内时触发此事件
ondragover 当被拖动的对象在另一对象容器范围内拖动时触发此事件
ondragleave 当被鼠标拖动的对象离开其容器范围内时触发此事件
ondrop 在一个拖动过程中,释放鼠标键时触发此事件


//触发上传事件
const uoload = (rawFile) => {
//先将input的value.value置为空
  excelUploadInput.value.value = null;
  //如果用户没有指定上传前回调的话
  if (!props.beforeUpload) {
  //执行解析函数
    readerData(rawFile);
    return;
  }
  //如果用户指定了上传前回调,那么只有返回为true的时候才会执行解析函数
  const before = props.beforeUpload(rawFile);
  if (before) {
    readerData(rawFile);
  }
};

拖拽上传


//拖拽上传
const handleDorp = (e) => {
  //如果当前文件正在上传中 return掉
  if (loading) return;
  // 获取当前上传的文件
  const files = e.dataTansfer.files;
  // 判断文件数量
  if (files.length !== 1) {
    ElMessage.error("必须要有一个文件");
    return;
  }
  // 因为获得的是一个数组  所以需要通过下标0取其中第一个
  const rawFile = files[0];
  // 判断文件格式   这里是自己封装的方法  代码在下边
  if (!isExcel(rawFile)) {
    ElMessage.error("文件必须是.xlsx,xls,csv格式");
    return;
  }
  // 解析
  upload(rawFile);
};

判断文件格式封装,也是通过正则实现的

export const isExcel = file => {
    return /\.(xlsx|xls|csv)$/.test(file.name)
}

readerData函数代码如下

//读取数据
const readerData = (rawFile) => {
  loading.value = true;
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    // 读取操作完成时
    reader.onload = (e) => {
      //1获取解析后的数据
      const data = e.target.result;
      // console.log(data);
      //2利用xslx解析数据
      const workbook = XLSX.read(data, { type: "array" });
      //3获取第一张表格
      const firstSheetName = workbook.SheetNames[0];
      // //4读取第一张biogenic的数据
      const workSheet = workbook.Sheets[firstSheetName];
      // // 5解析数据表头   这一步由于都是一摸一样的   百度一搜一大把 就不自己写了
      const header = getHeaderRow(workSheet);
      // //6解析数据体
      const results = XLSX.utils.sheet_to_json(workSheet);
      // // 7传输解析之后的数据
      console.log(header, results);
      generateData({ header, results });
      // //8处理loading
      loading.value = false;
      // 9成功回调
      resolve();
    };
    reader.readAsArrayBuffer(rawFile);
  });
};
//通过回调将处理之后的数据返回给父组件
const generateData = (excelData) => {
  props.onSuccess && props.onSuccess(excelData);
};

解析表头封装

import XLSX from 'xlsx'
/**
 * 获取表头(通用方式)
 */
export const getHeaderRow = sheet => {
    const headers = []
    const range = XLSX.utils.decode_range(sheet['!ref'])
    let C
    const R = range.s.r
    /* start in the first row */
    for (C = range.s.c; C <= range.e.c; ++C) {
        /* walk every column in the range */
        const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
        /* find the cell in the first row */
        let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
        if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
        headers.push(hdr)
    }
    return headers
}

export const isExcel = file => {
    return /\.(xlsx|xls|csv)$/.test(file.name)
}

解析完成后向父组件传递事件


const props = defineProps({
  beforeUpload: Function,
  onSuccess: Function,
});

父组件中直接使用传递过来的onSuccess来接收

	//dom
  <uploadExcel :onSuccess="onSuccess"></uploadExcel>
  //js
  
const onSuccess = async ({ header, results }) => {
//由于上传文件之后的key是中文的  所以需要通过函数来将key设置为可供上传的key
  const upDateData = generateData(results)
  await  userBatchImport(upDateData);
};

const generateData = (results) => {
  const arr = [];
  results.forEach((item) => {
    const userInfo = [];
    Object.keys(item).forEach((key) => {
    //USER_RELATIONS是自己导入的方法,需要根据文件的key替换上传所需要的key  代码在下方
      userInfo[USER_RELATIONS[key]] = item[key];
    });
    arr.push(userInfo);
  });
  return arr;
};
export const USER_RELATIONS = {
    姓名: 'username',
    联系方式: 'mobile',
    角色: 'role',
    开通时间: 'openTime'
}

到这里 导入就完成了

excel导出

导出这里呢需要先弹出一个dialog,输入导出文件的名字
在这里插入图片描述
在点击导出按钮之后的逻辑处理如下

const onComfirm = async () => {
//先将按钮加载置为true
  loading.value = true;
  //声明变量接收所有角色列表
  const allUser = (await getUserManageAllList()).data.list;
  //这里接收一个封装的方法,也是可以百度到的  下方赋代码1
  const excel = await import("@/utils/Export2Excel");
  //这里的formatJson是处理excel的表头,将后台的数据改为对应表格的表头
  const data = formatJson(USER_RELATIONS, allUser);
  //这里是将数据处理为可以被excel打开的格式,下方赋代码2
  excel.export_json_to_excel({
    header: Object.keys(USER_RELATIONS),
    data,
    filename:excelName.value || exportDfauleName
  });
};

附代码1

/* eslint-disable */
import { saveAs } from 'file-saver'
import XLSX from 'xlsx'

function datenum(v, date1904) {
  if (date1904) v += 1462
  var epoch = Date.parse(v)
  return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000)
}

function sheet_from_array_of_arrays(data, opts) {
  var ws = {}
  var range = {
    s: {
      c: 10000000,
      r: 10000000
    },
    e: {
      c: 0,
      r: 0
    }
  }
  for (var R = 0; R != data.length; ++R) {
    for (var C = 0; C != data[R].length; ++C) {
      if (range.s.r > R) range.s.r = R
      if (range.s.c > C) range.s.c = C
      if (range.e.r < R) range.e.r = R
      if (range.e.c < C) range.e.c = C
      var cell = {
        v: data[R][C]
      }
      if (cell.v == null) continue
      var cell_ref = XLSX.utils.encode_cell({
        c: C,
        r: R
      })

      if (typeof cell.v === 'number') cell.t = 'n'
      else if (typeof cell.v === 'boolean') cell.t = 'b'
      else if (cell.v instanceof Date) {
        cell.t = 'n'
        cell.z = XLSX.SSF._table[14]
        cell.v = datenum(cell.v)
      } else cell.t = 's'

      ws[cell_ref] = cell
    }
  }
  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range)
  return ws
}

function Workbook() {
  if (!(this instanceof Workbook)) return new Workbook()
  this.SheetNames = []
  this.Sheets = {}
}

function s2ab(s) {
  var buf = new ArrayBuffer(s.length)
  var view = new Uint8Array(buf)
  for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
  return buf
}

export const export_json_to_excel = ({
  multiHeader = [],
  header,
  data,
  filename,
  merges = [],
  autoWidth = true,
  bookType = 'xlsx'
} = {}) => {
  // 1. 设置文件名称
  filename = filename || 'excel-list'
  // 2. 把数据解析为数组,并把表头添加到数组的头部
  data = [...data]
  data.unshift(header)
  // 3. 解析多表头,把多表头的数据添加到数组头部(二维数组)
  for (let i = multiHeader.length - 1; i > -1; i--) {
    data.unshift(multiHeader[i])
  }
  // 4. 设置 Excel 表工作簿(第一张表格)名称
  var ws_name = 'SheetJS'
  // 5. 生成工作簿对象
  var wb = new Workbook()
  // 6. 将 data 数组(json格式)转化为 Excel 数据格式
  var ws = sheet_from_array_of_arrays(data)
  // 7. 合并单元格相关(['A1:A2', 'B1:D1', 'E1:E2'])
  if (merges.length > 0) {
    if (!ws['!merges']) ws['!merges'] = []
    merges.forEach((item) => {
      ws['!merges'].push(XLSX.utils.decode_range(item))
    })
  }
  // 8. 单元格宽度相关
  if (autoWidth) {
    /*设置 worksheet 每列的最大宽度*/
    const colWidth = data.map((row) =>
      row.map((val) => {
        /*先判断是否为null/undefined*/
        if (val == null) {
          return {
            wch: 10
          }
        } else if (val.toString().charCodeAt(0) > 255) {
          /*再判断是否为中文*/
          return {
            wch: val.toString().length * 2
          }
        } else {
          return {
            wch: val.toString().length
          }
        }
      })
    )
    /*以第一行为初始值*/
    let result = colWidth[0]
    for (let i = 1; i < colWidth.length; i++) {
      for (let j = 0; j < colWidth[i].length; j++) {
        if (result[j]['wch'] < colWidth[i][j]['wch']) {
          result[j]['wch'] = colWidth[i][j]['wch']
        }
      }
    }
    ws['!cols'] = result
  }

  // 9. 添加工作表(解析后的 excel 数据)到工作簿
  wb.SheetNames.push(ws_name)
  wb.Sheets[ws_name] = ws
  // 10. 写入数据
  var wbout = XLSX.write(wb, {
    bookType: bookType,
    bookSST: false,
    type: 'binary'
  })
  // 11. 下载数据
  saveAs(
    new Blob([s2ab(wbout)], {
      type: 'application/octet-stream'
    }),
    `${filename}.${bookType}`
  )
}

赋代码2

export const USER_RELATIONS = {
    姓名:'username',
    电话:'mobile',
    角色:'role',
    开通时间:'openTime',
}

这个时候就可以将文件下载到本地了

Logo

快速构建 Web 应用程序

更多推荐