Vue vite 框架

AVICCI 2024-08-16 08:03:02 阅读 56

锋选菁英招聘管理平台项目

一、项目架构搭建

1. vite搭建项目

Vite下一代的前端工具链,为开发提供极速响应。

创建项目

yarn create vite

填项目名

选vue

选Typescript

安装依赖

cd fxjy-admin

yarn

启动项目

yarn dev

2.目录架构认识及改造

src

api   管理异步请求方法

components 公共组件

layout   基本布局骨架

store   状态机

router   路由

utils   公共方法包

views   业务组件(页面)

assets   静态资源(img、css)

App.vue   根组件

main.ts   入口文件

3. vite配置路径别名@

vite.config.ts配置

...

import { join } from "path";

export default defineConfig({

  ...

  resolve: {

    alias: {

      "@": join(__dirname, "src"),

    },

  },

});

根目录新建 tsconfig.json 配置

{

  "compilerOptions": {

    "experimentalDecorators": true,

    "baseUrl": "./",

    "paths": {

      "@/*": ["src/*"],

      "components/*": ["src/components/*"],

      "assets/*": ["src/assets/*"],

      "views/*": ["src/views/*"],

      "common/*": ["src/common/*"]

    }

  },

  "exclude": ["node_modules", "dist"]

}

安装path的类型提示

yarn add @types/node

重启 vscode

4. AntDesign 组件库

Ant Design Vue 文档

安装组件库

yarn add ant-design-vue

按需导入辅助包

yarn add unplugin-vue-components --dev

修改vite.config.ts配置

import { defineConfig } from "vite";

import vue from "@vitejs/plugin-vue";

import Components from "unplugin-vue-components/vite";

import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";

// https://vitejs.dev/config/

export default defineConfig({

 plugins: [

   vue(),

   Components({

     resolvers: [AntDesignVueResolver()],

  }),

],

 ...

});

在任意.vue组件中,直接使用组件,如App.vue

<script setup lang="ts">

</script>

<template>

 <a-button type="primary">Primary Button</a-button>

</template>

<style scoped>

</style>

5. 搭建主面板骨架

从antd-vue官方获取layout布局

不要照着代码敲,直接复制即可,然后只需要为a-layout容器添加高度

// src/layout/index.vue 主要的布局文件

<template>

 <a-layout style="min-height: 100vh">

   <SiderMenu />

   <a-layout>

     <a-layout-header style="background: #fff; padding: 0" />

     <a-layout-content style="margin: 0 16px">

       <a-breadcrumb style="margin: 16px 0">

         <a-breadcrumb-item>User</a-breadcrumb-item>

         <a-breadcrumb-item>Bill</a-breadcrumb-item>

       </a-breadcrumb>

       <div

         :style="{ padding: '24px', background: '#fff', minHeight: '360px' }"

       >

        Bill is a cat.

       </div>

     </a-layout-content>

     <a-layout-footer style="text-align: center">

      Ant Design ©2018 Created by Ant UED

     </a-layout-footer>

   </a-layout>

 </a-layout>

</template>

<script setup lang="ts">

import {

 PieChartOutlined,

 DesktopOutlined,

 UserOutlined,

 TeamOutlined,

 FileOutlined,

} from "@ant-design/icons-vue";

import { ref } from "vue";

import SiderMenu from "./components/sider-menu.vue";

const collapsed = ref(false);

const selectedKeys = ref(["1"]);

</script>

<style scoped>

.logo {

 height: 32px;

 margin: 16px;

 background: rgba(255, 255, 255, 0.3);

}

.site-layout .site-layout-background {

 background: #fff;

}

[data-theme="dark"] .site-layout .site-layout-background {

 background: #141414;

}

</style>

App.vue引入 主界面的布局文件

<script setup lang="ts">

import MainLayout from "@/layout/index.vue";

</script>

<template>

 <MainLayout />

</template>

<style scoped></style>

查看浏览器,预览运行结果

二、VueRouter路由运用

2.1.路由基本配置

安装

默认安装的是vue-router@4,使用时需要注意版本号

yarn add vue-router

新建页面组件

└─views

  │ dashboard.vue   可视化图表页

  ├─category     分类管理

  │     list.vue

  │     pub.vue

  └─job         岗位管理

          list.vue

          pub.vue

配置路由

// src/router/index.ts

import { createRouter, createWebHashHistory } from "vue-router";

const router = createRouter({

 history: createWebHashHistory(),

 routes: [

  {

     path: "/",

     component: () => import("@/views/dashboard.vue"),

  },

  {

     path: "/category/list",

     component: () => import("@/views/category/list.vue"),

  },

  {

     path: "/category/pub",

     component: () => import("@/views/category/pub.vue"),

  },

],

});

export default router;

呈现路由组件

需要在MainLayout的内容区域添加一个RouterView组件,用来动态显示路由匹配到的组件。

通过手动修改地址栏地址,可以测试路由组件切换效果

2.2 动态渲染菜单

改造路由数据包

export const routes = [

{

   path: "/",

   component: () => import("@/views/dashboard.vue"),

   meta: {

     label: "数据可视化",

     icon: "area-chart-outlined",

  },

},

{

   path: "/category",

   component: () => import("@/views/category/index.vue"),

   meta: {

     label: "分类管理",

     icon: "area-chart-outlined",

  },

   children: [

    {

       path: "/category/list",

       component: () => import("@/views/category/list.vue"),

       meta: {

         label: "分类列表",

      },

    },

    {

       path: "/category/pub",

       component: () => import("@/views/category/pub.vue"),

       meta: {

         label: "发布分类",

      },

    },

  ],

},

];

const router = createRouter({

 history: createWebHashHistory(),

 routes,

});

单独拆分侧边菜单组件

考虑到代码的可维护性,我们将MainLayout中的侧边菜单逻辑交互单独拆分为一个组件

组件存放位置:/src/layout/components/side-menu.vue

在side-menu.vue中使用路由数据包动态渲染

<template>

 <a-layout-sider v-model:collapsed="collapsed" collapsible>

   <div class="logo" />

   <a-menu v-model:selectedKeys="selectedKeys" theme="dark" mode="inline">

     <template v-for="(item, index) in routes" :key="index">

       <a-menu-item v-if="!item.children" :key="item.path">

         <component :is="item.meta!.icon"></component>

         <span>{ -- -->{ item.meta!.label }}</span>

       </a-menu-item>

       <a-sub-menu v-else :key="index">

         <template #title>

           <span>

             <component :is="item.meta!.icon"></component>

             <span>{ -- -->{ item.meta!.label }}</span>

           </span>

         </template>

         <a-menu-item v-for="child in item.children" :key="child.path">{ -- -->{

          item.meta!.label

        }}</a-menu-item>

       </a-sub-menu>

     </template>

   </a-menu>

 </a-layout-sider>

</template>

<script setup lang="ts">

import { ref } from "vue";

import { routes } from "@/router";

const collapsed = ref(false);

const selectedKeys = ref(["1"]);

</script>

<style scoped></style>

通过useRouter触发路由切换

const router = useRouter();

const handleMenu = ({ key }: { key: string }) => { //此处的key是由menu组件点击事件提供

console.log(key);

router.push(key);

};

2.3 动态图标及路由跳转

安装

yarn add @ant-design/icons-vue

main.ts全局注册

import * as antIcons from "@ant-design/icons-vue";

const app = createApp(App);

// 注册组件

Object.keys(antIcons).forEach((key: any) => {

app.component(key, antIcons[key as keyof typeof antIcons]);

});

// 添加到全局

app.config.globalProperties.$antIcons = antIcons;

动态组件

<component is="xxx">

2.4 面包屑交互

封装面包屑组件

封装面包屑组件

使用useRoute、结合watch监听,在面包屑组件中监听路由路径的变化

// src/layout/component/app-breadcrumb.vue

<template>

<a-breadcrumb>

<a-breadcrumb-item>首页</a-breadcrumb-item>

</a-breadcrumb>

</template>

<script setup lang="ts">

import { watch } from "vue";

import { useRoute } from "vue-router";

const route = useRoute();

watch(

route,

(newValue) => {

console.log("路由发生变化了", newValue.path);

}

);

</script>

<style scoped></style>

整合面包屑数据包

封装一个方法函数,将路由数据包,处理为【path--菜单名】的映射格式:

{

'/dashboard':'数据统计',

'/category':'分类管理',

'/category/list':'分类列表'

......

}

代码参考

// utils/tools.ts

import { routes } from "@/router";

import { RouteRecordRaw } from "vue-router";

interface RouteMap {

[key: string]: any;

}

export function routeMapTool() {

let routeMap: RouteMap = {};

//递归函数

function loop(arr: RouteRecordRaw[]) {

arr.forEach((item: RouteRecordRaw) => {

routeMap[item.path] = item.meta!.label;

if (item.children) {

recursion(item.children);

}

});

}

recursion(routes[0].children!);

return routeMap;

}

封装方法函数,对路由路径进行处理

路由路径:/category/list

处理目标:[ '/category' , '/category/list' ]

import { routeMapTool } from "@/utils/tools";

const route = useRoute();

const routeMap = routeMapTool(); //调用方法,获取面包屑映射数据包

const pathList = ref<string[]>([]);

const pathToArr = (path: string) => {

let arr = path.split("/").filter((i) => i);

let pathArr = arr.map((item, index) => {

return "/" + arr.slice(0, index + 1).join("/");

});

console.log(arr);

return pathArr; //['/category','/category/list']

};

pathList.value = pathToArr(route.path); //初始化调用,保证刷新后面包屑显示正常

在watch监听内部,响应式保存处理后的路径数组

watch(

route,

(newValue) => {

pathList.value = pathToArr(newValue.path);

}

);

动态渲染面包屑

<a-breadcrumb>

<a-breadcrumb-item>首页</a-breadcrumb-item>

<a-breadcrumb-item v-for="item in pathList">

{ -- -->{routeMap[item]}}

</a-breadcrumb-item>

</a-breadcrumb>

2.5 登录页路由规划

搭建登录页

先新建空页面,等待路由调整完毕后再完成页面结构搭建

配置登录路由

如果按照目前的路由层级直接配置,会导致登录页也出现侧边菜单栏,所以需要在App.vue中再新增一层路由,这层路由只负责控制两个组件:

MainLayout组件

login组件

原本所有的路由都设置为MainLayout的子路由

export const routes: RouteRecordRaw[] = [

{

path: "/",

component: () => import("@/layout/index.vue"),

children: [

{

path: "/dashbord",

component: dashboardVue,

meta: {

label: "数据统计",

icon: "area-chart-outlined",

},

},

...其他业务页面路由

],

},

{

path: "/login",

component: () => import("@/views/login.vue"),

},

];

将原本用到了routes数据包的业务逻辑代码进行调整

原本使用了routes数据包的地方需要统一换为routes[0].children

side-menu.vue组件

tools.js 文件

在App.vue中新增RouterView组件,并测试路由是否正常

完成登录面板搭建

利用如下三个组件实现:

栅格式组件 a-row

卡片组件 a-card

表单组件 a-form 【重难点】

安装sass,提高样式编写效率

yarn add sass

三、后端服务及前端请求

3.1 LeanCloud云服务

扮演后端的角色

介绍LeanCloud

第三方公司开发的一套通用的后端接口云服务 ServerLess 无服务、弱服务 官网

使用流程

注册LeanCloud账号,并实名认证

进入控制台--新建应用--选择开发版

进入应用获取三条信息 -- 设置 -- 应用凭证

AppID

AppKey

RestApi 服务器地址

3.2 RestFull-API介绍

普通风格接口

通过后端给的不同url地址,来区分每个接口的作用

app.post('/banner/add') 新增轮播

app.post('/banner/list') 查询轮播

app.post('/banner/edit') 修改轮播

app.post('/banner/delete') 删除轮播

Rest风格接口

通过前后端对接时所用的不同方法来区分接口的作用

app.post('/banner') 新增轮播

app.get('/banner') 查询轮播

app.put('/banner') 修改轮播

app.delete('/banner') 删除轮播

看懂LeanCloud接口文档 示例

接口地址

请求方法

传递的参数

返回的结果

使用Rest接口访问LeanCloud

RestFull 风格的API LeanCloud接口文档

curl -X POST \ //请求所用的方法时POST

-H "X-LC-Id: PhooC2pGuFn5MkTPdTRn7O99-gzGzoHsz" \ //请求头headers相关信息

-H "X-LC-Key: 4x587AuiHPH0eZspQnvR5qaH" \

-H "Content-Type: application/json" \

-d '{"content": ""}' \ //请求接口时需要携带的数据包

https://API_BASE_URL/1.1/classes/自定义表名 //接口地址

3.3 ApiPost工具运用

开发过程中,可以使用ApiPost工具测试后端接口可用性

ApiPost官网

自行下载安装

使用ApiPost向LeanCloud新增数据

查询数据演示

更新数据演示

删除数据演示

3.4 axios发起异步请求

Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js axios文档

安装

npm i axios

引入配置

常用配置项

{

url: '/user', // `url` 是用于请求的服务器 URL

method: 'get', // `method` 是创建请求时使用的方法,默认GET

baseURL: 'https://some-domain.com/api/', //接口地址中,每个接口都一样的部分

headers: {AppId,Appkey}, //配置请求头,携带验证信息给后端

params: { //携带query数据给后端

ID: 12345

},

data: { //携带body数据给后端

firstName: 'Fred'

},

}

axios封装 request.js

// 在此处对axios做集中配置

import axios from 'axios'

const instance = axios.create({

baseURL:'https://phooc2pg.lc-cn-n1-shared.com/1.1', //配置通用基础地址

headers:{ //配置通用的请求头

'X-LC-Id': '务必使用自己的ID',

'X-LC-Key': '务必使用自己的Key',

'Content-Type': 'application/json'

}

})

export default instance //配置后的axios

发起请求

request.post('/classes/Banner',{ //此处request时封装过的axios

name:'价值100万的广告',

url:'http://www.1000phone.com',

desc:'我为千锋带盐',

isshow:true

})

常用的axios请求方法

axios.post(url[, data[, config]])

axios.put(url[, data[, config]])

axios.get(url[, config])

axios.delete(url[, config])

四、分类管理

4.1 分类发布

AntDesignVue版本升级

安装命令: yarn add ant-design-vue@4.x

AntDesignVueV4文档

分类数据字段分析

objectId 数据库自动分配的唯一id

name 分类名称 【a-input组件】

parentId 父级类目id 【a-select组件】

icon 分类图标 【a-upload图片上传组件】

搭建分类发布页

建议基于ant-design-vue官方案例进行改造,如:useForm 基本表单

<template>

<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 14 }">

<a-form-item label="分类名称" v-bind="validateInfos.name">

<a-input v-model:value="modelRef.name" />

</a-form-item>

<a-form-item label="父级类目" v-bind="validateInfos.parentId">

<a-select v-model:value="modelRef.parentId" placeholder="请选择父级类目">

<a-select-option value="0-0">顶级类目</a-select-option>

<a-select-option value="beijing">Zone two</a-select-option>

</a-select>

</a-form-item>

<a-form-item label="分类图标" v-bind="validateInfos.icon">

<a-input v-model:value="modelRef.icon" />

</a-form-item>

<a-form-item :wrapper-col="{ span: 14, offset: 4 }">

<a-button type="primary" @click.prevent="onSubmit">Create</a-button>

<a-button style="margin-left: 10px" @click="resetFields">Reset</a-button>

</a-form-item>

</a-form>

</template>

<script lang="ts" setup>

import { defineComponent, reactive, toRaw } from "vue";

import { Form } from "ant-design-vue";

const useForm = Form.useForm;

const modelRef = reactive({

name: "开发",

parentId: "0-0",

icon: "img.png",

});

const rulesRef = reactive({

name: [

{

required: true,

message: "请输入分类名称",

},

],

parentId: [

{

required: true,

message: "请选择父级类目",

},

],

icon: [

{

required: true,

message: "请上传分类图标",

},

],

});

const { resetFields, validate, validateInfos } = useForm(modelRef, rulesRef, {

onValidate: (...args) => console.log(...args),

});

const onSubmit = () => {

validate()

.then(() => {

console.log(toRaw(modelRef));

})

.catch((err) => {

console.log("error", err);

});

};

</script>

定义类型约束模块

// src/types/pro.d.ts

export interface CategoryType {

objectId: string;

name: string;

icon: string;

parentId: string;

}

封装api方法

// src/api/pro.ts

import request from "@/utils/request";

import { CategoryType } from "@/types/pro";

export const categoryPost = (cateObj: CategoryType) => {

return request.post("classes/category");

};

在分类录入表单中向数据库录入数据

import { categoryPost } from "@/api/pro";

const onSubmit = () => {

validate()

.then(() => {

categoryPost(modelRef); //使用api方法向后端发请求

})

.catch((err) => {

console.log("error", err);

});

};

录入7个顶级类目

开发、测试、设计、运维、运营、行政、数据

4.2 二级类类目录入

认识LeanCloud约束查询接口

查询约束接口文档

curl -X GET \

-H "X-LC-Id: 3YdGuHbgN2Z1M7RyG2tGynBs-gzGzoHsz" \

-H "X-LC-Key: zlEVi2que169Bl8zljJSKrGi" \

-H "Content-Type: application/json" \

-G \

--data-urlencode 'where={"pubUser":"官方客服"}' \

https://API_BASE_URL/1.1/classes/Post

封装类目查询api

//查询类目,支持查询所有类目,也支持指定查询主类目

export const categoryGet = (all?: boolean) => {

let where = all ? {} : { parentId: "0-0" };

return request.get("classes/category", {

params: {

where,

},

});

};

在分类发布页请求查询接口

//主类目列表渲染

const parentList = ref<CategoryType[]>([]);

categoryGet().then((res) => {

parentList.value = res.data.results;

});

渲染主类目列表

<a-select v-model:value="modelRef.parentId" placeholder="请选择父级类目">

<a-select-option value="0-0">顶级类目</a-select-option>

<a-select-option v-for="item in parentList" :value="item.objectId">

{ -- -->{ item.name }}

</a-select-option>

</a-select>

为开发,设计,录入部分子类目

开发:前端开发、Java开发...

设计:UI设计、室内设计...

4.3 分类列表渲染

搭建分类列表页并渲染

主要知识点:a-table组件的使用

columns 表格配置项

data-source 待渲染的数据包

自定义插槽

headerCell 自定义表格头

bodyCell 自定义表格内容

<template>

<a-table :columns="columns" :data-source="data">

<template #bodyCell="{ column, record }">

<template v-if="column.key === 'action'">

<a-space>

<a-button type="primary" size="small">编辑</a-button>

<a-button type="danger" size="small">删除</a-button>

</a-space>

</template>

</template>

</a-table>

</template>

<script lang="ts" setup>

import { categoryGet } from "@/api/pro";

import { CategoryType } from "@/types/pro";

import { ref } from "vue";

const columns = [

{

title: "分类名称",

dataIndex: "name",

key: "name",

},

{

title: "分类图标",

dataIndex: "icon",

key: "icon",

},

{

title: "操作",

key: "action",

},

];

const data = ref<Array<CategoryType>>([]);

categoryGet(true).then((res) => {

data.value = res.data.results;

});

</script>

树形表格渲染

树形表格示例文档

表格支持树形数据的展示,当数据中有 children 字段时会自动展示为树形表格,如果不需要或配置为其他字段可以用 childrenColumnName 进行配置。 可以通过设置 indentSize 以控制每一层的缩进宽度。

树形数据处理函数封装

// utils/tools.ts

//分类树形数据处理

export function categoryToTree(results: CategoryType[]) {

let parentArr = results.filter((item) => item.parentId == "0-0");

parentArr.forEach((item) => {

let child = results.filter((child) => child.parentId == item.objectId);

if (child.length) {

item.children = child;

}

});

return parentArr;

}

将处理后的数据渲染至表格

const data = ref<Array<CategoryType>>([]);

categoryGet(true).then((res) => {

data.value = categoryToTree(res.data.results);

});

4.4 图片上传

前端

a-upload 图片上传组件

方法 1:使用 action 后端提供的图片上传接口 (action 地址、on-success 成功回调)

方法 2:使用 customRequest 覆盖默认的 action 上传

资源对象转 base64 编码

function getBase64(img: Blob, callback: (base64Url: string) => void) {

const reader = new FileReader();

reader.addEventListener("load", () => callback(reader.result as string));

reader.readAsDataURL(img);

}

后端

后端的图片上传接口如何调用,是后端接口来决定的

LeanCloud

使用 SDK 上传 (SDK 就是一个 npm 模块,内部存放了一些方法函数)

SDK 使用流程

安装 文档

npm install leancloud-storage --save

封装init-leancloud.ts初始化 SDK

让 SDK 知道应该向哪个空间上传图片

Cloud.init({

appId: "自己的 ID",

appKey: "自己的 Key",

serverURL: "自己的域名"

});

main.js 中引入init-leancloud.ts

import "./utils/init-leancloud";

使用 (将本地资源转化为 LeanCloud 资源)文档

const handleCustomRequest = (info: any) => {

getBase64(info.file, (base64) => {

const data = { base64 };

const file = new Cloud.File("fxjy.png", data); //将base64的编码,构建为LeanCloud资源对象

//上传图片并获取后端下发的图片链接

file.save().then((res: any) => {

imageUrl.value = res.attributes.url; //展示预览图

});

});

};

代码

<template>

<a-upload

v-model:file-list="fileList"

name="avatar"

list-type="picture-card"

class="avatar-uploader"

:show-upload-list="false"

:before-upload="beforeUpload"

:customRequest="handleCustomRequest"

>

<img class="preview" v-if="imageUrl" :src="imageUrl" alt="avatar" />

<div v-else>

<loading-outlined v-if="loading"></loading-outlined>

<plus-outlined v-else></plus-outlined>

<div class="ant-upload-text">Upload</div>

</div>

</a-upload>

</template>

<script setup lang="ts">

import { PlusOutlined, LoadingOutlined } from "@ant-design/icons-vue";

import { message } from "ant-design-vue";

import { ref } from "vue";

import type { UploadChangeParam, UploadProps } from "ant-design-vue";

import Cloud from "leancloud-storage";

function getBase64(img: Blob, callback: (base64Url: string) => void) {

const reader = new FileReader();

reader.addEventListener("load", () => callback(reader.result as string));

reader.readAsDataURL(img);

}

const fileList = ref([]);

const loading = ref<boolean>(false);

const imageUrl = ref<string>("");

const handleCustomRequest = (info: any) => {

getBase64(info.file, (base64) => {

const data = { base64 };

const file = new Cloud.File("fxjy.png", data); //将base64的编码,构建为LeanCloud资源对象

//上传图片并获取后端下发的图片链接

file.save().then((res: any) => {

imageUrl.value = res.attributes.url; //展示预览图

});

});

};

const beforeUpload = (file: any) => {

const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png";

if (!isJpgOrPng) {

message.error("You can only upload JPG file!");

}

const isLt2M = file.size / 1024 / 1024 < 2;

if (!isLt2M) {

message.error("Image must smaller than 2MB!");

}

return isJpgOrPng && isLt2M;

};

</script>

<style>

.avatar-uploader > .ant-upload {

width: 128px;

height: 128px;

}

.ant-upload-select-picture-card i {

font-size: 32px;

color: #999;

}

.ant-upload-select-picture-card .ant-upload-text {

margin-top: 8px;

color: #666;

}

.preview {

height: 100%;

}

</style>

4.5 表单获取图片链接

使用 组件 v-model 的知识点 文档

调用ImgUpload 组件时绑定 v-model

// src/category/pub.vue

<a-form-item label="分类图标" v-bind="validateInfos.icon">

<!-- <a-input v-model:value="modelRef.icon" /> -->

<img-upload v-model="modelRef.icon" />

</a-form-item>

ImgUpload 组件内触发$emits

interface EmitsType { //1. 约束emit的类型

(e: "update:modelValue", url: string): void;

}

const emits = defineEmits<EmitsType>(); //2.使用泛型约束定义emit

const handleCustomRequest = (info: any) => {

getBase64(info.file, (base64) => {

...

file.save().then((res: any) => {

let { url } = res.attributes;

imageUrl.value = url; //展示预览图

emits("update:modelValue", url); // 3. 调用emits方法,将后端下发的图片链接透传给父组件

});

});

};

在表单中测试

测试a-form是否能够通过v-model成功获取其内部自定义img-upload组件提供的数据

// src/category/pub.vue

const onSubmit = () => {

validate()

.then(() => {

console.log(toRaw(modelRef));

// categoryPost(modelRef);

})

.catch((err) => {

console.log("error", err);

});

};

4.6 分类编辑

搭建分类编辑页

复用分类发布页pub.vue

配置路由

// router/index.ts

{

path: "/category",

redirect: "/category/list",

meta: {

label: "分类管理",

icon: "appstore-outlined",

},

children: [

...

{

path: "/category/edit",

component: () => import("@/views/category/edit.vue"),

meta: {

label: "分类编辑",

hidden: true, //在meta中新增一个自定义属性,用以控制侧边菜单隐藏

},

},

],

},

侧边菜单渲染控制

注意a-menu-item组件上使用v-show无效

<div

v-for="child in item.children"

v-show="!child.meta!.hidden"

:key="child.path"

>

<a-menu-item>{ -- -->{ child.meta!.label }}</a-menu-item>

</div>

分类列表跳转编辑页

Omit 从interface中排除某个指定的字段

Omit<CategoryType, "children"> 从CategoryType接口中排除children字段

【注意】在LeanCloud数据库将createAt等非必要字段设为客户端不可见,不然影响后续更新数据提交。

//编辑

const router = useRouter();

const handleEdit = (record: Omit<CategoryType, "children">) => {

router.push({

path: "/category/edit",

query: record,

});

};

详情页初始化渲染待编辑内容

// category/edit.vue

const modelRef = ref<CategoryType>({ //将原本的reactive修改为ref,方便整体进行响应式修改

name: "",

parentId: "",

icon: "",

});

...

//编辑

const route = useRoute();

let obj = route.query as unknown as CategoryType;

modelRef.value = obj;

封装更新api

//更新类目

export const categoryPut = (objectId: string, cateObj: CategoryType) => {

return request.put(`classes/category/${objectId}`, cateObj);

};

触发更新请求

const onSubmit = () => {

validate()

.then(() => {

console.log(toRaw(modelRef.value));

categoryPut(modelRef.value.objectId as string, modelRef.value);

})

.catch((err) => {

console.log("error", err);

});

};

表格图片自定义渲染

<a-table :columns="columns" :data-source="data" rowKey="objectId">

<template #bodyCell="{ column, record }">

<template v-if="column.key == 'icon'">

<img :src="record.icon" alt="" class="icon" />

</template>

...

</template>

</a-table>

五、角色权限管理

5.1 RBAC模型

RBAC模型介绍

Role-Based Access Control 基于角色的用户访问权限控制

在RBAC模型里面,有3个基础组成部分,分别是:用户、角色和权限,它们之间的关系如下图所示

User(用户):每个用户都有唯一的UID识别,并被授予不同的角色

Role(角色):不同角色具有不同的权限

Permission(权限):访问权限

用户-角色映射:用户和角色之间的映射关系

角色-权限映射:角色和权限之间的映射

例如下图,管理员和普通用户被授予不同的权限,普通用户只能去修改和查看个人信息,而不能创建用户和冻结用户,而管理员由于被授予所有权限,所以可以做所有操作。

RBAC实践流程

本项目的权限管理基于RBAC基本模型设计而成,具体实践流程如下图所示

RBAC开发流程

先设定角色

超级管理员 --- 有权访问所有页面 管理员 企业用户 普通员工

分配账号并关联角色

张三丰 ---- 超级管理员 无忌 --- 管理员

登录不同的账号,得到其所关联的角色,再根据角色获取对应的访问权限

5.2 角色管理

Drawer 组件

Tree 组件

渲染 tree 数据

#title插槽 自定义TreeNode显示的文本

fieldNames 自定义key

<a-tree

v-model:checkedKeys="formState.checkedKeys"

checkable

:tree-data="routes[0].children"

:fieldNames="{ key: 'path' }"

>

<template #title="scope">

{ -- -->{ scope.meta.label }}

</template>

</a-tree>

获取 checkedKeys

通过为 a-tree 组件绑定 v-model:selectedKeys 获取

<a-tree

v-model:checkedKeys="formState.checkedKeys"

checkable

:tree-data="routes[0].children"

:fieldNames="{ key: 'path' }"

>

<template #title="scope">

{ -- -->{ scope.meta.label }}

</template>

</a-tree>

将角色数据录入数据库

//新增角色

export const rolePost = (roleObj) => {

return request.post("classes/VueRole", roleObj);

};

5.3 角色列表

axios拦截器全局提示

import axios from "axios";

import { message } from "ant-design-vue";

const instance = axios.create({

baseURL: "https://wojhrvmp.lc-cn-n1-shared.com/1.1/",

headers: {

"X-LC-Id": "WojHRvmpUDdDfo2kr9mfUVc2-gzGzoHsz",

"X-LC-Key": "RIiXkMSxvm1XzeptOeTOgvik",

"Content-Type": "application/json",

},

});

// 添加请求拦截器

instance.interceptors.request.use(

function (config) {

// 在发送请求之前做些什么

return config;

},

function (error) {

// 对请求错误做些什么

return Promise.reject(error);

}

);

// 添加响应拦截器

instance.interceptors.response.use(

function (response) {

// 2xx 范围内的状态码都会触发该函数。

// 对响应数据做点什么

message.success("操作成功");

return response;

},

function (error) {

// 超出 2xx 范围的状态码都会触发该函数。

// 对响应错误做点什么

message.error("操作失败");

return Promise.reject(error);

}

);

export default instance;

角色列表自定义渲染

<template>

<a-button type="primary" @click="open = true">新增角色</a-button>

<a-table :columns="columns" :data-source="data">

<template #bodyCell="{ column, record }">

<template v-if="column.key === 'checkedKeys'">

<a-tag color="blue" v-for="item in record.checkedKeys">{ -- -->{

routeMap[item]

}}</a-tag>

</template>

<template v-if="column.key === 'action'">

<a-space>

<a-button type="primary" size="small">编辑</a-button>

<a-button type="primary" size="small" danger>删除</a-button>

</a-space>

</template>

</template>

</a-table>

<a-drawer

v-model:open="open"

class="custom-class"

root-class-name="root-class-name"

title="新增角色"

placement="right"

>

<RoleForm />

</a-drawer>

</template>

<script lang="ts" setup>

import RoleForm from "./components/role-form.vue";

import { ref } from "vue";

import { RoleType } from "@/types/user";

import { roleGet } from "@/api/user";

import { routeMapTool } from "@/utils/tools";

const routeMap = routeMapTool();

const columns = [

{

title: "角色名称",

dataIndex: "name",

key: "name",

},

{

title: "角色权限",

dataIndex: "checkedKeys",

key: "checkedKeys",

},

{

title: "操作",

key: "action",

},

];

const open = ref<boolean>(false);

const data = ref<RoleType[]>([]);

roleGet().then((res) => {

data.value = res.data.results;

});

</script>

5.4 角色编辑

复用 roleForm 组件(新增、修改)

如何让 roleForm 区分新增、修改? 【向 roleForm 传递数据、下标】

roleForm 根据传递的数据不同,渲染不同按钮【新增按钮】【修改按钮】

在 roleForm 中通过 mounted ,检测 props 变化

mounted() {

console.log("roleForm组件渲染了");

if (this.rowData) {

//修改

let { name, checkedKeys } = this.rowData;

this.name = name;

this.$refs.treeRef.setCheckedKeys(checkedKeys);

} else {

//新增

this.name = "";

this.$refs.treeRef.setCheckedKeys([]);

}

},

【修改按钮】触发相关逻辑

【!!注意!!】el-drawer 组件会导致内部 roleForm 的 mounted 无法实时触发

解决方案,每次关闭 drawer 时,销毁 roleForm 组件

<RoleForm :row-data="editData" v-if="drawer" />

5.5 删除判断

关联了账号的角色,需要先清除账号,才能删角色

_User 表默认不允许发起 get 查询,需要修改 LeanCloud 的数据表权限

通过 roleid 查询_User 表格 (get 请求如何带数据给后端)

export const userGet = (roleId) => {

return request.get("users", {

params: {

where: {

roleid: roleId,

},

},

});

};

5.6 账号分配

六、Pinia状态机的运用

6.1 Pinia 基本使用

Pinia | The intuitive store for Vue.js (vuejs.org)

vue2 --- vuex3 vue3 --- vuex4 vue3 --- pinia (vuex5) 状态机模块:vuex4、pinia 项目中有某些数据需要:跨组件、同步、共享,的时候,应该考虑使用状态机

函数式编程思想,更适合跟组合式API配合使用

安装

Pinia 文档

npm i pinia -S

项目中引入和使用

// main.ts

import { createPinia } from "pinia";

// 返回一个vue插件

const store = createPinia();

// 使用pinia插件

app.use(store);

创建 store

store 是一个用reactive 包裹的对象

可以根据需要定义任意数量的 store ,我们以一个 userStore 为例

pinia 怎么知道有多少个 store 呢?(答案是,只要定义的 store 执行了 defineStore 返回的函数,pinia 就认为这个 store 激活了)

import { defineStore } from "pinia";

// defineStore('user', ...) 和下面的形式相同

export const useUserStore = defineStore({

id: "user",

state: () => ({

userid: localStorage.getItem("userid") || "",

}),

actions: {

setUserId(payload: string) {

this.userid = payload;

},

},

});

6.2 pinia核心API

state的操作

获取 state的方式

<script setup lang="ts">

import { useUserStore } from "@/stores/user";

import { computed } from "vue";

import { storeToRefs } from "pinia";

// 方式1, 通过user.userid的方式使用 【推荐】

const user = useUserStore();

// 方式2, 借助pinia提供的api: storeToRefs 实现,直接解构会丢失响应性

const { userid } = storeToRefs(user);

</script>

修改 state的方式

<script setup lang="ts">

import { useUserStore } from "@/stores/user";

const user = useUserStore();

// 方式1: 直接修改,vuex不允许这种方式(需要提交mutation),但pinia是允许的

user.userid = "xxx"; 【推荐】

// 方式2:

user.$patch({ userid: "xxx" }); 【推荐】

</script>

actions的使用

actions 包含同步 actions 和异步 actions

const login = (): Promise<string> => {

return new Promise((resolve) => {

setTimeout(() => {

resolve("userid");

}, 2000);

});

};

// defineStore('user', ...) 和下面的形式相同

export const useUserStore = defineStore({

id: "user",

state: () => ({

userid: localStorage.getItem("userid") || "",

}),

actions: {

// 同步actions

setUserId(payload: string) {

console.log(payload);

this.userid = payload;

},

// 异步actions

async getUser() {

// actions可以互相调用

this.setUserId(await login());

},

},

});

actions 可以互相调用,我们把 actions 调用关系互换一下

actions: {

// 在同步actions中调用异步actions

setUserId(payload: string) {

console.log(payload)

this.getUser()

},

async getUser() {

// this.setUserId(await login())

this.userid = await login()

}

}

getters的使用

相当于组件中的 computed 也具有缓存

接收"状态"作为第一个参数

state: () => ({

userid: localStorage.getItem('userid') || '',

counter: 0

}),

getters: {

doubleCount: (state) => state.counter * 2,

},

6.3 plugins插件

默认情况下,状态机数据包刷新后会丢失

持久化插件配置后,能够自动将状态机数据同步存储到本地 sesseionStorage 或 localStorage

pinia 持久化插件

安装

npm i pinia-plugin-persistedstate

配置 main.ts

import { createPinia } from "pinia";

import piniaPluginPersistedstate from "pinia-plugin-persistedstate";

const pinia = createPinia();

pinia.use(piniaPluginPersistedstate); //给pinia配置plugin

启用

import { defineStore } from "pinia";

export const useCounter = defineStore("count", {

state: () => {

return {

num: 100,

};

},

getters: {

double(): number {

return this.num * 2;

},

},

persist: {

key: "pinia-count-num", //指定持久化存储的名称

},

});

6.4 登录功能

结合pinia状态机,实现用户的登录及用户信息的跨组件共享。

思考:实现登录流程时,是否有必要使用 pinia?

前端通过表单获取账号、密码

携带账号、密码向后端登录接口发请求

curl -X POST \

-H "Content-Type: application/json" \

-H "X-LC-Id: 务必使用自己的ID" \

-H "X-LC-Key: 务必使用自己的Key" \

-d '{"username":"tom","password":"f32@ds*@&dsa"}' \ 必须是username、password

https://API_BASE_URL/1.1/login

登录成功后,后端下发用户信息(id、账号、token、头像)

前端存储用户信息

存入状态机【跨组件实时同步】

新建 store/user.js

Login.vue 中触发改变用户信息 userStore

vuex 里面的数据都是临时存储,刷新后会丢失

存入本地存储 localStorage

由persist持久化插件自动完成

跳转主面板 router.push('/')

路由拦截中的登录判断,也使用状态机中的用户信息来判断

6.5 token 的作用

加密字符串 token 令牌 (authToken、authoration) 锦衣卫 令牌

登录成功后向后端换取 token 令牌 (sessionToken)

想要修改跟用户相关的信息时,后端要求必须在 headers 携带 token

6.6 退出登录

handleLogout() {

this.$store.commit('user/initInfoMut',null) //重置状态机

localStorage.removeItem('userInfo') //清除本地存储

this.$router.push('/login')

}

七、企业账号设置

7.1 账号设置页搭建

企业账号字段分析

objectId 企业ID

username 企业名称

logo 企业LOGO 【图片上传组件】

intro 企业简介 【富文本编辑器】

address 企业位置信息 【地图选址】

lnglat 企业位置经纬度 【地图选址】

province 省份 【地图选址】

city 城市 【地图选址】

district 地区 【地图选址】

7.2 账号信息初始化

从状态机提取数据

将数据设置给表单所绑定的响应式数据包

7.3 高德地图选址

搭建 a-drawer 弹窗

渲染地图 在 vue 中使用高德地图

点击地图时获取信息

省、市、区

具体位置名称信息

经纬度

高德地图使用流程

注册、登录、新建应用、创建 key

新建 god-map 组件

div 容器、样式

调用 initMap 进行地图的初始化

在 company/pub.vue 中引入、注册、调用地图组件

使用逆地理编码案例,实现点击地图获取位置信息 参考案例

配置安全秘钥【切记】

在 public/index.html 的 head 标签内中配置安全秘钥

<script type="text/javascript">

window._AMapSecurityConfig = {

securityJsCode: "1e9ae2832c334c3cfb0d30cff1decc14", //此处换成自己申请key时分配的安全秘钥

};

</script>

7.4 富文本编辑器使用

VueQuill 富文本编辑器文档

安装

npm install @vueup/vue-quill@latest --save

引入挂载

import { QuillEditor } from '@vueup/vue-quill'

import '@vueup/vue-quill/dist/vue-quill.snow.css';

调用

contentType="html" 指定富文本获取的内容类型,默认是一个对象

<el-form-item label="公司简介" prop="intro">

<div class="editor">

<QuillEditor v-model:content="ruleForm.intro" contentType="html" />

</div>

</el-form-item>

7.5 账号信息更新

账号更新接口

在通常的情况下,没有人会允许别人来改动他们自己的数据。为了做好权限认证,确保只有用户自己可以修改个人数据,在更新用户信息的时候,必须在 HTTP 头部加入一个 X-LC-Session 项来请求更新,这个 session token 在注册和登录时会返回。

为了改动一个用户已经有的数据,需要对这个用户的 URL 发送一个 PUT 请求。任何你没有指定的 key 都会保持不动,所以你可以只改动用户数据中的一部分。

curl -X PUT \

-H "X-LC-Id: { -- -->{appid}}" \

-H "X-LC-Key: { {appkey}}" \

-H "X-LC-Session: qmdj8pdidnmyzp0c7yqil91oc" \

-H "Content-Type: application/json" \

-d '{"phone":"18600001234"}' \

https://API_BASE_URL/1.1/users/55a47496e4b05001a7732c5f

封装更新api

触发更新请求

八、岗位管理

5.1 岗位录入

分析岗位数据库表结构

根据设计图的要求,按需规划岗位表:

企业LOGO -- 从用户信息中提取 -- brandLogo

企业名称 -- 从用户信息中提取 -- brandName

地理位置 -- 从用户信息中提取-- 城市cityName、地区areaDistrict、经纬度lnglat

岗位名称 -- 普通输入框 -- jobName

薪资待遇 -- 普通输入框 -- salaryDesc

归属类目 -- Cascader选择器 -- lv1、lv2

福利待遇 -- TreeSelect组件 -- welfareList

技能要求 -- TextArea 组件-- skills

使用form表单搭建基本结构

整合岗位数据与企业信息

录入岗位数据

export const jobPost = (job: JobType) => {

return request.post("classes/job", job);

};

5.2 岗位备份数据导入及渲染

在leancloud导入备份json数据

封装岗位列表请求api

//岗位列表

export const jobGet = () => {

return request.get("classes/job");

};

渲染岗位列表

<template>

<a-row class="search" gutter="20" justify="between">

<a-col :span="8">

<a-input placeholder="输入岗位名称查询"></a-input>

</a-col>

<a-col :span="8">

<a-select

:options="cateList"

:fieldNames="{ label: 'name', value: 'name' }"

placeholder="选择岗位类型进行筛选"

/>

</a-col>

<a-col :span="6">

<a-button type="primary">查询</a-button>

</a-col>

</a-row>

<a-table sticky :columns="columns" :data-source="data" :scroll="{ x: 1500 }">

<template #bodyCell="{ column }">

<template v-if="column.key === 'operation'">

<a-space>

<a-button type="primary" size="small">编辑</a-button>

<a-button type="primary" danger size="small">删除</a-button>

</a-space>

</template>

</template>

</a-table>

</template>

<script lang="ts" setup>

import { categoryGet, jobGet } from "@/api/pro";

import { CategoryType } from "@/types/pro";

import type { TableColumnsType } from "ant-design-vue";

import { ref } from "vue";

const columns = ref<TableColumnsType>([

{

title: "岗位名称",

dataIndex: "jobName",

key: "jobName",

fixed: "left",

},

{

title: "岗位薪资",

dataIndex: "salaryDesc",

key: "salaryDesc",

fixed: "left",

},

{

title: "岗位分类",

dataIndex: "lv1",

key: "lv1",

},

{

title: "城市",

dataIndex: "cityName",

key: "cityName",

},

{

title: "地区",

dataIndex: "areaDistrict",

key: "areaDistrict",

},

{

title: "公司规模",

dataIndex: "brandScaleName",

key: "brandScaleName",

},

{

title: "所属行业",

dataIndex: "brandIndustry",

key: "brandIndustry",

},

{

title: "Action",

key: "operation",

fixed: "right",

width: 150,

},

]);

const data: any = ref([]);

jobGet().then((res) => {

data.value = res.data.results;

});

//岗位类型

const cateList = ref<CategoryType[]>([]);

categoryGet().then((res) => {

cateList.value = res.data.results;

});

</script>

<style scoped>

#components-table-demo-summary tfoot th,

#components-table-demo-summary tfoot td {

background: #fafafa;

}

[data-theme="dark"] #components-table-demo-summary tfoot th,

[data-theme="dark"] #components-table-demo-summary tfoot td {

background: #1d1d1d;

}

.search {

margin-bottom: 20px;

}

</style>

5.3 岗位条件查询

约束查询接口

curl -X GET \

-H "X-LC-Id: { -- -->{appid}}" \

-H "X-LC-Key: { {appkey}}" \

-H "Content-Type: application/json" \

-G \

--data-urlencode 'where={"pubUser":"官方客服"}' \

https://{ -- -->{host}}/1.1/classes/Post

模糊查询接口

curl -X GET \

-H "X-LC-Id: { {appid}}" \

-H "X-LC-Key: { {appkey}}" \

-H "Content-Type: application/json" \

-G \

--data-urlencode 'where={"title":{"$regex":"^WTO.*","$options":"i"}}' \

https://{ {host}}/1.1/classes/Post

封装模糊查询api

//岗位列表

interface ConditionType {

jobName: string | { $regex: any; $options: "i" };

lv1: string;

}

export const jobGet = (condition: ConditionType = {} as ConditionType) => {

let { jobName, lv1 } = condition;

let query: ConditionType = {} as ConditionType;

if (jobName) {

// query.jobName = jobName; //普通约束查询

query.jobName = { $regex: jobName, $options: "i" }; //模糊查询

}

if (lv1) {

query.lv1 = lv1;

}

let params = JSON.stringify(query);

return request.get(`classes/job?where=${params}`);

};

九、访问权限控制

9.1 登录成功后获取角色权限信息

修改 store/modules/user.js,登录成功后用角色 id,请求角色表,获取角色数据

actions: {

userLoginAction(context, account) {

//拿到组件提交的账号密码、向后端发登录请求

// console.log(account);

userLogin(account).then(async (res) => {

console.log("登录成功", res);

let { roleId } = res.data;

let role = await roleGet(roleId); //以当前用户的角色id,获取角色权限

console.log("当前用户的角色数据", role);

res.data.checkedKeys = role.data.checkedKeys; //给用户信息中,追加权限信息

context.commit("initInfoMute", res.data); //存入状态机

localStorage.setItem("vue-admin-2301", JSON.stringify(res.data)); //存入本地存储

router.push("/"); //登录成功后跳转至主面板

});

},

},

调整 api/user.js 里面的 roleGet 方法,让它既支持加载角色列表,也支持获取单个角色数据

export const roleGet = (roleid) => {

let search = roleid ? `/${roleid}` : "";

return request.get(`classes/VueRole${search}`);

};

9.2 侧边菜单渲染控制

通过用户权限,处理menu数据

const account = useAccount();

const menuFn = (permission: string[]) => {

let menu = cloneDeep(routes[0].children) as RouteRecordRaw[];

function loop(arr: RouteRecordRaw[]) {

for (let i = arr.length - 1; i >= 0; i--) {

let bool = !permission.includes(arr[i].path); // 可能为/category

if (bool) {

// 在删除前查看permission中有没有相近的路径 ['/category/list']

let findChild = permission.filter(

(item) => item.indexOf(arr[i].path) != -1

);

//如果不做这一步判断,/category包会被整体删除,导致无法看到/category/list菜单

if (!findChild.length) {

arr.splice(i, 1);

continue;

}

}

if (arr[i].children) {

loop(arr[i].children!);

}

}

}

loop(menu);

return menu;

};

const menuList = ref<RouteRecordRaw[]>([]);

onMounted(() => {

menuList.value = menuFn(account.userInfo!.checkedKeys);

});

使用处理后的menu数据,渲染菜单

<a-menu theme="dark" mode="inline" @click="handleMenu">

<template v-for="item in menuList">

<a-menu-item v-if="!item.children" :key="item.path">

<component :is="item.meta!.icon" />

<span>{ -- -->{ item.meta!.label }}</span>

</a-menu-item>

<a-sub-menu v-else :key="item.path + '0'">

<template #title>

<span>

<component :is="item.meta!.icon" />

<span>{ -- -->{ item.meta!.label }}</span>

</span>

</template>

<!-- <div v-for="child in item.children" v-show="!child.meta.hidden"> -->

<div v-for="child in item.children">

<a-menu-item :key="child.path">

{ -- -->{ child.meta!.label }}

</a-menu-item>

</div>

</a-sub-menu>

</template>

</a-menu>

9.3 路由白名单思路

路由守卫拦截

检查已登录用户的 checkedKeys(路由白名单)

用户当前所访问的路由

如果在白名单内,直接 next

如果不在白名单内,跳转至【没有权限】提示页

9.4 权限控制

调整路由守卫

router.beforeEach((to, from, next) => {

// console.log(to, from, next);

let userInfo = store.state.user.userInfo;

if (!["/login", "/permission"].includes(to.path)) {

if (userInfo) {

let { checkedKeys } = userInfo; //获取当前用户有权访问的路由

let whiteList = ["/"].concat(checkedKeys);

let bool = whiteList.includes(to.path); //用户想要访问的路由,是否存在于权限数组中

if (bool) {

next(); //有权限用户直接放行

} else {

next("/permission");

}

} else {

next("/login"); //没登录的用户,强行跳转到登录

}

} else {

next(); //login、permission直接放行

}

});

登录不同账号,测试权限行为



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。