Kaynağa Gözat

初始化项目。

jiaxiaoqiang 11 ay önce
ebeveyn
işleme
7f317f1b41
67 değiştirilmiş dosya ile 1501 ekleme ve 173 silme
  1. 4 4
      .env.development
  2. 3 1
      .env.production
  3. 39 0
      docker/Dockerfile
  4. 1 1
      package.json
  5. 11 0
      src/api/auth/index.ts
  6. 3 1
      src/api/auth/types.ts
  7. 156 0
      src/api/system/user/index.ts
  8. 118 0
      src/api/system/user/types.ts
  9. 0 1
      src/assets/icons/api.svg
  10. 0 1
      src/assets/icons/cascader.svg
  11. 0 1
      src/assets/icons/client.svg
  12. 0 1
      src/assets/icons/close.svg
  13. 0 1
      src/assets/icons/close_all.svg
  14. 0 1
      src/assets/icons/close_left.svg
  15. 0 1
      src/assets/icons/close_other.svg
  16. 0 1
      src/assets/icons/close_right.svg
  17. 0 1
      src/assets/icons/dict.svg
  18. 0 1
      src/assets/icons/document.svg
  19. 0 1
      src/assets/icons/download.svg
  20. 0 1
      src/assets/icons/edit.svg
  21. 0 1
      src/assets/icons/eye-open.svg
  22. 0 1
      src/assets/icons/eye.svg
  23. 0 1
      src/assets/icons/fullscreen-exit.svg
  24. 0 1
      src/assets/icons/fullscreen.svg
  25. 0 1
      src/assets/icons/github.svg
  26. 0 1
      src/assets/icons/homepage.svg
  27. 0 1
      src/assets/icons/indent-decrease.svg
  28. 0 1
      src/assets/icons/ip.svg
  29. 0 1
      src/assets/icons/language.svg
  30. 0 1
      src/assets/icons/link.svg
  31. 0 1
      src/assets/icons/menu.svg
  32. 0 1
      src/assets/icons/message.svg
  33. 0 1
      src/assets/icons/money.svg
  34. 0 1
      src/assets/icons/monitor.svg
  35. 0 1
      src/assets/icons/moon.svg
  36. 0 1
      src/assets/icons/order.svg
  37. 0 1
      src/assets/icons/peoples.svg
  38. 0 1
      src/assets/icons/project.svg
  39. 0 1
      src/assets/icons/publish.svg
  40. 0 1
      src/assets/icons/refresh.svg
  41. 0 1
      src/assets/icons/role.svg
  42. 0 1
      src/assets/icons/security.svg
  43. 0 1
      src/assets/icons/setting.svg
  44. 0 1
      src/assets/icons/size.svg
  45. 0 1
      src/assets/icons/sunny.svg
  46. 0 1
      src/assets/icons/system.svg
  47. 0 1
      src/assets/icons/table.svg
  48. 0 1
      src/assets/icons/todolist.svg
  49. 0 1
      src/assets/icons/tree.svg
  50. 0 1
      src/assets/icons/visit.svg
  51. 122 16
      src/hooks/userCrud.ts
  52. 22 2
      src/plugins/permission.ts
  53. 18 84
      src/router/index.ts
  54. 3 0
      src/store/index.ts
  55. 1 2
      src/store/modules/common.ts
  56. 46 0
      src/store/modules/dictionary.ts
  57. 1 1
      src/store/modules/permission.ts
  58. 1 1
      src/store/modules/settings.ts
  59. 3 2
      src/store/modules/user.ts
  60. 61 0
      src/utils/axios.ts
  61. 66 0
      src/utils/common.ts
  62. 15 16
      src/utils/request.ts
  63. 202 0
      src/views/welcome/components/BarChart.vue
  64. 115 0
      src/views/welcome/components/FunnelChart.vue
  65. 89 0
      src/views/welcome/components/PieChart.vue
  66. 109 0
      src/views/welcome/components/RadarChart.vue
  67. 292 0
      src/views/welcome/index.vue

+ 4 - 4
.env.development

@@ -7,11 +7,11 @@ VITE_APP_PORT = 3005
 # 代理前缀
 VITE_APP_BASE_API = '/dev-api'
 
-# 线上接口地址
-# VITE_APP_API_URL = http://vapi.youlai.tech
+# 上传文件接口地址
+VITE_APP_UPLOAD_URL = 'http://192.168.101.4:9000'
 # 开发接口地址
-# VITE_APP_API_URL = 'http://192.168.101.4:8078'
-VITE_APP_API_URL = 'http://192.168.101.51:8078'
+ VITE_APP_API_URL = 'http://192.168.101.4:7104'
+
 
 # 是否启用 Mock 服务
 VITE_MOCK_DEV_SERVER = false

+ 3 - 1
.env.production

@@ -1,6 +1,8 @@
 ## 生产环境
 NODE_ENV='production'
 
+# 上传文件接口地址
+VITE_APP_UPLOAD_URL = ''
 # 代理前缀
-VITE_APP_BASE_API = '/prod-api'
+VITE_APP_BASE_API = '/mes-server'
 

+ 39 - 0
docker/Dockerfile

@@ -0,0 +1,39 @@
+FROM nginx
+MAINTAINER jgiot@163.com
+RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
+VOLUME /tmp
+ENV LANG en_US.UTF-8
+RUN echo "server {  \
+                      listen       80; \
+                      client_max_body_size 100m;\
+                      location   /mes-server/ { \
+                      proxy_pass              http://192.168.101.4:7104/; \
+                      proxy_redirect          off; \
+                      proxy_set_header        Host mes-server; \
+                      proxy_set_header        X-Real-IP \$remote_addr; \
+                      proxy_set_header        X-Forwarded-For \$proxy_add_x_forwarded_for; \
+                  } \
+                  location  /jgfile/ { \
+                              proxy_pass          http://192.168.101.4:9000/jgfile/; \
+                              proxy_redirect      off; \
+                              proxy_set_header    Host jgfile; \
+                              proxy_set_header    X-Real-IP \$remote_addr; \
+                              proxy_set_header    X-Forwarded-For \$proxy_add_x_forwarded_for; \
+                      } \
+                  #解决Router(mode: 'history')模式下,刷新路由地址不能找到页面的问题 \
+                  location / { \
+                     root   /var/www/html/; \
+                      index  index.html index.htm; \
+                      if (!-e \$request_filename) { \
+                          rewrite ^(.*)\$ /index.html?s=\$1 last; \
+                          break; \
+                      } \
+                  } \
+                  access_log  /var/log/nginx/access.log ; \
+              } " > /etc/nginx/conf.d/default.conf \
+    &&  mkdir  -p  /var/www \
+    &&  mkdir -p /var/www/html
+
+ADD dist/ /var/www/html/
+EXPOSE 80
+EXPOSE 443

+ 1 - 1
package.json

@@ -7,7 +7,6 @@
     "preinstall": "npx only-allow pnpm",
     "dev": "vite serve --mode development",
     "build:prod": "vite build --mode production && vue-tsc --noEmit",
-    "prepare": "husky",
     "lint:eslint": "eslint  --fix --ext .ts,.js,.vue ./src ",
     "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
     "lint:stylelint": "stylelint  \"**/*.{css,scss,vue}\" --fix",
@@ -57,6 +56,7 @@
     "path-browserify": "^1.0.1",
     "path-to-regexp": "^6.2.1",
     "pinia": "^2.1.7",
+    "pinia-plugin-persist": "^1.0.0",
     "sockjs-client": "1.6.1",
     "sortablejs": "^1.15.2",
     "stompjs": "^2.3.3",

+ 11 - 0
src/api/auth/index.ts

@@ -44,6 +44,17 @@ export function getCaptchaApi(): AxiosPromise<CaptchaResult> {
   });
 }
 
+/**
+ * 登录成功后获取用户字典
+ */
+export function getUserDicts(data: string[]): AxiosPromise {
+  return request({
+    url: "/api/v1/sys/dictData/queryByTypes",
+    method: "post",
+    data: data,
+  });
+}
+
 export function getOrgListApi(): AxiosPromise<any[]> {
   return request({
     url: "/api/v1/sys/dept/orgList",

+ 3 - 1
src/api/auth/types.ts

@@ -21,7 +21,9 @@ export interface LoginData {
    */
   captchaCode?: string;
 
-  orgId: number;
+  orgId?: number;
+
+  token?: string;
 }
 
 /**

+ 156 - 0
src/api/system/user/index.ts

@@ -0,0 +1,156 @@
+import request from "@/utils/request";
+import { AxiosPromise } from "axios";
+import { UserForm, UserInfo, UserPageVO, UserQuery } from "./types";
+
+/**
+ * 登录成功后获取用户信息(昵称、头像、权限集合和角色集合)
+ */
+export function getUserInfoApi(): AxiosPromise {
+  return request({
+    url: "/api/auth",
+    method: "get",
+  });
+}
+
+// 新增的获取用户详细信息的API
+export function getUserDetailApi(p: object): AxiosPromise {
+  return request({
+    url: "/api/v1/sys/user/get",
+    method: "post",
+    data: p,
+  });
+}
+/**
+ * 获取用户分页列表
+ *
+ * @param queryParams
+ */
+export function getUserList(queryParams: any): AxiosPromise<any[]> {
+  return request({
+    url: "/api/v1/sys/user/list",
+    method: "post",
+    data: queryParams,
+  });
+}
+
+export function getPostOptions(queryParams?: any): AxiosPromise<OptionType[]> {
+  return request({
+    url: "/api/v1/sys/post/list",
+    method: "post",
+    data: queryParams,
+  });
+}
+
+/**
+ * 获取用户分页列表
+ *
+ * @param queryParams
+ */
+export function getUserPage(queryParams: any): AxiosPromise<PageResult<any[]>> {
+  return request({
+    url: "/api/v1/sys/user/page",
+    method: "post",
+    data: queryParams,
+  });
+}
+
+/**
+ * 添加用户
+ *
+ * @param data
+ */
+export function addUser(data: any) {
+  return request({
+    url: "/api/v1/sys/user/add",
+    method: "post",
+    data: data,
+  });
+}
+
+/**
+ * 修改用户
+ *
+ * @param id
+ * @param data
+ */
+export function updateUser(id: number, data: any) {
+  return request({
+    url: "/api/v1/sys/user/update",
+    method: "post",
+    data: { id: id, ...data },
+  });
+}
+
+/**
+ * 修改用户密码
+ *
+ * @param id
+ * @param password
+ */
+export function updateUserPassword(obj: any) {
+  return request({
+    url: "/api/v1/sys/user/resetPwd",
+    method: "post",
+    data: obj,
+  });
+}
+
+/**
+ * 删除用户
+ *
+ * @param ids
+ */
+export function deleteUsers(ids: any) {
+  return request({
+    url: "/api/v1/sys/user/del",
+    method: "post",
+    data: { id: ids },
+  });
+}
+
+/**
+ * 下载用户导入模板
+ *
+ * @returns
+ */
+export function downloadTemplateApi() {
+  return request({
+    url: "/api/v1/sys/user/template",
+    method: "get",
+    responseType: "arraybuffer",
+  });
+}
+
+/**
+ * 导出用户
+ *
+ * @param queryParams
+ * @returns
+ */
+export function exportUser(queryParams: any) {
+  return request({
+    url: "/api/v1/sys/user/export",
+    method: "post",
+    data: queryParams,
+    responseType: "arraybuffer",
+  });
+}
+
+/**
+ * 导入用户
+ *
+ * @param file
+ */
+export function importUser(deptId: number, file: File) {
+  const formData = new FormData();
+  formData.append("file", file);
+  return request({
+    url: "/api/v1/sys/user/import",
+    method: "post",
+    params: { deptId: deptId },
+    data: formData,
+    headers: {
+      "Content-Type": "multipart/form-data",
+    },
+  });
+}

+ 118 - 0
src/api/system/user/types.ts

@@ -0,0 +1,118 @@
+/**
+ * 登录用户信息
+ */
+export interface UserInfo {
+  userId?: number;
+  username?: string;
+  nickname?: string;
+  avatar?: string;
+  roles: string[];
+  perms: string[];
+  deptId?: string;
+}
+
+/**
+ * 用户查询对象类型
+ */
+export interface UserQuery extends PageQuery {
+  keywords?: string;
+  status?: number;
+  deptId?: number;
+  startTime?: string;
+  endTime?: string;
+}
+
+/**
+ * 用户分页对象
+ */
+export interface UserPageVO {
+  /**
+   * 用户头像地址
+   */
+  avatar?: string;
+  /**
+   * 创建时间
+   */
+  createTime?: Date;
+  /**
+   * 部门名称
+   */
+  deptName?: string;
+  /**
+   * 用户邮箱
+   */
+  email?: string;
+  /**
+   * 性别
+   */
+  genderLabel?: string;
+  /**
+   * 用户ID
+   */
+  id?: number;
+  /**
+   * 手机号
+   */
+  mobile?: string;
+  /**
+   * 用户昵称
+   */
+  nickname?: string;
+  /**
+   * 角色名称,多个使用英文逗号(,)分割
+   */
+  roleNames?: string;
+  /**
+   * 用户状态(1:启用;0:禁用)
+   */
+  status?: number;
+  /**
+   * 用户名
+   */
+  username?: string;
+}
+
+/**
+ * 用户表单类型
+ */
+export interface UserForm {
+  /**
+   * 用户头像
+   */
+  avatar?: string;
+  /**
+   * 部门ID
+   */
+  deptId?: number;
+  /**
+   * 邮箱
+   */
+  email?: string;
+  /**
+   * 性别
+   */
+  gender?: number;
+  /**
+   * 用户ID
+   */
+  id?: number;
+  mobile?: string;
+  /**
+   * 昵称
+   */
+  nickname?: string;
+  /**
+   * 角色ID集合
+   */
+  roleIds?: number[];
+
+  deptIds?: number[];
+  /**
+   * 用户状态(1:正常;0:禁用)
+   */
+  status?: number;
+  /**
+   * 用户名
+   */
+  username?: string;
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/api.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/cascader.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/client.svg


+ 0 - 1
src/assets/icons/close.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 36 36"><path d="m19.41 18 8.29-8.29a1 1 0 0 0-1.41-1.41L18 16.59l-8.29-8.3a1 1 0 0 0-1.42 1.42l8.3 8.29-8.3 8.29A1 1 0 1 0 9.7 27.7l8.3-8.29 8.29 8.29a1 1 0 0 0 1.41-1.41z" fill="currentColor"/></svg>

+ 0 - 1
src/assets/icons/close_all.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 36 36"><path d="M26 17H10a1 1 0 0 0 0 2h16a1 1 0 0 0 0-2z" fill="currentColor"/></svg>

+ 0 - 1
src/assets/icons/close_left.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m7 12 7 7m-7-7 7-7" stroke-linejoin="round"/><path d="M21 12H7.5"/><path d="M3 3v18" stroke-linejoin="round"/></g></svg>

+ 0 - 1
src/assets/icons/close_other.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20"><path d="M3 5h14V3H3v2zm12 8V7H5v6h10zM3 17h14v-2H3v2z" fill="currentColor"/></svg>

+ 0 - 1
src/assets/icons/close_right.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m17 12-7 7m7-7-7-7" stroke-linejoin="round"/><path d="M3 12h13.5"/><path d="M21 3v18" stroke-linejoin="round"/></g></svg>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/dict.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/document.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/download.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/edit.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/eye-open.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/eye.svg


+ 0 - 1
src/assets/icons/fullscreen-exit.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"/></svg>

+ 0 - 1
src/assets/icons/fullscreen.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 3v2H4v4H2V3h6zM2 21v-6h2v4h4v2H2zm20 0h-6v-2h4v-4h2v6zm0-12h-2V5h-4V3h6v6z"/></svg>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/github.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/homepage.svg


+ 0 - 1
src/assets/icons/indent-decrease.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 4h18v2H3V4zm0 15h18v2H3v-2zm8-5h10v2H11v-2zm0-5h10v2H11V9zm-8 3.5L7 9v7l-4-3.5z"/></svg>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/ip.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/language.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/link.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/menu.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/message.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/money.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/monitor.svg


+ 0 - 1
src/assets/icons/moon.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.98 6.98 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></svg>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/order.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/peoples.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/project.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/publish.svg


+ 0 - 1
src/assets/icons/refresh.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="1em" height="1em" viewBox="0 0 512 512"><path d="m400 148-21.12-24.57A191.43 191.43 0 0 0 240 64C134 64 48 150 48 256s86 192 192 192a192.09 192.09 0 0 0 181.07-128" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="32"/><path d="M464 68.45V220a4 4 0 0 1-4 4H308.45a4 4 0 0 1-2.83-6.83L457.17 65.62a4 4 0 0 1 6.83 2.83z" fill="currentColor"/></svg>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/role.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/security.svg


+ 0 - 1
src/assets/icons/setting.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="m12 1 9.5 5.5v11L12 23l-9.5-5.5v-11L12 1zm0 2.311L4.5 7.653v8.694l7.5 4.342 7.5-4.342V7.653L12 3.311zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>

+ 0 - 1
src/assets/icons/size.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10 6v15H8V6H2V4h14v2h-6zm8 8v7h-2v-7h-3v-2h8v2h-3z"/></svg>

+ 0 - 1
src/assets/icons/sunny.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85 1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></svg>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/system.svg


+ 0 - 1
src/assets/icons/table.svg

@@ -1 +0,0 @@
-<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M0 64v896h1024V64H0zm384 576V448h256v192H384zm256 64v192H384V704h256zm0-512v192H384V192h256zm-320 0v192H64V192h256zM64 448h256v192H64V448zm640 0h256v192H704V448zm0-64V192h256v192H704zM64 704h256v192H64V704zm640 192V704h256v192H704z"/></svg>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/todolist.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/tree.svg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
src/assets/icons/visit.svg


+ 122 - 16
src/hooks/userCrud.ts

@@ -3,10 +3,14 @@ import { PageOption } from "@smallwei/avue";
 import { ElMessageBox, ElMessage } from "element-plus";
 import { useUserStoreHook } from "@/store/modules/user";
 import { checkPerm } from "@/directive/permission";
+import { configs } from "@typescript-eslint/eslint-plugin";
 
 interface UseCrudConfig {
   // 模块的url,用来进行增删改查
   src?: string;
+
+  dataListUrl?: string;
+
   // 需要操作的数据
   row?: any;
   // done用于结束操作
@@ -15,7 +19,7 @@ interface UseCrudConfig {
   index?: number;
   // 用于中断操作
   loading?: () => void;
-  // 查询参数 一般用search的值就可以了
+  // 额外查询参数 一般用search的值就可以了
   params?: object;
   // 是否是编辑,如果是编辑调用更新,否则调用新增
   isEdit?: boolean;
@@ -23,15 +27,19 @@ interface UseCrudConfig {
 
 export const useCrud = (config?: UseCrudConfig) => {
   const url = ref(config?.src);
-
+  const commonConfig = ref(config);
   /** 表格配置属性 */
   const option = ref({
     searchIcon: true,
+    // searchSpan: 4,
     searchIndex: 3, //searchIcon是否启用功能按钮, searchIndex配置收缩展示的个数,默认为2个
     index: true, //是否显示第几项
+    indexLabel: "序号",
+    indexWidth: "55px",
     refreshBtn: false,
     border: true,
     viewBtn: true,
+    tip: false, //选中的提示
   });
   const data = ref<any>([]); //表格数据
   const form = ref({}); //新增或者编辑弹出的表单绑定值
@@ -40,7 +48,7 @@ export const useCrud = (config?: UseCrudConfig) => {
 
   /** 表格的分页数据 v-model */
   const page = ref<PageOption>({
-    total: 12220,
+    total: 0,
     currentPage: 1,
     pageSize: 10,
   });
@@ -50,25 +58,24 @@ export const useCrud = (config?: UseCrudConfig) => {
   const toDeleteIds = ref<Array<string>>([]);
 
   const save = async (config?: UseCrudConfig) => {
-    const path = config?.isEdit ? "/update" : "/add";
     try {
-      const res = await request({
+      const path = config?.isEdit ? "/update" : "/add";
+
+      const res = (await request({
         url: `${url.value}${path}`,
         method: "post",
         data: form.value,
-      });
-      if (res?.data?.code == 200) {
+      })) as any;
+      if (res?.code == 200) {
         Methords.dataList();
         config?.done && config?.done();
+        ElMessage.success(res?.msg ?? "");
       } else {
-        ElMessage.error(res?.data?.msg ?? "");
+        config?.loading && config?.loading();
+        ElMessage.error(res?.msg ?? "");
       }
     } catch (err) {
-      ElMessage.error("Oops, this is a error message.");
-      // config?.loading && config?.loading();
-    } finally {
-      ElMessage.error("Oops, this is a error message.");
-      // config?.done && config?.done();
+      config?.loading && config?.loading();
     }
   };
 
@@ -95,17 +102,61 @@ export const useCrud = (config?: UseCrudConfig) => {
       handleSearchData();
       try {
         const res = await request({
+          url: commonConfig.value.dataListUrl ?? `${url.value}/page`,
+          method: "post",
+          data: {
+            pageNo: page.value.currentPage,
+            pageSize: page.value.pageSize,
+            ...search.value,
+            ...(commonConfig.value?.params ?? {}),
+          },
+        });
+        if (res?.data) {
+          if (res?.data instanceof Array) {
+            data.value = res?.data || [];
+            page.value.total = res?.data?.length || 0;
+          } else {
+            data.value = res?.data?.records || [];
+            page.value.total = res?.data?.totalCount || 0;
+          }
+        }
+        config?.done && config?.done();
+      } catch (err) {
+        config?.loading && config?.loading();
+      } finally {
+        config?.done && config?.done();
+      }
+    },
+    dataEditList: async (config?: UseCrudConfig) => {
+      handleSearchData();
+      try {
+        const res = await request({
           url: `${url.value}/page`,
           method: "post",
           data: {
             pageNo: page.value.currentPage,
             pageSize: page.value.pageSize,
             ...search.value,
+            ...(commonConfig.value?.params ?? {}),
           },
         });
         if (res?.data) {
-          data.value = res?.data?.records || [];
-          page.value.total = res?.data?.totalCount || 0;
+          if(res?.data instanceof Array){
+            data.value = res?.data || []
+            page.value.total = res?.data?.length || 0
+          }else{
+            data.value = res?.data?.records || [];
+            for (let i = 0; i < data.value.length; i++) {
+              data.value[i].$cellEdit = true;
+              if(data.value[i].children!=undefined&&data.value[i].children!=null&&data.value[i].children.length > 0 ){
+                for(let j=0;j < data.value[i].children.length; j++){
+                  data.value[i].children[j].$cellEdit = true;
+                }
+              }
+            }
+            page.value.total = res?.data?.totalCount || 0;
+          }
+
         }
         config?.done && config?.done();
       } catch (err) {
@@ -114,7 +165,27 @@ export const useCrud = (config?: UseCrudConfig) => {
         config?.done && config?.done();
       }
     },
+    dataNoPageList: async (config?: UseCrudConfig) => {
+      handleSearchData();
+      try {
+        const res = await request({
+          url: `${url.value}/list`,
+          method: "post",
+          data: {
+            ...search.value,
+          },
+        });
 
+        if (res?.data) {
+          data.value = res?.data || [];
+        }
+        config?.done && config?.done();
+      } catch (err) {
+        config?.loading && config?.loading();
+      } finally {
+        config?.done && config?.done();
+      }
+    },
     createRow: (row: any, done: () => void, loading: () => void) => {
       save({ row: row, done: done, loading: loading });
     },
@@ -140,11 +211,15 @@ export const useCrud = (config?: UseCrudConfig) => {
         cancelButtonText: "取消",
         type: "warning",
       }).then(async () => {
+        if(row.children && row.children.length > 0 ){
+          ElMessage.error("请先解绑下级关系")
+          return
+        }
         try {
           const res = await request({
             url: `${url.value}/del`,
             method: "post",
-            data: { id: row.id ?? "" },
+            data: row,
           });
           Methords.dataList();
           config?.done && config?.done();
@@ -190,6 +265,35 @@ export const useCrud = (config?: UseCrudConfig) => {
     },
 
     /**
+     *  表格拖拽后批量保存
+     * */
+    multipleUpdate: async () => {
+      try {
+        // 由于数据带有$开头的属性,所以需要处理下,改为只传id和sortNum。
+        const dtosArray: { id: string; sortNum: number }[] = [];
+        for (let i = 0; i < data.value.length; i++) {
+          let cur = page.value.currentPage ?? 1;
+          cur = cur - 1;
+          const size = page.value.pageSize ?? 10;
+          let sortNum = cur * size;
+          sortNum = sortNum + i;
+
+          dtosArray.push({ id: data.value[i].id, sortNum: sortNum });
+        }
+        const res = await request({
+          url: `${url.value}/batch-update`,
+          method: "post",
+          data: dtosArray,
+        });
+        Methords.dataList();
+        config?.done && config?.done();
+      } catch (err) {
+        config?.loading && config?.loading();
+      } finally {
+        config?.done && config?.done();
+      }
+    },
+    /**
      * 点击搜索按钮触发
      */
     searchChange: async (params: any, done: () => void) => {
@@ -261,6 +365,7 @@ export const useCrud = (config?: UseCrudConfig) => {
      * 根据搜索项导出数据
      */
     exportData: async (urlStr: string) => {
+      handleSearchData();
       const response = await request({
         url: urlStr,
         method: "post",
@@ -281,5 +386,6 @@ export const useCrud = (config?: UseCrudConfig) => {
     toDeleteIds,
     Methords,
     Utils,
+    commonConfig,
   };
 };

+ 22 - 2
src/plugins/permission.ts

@@ -1,11 +1,14 @@
 import router from "@/router";
 import { useUserStore } from "@/store/modules/user";
 import { usePermissionStore } from "@/store/modules/permission";
+import { useDictionaryStore } from "@/store/modules/dictionary";
+
 import NProgress from "@/utils/nprogress";
+import { getUserDicts } from "@/api/auth";
 
 export function setupPermission() {
   // 白名单路由
-  const whiteList = ["/login"];
+  const whiteList = [""];
 
   router.beforeEach(async (to, from, next) => {
     NProgress.start();
@@ -16,6 +19,17 @@ export function setupPermission() {
         next({ path: "/" });
         NProgress.done();
       } else {
+        const dictStore = useDictionaryStore();
+        if (
+          !dictStore.dicts.value ||
+          JSON.stringify(dictStore.dicts.value) === "{}"
+        ) {
+          const res = await getUserDicts(dictStore.types);
+          if (res.data) {
+            dictStore.dicts = res?.data;
+          }
+        }
+
         const userStore = useUserStore();
         // const hasRoles =
         //   userStore.user.roles && userStore.user.roles.length > 0;
@@ -49,9 +63,15 @@ export function setupPermission() {
     } else {
       // 未登录可以访问白名单页面
       if (whiteList.indexOf(to.path) !== -1) {
+        const dictStore = useDictionaryStore();
+        dictStore.checkDicts();
         next();
       } else {
-        next(`/login?redirect=${to.path}`);
+        if (to.path === "/login") {
+          next();
+        } else {
+          next(`/login`);
+        }
         NProgress.done();
       }
     }

+ 18 - 84
src/router/index.ts

@@ -1,4 +1,4 @@
-import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
+import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
 
 export const Layout = () => import("@/layout/index.vue");
 
@@ -22,100 +22,34 @@ export const constantRoutes: RouteRecordRaw[] = [
     meta: { hidden: true },
   },
 
-  // {
-  //   path: "/",
-  //   name: "/",
-  //   component: Layout,
-  //   redirect: "/dashboard",
-  //   children: [
-  //     {
-  //       path: "dashboard",
-  //       component: () => import("@/views/dashboard/index.vue"),
-  //       name: "Dashboard", // 用于 keep-alive, 必须与SFC自动推导或者显示声明的组件name一致
-  //       // https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude
-  //       meta: {
-  //         title: "dashboard",
-  //         icon: "homepage",
-  //         affix: true,
-  //         keepAlive: true,
-  //         alwaysShow: false,
-  //       },
-  //     },
-  //     {
-  //       path: "401",
-  //       component: () => import("@/views/error-page/401.vue"),
-  //       meta: { hidden: true },
-  //     },
-  //     {
-  //       path: "404",
-  //       component: () => import("@/views/error-page/404.vue"),
-  //       meta: { hidden: true },
-  //     },
-  //   ],
-  // },
+  {
+    path: "/",
+    name: "/",
+    meta: { hidden: true },
+    component: Layout,
+    redirect: "/welcome",
+    children: [
+      {
+        path: "welcome",
+        component: () => import("@/views/welcome/index.vue"),
+        name: "Welcome",
+        meta: { hidden: true },
+      },
+    ],
+  },
 
   {
     path: "/:pathMatch(.*)*", // 必备
+    meta: { hidden: true },
     component: () => import("@/views/error-page/404.vue"),
   },
-  // 外部链接
-  // {
-  //   path: "/external-link",
-  //   component: Layout,
-  //   children: [ {
-  //       component: () => import("@/views/external-link/index.vue"),
-  //       path: "https://www.cnblogs.com/haoxianrui/",
-  //       meta: { title: "外部链接", icon: "link" },
-  //     },
-  //   ],
-  // },
-  // 多级嵌套路由
-  /* {
-         path: '/nested',
-         component: Layout,
-         redirect: '/nested/level1/level2',
-         name: 'Nested',
-         meta: {title: '多级菜单', icon: 'nested'},
-         children: [
-             {
-                 path: 'level1',
-                 component: () => import('@/views/nested/level1/index.vue'),
-                 name: 'Level1',
-                 meta: {title: '菜单一级'},
-                 redirect: '/nested/level1/level2',
-                 children: [
-                     {
-                         path: 'level2',
-                         component: () => import('@/views/nested/level1/level2/index.vue'),
-                         name: 'Level2',
-                         meta: {title: '菜单二级'},
-                         redirect: '/nested/level1/level2/level3',
-                         children: [
-                             {
-                                 path: 'level3-1',
-                                 component: () => import('@/views/nested/level1/level2/level3/index1.vue'),
-                                 name: 'Level3-1',
-                                 meta: {title: '菜单三级-1'}
-                             },
-                             {
-                                 path: 'level3-2',
-                                 component: () => import('@/views/nested/level1/level2/level3/index2.vue'),
-                                 name: 'Level3-2',
-                                 meta: {title: '菜单三级-2'}
-                             }
-                         ]
-                     }
-                 ]
-             },
-         ]
-     }*/
 ];
 
 /**
  * 创建路由
  */
 const router = createRouter({
-  history: createWebHashHistory(),
+  history: createWebHistory(),
   routes: constantRoutes,
   // 刷新时,滚动条位置还原
   scrollBehavior: () => ({ left: 0, top: 0 }),

+ 3 - 0
src/store/index.ts

@@ -1,7 +1,9 @@
 import type { App } from "vue";
 import { createPinia } from "pinia";
+import piniaPersist from "pinia-plugin-persist";
 
 const store = createPinia();
+store.use(piniaPersist);
 
 // 全局注册 store
 export function setupStore(app: App<Element>) {
@@ -14,4 +16,5 @@ export * from "./modules/settings";
 export * from "./modules/tagsView";
 export * from "./modules/user";
 export * from "./modules/common";
+export * from "./modules/dictionary";
 export { store };

+ 1 - 2
src/store/modules/common.ts

@@ -4,8 +4,7 @@ import { defineStore } from "pinia";
 export const useCommonStore = defineStore("commonStore", {
   state: () => ({
     // 弹出公共Table的弹窗
-    isShowTable: false,
-    tableType: 1,
+    tableType: "MARTERIAL", //改变可以展示不同的数据
     tableTitle: "",
   }),
 });

+ 46 - 0
src/store/modules/dictionary.ts

@@ -0,0 +1,46 @@
+import { store } from "@/store";
+import { defineStore } from "pinia";
+import { getUserDicts } from "@/api/auth";
+
+export const useDictionaryStore = defineStore("dictionaryStore", () => {
+  const types = [
+    "station_type",
+    "station_operate_type",
+    "applicable_platforms",
+    "material_properties",
+    "quality_testing_plan",
+    "material_level",
+    "packaging_method",
+    "quality_grade",
+    "selection_type",
+    "device_type",
+    "stage",
+    "danwei_type",
+    "process_type",
+    "workshop_section",
+    "skill_requirements",
+    "accessories_type",
+    "trace_type",
+    "skill_type",
+    "drawing_type",
+    "vehicle_level",
+    "fault_current_state",
+    "escalation_fault_state",
+    "defect_mana",
+    "disposal_measures_type",
+    "process_check_result",
+    "excel_type",
+    "excel_states",
+  ];
+  const dicts = ref<{ [key: string]: any[] }>({});
+
+  return {
+    types,
+    dicts,
+  };
+});
+
+export function useDictionaryStoreHook() {
+  // console.log('dicts:',useDictionaryStore(store))
+  return useDictionaryStore(store);
+}

+ 1 - 1
src/store/modules/permission.ts

@@ -1,7 +1,7 @@
 import { RouteRecordRaw } from "vue-router";
 import { constantRoutes } from "@/router";
 import { store } from "@/store";
-import { listRoutes } from "@/api/menu";
+import { listRoutes } from "@/api/system/menu";
 
 const modules = import.meta.glob("../../views/**/**.vue");
 const Layout = () => import("@/layout/index.vue");

+ 1 - 1
src/store/modules/settings.ts

@@ -21,7 +21,7 @@ export const useSettingsStore = defineStore("setting", () => {
     defaultSettings.fixedHeader
   );
   // 布局模式:left-左侧模式(默认) top-顶部模式 mix-混合模式
-  const layout = useStorage<string>("layout", defaultSettings.layout);
+  const layout = "mix"; //useStorage<string>("layout", defaultSettings.layout);
   // 主题颜色
   const themeColor = useStorage<string>(
     "themeColor",

+ 3 - 2
src/store/modules/user.ts

@@ -1,10 +1,10 @@
 import { loginApi, logoutApi } from "@/api/auth";
-import { getUserInfoApi } from "@/api/user";
+import { getUserInfoApi } from "@/api/system/user";
 import { resetRouter } from "@/router";
 import { store } from "@/store";
 
 import { LoginData } from "@/api/auth/types";
-import { UserInfo } from "@/api/user/types";
+import { UserInfo } from "@/api/system/user/types";
 
 export const useUserStore = defineStore("user", () => {
   const user = ref<UserInfo>({
@@ -48,6 +48,7 @@ export const useUserStore = defineStore("user", () => {
           user.value.username = data.userName;
           user.value.roles = data.roles;
           user.value.deptId = data.deptId;
+          user.value.avatar = data.avatar;
 
           isGetAuth.value = true;
 

+ 61 - 0
src/utils/axios.ts

@@ -0,0 +1,61 @@
+import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
+import { useUserStoreHook } from "@/store/modules/user";
+
+// 请求拦截器
+axios.interceptors.request.use(
+  (config: InternalAxiosRequestConfig) => {
+    console.log("请求拦截了");
+    const accessToken = localStorage.getItem("token");
+    if (accessToken) {
+      config.headers.Authorization = accessToken;
+    }
+    return config;
+  },
+  (error: any) => {
+    return Promise.reject(error);
+  }
+);
+
+// 响应拦截器
+axios.interceptors.response.use(
+  (response: AxiosResponse) => {
+    console.log("ddddddddd");
+    const { code, msg } = response.data;
+    console.log("响应拦截了", JSON.stringify(response.data));
+
+    if (code === "200") {
+      return response.data;
+    }
+    // 响应数据为二进制流处理(Excel导出)
+    if (response.data instanceof ArrayBuffer) {
+      return response;
+    }
+
+    // token 过期,重新登录
+    if (code === "4106") {
+      ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+      }).then(() => {
+        const userStore = useUserStoreHook();
+        userStore.resetToken().then(() => {
+          location.reload();
+        });
+      });
+    } else {
+      ElMessage.error(msg || "系统出错");
+    }
+
+    return Promise.reject(new Error(msg || "Error"));
+  },
+  (error: any) => {
+    console.log("aaaaaa");
+    if (error.response.data) {
+      const { code, msg } = error.response.data;
+    }
+    return Promise.reject(error.message);
+  }
+);
+
+export default axios;

+ 66 - 0
src/utils/common.ts

@@ -0,0 +1,66 @@
+import html2canvas from "html2canvas";
+import printJS from "print-js";
+
+/**
+ * html转图片
+ * @param printContent 传入一个ref
+ * @param callback
+ */
+export const htmlToCanvas = (
+  printContent: HTMLElement,
+  callback: (url: string) => void
+) => {
+  // // 获取dom 宽度 高度
+  // const width = printContent.clientWidth;
+  // const height = printContent.clientHeight;
+  // // 创建一个canvas节点
+  // const canvas = document.createElement("canvas");
+  //
+  // const scale = 1; // 定义任意放大倍数,支持小数;越大,图片清晰度越高,生成图片越慢。
+  // canvas.width = width * scale; // 定义canvas 宽度 * 缩放
+  // canvas.height = height * scale; // 定义canvas高度 *缩放
+  // canvas.style.width = width * scale + "px";
+  // canvas.style.height = height * scale + "px";
+  // canvas.getContext("2d").scale(scale, scale); // 获取context,设置scale
+  //
+  // const scrollTop =
+  //   document.documentElement.scrollTop || document.body.scrollTop; // 获取滚动轴滚动的长度
+  // const scrollLeft =
+  //   document.documentElement.scrollLeft || document.body.scrollLeft; // 获取水平滚动轴的长度
+
+  html2canvas(printContent)
+    .then((canvas) => {
+      const url = canvas.toDataURL("image/png");
+      callback(url);
+    })
+    .catch((err) => {
+      console.error(err);
+    });
+};
+
+/**
+ * 用printJs打印图片
+ * @param url
+ * @param callback
+ */
+export const printImg = (url: string) => {
+  printJS({
+    printable: url,
+    type: "image",
+    documentTitle: "", // 标题
+    style: "@page{size:auto;margin: 1cm ;}", // 去除页眉页脚
+  });
+};
+
+/**
+ * html转图片打印
+ * @param dom
+ * @param callback
+ */
+export const html2CanvasPrint = (dom: HTMLElement, callback?: () => {}) => {
+  //1、html转图片
+  htmlToCanvas(dom, (url: string) => {
+    //2、打印图片
+    printImg(url);
+  });
+};

+ 15 - 16
src/utils/request.ts

@@ -35,28 +35,27 @@ service.interceptors.response.use(
       return response;
     }
 
-    ElMessage.error(msg || "系统出错");
+    // token 过期,重新登录
+    if (code === "4106") {
+      ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+      }).then(() => {
+        const userStore = useUserStoreHook();
+        userStore.resetToken().then(() => {
+          location.reload();
+        });
+      });
+    } else {
+      ElMessage.error(msg || "系统出错");
+    }
 
     return Promise.reject(new Error(msg || "Error"));
   },
   (error: any) => {
     if (error.response.data) {
       const { code, msg } = error.response.data;
-      // token 过期,重新登录
-      if (code === "A0230") {
-        ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning",
-        }).then(() => {
-          const userStore = useUserStoreHook();
-          userStore.resetToken().then(() => {
-            location.reload();
-          });
-        });
-      } else {
-        ElMessage.error(msg || "系统出错");
-      }
     }
     return Promise.reject(error.message);
   }

+ 202 - 0
src/views/welcome/components/BarChart.vue

@@ -0,0 +1,202 @@
+<!--  线 + 柱混合图 -->
+<template>
+  <el-card>
+    <template #header>
+      <div class="title">
+        产量柱状图
+        <el-tooltip effect="dark" content="点击试试下载" placement="bottom">
+          <i-ep-download class="download" @click="downloadEchart" />
+        </el-tooltip>
+      </div>
+    </template>
+
+    <div :id="id" :class="className" :style="{ height, width }"></div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import * as echarts from "echarts";
+
+const props = defineProps({
+  id: {
+    type: String,
+    default: "barChart",
+  },
+  className: {
+    type: String,
+    default: "",
+  },
+  width: {
+    type: String,
+    default: "200px",
+    required: true,
+  },
+  height: {
+    type: String,
+    default: "200px",
+    required: true,
+  },
+});
+
+const options = {
+  grid: {
+    left: "2%",
+    right: "2%",
+    bottom: "10%",
+    containLabel: true,
+  },
+  tooltip: {
+    trigger: "axis",
+    axisPointer: {
+      type: "cross",
+      crossStyle: {
+        color: "#999",
+      },
+    },
+  },
+  legend: {
+    x: "center",
+    y: "bottom",
+    data: ["收入", "毛利润", "收入增长率", "利润增长率"],
+    textStyle: {
+      color: "#999",
+    },
+  },
+  xAxis: [
+    {
+      type: "category",
+      data: ["1月", "2月", "3月", "4月", "5月","6月", "7月","8月", "9月","10月", "11月","12月"],
+      axisPointer: {
+        type: "shadow",
+      },
+    },
+  ],
+  yAxis: [
+    {
+      type: "value",
+      min: 0,
+      max: 10000,
+      interval: 2000,
+      axisLabel: {
+        formatter: "{value} ",
+      },
+    },
+    {
+      type: "value",
+      min: 0,
+      max: 100,
+      interval: 20,
+      axisLabel: {
+        formatter: "{value}%",
+      },
+    },
+  ],
+  series: [
+    {
+      name: "收入",
+      type: "bar",
+      data: [5000, 7100, 7200, 7300, 6000,7800, 3500, 7000, 7000, 7500,7500, 7100],
+      barWidth: 20,
+      itemStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: "#83bff6" },
+          { offset: 0.5, color: "#188df0" },
+          { offset: 1, color: "#188df0" },
+        ]),
+      },
+    },
+    {
+      name: "毛利润",
+      type: "bar",
+      data: [5100, 7200, 7300, 7800, 6100,7100, 3200, 7100, 7200, 7100,7200, 7200],
+      barWidth: 20,
+      itemStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: "#25d73c" },
+          { offset: 0.5, color: "#1bc23d" },
+          { offset: 1, color: "#179e61" },
+        ]),
+      },
+    },
+    {
+      name: "收入增长率",
+      type: "line",
+      yAxisIndex: 1,
+      data: [60, 65, 70, 75, 80,60, 65, 70, 75, 80,23,23],
+      itemStyle: {
+        color: "#67C23A",
+      },
+    },
+    {
+      name: "利润增长率",
+      type: "line",
+      yAxisIndex: 1,
+      data: [70, 75, 80, 85, 90,65, 70, 75, 80,60, 65, 70],
+      itemStyle: {
+        color: "#409EFF",
+      },
+    },
+  ],
+};
+
+const downloadEchart = () => {
+  // 获取画布图表地址信息
+  const img = new Image();
+  img.src = chart.value.getDataURL({
+    type: "png",
+    pixelRatio: 1,
+    backgroundColor: "#fff",
+  });
+  // 当图片加载完成后,生成 URL 并下载
+  img.onload = () => {
+    const canvas = document.createElement("canvas");
+    canvas.width = img.width;
+    canvas.height = img.height;
+    const ctx = canvas.getContext("2d");
+    if (ctx) {
+      ctx.drawImage(img, 0, 0, img.width, img.height);
+      const link = document.createElement("a");
+      link.download = `业绩柱状图.png`;
+      link.href = canvas.toDataURL("image/png", 0.9);
+      document.body.appendChild(link);
+      link.click();
+      link.remove();
+    }
+  };
+};
+
+const chart = ref<any>("");
+onMounted(() => {
+  // 图表初始化
+  chart.value = markRaw(
+    echarts.init(document.getElementById(props.id) as HTMLDivElement)
+  );
+
+  chart.value.setOption(options);
+
+  // 大小自适应
+  window.addEventListener("resize", () => {
+    chart.value.resize();
+  });
+});
+
+onActivated(() => {
+  if (chart.value) {
+    chart.value.resize();
+  }
+});
+</script>
+<style lang="scss" scoped>
+.title {
+  display: flex;
+  justify-content: space-between;
+
+  .download {
+    cursor: pointer;
+
+    &:hover {
+      color: #409eff;
+    }
+  }
+}
+</style>

+ 115 - 0
src/views/welcome/components/FunnelChart.vue

@@ -0,0 +1,115 @@
+<!-- 漏斗图 -->
+<template>
+  <div :id="id" :class="className" :style="{ height, width }"></div>
+</template>
+
+<script setup lang="ts">
+import * as echarts from "echarts";
+
+const props = defineProps({
+  id: {
+    type: String,
+    default: "funnelChart",
+  },
+  className: {
+    type: String,
+    default: "",
+  },
+  width: {
+    type: String,
+    default: "200px",
+    required: true,
+  },
+  height: {
+    type: String,
+    default: "200px",
+    required: true,
+  },
+});
+
+const options = {
+  title: {
+    show: true,
+    text: "订单线索转化漏斗图",
+    x: "center",
+    padding: 15,
+    textStyle: {
+      fontSize: 18,
+      fontStyle: "normal",
+      fontWeight: "bold",
+      color: "#337ecc",
+    },
+  },
+  grid: {
+    left: "2%",
+    right: "2%",
+    bottom: "10%",
+    containLabel: true,
+  },
+  legend: {
+    x: "center",
+    y: "bottom",
+    data: ["Show", "Click", "Visit", "Inquiry", "Order"],
+  },
+
+  series: [
+    {
+      name: "Funnel",
+      type: "funnel",
+      left: "20%",
+      top: 60,
+      bottom: 60,
+      width: "60%",
+      sort: "descending",
+      gap: 2,
+      label: {
+        show: true,
+        position: "inside",
+      },
+      labelLine: {
+        length: 10,
+        lineStyle: {
+          width: 1,
+          type: "solid",
+        },
+      },
+      itemStyle: {
+        borderColor: "#fff",
+        borderWidth: 1,
+      },
+      emphasis: {
+        label: {
+          fontSize: 20,
+        },
+      },
+      data: [
+        { value: 60, name: "Visit" },
+        { value: 40, name: "Inquiry" },
+        { value: 20, name: "Order" },
+        { value: 80, name: "Click" },
+        { value: 100, name: "Show" },
+      ],
+    },
+  ],
+};
+
+const chart = ref<any>("");
+
+onMounted(() => {
+  chart.value = markRaw(
+    echarts.init(document.getElementById(props.id) as HTMLDivElement)
+  );
+
+  chart.value.setOption(options);
+
+  window.addEventListener("resize", () => {
+    chart.value.resize();
+  });
+});
+
+onActivated(() => {
+  if (chart.value) {
+    chart.value.resize();
+  }
+});
+</script>

+ 89 - 0
src/views/welcome/components/PieChart.vue

@@ -0,0 +1,89 @@
+<!-- 饼图 -->
+<template>
+  <el-card>
+    <template #header> 产品分类饼图 </template>
+    <div :id="id" :class="className" :style="{ height, width }"></div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import * as echarts from "echarts";
+
+const props = defineProps({
+  id: {
+    type: String,
+    default: "pieChart",
+  },
+  className: {
+    type: String,
+    default: "",
+  },
+  width: {
+    type: String,
+    default: "200px",
+    required: true,
+  },
+  height: {
+    type: String,
+    default: "200px",
+    required: true,
+  },
+});
+const options = {
+  grid: {
+    left: "2%",
+    right: "2%",
+    bottom: "10%",
+    containLabel: true,
+  },
+  legend: {
+    top: "bottom",
+    textStyle: {
+      color: "#999",
+    },
+  },
+  series: [
+    {
+      name: "Nightingale Chart",
+      type: "pie",
+      radius: [50, 130],
+      center: ["50%", "50%"],
+      roseType: "area",
+      itemStyle: {
+        borderRadius: 1,
+        color: function (params: any) {
+          //自定义颜色
+          const colorList = ["#409EFF", "#67C23A", "#E6A23C", "#F56C6C"];
+          return colorList[params.dataIndex];
+        },
+      },
+      data: [
+        { value: 26, name: "工单" },
+        { value: 27, name: "工位" },
+        { value: 24, name: "订单" },
+        { value: 23, name: "报故单" },
+      ],
+    },
+  ],
+};
+
+const chart = ref<any>("");
+
+onMounted(() => {
+  chart.value = markRaw(
+    echarts.init(document.getElementById(props.id) as HTMLDivElement)
+  );
+
+  chart.value.setOption(options);
+
+  window.addEventListener("resize", () => {
+    chart.value.resize();
+  });
+});
+
+onActivated(() => {
+  if (chart.value) {
+    chart.value.resize();
+  }
+});
+</script>

+ 109 - 0
src/views/welcome/components/RadarChart.vue

@@ -0,0 +1,109 @@
+<!-- 雷达图 -->
+<template>
+  <el-card>
+    <template #header> 订单状态雷达图 </template>
+    <div :id="id" :class="className" :style="{ height, width }"></div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import * as echarts from "echarts";
+
+const props = defineProps({
+  id: {
+    type: String,
+    default: "radarChart",
+  },
+  className: {
+    type: String,
+    default: "",
+  },
+  width: {
+    type: String,
+    default: "200px",
+    required: true,
+  },
+  height: {
+    type: String,
+    default: "200px",
+    required: true,
+  },
+});
+
+const options = {
+  grid: {
+    left: "2%",
+    right: "2%",
+    bottom: "10%",
+    containLabel: true,
+  },
+  legend: {
+    x: "center",
+    y: "bottom",
+    data: ["预定数量", "下单数量", "发货数量"],
+    textStyle: {
+      color: "#999",
+    },
+  },
+  radar: {
+    // shape: 'circle',
+    radius: "60%",
+    indicator: [
+      { name: "家用电器" },
+      { name: "服装箱包" },
+      { name: "运动户外" },
+      { name: "手机数码" },
+      { name: "汽车用品" },
+      { name: "家具厨具" },
+    ],
+  },
+  series: [
+    {
+      name: "Budget vs spending",
+      type: "radar",
+      itemStyle: {
+        borderRadius: 6,
+        color: function (params: any) {
+          //自定义颜色
+          const colorList = ["#409EFF", "#67C23A", "#E6A23C", "#F56C6C"];
+          return colorList[params.dataIndex];
+        },
+      },
+      data: [
+        {
+          value: [400, 400, 400, 400, 400, 400],
+          name: "预定数量",
+        },
+        {
+          value: [300, 300, 300, 300, 300, 300],
+          name: "下单数量",
+        },
+        {
+          value: [200, 200, 200, 200, 200, 200],
+          name: "发货数量",
+        },
+      ],
+    },
+  ],
+};
+
+const chart = ref<any>("");
+
+onMounted(() => {
+  chart.value = markRaw(
+    echarts.init(document.getElementById(props.id) as HTMLDivElement)
+  );
+
+  chart.value.setOption(options);
+
+  window.addEventListener("resize", () => {
+    chart.value.resize();
+  });
+});
+
+onActivated(() => {
+  if (chart.value) {
+    chart.value.resize();
+  }
+});
+</script>

+ 292 - 0
src/views/welcome/index.vue

@@ -0,0 +1,292 @@
+<template>
+  <div class="dashboard-container">
+    <!-- github角标 -->
+    <!--    <github-corner class="github-corner" />-->
+
+    <el-card shadow="never">
+      <el-row justify="space-between">
+        <el-col :span="18" :xs="24">
+          <div class="flex h-full items-center">
+            <img
+              class="w-20 h-20 mr-5 rounded-full"
+              :src="userStore.user.avatar + '?imageView2/1/w/80/h/80'"
+            />
+            <div>
+              <p>{{ greetings }}</p>
+              <p class="text-sm text-gray">
+                今日天气晴朗,气温在15℃至25℃之间,东南风。
+              </p>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="6" :xs="24">
+          <div class="flex h-full items-center justify-around">
+            <el-statistic :value="99">
+              <template #title>
+                <div class="flex items-center">
+                  <svg-icon icon-class="message" size="20px" />
+                  <span class="text-[16px] ml-1">消息</span>
+                </div>
+              </template>
+            </el-statistic>
+
+            <el-statistic :value="50">
+              <template #title>
+                <div class="flex items-center">
+                  <svg-icon icon-class="todolist" size="20px" />
+                  <span class="text-[16px] ml-1">待办</span>
+                </div>
+              </template>
+              <template #suffix>/100</template>
+            </el-statistic>
+
+            <el-statistic :value="10">
+              <template #title>
+                <div class="flex items-center">
+                  <svg-icon icon-class="project" size="20px" />
+                  <span class="text-[16px] ml-1">项目</span>
+                </div>
+              </template>
+            </el-statistic>
+          </div>
+        </el-col>
+      </el-row>
+    </el-card>
+
+    <!-- 数据卡片 -->
+    <el-row :gutter="10" class="mt-3">
+      <el-col :xs="24" :sm="12" :lg="6">
+        <el-card shadow="never">
+          <template #header>
+            <div class="flex items-center justify-between">
+              <span class="text-[var(--el-text-color-secondary)]">访客数</span>
+              <el-tag type="success">日</el-tag>
+            </div>
+          </template>
+
+          <div class="flex items-center justify-between mt-5">
+            <div class="text-lg text-right">
+              {{ Math.round(visitCountOutput) }}
+            </div>
+            <svg-icon icon-class="visit" size="2em" />
+          </div>
+
+          <div
+            class="flex items-center justify-between mt-5 text-sm text-[var(--el-text-color-secondary)]"
+          >
+            <span> 总访客数 </span>
+            <span> {{ Math.round(visitCountOutput * 15) }} </span>
+          </div>
+        </el-card>
+      </el-col>
+
+      <!--消息数-->
+      <el-col :xs="24" :sm="12" :lg="6">
+        <el-card shadow="never">
+          <template #header>
+            <div class="flex items-center justify-between">
+              <span class="text-[var(--el-text-color-secondary)]">IP数</span>
+              <el-tag type="success">日</el-tag>
+            </div>
+          </template>
+
+          <div class="flex items-center justify-between mt-5">
+            <div class="text-lg text-right">
+              {{ Math.round(dauCountOutput) }}
+            </div>
+            <svg-icon icon-class="ip" size="2em" />
+          </div>
+
+          <div
+            class="flex items-center justify-between mt-5 text-sm text-[var(--el-text-color-secondary)]"
+          >
+            <span> 总IP数 </span>
+            <span> {{ Math.round(dauCountOutput) }} </span>
+          </div>
+        </el-card>
+      </el-col>
+
+      <!--销售额-->
+      <el-col :xs="24" :sm="12" :lg="6">
+        <el-card shadow="never">
+          <template #header>
+            <div class="flex items-center justify-between">
+              <span class="text-[var(--el-text-color-secondary)]">产品数</span>
+              <el-tag>月</el-tag>
+            </div>
+          </template>
+
+          <div class="flex items-center justify-between mt-5">
+            <div class="text-lg text-right">
+              {{ Math.round(amountOutput) }}
+            </div>
+            <svg-icon icon-class="money" size="2em" />
+          </div>
+
+          <div
+            class="flex items-center justify-between mt-5 text-sm text-[var(--el-text-color-secondary)]"
+          >
+            <span> 总产品数 </span>
+            <span> {{ Math.round(amountOutput * 15) }} </span>
+          </div>
+        </el-card>
+      </el-col>
+
+      <!--订单量-->
+      <el-col :xs="24" :sm="12" :lg="6">
+        <el-card shadow="never">
+          <template #header>
+            <div class="flex items-center justify-between">
+              <span class="text-[var(--el-text-color-secondary)]">订单量</span>
+              <el-tag type="danger">季</el-tag>
+            </div>
+          </template>
+
+          <div class="flex items-center justify-between mt-5">
+            <div class="text-lg text-right">
+              {{ Math.round(orderCountOutput) }}
+            </div>
+            <svg-icon icon-class="order" size="2em" />
+          </div>
+
+          <div
+            class="flex items-center justify-between mt-5 text-sm text-[var(--el-text-color-secondary)]"
+          >
+            <span> 总订单量 </span>
+            <span> {{ Math.round(orderCountOutput * 15) }} </span>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- Echarts 图表 -->
+    <el-row :gutter="10" class="mt-3">
+      <el-col :sm="24" :lg="8" class="mb-2">
+        <BarChart
+          id="barChart"
+          height="400px"
+          width="100%"
+          class="bg-[var(--el-bg-color-overlay)]"
+        />
+      </el-col>
+
+      <el-col :xs="24" :sm="12" :lg="8" class="mb-2">
+        <PieChart
+          id="pieChart"
+          height="400px"
+          width="100%"
+          class="bg-[var(--el-bg-color-overlay)]"
+        />
+      </el-col>
+
+      <el-col :xs="24" :sm="12" :lg="8" class="mb-2">
+        <RadarChart
+          id="radarChart"
+          height="400px"
+          width="100%"
+          class="bg-[var(--el-bg-color-overlay)]"
+        />
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+
+defineOptions({
+  name: "Dashboard",
+  inheritAttrs: false,
+});
+
+import { useUserStore } from "@/store/modules/user";
+import { useTransition, TransitionPresets } from "@vueuse/core";
+
+const userStore = useUserStore();
+const date: Date = new Date();
+
+const greetings = computed?.(() => {
+  const hours = date.getHours();
+  if (hours >= 6 && hours < 8) {
+    return "晨起披衣出草堂,轩窗已自喜微凉🌅!";
+  } else if (hours >= 8 && hours < 12) {
+    return "上午好!";
+  } else if (hours >= 12 && hours < 18) {
+    return "下午好!";
+  } else if (hours >= 18 && hours < 24) {
+    return "晚上好!";
+  } else if (hours >= 0 && hours < 6) {
+    return "偷偷向银河要了一把碎星,只等你闭上眼睛撒入你的梦中,晚安🌛!";
+  }
+});
+
+const duration = 5000;
+
+// 销售额
+const amount = ref(0);
+const amountOutput = useTransition(amount, {
+  duration: duration,
+  transition: TransitionPresets.easeOutExpo,
+});
+amount.value = 2000;
+
+// 访客数
+const visitCount = ref(0);
+const visitCountOutput = useTransition(visitCount, {
+  duration: duration,
+  transition: TransitionPresets.easeOutExpo,
+});
+visitCount.value = 2000;
+
+// IP数
+const dauCount = ref(0);
+const dauCountOutput = useTransition(dauCount, {
+  duration: duration,
+  transition: TransitionPresets.easeOutExpo,
+});
+dauCount.value = 2000;
+
+// 订单量
+const orderCount = ref(0);
+const orderCountOutput = useTransition(orderCount, {
+  duration: duration,
+  transition: TransitionPresets.easeOutExpo,
+});
+orderCount.value = 2000;
+</script>
+
+<style lang="scss" scoped>
+.dashboard-container {
+  position: relative;
+  padding: 24px;
+
+  .user-avatar {
+    width: 40px;
+    height: 40px;
+    border-radius: 50%;
+  }
+
+  .github-corner {
+    position: absolute;
+    top: 0;
+    right: 0;
+    z-index: 1;
+    border: 0;
+  }
+
+  .data-box {
+    display: flex;
+    justify-content: space-between;
+    padding: 20px;
+    font-weight: bold;
+    color: var(--el-text-color-regular);
+    background: var(--el-bg-color-overlay);
+    border-color: var(--el-border-color);
+    box-shadow: var(--el-box-shadow-dark);
+  }
+
+  .svg-icon {
+    fill: currentcolor !important;
+  }
+}
+</style>