Przeglądaj źródła

feat: 用户信息

wanggaokun 1 rok temu
rodzic
commit
2f7ec729f5

+ 2 - 0
package.json

@@ -44,6 +44,7 @@
     "echarts-liquidfill": "^3.1.0",
     "element-plus": "^2.4.3",
     "file-saver": "^2.0.5",
+    "image-conversion": "^2.1.1",
     "js-cookie": "^3.0.5",
     "jsencrypt": "^3.3.2",
     "md5": "^2.3.0",
@@ -57,6 +58,7 @@
     "screenfull": "^6.0.2",
     "sortablejs": "^1.15.1",
     "vue": "^3.3.11",
+    "vue-cropper": "^1.1.2",
     "vue-i18n": "^9.6.4",
     "vue-router": "^4.2.5",
     "vuedraggable": "^4.1.0"

+ 5 - 1
src/api/index.ts

@@ -29,10 +29,10 @@ const config = {
 // 是否显示重新登录
 export let isReLogin = { show: false }
 const axiosCanceler = new AxiosCanceler()
-
 class RequestHttp {
   service: AxiosInstance
   public constructor(config: AxiosRequestConfig) {
+    axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
     // instantiation
     this.service = axios.create(config)
 
@@ -92,6 +92,10 @@ class RequestHttp {
             }
           }
         }
+        // FormData数据去请求头Content-Type
+        if (config.data instanceof FormData) {
+          delete config.headers['Content-Type']
+        }
         return config
       },
       (error: AxiosError) => {

+ 25 - 0
src/api/interface/system/config.ts

@@ -0,0 +1,25 @@
+import { PageQuery, BaseEntity } from '@/api/interface/index'
+export interface ConfigVO extends BaseEntity {
+  configId: number | string
+  configName: string
+  configKey: string
+  configValue: string
+  configType: string
+  remark: string
+}
+
+export interface ConfigForm {
+  configId: number | string | undefined
+  configName: string
+  configKey: string
+  configValue: string
+  configType: string
+  version: number
+  remark: string
+}
+
+export interface ConfigQuery extends PageQuery {
+  configName: string
+  configKey: string
+  configType: string
+}

+ 28 - 0
src/api/interface/system/data.ts

@@ -0,0 +1,28 @@
+import { PageQuery, BaseEntity } from '@/api/interface/index'
+export interface DictDataQuery extends PageQuery {
+  dictName: string
+  dictType: string
+  dictLabel: string
+}
+
+export interface DictDataVO extends BaseEntity {
+  dictCode: string
+  dictLabel: string
+  dictValue: string
+  cssClass: string
+  listClass: ElTagType
+  dictSort: number
+  remark: string
+}
+
+export interface DictDataForm {
+  dictType?: string
+  dictCode: string | undefined
+  dictLabel: string
+  dictValue: string
+  cssClass: string
+  listClass: ElTagType
+  dictSort: number
+  version: number
+  remark: string
+}

+ 47 - 0
src/api/interface/system/dept.ts

@@ -0,0 +1,47 @@
+import { PageQuery, BaseEntity } from '@/api/interface/index'
+/**
+ * 部门查询参数
+ */
+export interface DeptQuery extends PageQuery {
+  deptName?: string
+  status?: number
+}
+
+/**
+ * 部门类型
+ */
+export interface DeptVO extends BaseEntity {
+  id: number | string
+  parentName: string
+  parentId: number | string
+  children: DeptVO[]
+  deptId: number | string
+  deptName: string
+  orderNum: number
+  leader: string
+  phone: string
+  email: string
+  status: string
+  delFlag: string
+  ancestors: string
+  menuId: string | number
+}
+
+/**
+ * 部门表单类型
+ */
+export interface DeptForm {
+  parentName?: string
+  parentId?: number | string
+  children?: DeptForm[]
+  deptId?: number | string
+  deptName?: string
+  orderNum?: number
+  leader?: string
+  phone?: string
+  email?: string
+  status?: string
+  version?: number
+  delFlag?: string
+  ancestors?: string
+}

+ 71 - 0
src/api/interface/system/menu.ts

@@ -0,0 +1,71 @@
+import { MenuTypeEnum } from '@/enums/MenuTypeEnum'
+import { BaseEntity } from '@/api/interface/index'
+
+/**
+ * 菜单树形结构类型
+ */
+export interface MenuTreeOption {
+  id: string | number
+  label: string
+  parentId: string | number
+  weight: number
+  children?: MenuTreeOption[]
+}
+
+export interface RoleMenuTree {
+  menus: MenuTreeOption[]
+  checkedKeys: string[]
+}
+
+/**
+ * 菜单查询参数类型
+ */
+export interface MenuQuery {
+  keywords?: string
+  menuName?: string
+  status?: string
+}
+
+/**
+ * 菜单视图对象类型
+ */
+export interface MenuVO extends BaseEntity {
+  parentName: string
+  parentId: string | number
+  children: MenuVO[]
+  menuId: string | number
+  menuName: string
+  orderNum: number
+  path: string
+  component: string
+  queryParam: string
+  isFrame: string
+  isCache: string
+  menuType: MenuTypeEnum
+  visible: string
+  status: string
+  icon: string
+  remark: string
+}
+
+export interface MenuForm {
+  parentName?: string
+  parentId?: string | number
+  children?: MenuForm[]
+  menuId?: string | number
+  menuName: string
+  orderNum: number
+  path: string
+  component?: string
+  queryParam?: string
+  isFrame?: string
+  isCache?: string
+  menuType?: MenuTypeEnum
+  visible?: string
+  status?: string
+  icon?: string
+  remark?: string
+  query?: string
+  perms?: string
+  version?: number
+}

+ 24 - 0
src/api/interface/system/oss.ts

@@ -0,0 +1,24 @@
+import { PageQuery, BaseEntity } from '@/api/interface/index'
+
+export interface OssVO extends BaseEntity {
+  ossId: string | number
+  fileName: string
+  originalName: string
+  fileSuffix: string
+  url: string
+  createByName: string
+  service: string
+}
+
+export interface OssQuery extends PageQuery {
+  fileName: string
+  originalName: string
+  fileSuffix: string
+  createTime: string
+  service: string
+  orderByColumn: string
+  isAsc: string
+}
+export interface OssForm {
+  file: undefined | string
+}

+ 41 - 0
src/api/interface/system/ossConfig.ts

@@ -0,0 +1,41 @@
+import { PageQuery, BaseEntity } from '@/api/interface/index'
+
+export interface OssConfigVO extends BaseEntity {
+  ossConfigId: number | string
+  configKey: string
+  accessKey: string
+  secretKey: string
+  bucketName: string
+  prefix: string
+  endpoint: string
+  domain: string
+  isHttps: string
+  region: string
+  status: string
+  ext1: string
+  remark: string
+  accessPolicy: string
+}
+
+export interface OssConfigQuery extends PageQuery {
+  configKey: string
+  bucketName: string
+  status: string
+}
+
+export interface OssConfigForm {
+  ossConfigId: string | number | undefined
+  configKey: string
+  accessKey: string
+  secretKey: string
+  bucketName: string
+  prefix: string
+  endpoint: string
+  domain: string
+  isHttps: string
+  accessPolicy: string
+  region: string
+  status: string
+  version: number
+  remark: string
+}

+ 25 - 0
src/api/interface/system/post.ts

@@ -0,0 +1,25 @@
+import { PageQuery, BaseEntity } from '@/api/interface/index'
+export interface PostVO extends BaseEntity {
+  postId: number | string
+  postCode: string
+  postName: string
+  postSort: number
+  status: string
+  remark: string
+}
+
+export interface PostForm {
+  postId: number | string | undefined
+  postCode: string
+  postName: string
+  postSort: number
+  status: string
+  version: number
+  remark: string
+}
+
+export interface PostQuery extends PageQuery {
+  postCode: string
+  postName: string
+  status: string
+}

+ 54 - 0
src/api/interface/system/role.ts

@@ -0,0 +1,54 @@
+import { PageQuery, BaseEntity } from '@/api/interface/index'
+/**
+ * 菜单树形结构类型
+ */
+export interface DeptTreeOption {
+  id: string
+  label: string
+  parentId: string
+  weight: number
+  children?: DeptTreeOption[]
+}
+
+export interface RoleDeptTree {
+  checkedKeys: string[]
+  depts: DeptTreeOption[]
+}
+
+export interface RoleVO extends BaseEntity {
+  roleId: string | number
+  roleName: string
+  roleKey: string
+  roleSort: number
+  dataScope: string
+  menuCheckStrictly: boolean
+  deptCheckStrictly: boolean
+  status: string
+  delFlag: string
+  remark?: any
+  flag: boolean
+  menuIds?: Array<string | number>
+  deptIds?: Array<string | number>
+  admin: boolean
+}
+
+export interface RoleQuery extends PageQuery {
+  roleName: string
+  roleKey: string
+  status: string
+}
+
+export interface RoleForm {
+  roleName: string
+  roleKey: string
+  roleSort: number
+  status: string
+  menuCheckStrictly: boolean
+  deptCheckStrictly: boolean
+  remark: string
+  dataScope?: string
+  roleId: string | undefined
+  menuIds: Array<string | number>
+  deptIds: Array<string | number>
+  version?: number
+}

+ 20 - 0
src/api/interface/system/type.ts

@@ -0,0 +1,20 @@
+import { PageQuery, BaseEntity } from '@/api/interface/index'
+export interface DictTypeVO extends BaseEntity {
+  dictId: number | string
+  dictName: string
+  dictType: string
+  remark: string
+}
+
+export interface DictTypeForm {
+  dictId: number | string | undefined
+  dictName: string
+  dictType: string
+  version: number
+  remark: string
+}
+
+export interface DictTypeQuery extends PageQuery {
+  dictName: string
+  dictType: string
+}

+ 87 - 0
src/api/interface/system/user.ts

@@ -0,0 +1,87 @@
+import { RoleVO } from '@/api/interface/system/role'
+import { PostVO } from '@/api/interface/system/post'
+import { DeptVO } from '@/api/interface/system/dept'
+import { PageQuery, BaseEntity } from '@/api/interface/index'
+/**
+ * 用户信息
+ */
+export interface UserInfo {
+  user: UserVO
+  roles: string[]
+  permissions: string[]
+}
+
+/**
+ * 用户查询对象类型
+ */
+export interface UserQuery extends PageQuery {
+  userName?: string
+  phonenumber?: string
+  status?: string
+  deptId?: string | number
+  roleId?: string | number
+}
+
+/**
+ * 用户返回对象
+ */
+export interface UserVO extends BaseEntity {
+  userId: string | number
+  deptId: number
+  userName: string
+  nickName: string
+  userType: string
+  email: string
+  phonenumber: string
+  dept: DeptVO
+  gender: string
+  avatar: string
+  url: string
+  status: string
+  delFlag: string
+  loginIp: string
+  loginDate: string
+  remark: string
+  deptName: string
+  roles: RoleVO[]
+  roleIds: any
+  postIds: any
+  roleId: any
+  admin: boolean
+}
+
+/**
+ * 用户表单类型
+ */
+export interface UserForm {
+  id?: string
+  userId?: string
+  deptId?: number
+  userName: string
+  nickName?: string
+  password: string
+  phonenumber?: string
+  email?: string
+  gender?: string
+  status: string
+  remark?: string
+  postIds: string[]
+  roleIds: string[]
+  version?: number
+}
+
+export interface UserInfoVO {
+  user: UserVO
+  roles: RoleVO[]
+  roleIds: string[]
+  posts: PostVO[]
+  postIds: string[]
+  roleGroup: string
+  postGroup: string
+}
+
+export interface ResetPwdForm {
+  oldPassword: string
+  newPassword: string
+  confirmPassword: string
+}

+ 70 - 0
src/api/modules/system/config.ts

@@ -0,0 +1,70 @@
+import http from '@/api'
+
+/**
+ * @name 查询参数配置列表
+ * @param query 参数
+ * @returns 返回列表
+ */
+export const listConfigApi = (query: any) => {
+  return http.get<any>('/system/config/list', query, { loading: true })
+}
+
+/**
+ * @name 查询参数配置详细
+ * @param configId configId
+ * @returns returns
+ */
+export const getConfigApi = (configId: any) => {
+  return http.get<any>(`/system/config/${configId}`)
+}
+
+/**
+ * @name 新增参数配置
+ * @param data data
+ * @returns returns
+ */
+export const addConfigApi = (data: any) => {
+  return http.post<any>('/system/config', data, { loading: false })
+}
+
+/**
+ * @name 修改参数配置
+ * @param data data
+ * @returns returns
+ */
+export const updateConfigApi = (data: any) => {
+  return http.put<any>('/system/config', data, { loading: false })
+}
+
+/**
+ * @name 删除参数配置
+ * @param configId configId
+ * @returns returns
+ */
+export const delConfigApi = (configId: any) => {
+  return http.delete<any>(`/system/config/${configId}`)
+}
+
+/**
+ * @name 下载模板
+ * @returns returns
+ */
+export const importTemplateApi = () => {
+  return http.downloadPost('/system/config/importTemplate', {})
+}
+
+/**
+ * @name 导入数据
+ * @returns returns
+ */
+export const importConfigDataApi = (data: any) => {
+  return http.post('/system/config/importData', data)
+}
+
+/**
+ * @name 导出数据
+ * @returns returns
+ */
+export const exportConfigApi = (data: any) => {
+  return http.downloadPost('/system/config/export', data)
+}

+ 34 - 0
src/api/modules/system/user.ts

@@ -94,3 +94,37 @@ export const importDataApi = (data: any) => {
 export const exportApi = (data: any) => {
   return http.downloadPost('/system/user/export', data)
 }
+
+/**
+ * @name 用户头像上传
+ * @param params params
+ * @returns returns
+ */
+export const uploadAvatarApi = (params: FormData) => {
+  return http.post<any>('/system/user/profile/avatar', params)
+}
+
+/**
+ * @name 查询用户个人信息
+ * @returns returns
+ */
+export const getUserProfileApi = () => {
+  return http.get<any>(`/system/user/profile`)
+}
+
+/**
+ * @name 修改用户个人信息
+ * @param data data
+ * @returns returns
+ */
+export const updateUserProfileApi = (data: any) => {
+  return http.put<any>('/system/user/profile', data, { loading: false })
+}
+/**
+ * @name 修改用户密码
+ * @param data data
+ * @returns returns
+ */
+export const updateUserPwdApi = (data: { oldPassword: string; newPassword: string }) => {
+  return http.put<any>('system/user/profile/updatePwd', data, { loading: false })
+}

+ 2 - 1
src/components/FormDialog/index.vue

@@ -56,11 +56,12 @@ const proFormRef = ref<InstanceType<typeof ProFrom> | null>(null)
 // 表单提交校验
 const handleSubmit = () => {
   const formEl = proFormRef.value?.proFormRef
+  const formModel = proFormRef.value?.formModel
   butLoading.value = true
   if (!formEl) return
   formEl.validate(valid => {
     if (valid) {
-      parameter.value.api!(parameter.value.model).then(res => {
+      parameter.value.api!(Object.assign({ ...parameter.value.model }, { ...formModel })).then(res => {
         if (res.code == 200) {
           proFormRef.value?.resetForm(formEl)
           ElMessage.success('操作成功')

+ 5 - 6
src/components/ProForm/index.vue

@@ -67,7 +67,7 @@ const formModel = ref<Record<string, any>>({})
 const proFormRef = ref<FormInstance>()
 // 默认值
 const props = withDefaults(defineProps<ProFormProps>(), {
-  items: () => [],
+  itemsOptions: () => [],
   model: () => ({})
 })
 const show = (showFunction: any) => {
@@ -87,7 +87,7 @@ const _formOptions: ComputedRef<ProForm.FormOptions> = computed(() => {
     labelWidth: 120,
     disabled: false,
     hasFooter: true,
-    labelSuffix: ': ',
+    labelSuffix: ':',
     submitButtonText: '提交',
     resetButtonText: '重置',
     cancelButtonText: '取消'
@@ -142,20 +142,19 @@ watch(
   () => {
     props.itemsOptions.map((item: ProForm.ItemsOptions) => {
       // 如果类型为checkbox,默认值需要设置一个空数组
-      const value = ['checkbox', 'transfer'].includes(item.compOptions.elTagName!) ? [] : ''
-      props.model ? (formModel.value = props.model) : (formModel.value[item.prop] = item.value || value)
+      let value = ['checkbox', 'transfer'].includes(item.compOptions.elTagName!) ? [] : props.model[item.prop]
+      props.model[item.prop] ? (formModel.value = props.model) : (formModel.value[item.prop] = item.compOptions.value || value)
     })
   },
   { immediate: true }
 )
-
 // 提交按钮
 const onSubmit = (formEl: FormInstance | undefined) => {
   console.log('表单提交数据', formModel.value)
   if (!formEl) return
   formEl.validate(valid => {
     if (valid) {
-      if (props.api) emits('submit', formModel.value)
+      if (!props.api) emits('submit', formModel.value)
       props.api!({ ...formModel }).then(res => {
         if (res.code == 200) {
           resetForm(formEl)

+ 1 - 1
src/languages/modules/en.ts

@@ -22,7 +22,7 @@ export default {
     weakMode: 'Weak mode',
     fullScreen: 'Full Screen',
     exitFullScreen: 'Exit Full Screen',
-    personalData: 'Personal Data',
+    personalCenter: 'Personal Center',
     changePassword: 'Change Password',
     logout: 'Logout'
   }

+ 1 - 1
src/languages/modules/zh.ts

@@ -22,7 +22,7 @@ export default {
     weakMode: '色弱模式',
     fullScreen: '全屏',
     exitFullScreen: '退出全屏',
-    personalData: '个人信息',
+    personalCenter: '个人中心',
     changePassword: '修改密码',
     logout: '退出登录'
   }

+ 21 - 9
src/layouts/components/Header/components/Avatar.vue

@@ -1,16 +1,16 @@
 <template>
   <el-dropdown trigger="click">
-    <div class="avatar">
+    <div class="avatar" v-if="userStore.avatar.indexOf('undefined') === -1">
+      <img :src="userStore.avatar" />
+    </div>
+    <div class="avatar-dft" v-else>
       <img src="@/assets/icons/avatar-user.svg" alt="avatar" />
     </div>
     <template #dropdown>
       <el-dropdown-menu>
-        <!-- <el-dropdown-item @click="openDialog('infoRef')">
-          <el-icon><User /></el-icon>{{ $t('header.personalData') }}
+        <el-dropdown-item @click="toProfile()">
+          <el-icon><User /></el-icon>{{ $t('header.personalCenter') }}
         </el-dropdown-item>
-        <el-dropdown-item @click="openDialog('passwordRef')">
-          <el-icon><Edit /></el-icon>{{ $t('header.changePassword') }}
-        </el-dropdown-item> -->
         <el-dropdown-item divided @click="logout">
           <el-icon><SwitchButton /></el-icon>{{ $t('header.logout') }}
         </el-dropdown-item>
@@ -32,10 +32,11 @@ import { useUserStore } from '@/stores/modules/user'
 import { ElMessageBox, ElMessage } from 'element-plus'
 import InfoDialog from './InfoDialog.vue'
 import PasswordDialog from './PasswordDialog.vue'
-
 const router = useRouter()
 const userStore = useUserStore()
-
+const toProfile = () => {
+  router.push('/system/user/profile')
+}
 // 退出登录
 const logout = () => {
   ElMessageBox.confirm('您是否确认退出登录?', '温馨提示', {
@@ -71,7 +72,18 @@ const passwordRef = ref<InstanceType<typeof PasswordDialog> | null>(null)
   img {
     width: 100%;
     height: 100%;
-    filter: drop-shadow(var(--el-color-primary) 100px 0);
+  }
+}
+.avatar-dft {
+  width: 40px;
+  height: 40px;
+  overflow: hidden;
+  cursor: pointer;
+  border-radius: 50%;
+  img {
+    width: 100%;
+    height: 100%;
+    filter: drop-shadow(var(--el-menu-active-color) 100px 0);
     transform: translateX(-100px);
   }
 }

+ 15 - 0
src/routers/modules/dynamicRouter.json

@@ -45,6 +45,21 @@
         "noCache": true
       }
     },
+    {
+      "path": "/system/user/profile",
+      "name": "UserProfile",
+      "component": "system/user/profile/index",
+      "hidden": true,
+      "meta": {
+        "icon": "",
+        "title": "个人中心",
+        "activeMenu": "/system/user",
+        "link": "",
+        "full": false,
+        "affix": false,
+        "noCache": true
+      }
+    },
     {
       "path": "/task/create",
       "name": "CreateTask",

+ 2 - 1
src/stores/modules/user.ts

@@ -36,9 +36,10 @@ export const useUserStore = defineStore('admin-user', {
         getInfoApi()
           .then((res: any) => {
             if (res.code === 200) {
+              debugger
               const data = res
               const user = data.user
-              const avatar = user.avatar == '' || user.avatar == null ? defAva : import.meta.env.VITE_APP_BASE_API + user.avatar
+              const avatar = user.avatar == '' || user.avatar == null ? defAva : import.meta.env.VITE_API_URL + user.avatar
               if (data.roles && data.roles.length > 0) {
                 // 验证返回的roles是否是一个非空数组
                 this.roles = data.roles

+ 33 - 0
src/styles/common.scss

@@ -1,3 +1,33 @@
+/* image */
+.img-circle {
+  border-radius: 50%;
+}
+.img-lg {
+  width: 120px;
+  height: 120px;
+}
+
+/** 表单布局 **/
+.form-header {
+  padding-bottom: 5px;
+  margin: 8px 10px 25px;
+  font-size: 15px;
+  border-bottom: 1px solid #dddddd;
+}
+.avatar-upload-preview {
+  position: absolute;
+  top: 50%;
+  width: 200px;
+  height: 200px;
+  overflow: hidden;
+  border-radius: 50%;
+  box-shadow: 0 0 4px #cccccc;
+  transform: translate(50%, -50%);
+}
+.pull-right {
+  float: right !important;
+}
+
 /* flex */
 .flx-center {
   display: flex;
@@ -29,6 +59,9 @@
   text-overflow: ellipsis;
   white-space: nowrap;
 }
+.text-center {
+  text-align: center;
+}
 
 /* 文字多行省略号 */
 .mle {

+ 7 - 0
src/styles/element.scss

@@ -14,6 +14,13 @@ label {
   // text-overflow: ellipsis;
   // white-space: nowrap;
 }
+.dialog-slot-c {
+  .el-tabs__content {
+    max-height: 52vh;
+    overflow: hidden;
+    overflow: auto;
+  }
+}
 
 // .el-form-item--default {
 //   margin-bottom: 12px;

+ 18 - 0
src/styles/var.scss

@@ -1,2 +1,20 @@
 /* global css variable */
 $primary-color: var(--el-color-primary);
+.list-group-striped > .list-group-item {
+  padding-right: 0;
+  padding-left: 0;
+  border-right: 0;
+  border-left: 0;
+  border-radius: 0;
+}
+.list-group {
+  padding-left: 0;
+  list-style: none;
+}
+.list-group-item {
+  padding: 11px 0;
+  margin-bottom: -1px;
+  font-size: 13px;
+  border-top: 1px solid #e7eaec;
+  border-bottom: 1px solid #e7eaec;
+}

+ 4 - 3
src/typings/ProForm.d.ts

@@ -25,6 +25,9 @@ declare namespace ProForm {
     | 'radio-group'
     | 'radio-button'
     | 'file-upload'
+    | 'img-upload'
+    | 'file-upload-s3'
+    | 'img-upload-s3'
     | 'slot'
     | 'rate'
     | 'slider'
@@ -53,7 +56,6 @@ declare namespace ProForm {
     label?: string
     labelWidth?: string | number // 标签宽度,例如 '50px'。 可以使用 auto。
     prop: string // prop
-    value?: any // 默认值
     tooltip?: string // 问号,tooltip提示
     required?: boolean
     hideLabelSuffix?: boolean // label后缀是否隐藏
@@ -85,7 +87,7 @@ declare namespace ProForm {
     clearable?: boolean // 是否可清空
     showPassword?: boolean // 是否显示切换密码图标
     enum?: EnumProps[] | Ref<EnumProps[]> | ((params?: any) => Promise<any>) // 枚举字典
-    onChange?: (value: any) => void
+    value?: string | number | boolean | any[] // 选项框值
     enumKey?: string
     labelKey?: string
     valueKey?: string
@@ -112,7 +114,6 @@ declare namespace ProForm {
     appendToBody?: boolean // 树下拉
     checkStrictly?: boolean // 可选
     renderAfterExpand?: boolean // 可选
-    validateEvent?: boolean // 可选如果您不想根据输入事件触发验证器, 在相应的输入类型组件上设置 validate-event 属性为 false
     controlsPosition?: 'left' | 'right' // 可选
     onChange?: (value: any) => void
     onSelect?: (value: any) => void

+ 3 - 37
src/views/manage/backupRestoreLog/index.vue

@@ -3,24 +3,15 @@
     <ProTable ref="proTable" :columns="columns" row-key="id" :request-api="listBackupRestoreLogApi" :data-callback="dataCallback">
       <!-- 表格 header 按钮 -->
       <template #tableHeader>
-        <el-button type="primary" v-auth="['manage:backupRestoreLog:add']" @click="backup()"> 备份文件 </el-button>
-        <el-button type="primary" v-auth="['manage:backupRestoreLog:export']" icon="Download" plain @click="downloadFile"> 导出 </el-button>
+        <el-button type="primary" @click="backup()"> 备份文件 </el-button>
+        <el-button type="primary" icon="Download" plain @click="downloadFile"> 导出 </el-button>
       </template>
       <!-- 表格操作 -->
       <template #operation="scope">
         <el-button type="primary" link icon="View" v-auth="['manage:backupRestoreLog:query']" @click="openDialog(3, '备份恢复日志查看', scope.row)">
           查看
         </el-button>
-        <el-button
-          type="primary"
-          v-if="scope.row.operateType === '备份'"
-          link
-          icon="View"
-          v-auth="['manage:backupRestoreLog:query']"
-          @click="restore()"
-        >
-          恢复
-        </el-button>
+        <el-button type="primary" v-if="scope.row.operateType === '备份'" link icon="View" @click="restore()"> 恢复 </el-button>
       </template>
     </ProTable>
     <FormDialog ref="formDialogRef" />
@@ -108,17 +99,6 @@ const columns = reactive<ColumnProps<any>[]>([
     prop: 'status',
     label: '备份状态'
   },
-  {
-    prop: 'source',
-    label: '数据源',
-    search: {
-      el: 'input'
-    }
-  },
-  {
-    prop: 'target',
-    label: '目标'
-  },
   {
     prop: 'updateTime',
     label: '更新时间'
@@ -149,20 +129,6 @@ const setItemsOptions = () => {
       compOptions: {
         placeholder: '请输入备份状态'
       }
-    },
-    {
-      label: '数据源',
-      prop: 'source',
-      compOptions: {
-        placeholder: '请输入数据源'
-      }
-    },
-    {
-      label: '目标',
-      prop: 'target',
-      compOptions: {
-        placeholder: '请输入目标'
-      }
     }
   ]
 }

+ 211 - 0
src/views/system/config/index.vue

@@ -0,0 +1,211 @@
+<template>
+  <div class="table-box">
+    <ProTable ref="proTable" :columns="columns" row-key="configId" :request-api="listConfigApi" :data-callback="dataCallback">
+      <!-- 表格 header 按钮 -->
+      <template #tableHeader="scope">
+        <el-button type="primary" v-auth="['system:config:add']" icon="CirclePlus" @click="openDialog(1, '参数配置新增')"> 新增 </el-button>
+        <el-button
+          type="danger"
+          v-auth="['system:config:remove']"
+          icon="Delete"
+          plain
+          :disabled="!scope.isSelected"
+          @click="batchDelete(scope.selectedListIds)"
+        >
+          批量删除
+        </el-button>
+      </template>
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <el-button type="primary" link icon="View" v-auth="['system:config:query']" @click="openDialog(3, '参数配置查看', scope.row)">
+          查看
+        </el-button>
+        <el-button type="primary" link icon="EditPen" v-auth="['system:config:edit']" @click="openDialog(2, '参数配置编辑', scope.row)">
+          编辑
+        </el-button>
+        <el-button type="primary" link icon="Delete" v-auth="['system:config:remove']" @click="deleteConfig(scope.row)"> 删除 </el-button>
+      </template>
+    </ProTable>
+    <FormDialog ref="formDialogRef" />
+    <ImportExcel ref="dialogRef" />
+  </div>
+</template>
+
+<script setup lang="tsx" name="Config">
+import { ref, reactive } from 'vue'
+import { useHandleData } from '@/hooks/useHandleData'
+// import { useDownload } from '@/hooks/useDownload'
+// import { ElMessageBox } from 'element-plus'
+import ProTable from '@/components/ProTable/index.vue'
+import ImportExcel from '@/components/ImportExcel/index.vue'
+import FormDialog from '@/components/FormDialog/index.vue'
+import { ProTableInstance, ColumnProps } from '@/components/ProTable/interface'
+import { listConfigApi, delConfigApi, addConfigApi, updateConfigApi, getConfigApi } from '@/api/modules/system/config'
+import { getDictsApi } from '@/api/modules/system/dictData'
+// ProTable 实例
+const proTable = ref<ProTableInstance>()
+const dataCallback = (data: any) => {
+  const page = proTable.value!.pageable
+  return {
+    list: data.data,
+    total: data.total,
+    pageNum: page.pageNum,
+    pageSize: page.pageSize
+  }
+}
+// 删除参数配置信息
+const deleteConfig = async (params: any) => {
+  await useHandleData(delConfigApi, params.configId, `删除【${params.configId}】参数配置`)
+  proTable.value?.getTableList()
+}
+
+// 批量删除参数配置信息
+const batchDelete = async (ids: string[]) => {
+  await useHandleData(delConfigApi, ids, '删除所选参数配置信息')
+  proTable.value?.clearSelection()
+  proTable.value?.getTableList()
+}
+
+// 导出参数配置列表
+// const downloadFile = async () => {
+//   ElMessageBox.confirm('确认导出参数配置数据?', '温馨提示', { type: 'warning' }).then(() =>
+//     useDownload(exportConfigApi, '参数配置列表', proTable.value?.searchParam)
+//   )
+// }
+
+// 批量添加参数配置
+// const dialogRef = ref<InstanceType<typeof ImportExcel> | null>(null)
+// const batchAdd = () => {
+//   const params = {
+//     title: '参数配置',
+//     tempApi: importTemplateApi,
+//     importApi: importConfigDataApi,
+//     getTableList: proTable.value?.getTableList
+//   }
+//   dialogRef.value?.acceptParams(params)
+// }
+
+const formDialogRef = ref<InstanceType<typeof FormDialog> | null>(null)
+// 打开弹框的功能
+const openDialog = async (type: number, title: string, row?: any) => {
+  let res = { data: {} }
+  if (row?.configId) {
+    res = await getConfigApi(row?.configId || null)
+  }
+  // 重置表单
+  setItemsOptions()
+  const params = {
+    title,
+    width: 580,
+    isEdit: type !== 3,
+    itemsOptions: itemsOptions,
+    model: type == 1 ? {} : res.data,
+    api: type == 1 ? addConfigApi : updateConfigApi,
+    getTableList: proTable.value?.getTableList
+  }
+  formDialogRef.value?.openDialog(params)
+}
+
+// 表格配置项
+const columns = reactive<ColumnProps<any>[]>([
+  { type: 'selection', fixed: 'left', width: 70 },
+  {
+    prop: 'configName',
+    label: '参数名称',
+    search: {
+      el: 'input'
+    }
+  },
+  {
+    prop: 'configKey',
+    label: '参数键名',
+    search: {
+      el: 'input'
+    }
+  },
+  {
+    prop: 'configValue',
+    label: '参数键值',
+    search: {
+      el: 'input'
+    }
+  },
+  {
+    prop: 'configType',
+    label: '系统内置',
+    tag: true,
+    enum: () => getDictsApi('sys_yes_no'),
+    search: {
+      el: 'select'
+    },
+    fieldNames: { label: 'dictLabel', value: 'dictValue' }
+  },
+  {
+    prop: 'createBy',
+    label: '创建者',
+    search: {
+      el: 'input'
+    },
+    width: 120
+  },
+  {
+    prop: 'createTime',
+    label: '创建时间'
+  },
+  {
+    prop: 'remark',
+    label: '备注'
+  },
+  { prop: 'operation', label: '操作', width: 230, fixed: 'right' }
+])
+// 表单配置项
+let itemsOptions: ProForm.ItemsOptions[] = []
+const setItemsOptions = () => {
+  itemsOptions = [
+    {
+      label: '参数名称',
+      prop: 'configName',
+      rules: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
+      compOptions: {
+        placeholder: '请输入参数名称'
+      }
+    },
+    {
+      label: '参数键名',
+      prop: 'configKey',
+      rules: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
+      compOptions: {
+        placeholder: '请输入参数键名'
+      }
+    },
+    {
+      label: '参数键值',
+      prop: 'configValue',
+      rules: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }],
+      compOptions: {
+        placeholder: '请输入参数键值'
+      }
+    },
+    {
+      label: '系统内置',
+      prop: 'configType',
+      compOptions: {
+        elTagName: 'radio-group',
+        enum: () => getDictsApi('sys_yes_no'),
+        // enumKey: 'sys_yes_no',
+        labelKey: 'dictLabel',
+        valueKey: 'dictValue'
+      }
+    },
+    {
+      label: '备注',
+      prop: 'remark',
+      compOptions: {
+        type: 'textarea',
+        rows: 4,
+        placeholder: '请输入备注'
+      }
+    }
+  ]
+}
+</script>

+ 9 - 0
src/views/system/user/profile/index.scss

@@ -0,0 +1,9 @@
+.form-box {
+  // display: flex;
+  // flex-direction: column; /* 竖直方向布局 */
+  // height: 100%;
+  .box-card {
+    // flex: 1;
+    height: calc(100vh - 110px);
+  }
+}

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

@@ -0,0 +1,222 @@
+<template>
+  <div class="form-box">
+    <el-row :gutter="20">
+      <el-col :span="6" :xs="24">
+        <el-card class="box-card">
+          <template #header>
+            <div class="clearfix">
+              <span>个人信息</span>
+            </div>
+          </template>
+          <div class="text-center">
+            <userAvatar />
+          </div>
+          <ul class="list-group list-group-striped">
+            <li class="list-group-item">
+              用户名称
+              <div class="pull-right">{{ state.user.userName }}</div>
+            </li>
+            <li class="list-group-item">
+              手机号码
+              <div class="pull-right">{{ state.user.phonenumber }}</div>
+            </li>
+            <li class="list-group-item">
+              用户邮箱
+              <div class="pull-right">{{ state.user.email }}</div>
+            </li>
+            <li class="list-group-item">
+              所属部门
+              <div v-if="state.user.dept" class="pull-right">{{ state.user.dept?.deptName }} / {{ state.postGroup }}</div>
+            </li>
+            <li class="list-group-item">
+              所属角色
+              <div class="pull-right">{{ state.roleGroup }}</div>
+            </li>
+            <li class="list-group-item">
+              创建日期
+              <div class="pull-right">{{ state.user.createTime }}</div>
+            </li>
+          </ul>
+        </el-card>
+      </el-col>
+      <el-col :span="18" :xs="24">
+        <el-card class="box-card">
+          <template #header>
+            <div class="clearfix">
+              <span>基本信息</span>
+            </div>
+          </template>
+          <el-tabs v-model="activeTab">
+            <el-tab-pane label="基本资料" name="userInfo">
+              <ProFrom
+                ref="proFormRef"
+                :items-options="itemsOptions"
+                :form-options="_options"
+                :model="userForm"
+                @submit="submit"
+                @cancel="closeCurrentTab"
+              />
+            </el-tab-pane>
+            <el-tab-pane label="修改密码" name="resetPwd">
+              <ProFrom
+                ref="proFormRef"
+                :items-options="itemsOptions1"
+                :form-options="_options"
+                :model="{}"
+                @submit="submitPwd"
+                @cancel="closeCurrentTab"
+              />
+            </el-tab-pane>
+          </el-tabs>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts" name="uploadFile">
+import userAvatar from './userAvatar.vue'
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getUserProfileApi, updateUserPwdApi, updateUserProfileApi } from '@/api/modules/system/user'
+import { UserVO } from '@/api/interface/system/user'
+import ProFrom from '@/components/ProForm/index.vue'
+import { getDictsApi } from '@/api/modules/system/dictData'
+import { useRoute } from 'vue-router'
+import { useTabsStore } from '@/stores/modules/tabs'
+const tabStore = useTabsStore()
+const route = useRoute()
+const activeTab = ref('userInfo')
+interface State {
+  user: Partial<UserVO>
+  roleGroup: string
+  postGroup: string
+}
+const state = ref<State>({
+  user: {},
+  roleGroup: '',
+  postGroup: ''
+})
+const proFormRef = ref<InstanceType<typeof ProFrom> | null>(null)
+const userForm = ref({})
+const getUser = async () => {
+  const res = await getUserProfileApi()
+  state.value.user = res.data.user
+  userForm.value = { ...res.data.user }
+  state.value.roleGroup = res.data.roleGroup
+  state.value.postGroup = res.data.postGroup
+}
+const submit = async form => {
+  const res = await updateUserProfileApi(form)
+  if (res.code === 200) {
+    ElMessage.success('修改成功')
+    proFormRef.value?.resetForm
+  }
+}
+
+const submitPwd = async form => {
+  const res = await updateUserPwdApi(form)
+  if (res.code === 200) {
+    ElMessage.success('修改成功')
+    proFormRef.value?.resetForm
+  }
+}
+const closeCurrentTab = () => {
+  if (route.meta.affix) return
+  tabStore.removeTabs(route.fullPath)
+}
+const _options: ProForm.FormOptions = {
+  labelWidth: 120,
+  hasFooter: true,
+  disabled: false,
+  showCancelButton: true,
+  cancelButtonText: '关闭'
+}
+// 表单配置项
+let itemsOptions: ProForm.ItemsOptions[] = [
+  {
+    label: '用户昵称',
+    prop: 'nickName',
+    rules: [{ required: true, message: '用户昵称不能为空' }],
+    compOptions: {
+      placeholder: '请输入用户昵称'
+    }
+  },
+  {
+    label: '手机号码',
+    prop: 'phonenumber',
+    rules: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }],
+    compOptions: {
+      placeholder: '请输入手机号码'
+    }
+  },
+  {
+    label: '邮箱',
+    prop: 'email',
+    rules: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
+    compOptions: {
+      placeholder: '请输入邮箱'
+    }
+  },
+  {
+    label: '性别',
+    span: 12,
+    prop: 'sex',
+    compOptions: {
+      elTagName: 'radio-group',
+      enum: () => getDictsApi('sys_user_sex'),
+      labelKey: 'dictLabel',
+      valueKey: 'dictValue',
+      placeholder: '请输入邮箱'
+    }
+  }
+]
+let itemsOptions1: ProForm.ItemsOptions[] = [
+  {
+    label: '旧密码',
+    prop: 'oldPassword',
+    rules: [
+      { required: true, message: '密码不能为空' },
+      { min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }
+    ],
+    compOptions: {
+      showPassword: true,
+      type: 'password',
+      placeholder: '请输入旧密码'
+    }
+  },
+  {
+    label: '新密码',
+    prop: 'newPassword',
+    rules: [
+      { required: true, message: '密码不能为空' },
+      { min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }
+    ],
+    compOptions: {
+      showPassword: true,
+      type: 'password',
+      placeholder: '请输入新密码'
+    }
+  },
+  {
+    label: '确认密码',
+    prop: 'confirmPassword',
+    rules: [
+      { required: true, message: '密码不能为空' },
+      { min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }
+    ],
+    compOptions: {
+      showPassword: true,
+      type: 'password',
+      placeholder: '请输入新密码'
+    }
+  }
+]
+onMounted(() => {
+  getUser()
+})
+</script>
+
+<style scoped lang="scss">
+@import './index.scss';
+</style>

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

@@ -0,0 +1,191 @@
+<template>
+  <div class="user-info-head" @click="editCropper()">
+    <img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
+    <el-dialog v-model="open" :title="title" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
+      <el-row>
+        <el-col :xs="24" :md="12" :style="{ height: '350px' }">
+          <vue-cropper
+            v-if="visible"
+            ref="cropper"
+            :img="options.img"
+            :info="true"
+            :auto-crop="options.autoCrop"
+            :auto-crop-width="options.autoCropWidth"
+            :auto-crop-height="options.autoCropHeight"
+            :fixed-box="options.fixedBox"
+            :output-type="options.outputType"
+            @real-time="realTime"
+          />
+        </el-col>
+        <el-col :xs="24" :md="12" :style="{ height: '350px' }">
+          <div class="avatar-upload-preview">
+            <img :src="options.previews.url" :style="options.previews.img" />
+          </div>
+        </el-col>
+      </el-row>
+      <br />
+      <el-row>
+        <el-col :lg="2" :md="2">
+          <el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
+            <el-button>
+              选择
+              <el-icon class="el-icon--right">
+                <Upload />
+              </el-icon>
+            </el-button>
+          </el-upload>
+        </el-col>
+        <el-col :lg="{ span: 1, offset: 2 }" :md="2">
+          <el-button icon="Plus" @click="changeScale(1)"></el-button>
+        </el-col>
+        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+          <el-button icon="Minus" @click="changeScale(-1)"></el-button>
+        </el-col>
+        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+          <el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
+        </el-col>
+        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+          <el-button icon="RefreshRight" @click="rotateRight()"></el-button>
+        </el-col>
+        <el-col :lg="{ span: 2, offset: 6 }" :md="2">
+          <el-button type="primary" @click="uploadImg()">提 交</el-button>
+        </el-col>
+      </el-row>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import 'vue-cropper/dist/index.css'
+import { VueCropper } from 'vue-cropper'
+import { uploadAvatarApi } from '@/api/modules/system/user'
+import { useUserStore } from '@/stores/modules/user'
+import { UploadRawFile } from 'element-plus'
+import { ElMessage } from 'element-plus'
+
+interface Options {
+  img: string | any // 裁剪图片的地址
+  autoCrop: boolean // 是否默认生成截图框
+  autoCropWidth: number // 默认生成截图框宽度
+  autoCropHeight: number // 默认生成截图框高度
+  fixedBox: boolean // 固定截图框大小 不允许改变
+  fileName: string
+  previews: any // 预览数据
+  outputType: string
+  visible: boolean
+}
+
+const userStore = useUserStore()
+
+// 弹出层
+const open = ref(false)
+
+// 是否显示cropper
+const visible = ref(false)
+
+// 弹出层标题
+const title = ref('修改头像')
+
+const cropper = ref<any>({})
+debugger
+//图片裁剪数据
+const options = reactive<Options>({
+  img: userStore.avatar,
+  autoCrop: true,
+  autoCropWidth: 200,
+  autoCropHeight: 200,
+  fixedBox: true,
+  outputType: 'png',
+  fileName: '',
+  previews: {},
+  visible: false
+})
+
+/** 编辑头像 */
+const editCropper = () => {
+  open.value = true
+}
+/** 打开弹出层结束时的回调 */
+const modalOpened = () => {
+  visible.value = true
+}
+/** 覆盖默认上传行为 */
+// eslint-disable-next-line @typescript-eslint/no-empty-function
+const requestUpload: any = () => {}
+/** 向左旋转 */
+const rotateLeft = () => {
+  cropper.value.rotateLeft()
+}
+/** 向右旋转 */
+const rotateRight = () => {
+  cropper.value.rotateRight()
+}
+/** 图片缩放 */
+const changeScale = (num: number) => {
+  num = num || 1
+  cropper.value.changeScale(num)
+}
+/** 上传预处理 */
+const beforeUpload = (file: UploadRawFile): any => {
+  if (file.type.indexOf('image/') == -1) {
+    ElMessage.error('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。')
+  } else {
+    const reader = new FileReader()
+    reader.readAsDataURL(file)
+    reader.onload = () => {
+      options.img = reader.result
+      options.fileName = file.name
+    }
+  }
+}
+const baseUrl = import.meta.env.VITE_API_URL
+/** 上传图片 */
+const uploadImg = async () => {
+  cropper.value.getCropBlob(async (data: any) => {
+    let formData = new FormData()
+    formData.append('avatarFile', data, options.fileName)
+    const res = await uploadAvatarApi(formData)
+    open.value = false
+    options.img = res.data.imgUrl
+    userStore.avatar = baseUrl + options.img
+    ElMessage.success('修改成功')
+    visible.value = false
+  })
+}
+/** 实时预览 */
+const realTime = (data: any) => {
+  options.previews = data
+}
+/** 关闭窗口 */
+const closeDialog = () => {
+  options.img = userStore.avatar
+  options.visible = false
+}
+</script>
+
+<style lang="scss" scoped>
+.user-info-head {
+  position: relative;
+  display: inline-block;
+
+  // width: 200px;
+  // height: 200px;
+
+  height: 120px;
+}
+.user-info-head:hover::after {
+  position: absolute;
+  inset: 0;
+  font-size: 24px;
+  font-style: normal;
+  line-height: 110px;
+  color: #eeeeee;
+  cursor: pointer;
+  content: '+';
+  background: rgb(0 0 0 / 50%);
+  border-radius: 50%;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+</style>

+ 10 - 0
yarn.lock

@@ -4463,6 +4463,11 @@ ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.0:
   resolved "https://registry.npmmirror.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
   integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
 
+image-conversion@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.npmmirror.com/image-conversion/-/image-conversion-2.1.1.tgz#8213fc19033f7dfdee20bb19792e8dcebd052417"
+  integrity sha512-hnMOmP7q2jxA+52FZ+wHNhg3fdFRlgfngsQH2JQHEQkafY7tj/8F15e6Rv/RxDegc872jvyaRHwMbkTZK1Cjbg==
+
 image-size@^0.5.1:
   version "0.5.5"
   resolved "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
@@ -7610,6 +7615,11 @@ vite@^5.0.8:
   optionalDependencies:
     fsevents "~2.3.3"
 
+vue-cropper@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.npmmirror.com/vue-cropper/-/vue-cropper-1.1.3.tgz#0664fc115138ee60afcbb4f72419f116ef68c19b"
+  integrity sha512-U1vBk/9M9Chp6iDWaDhC32SX7c5ndJrIzYgXndJH7wjejdriE0bzJsh6waQz9CRM94savFAw8FK1Q3r+I71Xgw==
+
 vue-demi@*, vue-demi@>=0.14.5, vue-demi@>=0.14.6:
   version "0.14.6"
   resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92"