Browse Source

feature: 卡务中心-卡片操作 错扣补款界面样式
支付管理->账户管理 重置卡类有有效期无法打开界面错误

autumnal_wind@yeah.net 1 month ago
parent
commit
99dd1d945c

+ 229 - 0
src/api/system/right/user/index.ts

@@ -0,0 +1,229 @@
1
+import { DeptVO } from './../dept/types';
2
+import { RoleVO } from '@/api/system/role/types';
3
+import request from '@/utils/request';
4
+import { AxiosPromise } from 'axios';
5
+import { UserForm, UserQuery, UserVO, UserInfoVO } from './types';
6
+import { parseStrEmpty } from '@/utils/ruoyi';
7
+
8
+/**
9
+ * 查询用户列表
10
+ * @param query
11
+ */
12
+export const listUser = (query: UserQuery): AxiosPromise<UserVO[]> => {
13
+  return request({
14
+    url: '/system/user/list',
15
+    method: 'get',
16
+    params: query
17
+  });
18
+};
19
+
20
+/**
21
+ * 通过用户ids查询用户
22
+ * @param userIds
23
+ */
24
+export const optionSelect = (userIds: (number | string)[]): AxiosPromise<UserVO[]> => {
25
+  return request({
26
+    url: '/system/user/optionselect?userIds=' + userIds,
27
+    method: 'get'
28
+  });
29
+};
30
+
31
+/**
32
+ * 获取用户详情
33
+ * @param userId
34
+ */
35
+export const getUser = (userId?: string | number): AxiosPromise<UserInfoVO> => {
36
+  return request({
37
+    url: '/system/user/' + parseStrEmpty(userId),
38
+    method: 'get'
39
+  });
40
+};
41
+
42
+/**
43
+ * 新增用户
44
+ */
45
+export const addUser = (data: UserForm) => {
46
+  return request({
47
+    url: '/system/user',
48
+    method: 'post',
49
+    data: data
50
+  });
51
+};
52
+
53
+/**
54
+ * 修改用户
55
+ */
56
+export const updateUser = (data: UserForm) => {
57
+  return request({
58
+    url: '/system/user',
59
+    method: 'put',
60
+    data: data
61
+  });
62
+};
63
+
64
+/**
65
+ * 删除用户
66
+ * @param userId 用户ID
67
+ */
68
+export const delUser = (userId: Array<string | number> | string | number) => {
69
+  return request({
70
+    url: '/system/user/' + userId,
71
+    method: 'delete'
72
+  });
73
+};
74
+
75
+/**
76
+ * 用户密码重置
77
+ * @param userId 用户ID
78
+ * @param password 密码
79
+ */
80
+export const resetUserPwd = (userId: string | number, password: string) => {
81
+  const data = {
82
+    userId,
83
+    password
84
+  };
85
+  return request({
86
+    url: '/system/user/resetPwd',
87
+    method: 'put',
88
+    headers: {
89
+      isEncrypt: true,
90
+      repeatSubmit: false
91
+    },
92
+    data: data
93
+  });
94
+};
95
+
96
+/**
97
+ * 用户状态修改
98
+ * @param userId 用户ID
99
+ * @param status 用户状态
100
+ */
101
+export const changeUserStatus = (userId: number | string, status: string) => {
102
+  const data = {
103
+    userId,
104
+    status
105
+  };
106
+  return request({
107
+    url: '/system/user/changeStatus',
108
+    method: 'put',
109
+    data: data
110
+  });
111
+};
112
+
113
+/**
114
+ * 查询用户个人信息
115
+ */
116
+export const getUserProfile = (): AxiosPromise<UserInfoVO> => {
117
+  return request({
118
+    url: '/system/user/profile',
119
+    method: 'get'
120
+  });
121
+};
122
+
123
+/**
124
+ * 修改用户个人信息
125
+ * @param data 用户信息
126
+ */
127
+export const updateUserProfile = (data: UserForm) => {
128
+  return request({
129
+    url: '/system/user/profile',
130
+    method: 'put',
131
+    data: data
132
+  });
133
+};
134
+
135
+/**
136
+ * 用户密码重置
137
+ * @param oldPassword 旧密码
138
+ * @param newPassword 新密码
139
+ */
140
+export const updateUserPwd = (oldPassword: string, newPassword: string) => {
141
+  const data = {
142
+    oldPassword,
143
+    newPassword
144
+  };
145
+  return request({
146
+    url: '/system/user/profile/updatePwd',
147
+    method: 'put',
148
+    headers: {
149
+      isEncrypt: true,
150
+      repeatSubmit: false
151
+    },
152
+    data: data
153
+  });
154
+};
155
+
156
+/**
157
+ * 用户头像上传
158
+ * @param data 头像文件
159
+ */
160
+export const uploadAvatar = (data: FormData) => {
161
+  return request({
162
+    url: '/system/user/profile/avatar',
163
+    method: 'post',
164
+    data: data
165
+  });
166
+};
167
+
168
+/**
169
+ * 查询授权角色
170
+ * @param userId 用户ID
171
+ */
172
+export const getAuthRole = (userId: string | number): AxiosPromise<{ user: UserVO; roles: RoleVO[] }> => {
173
+  return request({
174
+    url: '/system/user/authRole/' + userId,
175
+    method: 'get'
176
+  });
177
+};
178
+
179
+/**
180
+ * 保存授权角色
181
+ * @param data 用户ID
182
+ */
183
+export const updateAuthRole = (data: { userId: string; roleIds: string }) => {
184
+  return request({
185
+    url: '/system/user/authRole',
186
+    method: 'put',
187
+    params: data
188
+  });
189
+};
190
+
191
+/**
192
+ * 查询当前部门的所有用户信息
193
+ * @param deptId
194
+ */
195
+export const listUserByDeptId = (deptId: string | number): AxiosPromise<UserVO[]> => {
196
+  return request({
197
+    url: '/system/user/list/dept/' + deptId,
198
+    method: 'get'
199
+  });
200
+};
201
+
202
+/**
203
+ * 查询部门下拉树结构
204
+ */
205
+export const deptTreeSelect = (): AxiosPromise<DeptVO[]> => {
206
+  return request({
207
+    url: '/system/user/deptTree',
208
+    method: 'get'
209
+  });
210
+};
211
+
212
+export default {
213
+  listUser,
214
+  getUser,
215
+  optionSelect,
216
+  addUser,
217
+  updateUser,
218
+  delUser,
219
+  resetUserPwd,
220
+  changeUserStatus,
221
+  getUserProfile,
222
+  updateUserProfile,
223
+  updateUserPwd,
224
+  uploadAvatar,
225
+  getAuthRole,
226
+  updateAuthRole,
227
+  deptTreeSelect,
228
+  listUserByDeptId
229
+};

+ 89 - 0
src/api/system/right/user/types.ts

@@ -0,0 +1,89 @@
1
+import { RoleVO } from '@/api/system/role/types';
2
+import { PostVO } from '@/api/system/params/post/types';
3
+
4
+/**
5
+ * 用户信息
6
+ */
7
+export interface UserInfo {
8
+  user: UserVO;
9
+  roles: string[];
10
+  permissions: string[];
11
+}
12
+
13
+/**
14
+ * 用户查询对象类型
15
+ */
16
+export interface UserQuery extends PageQuery {
17
+  userName?: string;
18
+  realName?: string;
19
+  phone?: string;
20
+  status?: string;
21
+  deptId?: string | number;
22
+  roleId?: string | number;
23
+}
24
+
25
+/**
26
+ * 用户返回对象
27
+ */
28
+export interface UserVO extends BaseEntity {
29
+  userId: string | number;
30
+  tenantId: string;
31
+  deptId: number;
32
+  userName: string;
33
+  nickName: string;
34
+  realName: string;
35
+  userType: string;
36
+  email: string;
37
+  phone: string;
38
+  sex: string;
39
+  avatar: string;
40
+  status: string;
41
+  delFlag: string;
42
+  loginIp: string;
43
+  loginDate: string;
44
+  remark: string;
45
+  deptName: string;
46
+  roles: RoleVO[];
47
+  roleIds: any;
48
+  postIds: any;
49
+  roleId: any;
50
+  admin: boolean;
51
+}
52
+
53
+/**
54
+ * 用户表单类型
55
+ */
56
+export interface UserForm {
57
+  userId?: string;
58
+  deptId?: number;
59
+  postId?: number;
60
+  userName?: string;
61
+  userNumb?: string;
62
+  nickName?: string;
63
+  realName?: string;
64
+  loginPwd?: string;
65
+  phone?: string;
66
+  email?: string;
67
+  sex?: string;
68
+  category?: string;
69
+  status: string;
70
+  remark?: string;
71
+  postIds: string[];
72
+  roleIds: string[];
73
+}
74
+
75
+export interface UserInfoVO {
76
+  user: UserVO;
77
+  roles: RoleVO[];
78
+  roleIds: string[];
79
+  posts: PostVO[];
80
+  postIds: string[];
81
+  roleGroup: string;
82
+  postGroup: string;
83
+}
84
+
85
+export interface ResetPwdForm {
86
+  oldPassword: string;
87
+  newPassword: string;
88
+  confirmPassword: string;
89
+}

+ 166 - 0
src/views/system/right/user/UserDetailForm.vue

@@ -0,0 +1,166 @@
1
+<template>
2
+  <div class="e-dialog">
3
+  </div>
4
+</template>
5
+
6
+<script setup name="UserDetailForm" lang="ts">
7
+/**  模块导入 */
8
+import { UserForm } from '@/api/system/right/user/types';
9
+import api from '@/api/system/right/user';
10
+
11
+/** 当前组件属性 */
12
+defineOptions({ name: 'RecompenseForm' });
13
+// 国际化
14
+const { t } = useI18n();
15
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
16
+// 当前页面引用的数据字典
17
+// 对话框
18
+const dialog = reactive<DialogOption>({
19
+  visible: false,
20
+  title: '',
21
+  draggable: true
22
+});
23
+// 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
24
+const formLoading = ref(false);
25
+// 表单的类型:create - 新增;update - 修改
26
+const formType = ref('');
27
+// 操作类型
28
+const command = ref('');
29
+// 表单引用
30
+const formRef = ref<ElFormInstance>();
31
+// 表单数据
32
+const formData = ref<UserForm>({
33
+  userId: undefined,
34
+  deptId: undefined,
35
+  postId: undefined,
36
+  userName: '',
37
+  nickName: undefined,
38
+  realName: undefined,
39
+  userNumb: '',
40
+  loginPwd: '',
41
+  category: undefined,
42
+  phone: undefined,
43
+  email: undefined,
44
+  sex: undefined,
45
+  status: '0',
46
+  remark: '',
47
+  postIds: [],
48
+  roleIds: []
49
+});
50
+const formRules = reactive({
51
+  deptId: [{ required: true, message: '部门名称间不能为空', trigger: 'blur' }],
52
+  postId: [{ required: true, message: '岗位名称不能为空', trigger: 'blur' }],
53
+  userName: [{ required: true, message: '登录账号不能为空', trigger: 'blur' }],
54
+  realName: [{ required: true, message: '用户姓名不能为空', trigger: 'blur' }],
55
+  userNumb: [{ required: true, message: '学/工号不能为空', trigger: 'blur' }],
56
+  loginPwd: [{ required: true, message: '登录密码不能为空', trigger: 'blur' }],
57
+  category: [{ required: true, message: '用户身份不能为空', trigger: 'blur' }]
58
+  // operatorMoney: [
59
+  //   { required: true, message: '补款金额不能为空', trigger: 'blur' },
60
+  //   { pattern: /^\d+(\.\d{1,2})?$/, message: '补款金额只能为两位小数的数字', trigger: 'blur' }
61
+  // ]
62
+});
63
+/** 当前组件方法 */
64
+const open = async (cmd: string, id?: string | number) => {
65
+  dialog.visible = true;
66
+  dialog.title = t('action.' + cmd);
67
+  formType.value = cmd;
68
+  command.value = cmd;
69
+  resetForm();
70
+  // 修改时,设置数据
71
+  if (id) {
72
+    formLoading.value = true;
73
+    try {
74
+      const res = await api.getUser(id);
75
+      Object.assign(formData.value, res.data);
76
+    } finally {
77
+      formLoading.value = false;
78
+    }
79
+  }
80
+};
81
+// 重置表单
82
+const resetForm = () => {
83
+  formData.value = {
84
+    userId: undefined,
85
+    deptId: undefined,
86
+    postId: undefined,
87
+    userName: '',
88
+    nickName: undefined,
89
+    realName: undefined,
90
+    userNumb: '',
91
+    loginPwd: '',
92
+    category: undefined,
93
+    phone: undefined,
94
+    email: undefined,
95
+    sex: undefined,
96
+    status: '0',
97
+    remark: '',
98
+    postIds: [],
99
+    roleIds: []
100
+  };
101
+  formRef.value?.resetFields();
102
+};
103
+// 传回给父组件的属性与方法
104
+defineExpose({});
105
+
106
+// 触发父组件的事件
107
+const emits = defineEmits([]);
108
+
109
+// 初始化
110
+</script>
111
+
112
+<style scoped lang="scss">
113
+.e-dialog {
114
+  .slide-fade-enter-active {
115
+    transition: all 0.3s ease;
116
+  }
117
+
118
+  .slide-fade-leave-active {
119
+    transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
120
+  }
121
+
122
+  .slide-fade-enter,
123
+  .slide-fade-leave-to {
124
+    opacity: 0;
125
+  }
126
+}
127
+
128
+.card-header {
129
+  color: #1c84c6;
130
+  font-size: medium;
131
+}
132
+
133
+.consume-select-header {
134
+  color: #1c84c6;
135
+  font-size: medium;
136
+}
137
+
138
+:deep() .el-dialog {
139
+  border-radius: 10px;
140
+}
141
+
142
+:deep() .el-dialog__body {
143
+  padding: 1px 1px !important;
144
+  max-height: 90vh !important;
145
+}
146
+
147
+:deep() .el-dialog__header {
148
+  background: #1d78d5;
149
+  border-radius: 6px 6px 5px 5px;
150
+}
151
+
152
+:deep() .el-dialog__title {
153
+  color: white;
154
+  margin-top: 1px;
155
+}
156
+
157
+:deep() .el-dialog__headerbtn .el-dialog__close {
158
+  color: white !important;
159
+  top: 15px;
160
+  right: 10px;
161
+}
162
+
163
+:deep() .el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
164
+  padding: 1px !important;
165
+}
166
+</style>

+ 139 - 0
src/views/system/right/user/authRole.vue

@@ -0,0 +1,139 @@
1
+<template>
2
+  <div class="p-2">
3
+    <div class="panel">
4
+      <h4 class="panel-title">基本信息</h4>
5
+      <el-form :model="form" :inline="true">
6
+        <el-row :gutter="10">
7
+          <el-col :span="2.5">
8
+            <el-form-item label="用户昵称" prop="nickName">
9
+              <el-input v-model="form.nickName" disabled />
10
+            </el-form-item>
11
+          </el-col>
12
+          <el-col :span="2.5">
13
+            <el-form-item label="登录账号" prop="userName">
14
+              <el-input v-model="form.userName" disabled />
15
+            </el-form-item>
16
+          </el-col>
17
+        </el-row>
18
+      </el-form>
19
+    </div>
20
+    <div class="panel">
21
+      <h4 class="panel-title">角色信息</h4>
22
+      <div>
23
+        <el-table
24
+          ref="tableRef"
25
+          v-loading="loading"
26
+          :row-key="getRowKey"
27
+          :data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)"
28
+          @row-click="clickRow"
29
+          @selection-change="handleSelectionChange"
30
+        >
31
+          <el-table-column label="序号" width="55" type="index" align="center">
32
+            <template #default="scope">
33
+              <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
34
+            </template>
35
+          </el-table-column>
36
+          <el-table-column type="selection" :reserve-selection="true" width="55"></el-table-column>
37
+          <el-table-column label="角色编号" align="center" prop="roleId" />
38
+          <el-table-column label="角色名称" align="center" prop="roleName" />
39
+          <el-table-column label="权限字符" align="center" prop="roleKey" />
40
+          <el-table-column label="创建时间" align="center" prop="createTime" width="180">
41
+            <template #default="scope">
42
+              <span>{{ parseTime(scope.row.createTime) }}</span>
43
+            </template>
44
+          </el-table-column>
45
+        </el-table>
46
+        <pagination v-show="total > 0" v-model:page="pageNum" v-model:limit="pageSize" :total="total" />
47
+        <div style="text-align: center; margin-left: -120px; margin-top: 30px">
48
+          <el-button type="primary" @click="submitForm()">提交</el-button>
49
+          <el-button @click="close()">返回</el-button>
50
+        </div>
51
+        <div></div>
52
+      </div>
53
+    </div>
54
+  </div>
55
+</template>
56
+
57
+<script setup name="AuthRole" lang="ts">
58
+import { RoleVO } from '@/api/system/role/types';
59
+import { getAuthRole, updateAuthRole } from '@/api/system/user';
60
+import { UserForm } from '@/api/system/user/types';
61
+import { RouteLocationNormalized } from 'vue-router';
62
+import { parseTime } from '@/utils/ruoyi';
63
+
64
+const route = useRoute();
65
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
66
+
67
+const loading = ref(true);
68
+const total = ref(0);
69
+const pageNum = ref(1);
70
+const pageSize = ref(10);
71
+const roleIds = ref<Array<string | number>>([]);
72
+const roles = ref<RoleVO[]>([]);
73
+const form = ref<Partial<UserForm>>({
74
+  nickName: undefined,
75
+  userName: '',
76
+  userId: undefined
77
+});
78
+
79
+const tableRef = ref<ElTableInstance>();
80
+
81
+/** 单击选中行数据 */
82
+const clickRow = (row: RoleVO) => {
83
+  // ele的方法有问题,selected应该为可选参数
84
+  tableRef.value?.toggleRowSelection(row, false);
85
+};
86
+/** 多选框选中数据 */
87
+const handleSelectionChange = (selection: RoleVO[]) => {
88
+  roleIds.value = selection.map((item) => item.roleId);
89
+};
90
+/** 保存选中的数据编号 */
91
+const getRowKey = (row: RoleVO): string => {
92
+  return String(row.roleId);
93
+};
94
+/** 关闭按钮 */
95
+const close = () => {
96
+  const obj: RouteLocationNormalized = {
97
+    fullPath: '',
98
+    hash: '',
99
+    matched: [],
100
+    meta: undefined,
101
+    name: undefined,
102
+    params: undefined,
103
+    query: undefined,
104
+    redirectedFrom: undefined,
105
+    path: '/system/right/user'
106
+  };
107
+  proxy?.$tab.closeOpenPage(obj);
108
+};
109
+/** 提交按钮 */
110
+const submitForm = async () => {
111
+  const userId = form.value.userId;
112
+  const rIds = roleIds.value.join(',');
113
+  await updateAuthRole({ userId: userId as string, roleIds: rIds });
114
+  proxy?.$modal.msgSuccess('授权成功');
115
+  close();
116
+};
117
+
118
+const getList = async () => {
119
+  const userId = route.params && route.params.userId;
120
+  if (userId) {
121
+    loading.value = true;
122
+    const res = await getAuthRole(userId as string);
123
+    Object.assign(form.value, res.data.user);
124
+    Object.assign(roles.value, res.data.roles);
125
+    total.value = roles.value.length;
126
+    await nextTick(() => {
127
+      roles.value.forEach((row) => {
128
+        if (row?.flag) {
129
+          tableRef.value?.toggleRowSelection(row, true);
130
+        }
131
+      });
132
+    });
133
+    loading.value = false;
134
+  }
135
+};
136
+onMounted(() => {
137
+  getList();
138
+});
139
+</script>

+ 547 - 0
src/views/system/right/user/index.vue

@@ -0,0 +1,547 @@
1
+<template>
2
+  <div class="p-2 auto-overflow-y">
3
+    <el-row :gutter="20">
4
+      <!-- 部门树-->
5
+      <el-col :lg="4" :xs="24" style="">
6
+        <el-card shadow="hover" class="h-800px">
7
+          <el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
8
+          <el-tree
9
+            ref="deptTreeRef"
10
+            class="mt-2 w-full h-800px overflow-auto inline-block"
11
+            node-key="id"
12
+            :data="deptOptions"
13
+            :props="{ label: 'label', children: 'children' }"
14
+            :expand-on-click-node="false"
15
+            :filter-node-method="filterNode"
16
+            highlight-current
17
+            default-expand-all
18
+            @node-click="handleNodeClick"
19
+          />
20
+        </el-card>
21
+      </el-col>
22
+      <el-col :lg="20" :xs="24">
23
+        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
24
+          <div v-show="showSearch" class="mb-[5px]">
25
+            <el-card shadow="hover">
26
+              <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto" class="-mb-25px -mt-5px">
27
+                <el-form-item label="用户姓名" prop="realName">
28
+                  <el-input v-model="queryParams.realName" placeholder="请输入用户姓名" clearable @keyup.enter="handleQuery" />
29
+                </el-form-item>
30
+                <el-form-item label="手机号码" prop="phone">
31
+                  <el-input v-model="queryParams.phone" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
32
+                </el-form-item>
33
+                <el-form-item>
34
+                  <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
35
+                  <el-button icon="Refresh" @click="resetQuery">重置</el-button>
36
+                </el-form-item>
37
+              </el-form>
38
+            </el-card>
39
+          </div>
40
+        </transition>
41
+        <el-card shadow="hover">
42
+          <template #header>
43
+            <el-row :gutter="10">
44
+              <el-col :span="1.5">
45
+                <el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
46
+              </el-col>
47
+              <el-col :span="1.5">
48
+                <el-button v-has-permi="['system:user:add']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">
49
+                  修改
50
+                </el-button>
51
+              </el-col>
52
+              <el-col :span="1.5">
53
+                <el-button v-has-permi="['system:user:delete']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">
54
+                  删除
55
+                </el-button>
56
+              </el-col>
57
+              <el-col :span="1.5">
58
+                <el-dropdown class="mt-[1px]">
59
+                  <el-button plain type="info">
60
+                    更多
61
+                    <el-icon class="el-icon--right"><arrow-down /></el-icon
62
+                  ></el-button>
63
+                  <template #dropdown>
64
+                    <el-dropdown-menu>
65
+                      <el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item>
66
+                      <el-dropdown-item icon="Top" @click="handleImport"> 导入数据</el-dropdown-item>
67
+                      <el-dropdown-item icon="Download" @click="handleExport"> 导出数据</el-dropdown-item>
68
+                    </el-dropdown-menu>
69
+                  </template>
70
+                </el-dropdown>
71
+              </el-col>
72
+              <right-toolbar v-model:showSearch="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
73
+            </el-row>
74
+          </template>
75
+          <el-table v-loading="loading" :data="userList" height="calc(100vh - 22rem)" @selection-change="handleSelectionChange">
76
+            <el-table-column type="selection" width="50" align="center" />
77
+            <el-table-column v-if="columns[0].visible" key="userId" label="用户编号" align="center" prop="userId" />
78
+            <el-table-column v-if="columns[1].visible" key="userName" label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" />
79
+            <el-table-column v-if="columns[2].visible" key="nickName" label="用户昵称" align="center" prop="nickName" :show-overflow-tooltip="true" />
80
+            <el-table-column v-if="columns[3].visible" key="deptName" label="部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
81
+            <el-table-column v-if="columns[4].visible" key="phonenumber" label="手机号码" align="center" prop="phonenumber" width="120" />
82
+            <el-table-column v-if="columns[5].visible" key="status" label="状态" align="center">
83
+              <template #default="scope">
84
+                <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
85
+              </template>
86
+            </el-table-column>
87
+
88
+            <el-table-column v-if="columns[6].visible" label="创建时间" align="center" prop="createTime" width="160">
89
+              <template #default="scope">
90
+                <span>{{ scope.row.createTime }}</span>
91
+              </template>
92
+            </el-table-column>
93
+
94
+            <el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
95
+              <template #default="scope">
96
+                <el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top">
97
+                  <el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
98
+                </el-tooltip>
99
+                <el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top">
100
+                  <el-button v-hasPermi="['system:user:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
101
+                </el-tooltip>
102
+
103
+                <el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top">
104
+                  <el-button v-hasPermi="['system:user:resetPwd']" link type="primary" icon="Key" @click="handleResetPwd(scope.row)"></el-button>
105
+                </el-tooltip>
106
+
107
+                <el-tooltip v-if="scope.row.userId !== 1" content="分配角色" placement="top">
108
+                  <el-button v-hasPermi="['system:user:edit']" link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)"></el-button>
109
+                </el-tooltip>
110
+              </template>
111
+            </el-table-column>
112
+          </el-table>
113
+          <pagination
114
+            v-show="total > 0"
115
+            v-model:page="queryParams.pageNum"
116
+            v-model:limit="queryParams.pageSize"
117
+            :total="total"
118
+            @pagination="getList"
119
+          />
120
+        </el-card>
121
+      </el-col>
122
+    </el-row>
123
+    <!-- 添加或修改用户配置对话框 -->
124
+    <user-detail-form ref="userDetailFormRef" apped-to-body @success="getList" />
125
+    <!-- 用户导入对话框 -->
126
+    <el-dialog v-model="upload.open" :title="upload.title" width="400px" append-to-body>
127
+      <el-upload
128
+        ref="uploadRef"
129
+        :limit="1"
130
+        accept=".xlsx, .xls"
131
+        :headers="upload.headers"
132
+        :action="upload.url + '?updateSupport=' + upload.updateSupport"
133
+        :disabled="upload.isUploading"
134
+        :on-progress="handleFileUploadProgress"
135
+        :on-success="handleFileSuccess"
136
+        :auto-upload="false"
137
+        drag
138
+      >
139
+        <el-icon class="el-icon--upload">
140
+          <i-ep-upload-filled />
141
+        </el-icon>
142
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
143
+        <template #tip>
144
+          <div class="text-center el-upload__tip">
145
+            <div class="el-upload__tip"><el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据</div>
146
+            <span>仅允许导入xls、xlsx格式文件。</span>
147
+            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
148
+          </div>
149
+        </template>
150
+      </el-upload>
151
+      <template #footer>
152
+        <div class="dialog-footer">
153
+          <el-button type="primary" @click="submitFileForm">确 定</el-button>
154
+          <el-button @click="upload.open = false">取 消</el-button>
155
+        </div>
156
+      </template>
157
+    </el-dialog>
158
+  </div>
159
+</template>
160
+
161
+<script setup name="User" lang="ts">
162
+import api from '@/api/system/right/user';
163
+import { UserForm, UserQuery, UserVO } from '@/api/system/right/user/types';
164
+import { DeptVO } from '@/api/system/dept/types';
165
+import { RoleVO } from '@/api/system/role/types';
166
+import { PostVO } from '@/api/system/params/post/types';
167
+import { treeselect } from '@/api/system/dept';
168
+import { globalHeaders } from '@/utils/request';
169
+import { to } from 'await-to-js';
170
+import UserDetailForm from '@/views/system/right/user/UserDetailForm.vue';
171
+
172
+const router = useRouter();
173
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
174
+const { sys_normal_disable, sys_user_sex } = toRefs<any>(proxy?.useDict('sys_normal_disable', 'sys_user_sex'));
175
+const userList = ref<UserVO[]>();
176
+const loading = ref(true);
177
+const showSearch = ref(true);
178
+const ids = ref<Array<number | string>>([]);
179
+const single = ref(true);
180
+const multiple = ref(true);
181
+const total = ref(0);
182
+const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
183
+const deptName = ref('');
184
+const deptOptions = ref<DeptVO[]>([]);
185
+const initPassword = ref<string>('');
186
+const postOptions = ref<PostVO[]>([]);
187
+const roleOptions = ref<RoleVO[]>([]);
188
+/*** 用户导入参数 */
189
+const upload = reactive<ImportOption>({
190
+  // 是否显示弹出层(用户导入)
191
+  open: false,
192
+  // 弹出层标题(用户导入)
193
+  title: '',
194
+  // 是否禁用上传
195
+  isUploading: false,
196
+  // 是否更新已经存在的用户数据
197
+  updateSupport: 0,
198
+  // 设置上传的请求头部
199
+  headers: globalHeaders(),
200
+  // 上传的地址
201
+  url: import.meta.env.VITE_APP_BASE_API + '/system/user/importData'
202
+});
203
+// 列显隐信息
204
+const columns = ref<FieldOption[]>([
205
+  { key: 0, label: `用户编号`, visible: false, children: [] },
206
+  { key: 1, label: `用户名称`, visible: true, children: [] },
207
+  { key: 2, label: `用户昵称`, visible: true, children: [] },
208
+  { key: 3, label: `部门`, visible: true, children: [] },
209
+  { key: 4, label: `手机号码`, visible: true, children: [] },
210
+  { key: 5, label: `状态`, visible: true, children: [] },
211
+  { key: 6, label: `创建时间`, visible: true, children: [] }
212
+]);
213
+const deptTreeRef = ref<ElTreeInstance>();
214
+// 查询客体ref
215
+const queryFormRef = ref<ElFormInstance>();
216
+const userFormRef = ref<ElFormInstance>();
217
+const uploadRef = ref<ElUploadInstance>();
218
+const formDialogRef = ref<ElDialogInstance>();
219
+
220
+const dialog = reactive<DialogOption>({
221
+  visible: false,
222
+  title: ''
223
+});
224
+
225
+const initFormData: UserForm = {
226
+  userId: undefined,
227
+  deptId: undefined,
228
+  userName: '',
229
+  nickName: undefined,
230
+  password: '',
231
+  phonenumber: undefined,
232
+  email: undefined,
233
+  sex: undefined,
234
+  status: '0',
235
+  remark: '',
236
+  postIds: [],
237
+  roleIds: []
238
+};
239
+
240
+const initData: PageData<UserForm, UserQuery> = {
241
+  form: { ...initFormData },
242
+  queryParams: {
243
+    pageNum: 1,
244
+    pageSize: 10,
245
+    userName: '',
246
+    realName: '',
247
+    phone: '',
248
+    status: '',
249
+    deptId: '',
250
+    roleId: ''
251
+  },
252
+  rules: {
253
+    userName: [
254
+      { required: true, message: '用户名称不能为空', trigger: 'blur' },
255
+      {
256
+        min: 2,
257
+        max: 20,
258
+        message: '用户名称长度必须介于 2 和 20 之间',
259
+        trigger: 'blur'
260
+      }
261
+    ],
262
+    nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
263
+    password: [
264
+      { required: true, message: '用户密码不能为空', trigger: 'blur' },
265
+      {
266
+        min: 5,
267
+        max: 20,
268
+        message: '用户密码长度必须介于 5 和 20 之间',
269
+        trigger: 'blur'
270
+      },
271
+      { pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\\ |', trigger: 'blur' }
272
+    ],
273
+    email: [
274
+      {
275
+        type: 'email',
276
+        message: '请输入正确的邮箱地址',
277
+        trigger: ['blur', 'change']
278
+      }
279
+    ],
280
+    phonenumber: [
281
+      {
282
+        pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
283
+        message: '请输入正确的手机号码',
284
+        trigger: 'blur'
285
+      }
286
+    ],
287
+    roleIds: [{ required: true, message: '用户角色不能为空', trigger: 'blur' }]
288
+  }
289
+};
290
+const data = reactive<PageData<UserForm, UserQuery>>(initData);
291
+
292
+const { queryParams, form, rules } = toRefs<PageData<UserForm, UserQuery>>(data);
293
+
294
+/** 通过条件过滤节点  */
295
+const filterNode = (value: string, data: any) => {
296
+  if (!value) return true;
297
+  return data.label.indexOf(value) !== -1;
298
+};
299
+/** 根据名称筛选部门树 */
300
+watchEffect(
301
+  () => {
302
+    deptTreeRef.value?.filter(deptName.value);
303
+  },
304
+  {
305
+    flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
306
+  }
307
+);
308
+
309
+/** 查询部门下拉树结构 */
310
+const getTreeSelect = async () => {
311
+  const res = await api.deptTreeSelect();
312
+  deptOptions.value = res.data;
313
+};
314
+
315
+/** 查询用户列表 */
316
+const getList = async () => {
317
+  loading.value = true;
318
+  const res = await api.listUser(proxy?.addDateRange(queryParams.value, dateRange.value));
319
+  loading.value = false;
320
+  userList.value = res.rows;
321
+  total.value = res.total;
322
+};
323
+
324
+/** 节点单击事件 */
325
+const handleNodeClick = (data: DeptVO) => {
326
+  queryParams.value.deptId = data.id;
327
+  handleQuery();
328
+};
329
+
330
+/** 搜索按钮操作 */
331
+const handleQuery = () => {
332
+  queryParams.value.pageNum = 1;
333
+  getList();
334
+};
335
+/** 重置按钮操作 */
336
+const resetQuery = () => {
337
+  dateRange.value = ['', ''];
338
+  queryFormRef.value?.resetFields();
339
+  queryParams.value.pageNum = 1;
340
+  queryParams.value.deptId = undefined;
341
+  deptTreeRef.value?.setCurrentKey(undefined);
342
+  handleQuery();
343
+};
344
+
345
+/** 删除按钮操作 */
346
+const handleDelete = async (row?: UserVO) => {
347
+  const userIds = row?.userId || ids.value;
348
+  const [err] = await to(proxy?.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?') as any);
349
+  if (!err) {
350
+    await api.delUser(userIds);
351
+    await getList();
352
+    proxy?.$modal.msgSuccess('删除成功');
353
+  }
354
+};
355
+
356
+/** 用户状态修改  */
357
+const handleStatusChange = async (row: UserVO) => {
358
+  let text = row.status === '0' ? '启用' : '停用';
359
+  try {
360
+    await proxy?.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?');
361
+    await api.changeUserStatus(row.userId, row.status);
362
+    proxy?.$modal.msgSuccess(text + '成功');
363
+  } catch (err) {
364
+    row.status = row.status === '0' ? '1' : '0';
365
+  }
366
+};
367
+/** 跳转角色分配 */
368
+const handleAuthRole = (row: UserVO) => {
369
+  const userId = row.userId;
370
+  router.push('/system/user-auth/role/' + userId);
371
+};
372
+
373
+/** 重置密码按钮操作 */
374
+const handleResetPwd = async (row: UserVO) => {
375
+  const [err, res] = await to(
376
+    ElMessageBox.prompt('请输入"' + row.userName + '"的新密码', '提示', {
377
+      confirmButtonText: '确定',
378
+      cancelButtonText: '取消',
379
+      closeOnClickModal: false,
380
+      inputPattern: /^.{5,20}$/,
381
+      inputErrorMessage: '用户密码长度必须介于 5 和 20 之间',
382
+      inputValidator: (value) => {
383
+        if (/<|>|"|'|\||\\/.test(value)) {
384
+          return '不能包含非法字符:< > " \' \\\ |';
385
+        }
386
+      }
387
+    })
388
+  );
389
+  if (!err && res) {
390
+    await api.resetUserPwd(row.userId, res.value);
391
+    proxy?.$modal.msgSuccess('修改成功,新密码是:' + res.value);
392
+  }
393
+};
394
+
395
+/** 选择条数  */
396
+const handleSelectionChange = (selection: UserVO[]) => {
397
+  ids.value = selection.map((item) => item.userId);
398
+  single.value = selection.length != 1;
399
+  multiple.value = !selection.length;
400
+};
401
+
402
+/** 导入按钮操作 */
403
+const handleImport = () => {
404
+  upload.title = '用户导入';
405
+  upload.open = true;
406
+};
407
+/** 导出按钮操作 */
408
+const handleExport = () => {
409
+  proxy?.download(
410
+    'system/user/export',
411
+    {
412
+      ...queryParams.value
413
+    },
414
+    `user_${new Date().getTime()}.xlsx`
415
+  );
416
+};
417
+/** 下载模板操作 */
418
+const importTemplate = () => {
419
+  proxy?.download('system/user/importTemplate', {}, `user_template_${new Date().getTime()}.xlsx`);
420
+};
421
+
422
+/**文件上传中处理 */
423
+const handleFileUploadProgress = () => {
424
+  upload.isUploading = true;
425
+};
426
+/** 文件上传成功处理 */
427
+const handleFileSuccess = (response: any, file: UploadFile) => {
428
+  upload.open = false;
429
+  upload.isUploading = false;
430
+  uploadRef.value?.handleRemove(file);
431
+  ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', {
432
+    dangerouslyUseHTMLString: true
433
+  });
434
+  getList();
435
+};
436
+
437
+/** 提交上传文件 */
438
+function submitFileForm() {
439
+  uploadRef.value?.submit();
440
+}
441
+
442
+/** 初始化部门数据 */
443
+const initTreeData = async () => {
444
+  // 判断部门的数据是否存在,存在不获取,不存在则获取
445
+  if (deptOptions.value === undefined) {
446
+    const { data } = await treeselect();
447
+    deptOptions.value = data;
448
+  }
449
+};
450
+
451
+/** 重置操作表单 */
452
+const reset = () => {
453
+  form.value = { ...initFormData };
454
+  userFormRef.value?.resetFields();
455
+};
456
+/** 取消按钮 */
457
+const cancel = () => {
458
+  dialog.visible = false;
459
+  reset();
460
+};
461
+
462
+/** 增加/修改按钮操作 */
463
+const userDetailFormRef = ref();
464
+const handleOpenDetail = async (cmd: string, row?: UserVO) => {
465
+  const userId = row?.userId || ids.value[0];
466
+  const realNames = row?.realName || names.value;
467
+  userDetailFormRef?.value.open(cmd, userId);
468
+};
469
+/** 新增按钮操作 */
470
+// const handleAdd = async () => {
471
+//   reset();
472
+//   const { data } = await api.getUser();
473
+//   dialog.visible = true;
474
+//   dialog.title = '新增用户';
475
+//   await initTreeData();
476
+//   postOptions.value = data.posts;
477
+//   roleOptions.value = data.roles;
478
+//   form.value.password = initPassword.value.toString();
479
+// };
480
+
481
+/** 修改按钮操作 */
482
+// const handleUpdate = async (row?: UserForm) => {
483
+//   reset();
484
+//   const userId = row?.userId || ids.value[0];
485
+//   const { data } = await api.getUser(userId);
486
+//   dialog.visible = true;
487
+//   dialog.title = '修改用户';
488
+//   await initTreeData();
489
+//   Object.assign(form.value, data.user);
490
+//   postOptions.value = data.posts;
491
+//   roleOptions.value = data.roles;
492
+//   form.value.postIds = data.postIds;
493
+//   form.value.roleIds = data.roleIds;
494
+//   form.value.password = '';
495
+// };
496
+
497
+/** 提交按钮 */
498
+const submitForm = () => {
499
+  userFormRef.value?.validate(async (valid: boolean) => {
500
+    if (valid) {
501
+      form.value.userId ? await api.updateUser(form.value) : await api.addUser(form.value);
502
+      proxy?.$modal.msgSuccess('操作成功');
503
+      dialog.visible = false;
504
+      await getList();
505
+    }
506
+  });
507
+};
508
+
509
+/**
510
+ * 关闭用户弹窗
511
+ */
512
+const closeDialog = () => {
513
+  dialog.visible = false;
514
+  resetForm();
515
+};
516
+
517
+/**
518
+ * 重置表单
519
+ */
520
+const resetForm = () => {
521
+  userFormRef.value?.resetFields();
522
+  userFormRef.value?.clearValidate();
523
+
524
+  form.value.id = undefined;
525
+  form.value.status = '1';
526
+};
527
+onMounted(() => {
528
+  getTreeSelect(); // 初始化部门数据
529
+  getList(); // 初始化列表数据
530
+  proxy?.getConfigKey('sys.user.initPassword').then((response) => {
531
+    initPassword.value = response.data;
532
+  });
533
+});
534
+
535
+async function handleDeptChange(value: number | string) {
536
+  const response = await optionselect(value);
537
+  postOptions.value = response.data;
538
+  form.value.postIds = [];
539
+}
540
+</script>
541
+
542
+<style lang="scss" scoped>
543
+.el-tree > :nth-child(n + 1) {
544
+  display: inline-block;
545
+  min-width: 100%;
546
+}
547
+</style>

+ 122 - 0
src/views/system/right/user/profile/index.vue

@@ -0,0 +1,122 @@
1
+<template>
2
+  <div class="p-2">
3
+    <el-row :gutter="20">
4
+      <el-col :span="6" :xs="24">
5
+        <el-card class="box-card">
6
+          <template #header>
7
+            <div class="clearfix">
8
+              <span>个人信息</span>
9
+            </div>
10
+          </template>
11
+          <div>
12
+            <div class="text-center">
13
+              <userAvatar />
14
+            </div>
15
+            <ul class="list-group list-group-striped">
16
+              <li class="list-group-item">
17
+                <svg-icon icon-class="user" />用户名称
18
+                <div class="pull-right">{{ state.user.userName }}</div>
19
+              </li>
20
+              <li class="list-group-item">
21
+                <svg-icon icon-class="phone" />手机号码
22
+                <div class="pull-right">{{ state.user.phonenumber }}</div>
23
+              </li>
24
+              <li class="list-group-item">
25
+                <svg-icon icon-class="email" />用户邮箱
26
+                <div class="pull-right">{{ state.user.email }}</div>
27
+              </li>
28
+              <li class="list-group-item">
29
+                <svg-icon icon-class="tree" />所属部门
30
+                <div v-if="state.user.deptName" class="pull-right">{{ state.user.deptName }} / {{ state.postGroup }}</div>
31
+              </li>
32
+              <li class="list-group-item">
33
+                <svg-icon icon-class="peoples" />所属角色
34
+                <div class="pull-right">{{ state.roleGroup }}</div>
35
+              </li>
36
+              <li class="list-group-item">
37
+                <svg-icon icon-class="date" />创建日期
38
+                <div class="pull-right">{{ state.user.createTime }}</div>
39
+              </li>
40
+            </ul>
41
+          </div>
42
+        </el-card>
43
+      </el-col>
44
+      <el-col :span="18" :xs="24">
45
+        <el-card>
46
+          <template #header>
47
+            <div class="clearfix">
48
+              <span>基本资料</span>
49
+            </div>
50
+          </template>
51
+          <el-tabs v-model="activeTab">
52
+            <el-tab-pane label="基本资料" name="userinfo">
53
+              <userInfo :user="userForm" />
54
+            </el-tab-pane>
55
+            <el-tab-pane label="修改密码" name="resetPwd">
56
+              <resetPwd />
57
+            </el-tab-pane>
58
+            <el-tab-pane label="第三方应用" name="thirdParty">
59
+              <thirdParty :auths="state.auths" />
60
+            </el-tab-pane>
61
+            <el-tab-pane label="在线设备" name="onlineDevice">
62
+              <onlineDevice :devices="state.devices" />
63
+            </el-tab-pane>
64
+          </el-tabs>
65
+        </el-card>
66
+      </el-col>
67
+    </el-row>
68
+  </div>
69
+</template>
70
+
71
+<script setup name="Profile" lang="ts">
72
+import UserAvatar from './userAvatar.vue';
73
+import UserInfo from './userInfo.vue';
74
+import ResetPwd from './resetPwd.vue';
75
+import ThirdParty from './thirdParty.vue';
76
+import OnlineDevice from './onlineDevice.vue';
77
+import { getAuthList } from '@/api/system/social/auth';
78
+import { getUserProfile } from '@/api/system/user';
79
+import { getOnline } from '@/api/monitor/online';
80
+import { UserVO } from '@/api/system/user/types';
81
+
82
+const activeTab = ref('userinfo');
83
+interface State {
84
+  user: Partial<UserVO>;
85
+  roleGroup: string;
86
+  postGroup: string;
87
+  auths: any;
88
+  devices: any;
89
+}
90
+const state = ref<State>({
91
+  user: {},
92
+  roleGroup: '',
93
+  postGroup: '',
94
+  auths: [],
95
+  devices: []
96
+});
97
+
98
+const userForm = ref({});
99
+
100
+const getUser = async () => {
101
+  const res = await getUserProfile();
102
+  state.value.user = res.data.user;
103
+  userForm.value = { ...res.data.user };
104
+  state.value.roleGroup = res.data.roleGroup;
105
+  state.value.postGroup = res.data.postGroup;
106
+};
107
+
108
+const getAuths = async () => {
109
+  const res = await getAuthList();
110
+  state.value.auths = res.data;
111
+};
112
+const getOnlines = async () => {
113
+  const res = await getOnline();
114
+  state.value.devices = res.rows;
115
+};
116
+
117
+onMounted(() => {
118
+  getUser();
119
+  getAuths();
120
+  getOnlines();
121
+});
122
+</script>

+ 57 - 0
src/views/system/right/user/profile/onlineDevice.vue

@@ -0,0 +1,57 @@
1
+<template>
2
+  <div>
3
+    <el-table :data="devices" style="width: 100%; height: 100%; font-size: 14px">
4
+      <el-table-column label="设备类型" align="center">
5
+        <template #default="scope">
6
+          <dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
7
+        </template>
8
+      </el-table-column>
9
+      <el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
10
+      <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
11
+      <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
12
+      <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
13
+      <el-table-column label="登录时间" align="center" prop="loginTime" width="180">
14
+        <template #default="scope">
15
+          <span>{{ parseTime(scope.row.loginTime) }}</span>
16
+        </template>
17
+      </el-table-column>
18
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
19
+        <template #default="scope">
20
+          <el-tooltip content="删除" placement="top">
21
+            <el-button link type="primary" icon="Delete" @click="handldDelOnline(scope.row)"> </el-button>
22
+          </el-tooltip>
23
+        </template>
24
+      </el-table-column>
25
+    </el-table>
26
+  </div>
27
+</template>
28
+
29
+<script name="Online" lang="ts" setup>
30
+import { delOnline } from '@/api/monitor/online';
31
+import { propTypes } from '@/utils/propTypes';
32
+
33
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
34
+const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type'));
35
+
36
+const props = defineProps({
37
+  devices: propTypes.any.isRequired
38
+});
39
+const devices = computed(() => props.devices);
40
+
41
+/** 删除按钮操作 */
42
+const handldDelOnline = (row: any) => {
43
+  ElMessageBox.confirm('删除设备后,在该设备登录需要重新进行验证')
44
+    .then(() => {
45
+      return delOnline(row.tokenId);
46
+    })
47
+    .then((res: any) => {
48
+      if (res.code === 200) {
49
+        proxy?.$modal.msgSuccess('删除成功');
50
+        proxy?.$tab.refreshPage();
51
+      } else {
52
+        proxy?.$modal.msgError(res.msg);
53
+      }
54
+    })
55
+    .catch(() => {});
56
+};
57
+</script>

+ 73 - 0
src/views/system/right/user/profile/resetPwd.vue

@@ -0,0 +1,73 @@
1
+<template>
2
+  <el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
3
+    <el-form-item label="旧密码" prop="oldPassword">
4
+      <el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password />
5
+    </el-form-item>
6
+    <el-form-item label="新密码" prop="newPassword">
7
+      <el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password />
8
+    </el-form-item>
9
+    <el-form-item label="确认密码" prop="confirmPassword">
10
+      <el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password />
11
+    </el-form-item>
12
+    <el-form-item>
13
+      <el-button type="primary" @click="submit">保存</el-button>
14
+      <el-button type="danger" @click="close">关闭</el-button>
15
+    </el-form-item>
16
+  </el-form>
17
+</template>
18
+
19
+<script setup lang="ts">
20
+import { updateUserPwd } from '@/api/system/user';
21
+import type { ResetPwdForm } from '@/api/system/user/types';
22
+
23
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
24
+const pwdRef = ref<ElFormInstance>();
25
+const user = ref<ResetPwdForm>({
26
+  oldPassword: '',
27
+  newPassword: '',
28
+  confirmPassword: ''
29
+});
30
+
31
+const equalToPassword = (rule: any, value: string, callback: any) => {
32
+  if (user.value.newPassword !== value) {
33
+    callback(new Error('两次输入的密码不一致'));
34
+  } else {
35
+    callback();
36
+  }
37
+};
38
+const rules = ref({
39
+  oldPassword: [{ required: true, message: '旧密码不能为空', trigger: 'blur' }],
40
+  newPassword: [
41
+    { required: true, message: '新密码不能为空', trigger: 'blur' },
42
+    {
43
+      min: 6,
44
+      max: 20,
45
+      message: '长度在 6 到 20 个字符',
46
+      trigger: 'blur'
47
+    },
48
+    { pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\\ |', trigger: 'blur' }
49
+  ],
50
+  confirmPassword: [
51
+    { required: true, message: '确认密码不能为空', trigger: 'blur' },
52
+    {
53
+      required: true,
54
+      validator: equalToPassword,
55
+      trigger: 'blur'
56
+    }
57
+  ]
58
+});
59
+
60
+/** 提交按钮 */
61
+const submit = () => {
62
+  pwdRef.value?.validate(async (valid: boolean) => {
63
+    if (valid) {
64
+      await updateUserPwd(user.value.oldPassword, user.value.newPassword);
65
+      proxy?.$modal.msgSuccess('修改成功');
66
+    }
67
+  });
68
+};
69
+/** 关闭按钮 */
70
+const close = () => {
71
+  proxy?.$tab.closePage();
72
+};
73
+</script>

+ 144 - 0
src/views/system/right/user/profile/thirdParty.vue

@@ -0,0 +1,144 @@
1
+<template>
2
+  <div>
3
+    <el-table :data="auths" style="width: 100%; height: 100%; font-size: 14px">
4
+      <el-table-column label="序号" width="50" type="index" />
5
+      <el-table-column label="绑定账号平台" width="140" align="center" prop="source" show-overflow-tooltip />
6
+      <el-table-column label="头像" width="120" align="center" prop="avatar">
7
+        <template #default="scope">
8
+          <img :src="scope.row.avatar" style="width: 45px; height: 45px" />
9
+        </template>
10
+      </el-table-column>
11
+      <el-table-column label="系统账号" width="180" align="center" prop="userName" :show-overflow-tooltip="true" />
12
+      <el-table-column label="绑定时间" width="180" align="center" prop="createTime" />
13
+      <el-table-column label="操作" width="80" align="center" class-name="small-padding fixed-width">
14
+        <template #default="scope">
15
+          <el-button size="small" type="text" @click="unlockAuth(scope.row)">解绑</el-button>
16
+        </template>
17
+      </el-table-column>
18
+    </el-table>
19
+
20
+    <div id="git-user-binding">
21
+      <h4 class="provider-desc">你可以绑定以下第三方帐号</h4>
22
+      <div id="authlist" class="user-bind">
23
+        <a class="third-app" href="#" title="使用 微信 账号授权登录" @click="authUrl('wechat')">
24
+          <div class="git-other-login-icon">
25
+            <svg-icon icon-class="wechat" />
26
+          </div>
27
+          <span class="app-name">WeiXin</span>
28
+        </a>
29
+        <a class="third-app" href="#" title="使用 MaxKey 账号授权登录" @click="authUrl('maxkey')">
30
+          <div class="git-other-login-icon">
31
+            <svg-icon icon-class="maxkey" />
32
+          </div>
33
+          <span class="app-name">MaxKey</span>
34
+        </a>
35
+        <a class="third-app" href="#" title="使用 TopIam 账号授权登录" @click="authUrl('topiam')">
36
+          <div class="git-other-login-icon">
37
+            <svg-icon icon-class="topiam" />
38
+          </div>
39
+          <span class="app-name">TopIam</span>
40
+        </a>
41
+        <a class="third-app" href="#" title="使用 Gitee 账号授权登录" @click="authUrl('gitee')">
42
+          <div class="git-other-login-icon">
43
+            <svg-icon icon-class="gitee" />
44
+          </div>
45
+          <span class="app-name">Gitee</span>
46
+        </a>
47
+        <a class="third-app" href="#" title="使用 GitHub 账号授权登录" @click="authUrl('github')">
48
+          <div class="git-other-login-icon">
49
+            <svg-icon icon-class="github" />
50
+          </div>
51
+          <span class="app-name">Github</span>
52
+        </a>
53
+      </div>
54
+    </div>
55
+  </div>
56
+</template>
57
+
58
+<script lang="ts" setup>
59
+import { authUnlock, authBinding } from '@/api/system/social/auth';
60
+import { propTypes } from '@/utils/propTypes';
61
+import useUserStore from '@/store/modules/user';
62
+
63
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
64
+
65
+const props = defineProps({
66
+  auths: propTypes.any.isRequired
67
+});
68
+const auths = computed(() => props.auths);
69
+
70
+const unlockAuth = (row: any) => {
71
+  ElMessageBox.confirm('您确定要解除"' + row.source + '"的账号绑定吗?')
72
+    .then(() => {
73
+      return authUnlock(row.id);
74
+    })
75
+    .then((res: any) => {
76
+      if (res.code === 200) {
77
+        proxy?.$modal.msgSuccess('解绑成功');
78
+        proxy?.$tab.refreshPage();
79
+      } else {
80
+        proxy?.$modal.msgError(res.msg);
81
+      }
82
+    })
83
+    .catch(() => {});
84
+};
85
+
86
+const authUrl = (source: string) => {
87
+  authBinding(source, useUserStore().tenantId).then((res: any) => {
88
+    if (res.code === 200) {
89
+      window.location.href = res.data;
90
+    } else {
91
+      proxy?.$modal.msgError(res.msg);
92
+    }
93
+  });
94
+};
95
+</script>
96
+
97
+<style type="text/css">
98
+.user-bind .third-app {
99
+  display: -webkit-box;
100
+  display: -ms-flexbox;
101
+  display: flex;
102
+  -webkit-box-orient: vertical;
103
+  -webkit-box-direction: normal;
104
+  -ms-flex-direction: column;
105
+  flex-direction: column;
106
+  -webkit-box-align: center;
107
+  -ms-flex-align: center;
108
+  align-items: center;
109
+  min-width: 80px;
110
+  float: left;
111
+}
112
+
113
+.user-bind {
114
+  font-size: 1rem;
115
+  text-align: start;
116
+  height: 50px;
117
+  margin-top: 10px;
118
+}
119
+
120
+.git-other-login-icon > img {
121
+  height: 32px;
122
+}
123
+
124
+a {
125
+  text-decoration: none;
126
+  cursor: pointer;
127
+  color: #005980;
128
+}
129
+
130
+.provider-desc {
131
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
132
+    'Liberation Sans', 'PingFang SC', 'Microsoft YaHei', 'Hiragino Sans GB', 'Wenquanyi Micro Hei', 'WenQuanYi Zen Hei', 'ST Heiti', SimHei, SimSun,
133
+    'WenQuanYi Zen Hei Sharp', sans-serif;
134
+  font-size: 1.071rem;
135
+}
136
+
137
+td > img {
138
+  height: 20px;
139
+  width: 20px;
140
+  display: inline-block;
141
+  border-radius: 50%;
142
+  margin-right: 5px;
143
+}
144
+</style>

+ 182 - 0
src/views/system/right/user/profile/userAvatar.vue

@@ -0,0 +1,182 @@
1
+<template>
2
+  <div class="user-info-head" @click="editCropper()">
3
+    <img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
4
+    <el-dialog v-model="open" :title="title" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
5
+      <el-row>
6
+        <el-col :xs="24" :md="12" :style="{ height: '350px' }">
7
+          <vue-cropper
8
+            v-if="visible"
9
+            ref="cropper"
10
+            :img="options.img"
11
+            :info="true"
12
+            :auto-crop="options.autoCrop"
13
+            :auto-crop-width="options.autoCropWidth"
14
+            :auto-crop-height="options.autoCropHeight"
15
+            :fixed-box="options.fixedBox"
16
+            :output-type="options.outputType"
17
+            @real-time="realTime"
18
+          />
19
+        </el-col>
20
+        <el-col :xs="24" :md="12" :style="{ height: '350px' }">
21
+          <div class="avatar-upload-preview">
22
+            <img :src="options.previews.url" :style="options.previews.img" />
23
+          </div>
24
+        </el-col>
25
+      </el-row>
26
+      <br />
27
+      <el-row>
28
+        <el-col :lg="2" :md="2">
29
+          <el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
30
+            <el-button>
31
+              选择
32
+              <el-icon class="el-icon--right">
33
+                <Upload />
34
+              </el-icon>
35
+            </el-button>
36
+          </el-upload>
37
+        </el-col>
38
+        <el-col :lg="{ span: 1, offset: 2 }" :md="2">
39
+          <el-button icon="Plus" @click="changeScale(1)"></el-button>
40
+        </el-col>
41
+        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
42
+          <el-button icon="Minus" @click="changeScale(-1)"></el-button>
43
+        </el-col>
44
+        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
45
+          <el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
46
+        </el-col>
47
+        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
48
+          <el-button icon="RefreshRight" @click="rotateRight()"></el-button>
49
+        </el-col>
50
+        <el-col :lg="{ span: 2, offset: 6 }" :md="2">
51
+          <el-button type="primary" @click="uploadImg()">提 交</el-button>
52
+        </el-col>
53
+      </el-row>
54
+    </el-dialog>
55
+  </div>
56
+</template>
57
+
58
+<script setup lang="ts">
59
+import 'vue-cropper/dist/index.css';
60
+import { VueCropper } from 'vue-cropper';
61
+import { uploadAvatar } from '@/api/system/user';
62
+import useUserStore from '@/store/modules/user';
63
+import { UploadRawFile } from 'element-plus';
64
+
65
+interface Options {
66
+  img: string | any; // 裁剪图片的地址
67
+  autoCrop: boolean; // 是否默认生成截图框
68
+  autoCropWidth: number; // 默认生成截图框宽度
69
+  autoCropHeight: number; // 默认生成截图框高度
70
+  fixedBox: boolean; // 固定截图框大小 不允许改变
71
+  fileName: string;
72
+  previews: any; // 预览数据
73
+  outputType: string;
74
+  visible: boolean;
75
+}
76
+
77
+const userStore = useUserStore();
78
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
79
+
80
+const open = ref(false);
81
+const visible = ref(false);
82
+const title = ref('修改头像');
83
+
84
+const cropper = ref<any>({});
85
+//图片裁剪数据
86
+const options = reactive<Options>({
87
+  img: userStore.avatar,
88
+  autoCrop: true,
89
+  autoCropWidth: 200,
90
+  autoCropHeight: 200,
91
+  fixedBox: true,
92
+  outputType: 'png',
93
+  fileName: '',
94
+  previews: {},
95
+  visible: false
96
+});
97
+
98
+/** 编辑头像 */
99
+const editCropper = () => {
100
+  open.value = true;
101
+};
102
+/** 打开弹出层结束时的回调 */
103
+const modalOpened = () => {
104
+  visible.value = true;
105
+};
106
+/** 覆盖默认上传行为 */
107
+const requestUpload = (): any => {};
108
+/** 向左旋转 */
109
+const rotateLeft = () => {
110
+  cropper.value.rotateLeft();
111
+};
112
+/** 向右旋转 */
113
+const rotateRight = () => {
114
+  cropper.value.rotateRight();
115
+};
116
+/** 图片缩放 */
117
+const changeScale = (num: number) => {
118
+  num = num || 1;
119
+  cropper.value.changeScale(num);
120
+};
121
+/** 上传预处理 */
122
+const beforeUpload = (file: UploadRawFile): any => {
123
+  if (file.type.indexOf('image/') == -1) {
124
+    proxy?.$modal.msgError('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。');
125
+  } else {
126
+    const reader = new FileReader();
127
+    reader.readAsDataURL(file);
128
+    reader.onload = () => {
129
+      options.img = reader.result;
130
+      options.fileName = file.name;
131
+    };
132
+  }
133
+};
134
+/** 上传图片 */
135
+const uploadImg = async () => {
136
+  cropper.value.getCropBlob(async (data: any) => {
137
+    let formData = new FormData();
138
+    formData.append('avatarfile', data, options.fileName);
139
+    const res = await uploadAvatar(formData);
140
+    open.value = false;
141
+    options.img = res.data.imgUrl;
142
+    userStore.setAvatar(options.img);
143
+    proxy?.$modal.msgSuccess('修改成功');
144
+    visible.value = false;
145
+  });
146
+};
147
+/** 实时预览 */
148
+const realTime = (data: any) => {
149
+  options.previews = data;
150
+};
151
+/** 关闭窗口 */
152
+const closeDialog = () => {
153
+  options.img = userStore.avatar;
154
+  options.visible = false;
155
+};
156
+</script>
157
+
158
+<style lang="scss" scoped>
159
+.user-info-head {
160
+  position: relative;
161
+  display: inline-block;
162
+  height: 120px;
163
+}
164
+
165
+.user-info-head:hover:after {
166
+  content: '+';
167
+  position: absolute;
168
+  left: 0;
169
+  right: 0;
170
+  top: 0;
171
+  bottom: 0;
172
+  color: #eee;
173
+  background: rgba(0, 0, 0, 0.5);
174
+  font-size: 24px;
175
+  font-style: normal;
176
+  -webkit-font-smoothing: antialiased;
177
+  -moz-osx-font-smoothing: grayscale;
178
+  cursor: pointer;
179
+  line-height: 110px;
180
+  border-radius: 50%;
181
+}
182
+</style>

+ 69 - 0
src/views/system/right/user/profile/userInfo.vue

@@ -0,0 +1,69 @@
1
+<template>
2
+  <el-form ref="userRef" :model="userForm" :rules="rules" label-width="80px">
3
+    <el-form-item label="用户昵称" prop="nickName">
4
+      <el-input v-model="userForm.nickName" maxlength="30" />
5
+    </el-form-item>
6
+    <el-form-item label="手机号码" prop="phonenumber">
7
+      <el-input v-model="userForm.phonenumber" maxlength="11" />
8
+    </el-form-item>
9
+    <el-form-item label="邮箱" prop="email">
10
+      <el-input v-model="userForm.email" maxlength="50" />
11
+    </el-form-item>
12
+    <el-form-item label="性别">
13
+      <el-radio-group v-model="userForm.sex">
14
+        <el-radio value="0">男</el-radio>
15
+        <el-radio value="1">女</el-radio>
16
+      </el-radio-group>
17
+    </el-form-item>
18
+    <el-form-item>
19
+      <el-button type="primary" @click="submit">保存</el-button>
20
+      <el-button type="danger" @click="close">关闭</el-button>
21
+    </el-form-item>
22
+  </el-form>
23
+</template>
24
+
25
+<script setup lang="ts">
26
+import { updateUserProfile } from '@/api/system/user';
27
+import { propTypes } from '@/utils/propTypes';
28
+
29
+const props = defineProps({
30
+  user: propTypes.any.isRequired
31
+});
32
+const userForm = computed(() => props.user);
33
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
34
+const userRef = ref<ElFormInstance>();
35
+const rule: ElFormRules = {
36
+  nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
37
+  email: [
38
+    { required: true, message: '邮箱地址不能为空', trigger: 'blur' },
39
+    {
40
+      type: 'email',
41
+      message: '请输入正确的邮箱地址',
42
+      trigger: ['blur', 'change']
43
+    }
44
+  ],
45
+  phonenumber: [
46
+    {
47
+      required: true,
48
+      message: '手机号码不能为空',
49
+      trigger: 'blur'
50
+    },
51
+    { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
52
+  ]
53
+};
54
+const rules = ref<ElFormRules>(rule);
55
+
56
+/** 提交按钮 */
57
+const submit = () => {
58
+  userRef.value?.validate(async (valid: boolean) => {
59
+    if (valid) {
60
+      await updateUserProfile(props.user);
61
+      proxy?.$modal.msgSuccess('修改成功');
62
+    }
63
+  });
64
+};
65
+/** 关闭按钮 */
66
+const close = () => {
67
+  proxy?.$tab.closePage();
68
+};
69
+</script>