当前位置:   article > 正文

使用 SpringBoot + Redis + Vue3 + ArcoPro 开发管理系统_arco pro

arco pro

使用 SpringBoot + Redis + Vue3 + ArcoPro 开发管理系统


前言

本篇文章主要用于完全新手入门,实战操作使用 SpringBoot + Redis + Vue3 + ArcoPro 等开发一个简单管理系统的CRUD(增删改查)操作。并不做过多深入的讲解。

本文将分为两部分:后端开发、前端开发。

其中涉及到的环境安装需自行安装好 JDK1.8、MySQL5.7+、Redis 5.0、Node 14.18.1、IDEA、VSCode、Navicat premium 数据管理工具

如果已安装过,就无需在重复安装。切记Node版本最好一致,以免出现各种奇奇怪怪的问题

后面会继续写 搭配 ElasticSearch 、RabbitMQ 完成搜索推荐、位置距离推荐使用的,包含小程序、APP等 结合本套 教程以及完善如密码、安全、OSS存储 等功能案例

内容有不明之处或错误之处可指出,看到第一时间更正

适用于Windows 操作系统,这里以 Windows11 为例。其中不包含IDEA与VSCode两款工具的安装,请自行安装开发工具和环境
所需开发软件下载地址以及代码在本章末尾提供下载

最终效果图
在这里插入图片描述

一、后端开发

1. 数据库创建

这里创建一个名为 give 的数据库,在本地数据库上右键 新建数据库,按如图所示选择对应字符集与排序规则。
give数据库创建
为方便大家节省时间一个个字段创建,这里提供一个简单的用户表示例,可以用于直接创建,在give上右键新建查询,将SQL语句粘贴进去点击运行即可。或在提供的资料代码中找到 give.sql 在give上右键导入运行SQL文件,导入前请先确保数据库创建好。
数据库表创建

/*
 Source Server         : 本地数据库
 Source Server Type    : MySQL
 Source Server Version : 50737
 Source Host           : localhost:3306
 Source Schema         : give

 Target Server Type    : MySQL
 Target Server Version : 50737
 File Encoding         : 65001

 Date: 28/03/2022
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for cls_user
-- ----------------------------
DROP TABLE IF EXISTS `cls_user`;
CREATE TABLE `cls_user`  (
  `id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'ID',
  `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '账号',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
  `nickname` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称',
  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像',
  `age` int(3) NULL DEFAULT NULL COMMENT '年龄',
  `sex` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '性别',
  `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号码',
  `role` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of cls_user
-- ----------------------------
INSERT INTO `cls_user` VALUES ('1498674755007102976', 'admin', 'admin', '小可', 'https://www.proyy.com/api/pictures/random/index.php?type=tx', 18, '1', 'admin@clstech.cn', '19911110000', 'admin', '2022-03-14 21:56:21', '2022-03-14 21:56:25');

SET FOREIGN_KEY_CHECKS = 1;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

2. 项目创建

可能由于网络原因、此步骤部分学友可能无法操作,可以使用文章末尾提供的网盘中 give-init.zip 压缩包,将其解压后,点击Open选项打开即可。

如图所示,打开IDEA后,点击 New Project 在弹出框中选择 Spring Initializr 之后点击Next下一步操作。填写项目信息,这里仅供参考。
项目创建
项目基本信息
根据如图所示选择Lombok、Spring Web、 MySQL Driver 后点击Next 之后 Finish 即可创建SpringBoot项目的基本模板。
在这里插入图片描述

3. 增加依赖包与修改配置

在 pom.xml 的 dependencies 中 增加如下依赖

<!--Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.29.0</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.21</version>
</dependency>

<!-- Dynamic datasource 动态数据源 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

<!-- JSON 解 析 工 具 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.78</version>
</dependency>

<!-- Sa-Token 整合 Redis (使用jdk默认序列化方式) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-dao-redis</artifactId>
    <version>1.29.0</version>
</dependency>

<!-- 提供Redis连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

由于个人习惯yaml风格、所以这里 application.properties 修改为 application.yaml ,并删除static、templates两个文件夹
修改配置文件
在 application.yaml 中加入如下配置参数

server:
  # 端口
  port: 9001

# Sa-Token配置
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: token
  # token有效期,单位s 默认30天, -1代表永不过期
  timeout: -1
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: false
  # token风格
  token-style: uuid
  # 是否输出操作日志
  is-log: false


spring:
  # redis配置
  redis:
    # Redis数据库索引(默认为0)
    database: 2
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password:
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3306}/${MYSQL_DB:give}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true
          username: ${MYSQL_USER:root}
          password: ${MYSQL_PWD:123456}
  servlet:
    multipart:
      # 配置上传文件大小限制
      max-file-size: 10MB
      max-request-size: 100MB
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

3. 使用插件快速创建 mapper、xml、service

插件名称 mybatisCodeHelperPro 可在 IDEA 插件中心下载安装。

配置IDEA中数据库连接,如图所示,选择连接数据源 MySQL
数据库连接
填写我们的 数据库地址、账号和密码以及数据库名称give即可
数据库连接配置
在 schemas 下 选中 cls_user 右键选择 MyBatis generator
在这里插入图片描述
按照如下填写与选择 其中 com.example.give 为项目包名
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
确认无误后,点击OK得到如下 文件

在这里插入图片描述

4. 创建 controller

在 com.example.give 下创建 controller 、 utils两个目录,包含 UserController 与 R 两个 Java 类文件

在 utils 下 修改 R 统一返回结果类
返回结果类

/**
 * 统一返回结果类
 */
@Data
@AllArgsConstructor
public class R {

    // 返回状态码
    private int code;

    // 返回消息
    private String msg;

    // 状态
    private String status;

    // 返回数据
    private Object data;

    // 成功(无数据)
    public static R success(String message) {
        return new R(20000, message, "ok", null);
    }

    // 成功
    public static R success(String message, Object data) {
        return new R(20000, message,"ok",  data);
    }

    // 失败(无数据)
    public static R fail(int code, String message) {
        return new R(code, message, "fail", null);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

在 新建的 UserController 中加入如下代码,实际业务中 主要密码加密解密判断等操作,这里就不做密码判断校验

/**
 * 用户管理
 */
@RestController
@RequestMapping(value = "/api/user")
public class UserController {


    @Autowired
    private ClsUserService clsUserService;


    /**
     * 用户登录
     * @param accouut 账号和密码
     * @return 登录结果
     */
    @PostMapping(value = "/login")
    public R login(@RequestBody Map<String, Object> accouut) {
        String username = String.valueOf(accouut.get("username"));
        String password = String.valueOf(accouut.get("password"));

        final ClsUser clsUser = clsUserService.getOne(new QueryWrapper<ClsUser>().eq("username", username));

        // 登录
        // 实际业务中 主要密码加密解密判断等操作
        StpUtil.login(clsUser.getUsername());
        return R.success("登录成功", StpUtil.getTokenInfo());

    }


    /**
     * 注销登录
     */
    @PostMapping(value = "/logout")
    public R login() {
        StpUtil.logout();
        return R.success("注销成功");
    }

    /**
     * 获取所有用户数据
     *
     * @return 所有用户数据
     */
    @GetMapping(value = "/list")
    public R list(ClsUser clsUser, @RequestParam(value = "current") int current, @RequestParam(value = "pageSize") int pageSize) {
        IPage page = clsUserService.page(new Page<>(current, pageSize), Wrappers.query(clsUser));
        return R.success("获取用户列表", page);
    }


    /**
     * 获取个人信息
     *
     * @return
     */
    @GetMapping(value = "/info")
    @SaCheckLogin
    public R info() {
        final ClsUser clsUser = clsUserService.getOne(new QueryWrapper<ClsUser>().eq("username", StpUtil.getLoginIdAsString()));
        return R.success("获取用户信息成功", clsUser);
    }



    @PostMapping(value ="/save")
    public R save(@RequestBody ClsUser clsUser) {
        boolean save = false;

        // 实际业务中 主要密码加密解密判断等操作

        if(ObjectUtil.isNotNull(clsUser.getPassword()) && ObjectUtil.isNotEmpty(clsUser.getPassword()) && ObjectUtil.isNotEmpty(clsUser.getUsername())) {
            clsUser.setId(IdUtil.getSnowflakeNextIdStr());
            clsUser.setCreateTime(DateUtil.date());
            save = clsUserService.save(clsUser);
        }
        if (save) {
            return R.success("新增用户成功", clsUser);
        } else {
            return R.fail(20001, "新增用户失败");
        }
    }

    @PutMapping(value = "/update")
    public R update(@RequestBody ClsUser clsUser) {
        clsUser.setUpdateTime(DateUtil.date());
        boolean b = clsUserService.updateById(clsUser);
        if (b) {
            return R.success("更新成功");
        }else {
            return R.fail(20001, "更新失败");
        }
    }

    @DeleteMapping(value = "/delete")
    public R delUser(@RequestParam(value = "id") String id) {
        final boolean res = clsUserService.removeById(id);
        if (res) {
            return R.success("删除用户成功", 20000);
        } else {
            return R.fail(20001, "删除用户失败");
        }
    }


}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109

二、前端开发

安装项目模版的工具,在命令行中 执行 如下命令

npm i -g arco-cli
  • 1

在任意盘符下,创建一个文件夹, 这里是在D盘VueProjects文件夹下执行如下命令新建项目

arco init arco-give
  • 1

在这里插入图片描述
按照提示选择如下选项
在这里插入图片描述

1.使用VSCode 打开修改参数配置等

修改 .env.development 文件中 VITE_API_BASE_URL 后端接口请求地址,端口在 后端 application.yaml 配置文件配置为 9001
修改后端API地址
修改 src\store\modules\user 目录下 index.ts 与 types.ts

types

export type RoleType = '' | '*' | 'admin' | 'user';
export interface UserState {
  id?: string;
  username?: string;
  password?: string;
  nickname?: string;
  avatar?: string;
  age?: number;
  sex?: string;
  idcard?: string;
  name?: string;
  email?: string;
  phone?: string;
  tel?: string;
  enable?: string;
  createTime?: string;
  updateTime?: string;
  roleId?: string;
  role: RoleType;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

修改红框部分
在这里插入图片描述

state: (): UserState => ({
    id: undefined,
    username: undefined,
    password: undefined,
    nickname: undefined,
    avatar: undefined,
    age: undefined,
    sex: undefined,
    idcard: undefined,
    name: undefined,
    email: undefined,
    phone: undefined,
    tel: undefined,
    enable: undefined,
    createTime: undefined,
    updateTime: undefined,
    roleId: undefined,
    role: '',
  }),
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在 src\mock 目录下 修改 user.ts 增加 mock: false
在这里插入图片描述
修改 src\api 下 user.ts 的 getUserInfo方法请求方式POST 修改为 Get

在这里插入图片描述
修改 src\api 下的 interceptor.ts 在请求时附带 Token
在这里插入图片描述

axios.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    // let each request carry token
    // this example using the JWT token
    // Authorization is a custom headers key
    // please modify it according to the actual situation
    const token = getToken();
    if (token) {
      config.headers = {
        token,
      };
      // if (!config.headers) {
      //   config.headers = {
      //     token,
      //   };
      // }
      // config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    // do something
    return Promise.reject(error);
  }
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

修改ArcoPro在登录时候,获取token值进行保存相关配置
1、 修改 src\api\user.ts 文件中 LoginRes 接口中 token 更改为 tokenValue
在这里插入图片描述
2、修改 src\store\modules\user\index.ts 中的setToken(res.data.token); 修改为 setToken(res.data.tokenValue);
在这里插入图片描述
后端使用IDEA启动方式很简单,点击绿色播放小按钮即可启动,祝你一路不爆红。
前端启动 在终端中输入 yarn dev 即可启动成功。
到此为止你就可以尝试 访问 http://localhost:3000进行登录了,账号密码 就 创建数据库时候插入的 一条数据 都是 admin

如果没有安装yarn 在终端命令行中输入 如下 进行安装即可

npm install -g yarn --registry=https://registry.npm.taobao.org
  • 1

2. 用户管理CRUD添加

(1)、添加用户管理路由配置

在 src\router\routes\modules\dashboard.ts 文件中 增加如下路由配置

在这里插入图片描述

{
   path: 'users',
   name: 'users',
   component: () => import('@/views/dashboard/users/index.vue'),
   meta: {
     locale: 'menu.dashboard.users', // 二级菜单名(语言包键名)
     requiresAuth: true, // 是否需要鉴权
     roles: ['admin'], // 权限角色
   },
 },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

(2) 、添加用户管理国际化配置

为 src\views\dashboard\workplace\locale 目录下的 en-US.ts 和 zh-CN.ts 分别增加国际化配置
在这里插入图片描述

'menu.dashboard.users': 'User Manage',
  • 1

在这里插入图片描述

'menu.dashboard.users': '用户管理',
  • 1

此时后台中就出现 用户管理
在这里插入图片描述

(3) 、添加用户管理页面内容

  1. 在 src\api 增加 base.ts 文件用于统一请求后返回结果
    在这里插入图片描述
export interface ResponseResult<T = any> {
  code: number;
  status: string;
  msg: string;
  data: T;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  1. 在 src\api\user.ts 文件中 添加如下 接口请求封装

在文件开始部分加入引用

import qs from 'query-string';
import { UserState } from '@/store/modules/user/types';
import { ResponseResult } from './base';
  • 1
  • 2
  • 3

在末尾处增加如下内容


export interface UserParams extends Partial<UserState> {
  current: number;
  pageSize: number;
}

export interface UserListRes {
  records: UserState[];
  total: number;
}

export function queryUserList(params: UserParams) {
  return axios.get<UserListRes>('/api/user/list', {
    params,
    paramsSerializer: (obj) => {
      return qs.stringify(obj, {
        // 如果为Null或undefined、不参与拼接
        skipNull: true,
        // 如果为空字符串、不参与拼接
        skipEmptyString: true,
      });
    },
  });
}

/// 更新操作
export function addUser(params: {
  id: string;
  username: string;
  password: string;
  nickname?: string;
  avatar?: string;
  age?: number;
  sex: string;
  email?: string;
  phone?: string;
  role?: string;
  createTime?: string;
  updateTime?: string;
}) {
  return axios.post<ResponseResult, ResponseResult>('/api/user/save', params);
}

/// 删除账号
export function delUser(params: { id: string }) {
  return axios.delete<ResponseResult, ResponseResult>('/api/user/delete', {
    params,
    paramsSerializer: (obj) => {
      return qs.stringify(obj);
    },
  });
}

/// 更新操作
export function updateUser(params: {
  id: string;
  username: string;
  nickname?: string;
  avatar?: string;
  age?: number;
  sex: string;
  email?: string;
  phone?: string;
  role?: string;
  updateTime?: string;
}) {
  return axios.put<ResponseResult, ResponseResult>('/api/user/update', params);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  1. 在 src\views\dashboard 目录下新建 users 目录 并且在 该目录下新建 index.vue 文件
    增加用户页面
    加入如下代码即可、关于什么意思,那就只能麻烦大家去多多学习了,本文只描述搭建开发过程
<template>
  <div class="container">
    <Breadcrumb :items="['menu.system', 'menu.system.user']" />
    <a-card class="general-card" :title="$t('menu.system.user')">
      <a-row>
        <a-col :flex="1">
          <a-form
            :model="formModel"
            :label-col-props="{ span: 6 }"
            :wrapper-col-props="{ span: 18 }"
            label-align="left"
          >
            <a-row :gutter="16">
              <a-col :span="8">
                <a-form-item
                  field="number"
                  :label="$t('searchTable.form.number')"
                >
                  <a-input
                    v-model="formModel.id"
                    :placeholder="$t('searchTable.form.number.placeholder')"
                  />
                </a-form-item>
              </a-col>
              <a-col :span="8">
                <a-form-item
                  field="username"
                  :label="$t('searchTable.form.username')"
                >
                  <a-input
                    v-model="formModel.username"
                    :placeholder="$t('searchTable.form.username.placeholder')"
                  />
                </a-form-item>
              </a-col>
              <a-col :span="8">
                <a-form-item
                  field="nickname"
                  :label="$t('searchTable.form.nickname')"
                >
                  <a-input
                    v-model="formModel.nickname"
                    :placeholder="$t('searchTable.form.nickname.placeholder')"
                  />
                </a-form-item>
              </a-col>
            </a-row>
          </a-form>
        </a-col>
        <a-divider style="height: 84px" direction="vertical" />
        <a-col :flex="'86px'" style="text-align: right">
          <a-space direction="vertical" :size="18">
            <a-button type="primary" @click="search">
              <template #icon>
                <icon-search />
              </template>
              {{ $t('searchTable.form.search') }}
            </a-button>
            <a-button @click="reset">
              <template #icon>
                <icon-refresh />
              </template>
              {{ $t('searchTable.form.reset') }}
            </a-button>
          </a-space>
        </a-col>
      </a-row>
      <a-divider style="margin-top: 0" />
      <a-row style="margin-bottom: 16px">
        <a-col :span="16">
          <a-space>
            <a-button type="primary" @click="addClick">
              <template #icon>
                <icon-plus />
              </template>
              {{ $t('searchTable.operation.create') }}
            </a-button>
            <a-upload action="/">
              <template #upload-button>
                <a-button>
                  {{ $t('searchTable.operation.import') }}
                </a-button>
              </template>
            </a-upload>
          </a-space>
        </a-col>
        <a-col :span="8" style="text-align: right">
          <a-button>
            <template #icon>
              <icon-download />
            </template>
            {{ $t('searchTable.operation.download') }}
          </a-button>
        </a-col>
      </a-row>
      <a-table
        row-key="id"
        :loading="loading"
        :pagination="pagination"
        :data="renderData"
        :bordered="false"
        @page-change="onPageChange"
      >
        <template #columns>
          <a-table-column
            :title="$t('searchTable.columns.number')"
            data-index="id"
          />
          <a-table-column
            :title="$t('searchTable.columns.username')"
            data-index="username"
          />
          <a-table-column
            :title="$t('searchTable.columns.nickname')"
            data-index="nickname"
          />
          <a-table-column
            :title="$t('searchTable.columns.avatar')"
            data-index="avatar"
          >
            <template #cell="{ record }">
              <a-space>
                <a-avatar>
                  <img :src="record.avatar" alt="" />
                </a-avatar>
              </a-space>
            </template>
          </a-table-column>
          <a-table-column
            :title="$t('searchTable.columns.age')"
            data-index="age"
          />
          <a-table-column
            :title="$t('searchTable.columns.sex')"
            data-index="sex"
          >
            <template #cell="{ record }">
              {{ $t(`searchTable.form.sex.${record.sex}`) }}
            </template>
          </a-table-column>
          <a-table-column
            :title="$t('searchTable.columns.email')"
            data-index="email"
          />
          <a-table-column
            :title="$t('searchTable.columns.phone')"
            data-index="phone"
          />
          <a-table-column
            :title="$t('searchTable.columns.role')"
            data-index="role"
          >
            <template #cell="{ record }">
              <span v-if="record.role === 'admin'">管理员</span>
              <span v-if="record.role === 'user'">用户</span>
            </template>
          </a-table-column>
          <a-table-column
            :title="$t('searchTable.columns.createTime')"
            data-index="createTime"
          />
          <a-table-column
            :title="$t('searchTable.columns.updateTime')"
            data-index="updateTime"
          />
          <a-table-column
            :title="$t('searchTable.columns.operations')"
            data-index="operations"
          >
            <template #cell="{ record }">
              <!-- <a-button type="text" size="small" @click="showData(record)">
                {{ $t('searchTable.columns.operations.view') }}
              </a-button> -->
              <a-button type="text" size="small" @click="addClick(record)">
                {{ $t('searchTable.columns.operations.update') }}
              </a-button>
              <a-popconfirm
                position="lt"
                :content="
                  $t('searchTable.columns.operations.confirm.delete') +
                  record.username
                "
                @cancel="onCancel()"
                @ok="deleteUser({ id: record.id })"
              >
                <a-button type="text" size="small">
                  {{ $t('searchTable.columns.operations.delete') }}
                </a-button>
              </a-popconfirm>
            </template>
          </a-table-column>
        </template>
      </a-table>
    </a-card>

    <!-- 新增弹窗 -->
    <a-modal
      v-model:visible="addVisible"
      width="auto"
      :mask-closable="false"
      @before-ok="addOk"
      @cancel="addCancel"
    >
      <template #title>
        {{ $t('searchTable.columns.modal.add.title') }}
        <a-progress
          v-if="percent != 0.0"
          type="circle"
          size="mini"
          :percent="percent"
        />
      </template>
      <div>
        <a-form :ref="formRef" :model="form" :style="{ width: '600px' }">
          <a-form-item field="username" label="账号" validate-trigger="input">
            <a-input v-model="form.username" placeholder="请输入用户名称..." />
          </a-form-item>
          <a-form-item
            v-if="isShow"
            field="password"
            label="密码"
            validate-trigger="input"
          >
            <a-input-password
              v-model="form.password"
              placeholder="请输入用户密码..."
            />
          </a-form-item>
          <a-form-item field="nickname" label="昵称" validate-trigger="input">
            <a-input v-model="form.nickname" placeholder="请输入昵称..." />
          </a-form-item>

          <!-- 头像 -->
          <a-form-item field="avatar" label="头像" validate-trigger="input">
            <a-upload
              list-type="picture"
              action="/api/file/upload"
              :limit="1"
              :file-list="file ? [file] : []"
              @success="onSuccess"
            />
          </a-form-item>

          <a-form-item field="age" label="年龄" validate-trigger="input">
            <a-input-number v-model="form.age" placeholder="请输入年龄..." />
          </a-form-item>
          <a-form-item field="sex" label="性别" validate-trigger="input">
            <a-select v-model="form.sex" placeholder="请选择性别 ...">
              <a-option value="0"></a-option>
              <a-option value="1"></a-option>
            </a-select>
          </a-form-item>
          <a-form-item field="email" label="邮箱" validate-trigger="input">
            <a-input v-model="form.email" placeholder="请输入邮箱..." />
          </a-form-item>
          <a-form-item field="phone" label="联系方式" validate-trigger="input">
            <a-input v-model="form.phone" placeholder="请输入联系方式..." />
          </a-form-item>
          <a-form-item field="role" label="角色" validate-trigger="input">
            <a-select v-model="form.role" placeholder="请选择角色 ...">
              <a-option value="admin">管理员</a-option>
              <a-option value="user">用户</a-option>
            </a-select>
          </a-form-item>
        </a-form>
      </div>
    </a-modal>
  </div>
</template>

<script lang="ts">
  import { defineComponent, ref, reactive } from 'vue';
  import { useI18n } from 'vue-i18n';
  import useLoading from '@/hooks/loading';
  import { UserState } from '@/store/modules/user/types';
  import { Pagination } from '@/types/global';
  import {
    UserParams,
    queryUserList,
    delUser,
    updateUser,
    addUser,
  } from '@/api/user';
  import { Message, Notification } from '@arco-design/web-vue';
  import { FormInstance } from '@arco-design/web-vue/es/form';
  import IconPlus from '@arco-design/web-vue/es/icon/icon-plus';

  const generateFormModel = () => {
    return {
      id: '',
      username: '',
      password: '',
      nickname: '',
      avatar: '',
      age: 0,
      sex: '',
      email: '',
      phone: '',
      role: '',
      createTime: '',
      updateTime: '',
    };
  };

  const serarchModel = () => {
    return {
      id: '',
      username: '',
      nickname: '',
      sex: '',
      email: '',
      phone: '',
      role: '',
      createTime: '',
      updateTime: '',
    };
  };

  export default defineComponent({
    components: { IconPlus },
    setup() {
      const { loading, setLoading } = useLoading(true);
      const { t } = useI18n();
      const renderData = ref<UserState[]>([]);
      const formModel = ref(serarchModel());
      const form = ref(generateFormModel());
      const addVisible = ref(false);
      const percent = ref(0.0);
      const formRef = ref<FormInstance>();
      const isShow = ref(true);
      const file = ref('');

      const basePagination: Pagination = {
        current: 1,
        pageSize: 20,
      };

      const pagination = reactive({
        ...basePagination,
      });

      const onSuccess = (currentFile: any) => {
        form.value.avatar = currentFile.response.data.fileName;
        Message.success(currentFile.response.msg);
      };

      const onCancel = () => {
        Message.success(t('searchTable.columns.operations.confirm.cancel'));
      };

      // 获取数据
      const fetchData = async (
        params: UserParams = { current: 1, pageSize: 20 }
      ) => {
        setLoading(true);
        try {
          const { data } = await queryUserList(params);
          renderData.value = data.records;
          pagination.current = params.current;
          pagination.total = data.total;
        } catch (err) {
          // you can report use errorHandler or other
        } finally {
          setLoading(false);
        }
      };

      // 删除账号
      const deleteUser = async (params: { id: string }) => {
        setLoading(true);
        try {
          const { code } = await delUser(params);
          if (code === 20000) {
            Message.success(t('searchTable.operation.success'));
            fetchData();
          } else {
            Message.error(t('searchTable.operation.fail'));
          }
        } catch (err) {
          // console.log(err);
        } finally {
          setLoading(false);
        }
      };

      // 搜索
      const search = () => {
        fetchData({
          ...basePagination,
          ...formModel.value,
        } as unknown as UserParams);
      };

      // 添加弹窗
      const addClick = (params: {
        id: string;
        username: string;
        password: string;
        nickname: string;
        avatar: string;
        age: number;
        sex: string;
        email: string;
        phone: string;
        role: string;
        createTime: string;
        updateTime: string;
      }) => {
        form.value = params;
        if (form.value.id !== undefined) {
          isShow.value = false;
          addVisible.value = true;
        } else {
          isShow.value = true;
          addVisible.value = true;
        }
      };

      // 确认添加和修改
      const addOk = async (done: () => void) => {
        done();

        if (form.value.id === undefined) {
          const { code } = await addUser(form.value);
          if (code === 20001) {
            Notification.error('添加失败!');
          }
          fetchData();
          Notification.success('添加成功!');
        } else {
          const { code } = await updateUser(form.value);
          if (code === 20001) {
            Notification.error('更新失败!');
          }
          fetchData();
          Notification.success('更新成功!');
        }
      };

      // 确认取消
      const addCancel = () => {
        Message.info(t('searchTable.columns.modal.add.confirm.cancel'));
      };

      const onPageChange = (current: number) => {
        fetchData({ ...basePagination, current });
      };

      fetchData();

      // 重置
      const reset = () => {
        formModel.value = serarchModel();
        fetchData();
      };

      return {
        loading,
        search,
        onPageChange,
        renderData,
        pagination,
        formModel,
        reset,
        deleteUser,
        addVisible,
        addClick,
        addOk,
        addCancel,
        onCancel,
        percent,
        form,
        formRef,
        isShow,
        file,
        onSuccess,
      };
    },
  });
</script>

<style scoped lang="less">
  :deep(.arco-table-th) {
    &:last-child {
      .arco-table-th-item-title {
        margin-left: 16px;
      }
    }
  }
</style>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
  • 458
  • 459
  • 460
  • 461
  • 462
  • 463
  • 464
  • 465
  • 466
  • 467
  • 468
  • 469
  • 470
  • 471
  • 472
  • 473
  • 474
  • 475
  • 476
  • 477
  • 478
  • 479
  • 480
  • 481
  • 482
  • 483
  • 484
  • 485
  • 486
  • 487
  • 488
  • 489
  • 490
  • 491

到此你的页面应该是这样的,可以看出各个字段地方国际化没有显示出来,别急,我们来添加关于用户的国际化配置文件
在这里插入图片描述
在 src\views\dashboard\users 目录下 新建一个 locale 目录,创建两个文件 en-US.ts 和 zh-CN.ts 并在各个文件中加入国际化配置参数

en-US.ts 部分

export default {
  'menu.system.user': 'User Manage',
  'searchTable.form.number': 'User Number',
  'searchTable.form.number.placeholder': 'Please enter Set User Number',
  'searchTable.form.username': 'Username',
  'searchTable.form.username.placeholder': 'Please enter Set Username',
  'searchTable.form.nickname': 'Nickname',
  'searchTable.form.nickname.placeholder': 'Please enter Set Nickname',
  'searchTable.form.status': 'Status',
  'searchTable.form.status.placeholder': 'Please enter Set Status',
  'searchTable.form.selectDefault': 'All',

  'searchTable.form.search': 'Search',
  'searchTable.form.reset': 'Reset',
  'searchTable.form.sex.0': 'Female',
  'searchTable.form.sex.1': 'Male',
  'searchTable.form.status.0': 'Enable',
  'searchTable.form.status.1': 'Disable',
  'searchTable.operation.create': 'Create',
  'searchTable.operation.import': 'Import',
  'searchTable.operation.download': 'Download',
  'searchTable.operation.success': 'Success',
  'searchTable.operation.fail': 'Fail',
  // columns
  'searchTable.columns.number': 'User Number',
  'searchTable.columns.username': 'Username',
  'searchTable.columns.password': 'Password',
  'searchTable.columns.nickname': 'Nickname',
  'searchTable.columns.avatar': 'Avatar',
  'searchTable.columns.age': 'Age',
  'searchTable.columns.sex': 'Sex',
  'searchTable.columns.email': 'E-Mail',
  'searchTable.columns.phone': 'Phone',
  'searchTable.columns.role': 'Role',
  'searchTable.columns.createTime': 'Create Time',
  'searchTable.columns.updateTime': 'Update Time',

  'searchTable.columns.operations': 'Operations',
  'searchTable.columns.operations.view': 'View',
  'searchTable.columns.operations.update': 'Edit',
  'searchTable.columns.operations.delete': 'Delete',
  'searchTable.columns.operations.confirm.delete': 'Confirm deletion',
  'searchTable.columns.operations.confirm.cancel': 'Cancel deletion',

  'searchTable.columns.modal.add.title': 'Add User',
  'searchTable.columns.modal.add.confirm.determine': 'Confirm add',
  'searchTable.columns.modal.add.confirm.cancel': 'Cancel adding',
  'searchTable.columns.modal.update.title': 'Update information',
  'searchTable.columns.modal.update.confirm.determine': 'Confirm update',
  'searchTable.columns.modal.update.confirm.cancel': 'Cancel update',
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

zh-CN.ts 部分

export default {
  'menu.system.user': '用户管理',
  'searchTable.form.number': '用户编号',
  'searchTable.form.number.placeholder': '请输入用户编号',
  'searchTable.form.username': '账号',
  'searchTable.form.username.placeholder': '请输入账号',
  'searchTable.form.nickname': '昵称',
  'searchTable.form.nickname.placeholder': '请输入昵称',
  'searchTable.form.status': '状态',
  'searchTable.form.status.placeholder': '请选择状态',
  'searchTable.form.selectDefault': '全部',

  'searchTable.form.search': '查询',
  'searchTable.form.reset': '重置',
  'searchTable.form.sex.0': '女',
  'searchTable.form.sex.1': '男',
  'searchTable.form.status.0': '启用',
  'searchTable.form.status.1': '禁用',
  'searchTable.operation.create': '新建',
  'searchTable.operation.import': '批量导入',
  'searchTable.operation.download': '下载',
  'searchTable.operation.success': '成功',
  'searchTable.operation.fail': '失败',
  // columns
  'searchTable.columns.number': '编号',
  'searchTable.columns.username': '名称',
  'searchTable.columns.password': '密码',
  'searchTable.columns.nickname': '昵称',
  'searchTable.columns.avatar': '头像',
  'searchTable.columns.age': '年龄',
  'searchTable.columns.sex': '性别',
  'searchTable.columns.email': '邮箱',
  'searchTable.columns.phone': '手机号',
  'searchTable.columns.role': '角色',
  'searchTable.columns.enable': '状态',
  'searchTable.columns.createTime': '创建时间',
  'searchTable.columns.updateTime': '更新时间',

  'searchTable.columns.operations': '操作',
  'searchTable.columns.operations.view': '查看',
  'searchTable.columns.operations.update': '修改',
  'searchTable.columns.operations.delete': '删除',
  'searchTable.columns.operations.confirm.delete': '确认删除',
  'searchTable.columns.operations.confirm.cancel': '取消删除',

  'searchTable.columns.modal.add.title': '添加用户',
  'searchTable.columns.modal.add.confirm.determine': '确定添加',
  'searchTable.columns.modal.add.confirm.cancel': '取消添加',
  'searchTable.columns.modal.update.title': '更新信息',
  'searchTable.columns.modal.update.confirm.determine': '确定更新',
  'searchTable.columns.modal.update.confirm.cancel': '取消更新',
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

添加完成后,在加入这两个文件的引用
在 src\locale 目录下中 有两个 en-US.ts 和 zh-CN.ts 分别在其中增加 引用

zh-CN.ts 部分

在这里插入图片描述

en-US.ts 部分

在这里插入图片描述

到此就结束了,你得页面应该如下、并筛选查询、修改、删除、新增都可以正常使用

在这里插入图片描述

以上内容,仅仅是实现过程,更多的描述时间原因不做说明,可看注释或查阅资料

关于头像上传,可先自行实现逻辑,需要的可以代开发系统

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/268149
推荐阅读
相关标签
  

闽ICP备14008679号