# 硅谷甄选_vue3
**Repository Path**: yys233/ggzs
## Basic Information
- **Project Name**: 硅谷甄选_vue3
- **Description**: 这是一个Vue3项目
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2024-01-21
- **Last Updated**: 2024-09-05
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
| 文件/文件夹 | 说明 |
| ------------------- | -------------- |
| `src/store/module` | 用于存放小仓库 |
| `src/permission.ts` | 路由鉴权 |
| | |
## `svg`图标的使用
```html
```
> 图标存放在`src/assets`目录下
封装全局组件
```vue
```
其他组件使用
```html
```
## `Element-plus`
### `layout`布局
- `el-row`:表示一行,一行有`24`个分栏
- `el-col`:分栏
- `span`:用于指定该分栏占多大,值为`number`
- `xs`:响应式布局,当窗口宽度`<=768`时,将`span`的值更改为`xs`的值,值为`number`
```html
你好
```
### 表单
表单使用`el-form`包裹
每一个表单项要使用`el-form-item`包裹
```html
登录
```
#### 表单校验
1. 为`el-form`添加`:model=""`,值为收集表单的对象`(reactive)`
2. 为`el-form`添加`:rules=""`,值为规则对象
3. 为需要校验的表单的父元素`el-form-item`添加`prop=""`,值为该字段的名字(如`username`,`password`)
4. `prop`所指定的名字需要和`model`指定的对象的属性名相对应
5. 在`el-form`上,有一个`validate`方法,该方法返回一个`Promise`对象,返回值的状态,表明着整个表单是否校验成功。
5. 若想清除提示的错误信息,可以调用`el-form`上的`clearValidate`方法
代码示例
```vue
登录
```
##### 自定义校验规则
若需要更精确的定义规则,则需要使用到`validator`这个配置项,值为一个函数,该函数接受三个参数
- `rule`:规则对象
- `value`:元素的文本内容
- `callback`:校验函数
- 若直接调用,表示校验成功
- 若传入一个错误对象,表示校验失败
```ts
// 规则对象
const rules = {
username: [
// 每一个对象就是一套规则,当规则内的条件不满足时,就会弹出提示信息
{
validator: validatorUsername, // 指定回调
trigger: "change", // 指定触发类型
},
],
password: [
{
validator: validatorPassword,
trigger: "change",
},
],
};
// 自定义校验规则
function validatorUsername(_: any, value: string, callback: any) {
if (value.length >= 5 && value.length <= 10) {
callback(); // 校验通过
} else {
callback(new Error("账号至少5位,最多10位")); // 校验失败
}
}
function validatorPassword(_: any, value: string, callback: any) {
if (value.length >= 6 && value.length <= 15) {
callback();
} else {
callback(new Error("密码至少5位,最多15位"));
}
}
```
### `Scrollbar` 滚动条
用于替换浏览器原生滚动条
### 菜单
表单使用`el-menu`包裹
每一个菜单项要使用`el-menu-item`包裹
具体属性参考官网
```html
首页
数据大屏
权限管理
用户管理
角色管理
菜单管理
```
### 表格
表格组件使用`el-table`标签包裹,表格的每一列使用`el-table-column`包裹
### `pagination` 分页器
### `Dialog` 对话框
## 项目的资源路径
贾成豪老师代码仓库地址:https://gitee.com/jch1011/vue3_admin_template-bj1.git
项目在线文档:
服务器域名:http://sph-api.atguigu.cn
swagger文档:
http://139.198.104.58:8209/swagger-ui.html
http://139.198.104.58:8212/swagger-ui.html#/
echarts:国内镜像网站
https://www.isqqw.com/echarts-doc/zh/option.html#title
http://datav.aliyun.com/portal/school/atlas/area_selector
## 笔记
### 动态生成菜单
菜单需要通过路由动态生成,所以将路由存储在仓库中,方便读写
```ts
export const useUserStore = defineStore("User", () => {
import { RouteRecordRaw } from "vue-router";
let menuRoutes = ref(constRoute);
return {
menuRoutes
};
}
```
存储数据后,由其父组件获取数据,再传递给`Menu`组件
为每一个路由项添加一个`title`用于展示
```ts
meta: {
title: "layout", // 菜单标题
hidden: false, // 在菜单栏是否隐藏
icon: "Avatar", // 展示时的图标
},
```
最终的结构如下
```html
标
{{ item.meta.title }}
{{ item.children[0].meta.title }}
{{ item.meta.title }}
```
用`JS`描述
```ts
function menu(menuList){
for(let item in menuList){
// 不显示的直接跳过
if(item.meta.hidden){
// 如果该路由项没有子路由
if(!item.children){
// ...
// 如果该路由项只有一个子路由,直接展示子路由
}else if(item.children && item.children.length === 1){
// ...
}else { // 如果该路由项有多个子路由
// ...
menu(item.children);
}
}
}
}
```
### 页面刷新后,`menu`菜单丢失问题
当用户刷新页面时,或是用户使用地址栏进入某个子路由时,菜单栏应该选中对应的子菜单,可以使用`element-plus`提供的`default-active`属性来实现
- 由于`el-menu-item`所绑定的唯一索引`(index)`,就是路由项的地址,所以便可以通过当前页面的`path`,选中对应的`menu`
```vue
```
- `default-active`的值,就是`el-menu-item`的`index`
### 折叠左侧菜单栏
折叠菜单涉及到多个组件之间的通信,所以将数据定义在仓库中
- 定义了一个变量,内容区,左侧菜单栏、顶部导航栏根据该变量的值,来决定是否展开
定义小仓库
`src/store/modules/setting.ts`
```ts
/* layout相关的小仓库 */
import { defineStore } from "pinia";
import { ref } from "vue";
export const useLayoutSettingStore = defineStore("SettingStore", () => {
// 左侧导航是否收起
let isFolded = ref(false);
// 折叠/展开左侧菜单栏
function changeFold() {
isFolded.value = !isFolded.value;
}
return {
isFolded,
changeFold,
};
});
```
- 为`icon`图标绑定点击事件,触发时调用`changeFold`改变状态,并基于状态改变图标
```html
```
- 其他组件则绑定动态类名,计算出展示时应有的宽度,并添加过渡效果
```html
...
...
...
```
- 菜单栏折叠后的效果,可以直接添加`collapse`属性来实现
```html
...
```
### 面包屑的功能实现
实现面包屑,可以通过路由对象的`matched`属性获取,该属性是一个数组,里面存放着每一层路径
```html
{{ item.meta.title }}
```
```ts
// 引入小仓库
import { useLayoutSettingStore } from "@/store/modules/setting";
// 路由
import { useRoute } from "vue-router";
const layoutSettingStore = useLayoutSettingStore();
const $route = useRoute();
console.log($route.matched);
```
- `html`结构中,`v-show`是为了**一个路由只有一个子路由时,只显示其子路由**的情况下,隐藏其父路由,当然需要将路由中对应的`title`属性去掉
- `to`:是`element-plus`所提供的的属性,用于鼠标点击时路由跳转,相应的还有`replace`属性,这里为一级路由设置了重定向
### 刷新功能
这里的刷新功能利用了`v-if`的特性
- 在仓库中定义数据
```ts
export const useLayoutSettingStore = defineStore("SettingStore", () => {
// 控制顶部导航刷新按钮
let reFresh = ref(false);
return {
reFresh
}
}
```
- 为刷新按钮绑定点击事件,每当点击时,修改`refresh`的值
```html
```
```ts
import { toRef } from "vue";
// 引入小仓库
import { useLayoutSettingStore } from "@/store/modules/setting";
let reFresh = toRef(useLayoutSettingStore(), "reFresh");
// 刷新页面
function changeRefresh() {
reFresh.value = !reFresh.value;
}
```
- 监听`refresh`属性,当其发生变化时,卸载组件并重新挂载组件
```html
```
```ts
/*
页面的刷新功能,利用了v-if为false时,会直接销毁组件,为true则会重新创建组件
*/
import { ref, toRef, watch, nextTick, type Ref } from "vue";
// 引入小仓库
import { useLayoutSettingStore } from "@/store/modules/setting";
// 操作该变量的方法,在setting/index.vue
let refresh: Ref = toRef(useLayoutSettingStore(), "reFresh");
// 控制组件显示(销毁)
let flag = ref(true);
watch(refresh, () => {
flag.value = false;
// 在模板更新完毕之后,重新挂载组件
nextTick(() => {
flag.value = true;
});
});
```
### 全屏功能的实现
```ts
// 全屏/取消全屏
function fullScreen() {
// 当处于全屏模式时,返回当前页面中以全屏模式呈现的 Element,否则返回null(不同的浏览器可能会有兼容性问题)
let full = document.fullscreenElement;
console.log(full);
if (!full) {
// 全屏模式
document.documentElement.requestFullscreen();
} else {
// 取消全屏
document.exitFullscreen();
}
}
```
### 退出登录
1. 向服务器发请求,表示此次`token`作废(没有这个接口,就不做了)
2. 清空仓库中有关的数据`(token、username、avatar)`
3. 跳转至登录页面
4. 如果用户误操作退出登录,下一次登录时只能回到首页,可以在退出登录时携带`query`参数(虽然我觉得让服务器存着比较好,这样无论哪个用户登录,都可以回到上一次所停留的页面),用户登录时,将其携带可以再次访问上一次的页面
```ts
function userLogout() {
// 清空仓库中的数据
token.value = undefined;
avatar.value = "";
username.value = "";
// 移除本地存储中的数据
REMOVE_TOKEN();
$router.replace({
path: "/login",
// 保存用户当前的路径,让其再次登录时,可以跳转至之前的页面
query: { redirect: $route.path },
});
}
```
### 路由鉴权
`src/permission.ts`文件,用于配置路由鉴权
```ts
// 路由鉴权
import router from "@/router";
// 全局前置守卫,初始化时执行、每次路由切换前执行
/*
to:将要访问的路由对象
from:当前路由对象
next:放行函数(Vue2)
返回值:boolean,放行返回true,不放行返回false
也可以传入一个对象,用法类似push/replace
*/
router.beforeEach((to, from) => {
});
// 全局后置守卫,初始化时执行、每次路由切换后执行
router.afterEach((to, from) => {
});
```
#### 进度条
安装插件
```sh
pnpm i nprogress
```
```sh
pnpm i --save-dev @types/nprogress
```
`src/permission.ts`
```ts
/* nprogress */
import NProgress from "nprogress";
import "nprogress/nprogress.css";
// 全局前置守卫
router.beforeEach((to, from) => {
NProgress.start(); // 进度条开始
});
// 全局后置守卫
router.afterEach((to, from) => {
NProgress.done(); // 进度条结束
});
```
#### 完整逻辑
- 若用户未登录,只能访问`login`页面,
- 若用户已登录,可以访问除了`login`之外的页面
``src/permission.ts``
```ts
// 路由鉴权
import router from "@/router";
/* nprogress */
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import setting from "./setting";
// 获取用户相关的仓库
import { useUserStore } from "./store/modules/user";
// 用到的类型
import type { RouteLocationNormalized } from "vue-router";
/* nprogerss的配置 */
NProgress.configure({
showSpinner: false, // 关闭右上方加载时的小圆球
});
/*
to:将要访问的路由对象
from:当前路由对象
next:放行函数(Vue2)
返回值:boolean,放行返回true,不放行返回false,
也可以传入一个对象,用法类似push/replace
*/
// 全局前置守卫,初始化时执行、每次路由切换前执行
/*
分两种情况
用户已登录(具有有效的token):
1. 若用户直接访问登录页面,将其重定向至首页
2. 判断当前页面是否有数据(头像、用户名),没有则发送请求获取数据(判断一个字段即可)
- 若发送请求是出现问题,则代表token过期或人为修改,需要退出登录
用户未登录(无token或token无效):
1. 判断用户当前所访问的页面是否为登录页面
- 若是,直接放行
- 若不是,则携带当前的路径(query),跳转至登录页面
*/
router.beforeEach(async (to): Promise => {
const userStore = useUserStore();
let { token, username } = useUserStore();
NProgress.start(); // 进度条开始
document.title = `${setting.title}-${to.meta.title}`; // 设置页面标题
if (token) {
return handleLoggedinInUser(
username,
userStore.userInfo,
userStore.userLogout,
to
);
} else {
return handleLoggedOutUser(to);
}
// 管理用户已登录的情况
async function handleLoggedinInUser(
username: string,
userInfo: (redirect: string) => Promise,
userLogout: (redirect: string) => void,
to: RouteLocationNormalized
) {
// 若用户直接访问登录页面,将其重定向至首页
if (to.path === "/login") {
return { path: "/home" };
}
// 判断当前页面是否有数据
if (username) {
return true; // 放行
} else {
// 发送请求获取数据
try {
await userInfo(to.path);
} catch (error) {
// token过期或人为修改
console.log(error);
userLogout(to.path);
return Promise.reject("获取数据失败");
}
}
}
// 管理用户未登录时的情况
function handleLoggedOutUser(to: RouteLocationNormalized) {
if (to.path === "/login") {
return true; // 放行
}
return {
// 携带当前的路径(query),跳转至登录页面
path: "/login",
redirect: to.path,
};
}
});
// 全局后置守卫,初始化时执行、每次路由切换后执行
router.afterEach((to, from) => {
NProgress.done(); // 进度条结束
});
```
### 配置代理
- 在`.env.development`,`.env.production`和`.env.test`中设置服务器地址和基地址
```sh
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = "硅谷甄选运营平台"
VITE_APP_BASE_API = "/api"
# 服务器地址
VITE_SERVE = "http://sph-api.atguigu.cn"
```
```sh
NODE_ENV = 'production'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/prod-api'
# 服务器地址
VITE_SERVE = "http://sph-api.atguigu.cn"
```
```sh
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/test-api'
# 服务器地址
VITE_SERVE = "http://sph-api.atguigu.cn"
```
在`vite.config.ts`中添加如下代码(需要先将`defineConfig`的参数改写为函数)
```ts
import { defineConfig, loadEnv } from "vite";
export default defineConfig(({ mode }) => {
/*
加载环境变量(.env配置文件)
process.cwd(可以得到项目的根目录
*/
let env = loadEnv(mode, process.cwd());
return {
plugins:[
// ...
],
// 配置代理服务器(解决跨域)
server: {
proxy: {
[env.VITE_APP_BASE_API]: {
// 服务器的地址
target: env.VITE_SERVE,
// 控制服务器收到的请求头中Host字段的值(让服务器收到“假冒的”host值)
changeOrigin: true,
// 路径重写
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
}
}
```
### 品牌管理模块添加品牌功能
代码太多了,主要记录思路
- 定义响应式数据,存储**品牌名称**和**品牌Logo**
```ts
let trademarkParams = reactive({
tmName: "", // 品牌名
logoUrl: "", // logo图的路径
});
```
- 将品牌名称属性与文本框双向绑定
- 品牌Logo需要先发送请求上传图片至服务器,然后根据服务器所返回的数据(图片的网络路径)将图片展示在下方
- 用到了`el-load`组件
- `action`:指定文件上传的路径
- `before-upload`:上传之前调用的钩子函数,会注入图片对象(可以在此处限制上传的文件类型、
- 大小......)
- `on-success`:上传成功时调用的钩子函数,会注入服务器的返回值,和图片对象
```html
```
- 在`on-success`所绑定的回调中,将服务器返回的网络图片路径交给响应式数据`logoUrl`
- 图片的显示可以这样写
```html
```
- 最后为确认按钮绑定回调,将数据存储在服务器中
### 属性管理相关难点
当选择一级分类之后,才可以选择二级分类;选择二级分类之后,才可以选择三级分类,若同时具有三种分类的id,才可以向服务器发送请求获取数据。
1. 获取一级分类的数据
2. 携带一级分类的id去获取二级分类的数据
3. 携带二级分类的id去获取三级分类的数据
4. 携带三种id去获取需要展示的数据
在点击了一级分类之后,二级分类才会发请求获取数据,此项目中使用的是`el-select`组件,可以为其绑定`change`事件,当选中的值发生变化时,会触发其绑定的回调
- 当选择其他一级分类时,要清除二三级分类
- 当选择其他二级分类时,要清楚三级分类
### 使用`scale`解决屏幕适配问题
CSS方面
1. 为盒子开启固定定位,并将其定位至正中心(左上角中心即可)
2. 将变换原点更改至左上角
JS方面
1. 根据设计稿的宽高计算缩放的比例
2. 页面加载完毕时,以及每次页面大小发生变化时,使用`scale`缩放盒子
```html
Document
```
若不想留白,可以稍作修改代码
```js
// 页面加载完毕时调用
window.onload = function () {
scaleBox(getScale(1920, 1080));
}
// 当浏览器窗口的大小发生变化时调用
window.onresize = function () {
scaleBox(getScale(1920, 1080));
}
unction scaleBox({ widthScale, heightScale }) {
const box = document.querySelector(".box");
// 缩放盒子,并把盒子拽回去
box.style.transform = `scale(${widthScale},${heightScale}) translate(-50%,-50%)`;
}
// 计算缩放的比例
function getScale(width = 1920, height = 1080) {
let widthScale = window.innerWidth / width;
let heightScale = window.innerHeight / height;
return { widthScale, heightScale };
}
```
### 菜单权限与按钮权限
- **静态(常量)路由**:所有人都可以访问的路由
- 登录
- 首页
- 数据大屏
- 404
- **异步路由**:只有拥有特定权限才能访问的路由
- **任意路由**:跳转至404
- 拆分完毕后,路由注册时,只注册**常量路由**,**异步路由**和**任意路由**在发送请求过滤完毕之后再进行添加(这里是在`user`仓库中发送的请求,获取用户信息时,携带了菜单权限的数组。`user`仓库中也引入了所有路由表)
- 服务器根据对应的用户,返回不同的权限信息,比如返回一个`routes`数组,数组中存放了异步路由的`name`,而这个`name`正好对应路由里的`name`属性,随后进行数据的过滤即可
```js
// 异步路由
const routes = [
{
name: "father",
children: [{ name: "child1" }, { name: "child2" }]
},
{
name: 'mother',
children: [{ name: "child3" }]
}
];
// 服务器返回的权限列表
routesResponse = ['father', "child1", 'mother'];
function filterRoutes(rawRoutes, arr) {
const raw = JSON.parse(JSON.stringify(rawRoutes));
return raw.filter(item => {
// 挑选出符合要求的数据
if (arr.includes(item.name)) {
// 对子路由进行过滤,并将其赋值给item的子路由
if (item.children) {
// 修改filter中的item并不会导致其原数据改变,但由于item.children为引用数据类型,所以要使用深拷贝来避免修改原数据
item.children = filterRoutes(item.children, arr);
}
return true;
}
});
}
let result = filterRoutes(routes, routesResponse);
console.log(result);
console.log(routes);
```
- 使用addRoute即可添加路由项
```js
import { useRouter } from "vue-router";
useRouter().addRouter(路由对象)
```