Pārlūkot izejas kodu

实现个人更新上传头像和修改密码功能
更新国际化布局界面内容

zhontai 2 gadi atpakaļ
vecāks
revīzija
c6408c7604

+ 1 - 1
.env.development

@@ -2,4 +2,4 @@
 ENV = 'development'
 
 # 本地环境接口地址
-VITE_API_URL = 'http://localhost:8100/'
+VITE_API_URL = 'http://localhost:8000/'

+ 1 - 1
.env.production

@@ -2,4 +2,4 @@
 ENV = 'production'
 
 # 线上环境接口地址
-VITE_API_URL = 'http://localhost:8100/'
+VITE_API_URL = 'http://localhost:8000/'

+ 1 - 1
gen/gen-admin-api.js

@@ -1,6 +1,6 @@
 const { generateApi } = require('swagger-typescript-api')
 const path = require('path')
-const fs = require('fs')
+// const fs = require('fs')
 
 const apis = [
   {

+ 1 - 1
gen/gen-templates.js

@@ -1,7 +1,7 @@
 const { generateTemplates } = require('swagger-typescript-api')
 const path = require('path')
 
-//导出默认模板
+//导出swagger-typescript-api内置模板
 generateTemplates({
   cleanOutput: false,
   output: path.resolve(__dirname, './templates'),

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
src/assets/login-main.svg


+ 9 - 9
src/i18n/lang/zh-cn.ts

@@ -23,7 +23,7 @@ export default {
     menu13: '菜单13',
     menu2: '菜单2',
     funIndex: '功能',
-    funTagsView: 'tagsView 操作',
+    funTagsView: '标签页操作',
     funCountup: '数字滚动',
     funWangEditor: 'Editor 编辑器',
     funCropper: '图片裁剪',
@@ -153,13 +153,13 @@ export default {
     threeLockScreenTime: '自动锁屏(s/秒)',
     fourTitle: '界面显示',
     fourIsShowLogo: '侧边栏 Logo',
-    fourIsBreadcrumb: '开启 Breadcrumb',
-    fourIsBreadcrumbIcon: '开启 Breadcrumb 图标',
-    fourIsTagsview: '开启 Tagsview',
-    fourIsTagsviewIcon: '开启 Tagsview 图标',
-    fourIsCacheTagsView: '开启 TagsView 缓存',
-    fourIsSortableTagsView: '开启 TagsView 拖拽',
-    fourIsShareTagsView: '开启 TagsView 共用',
+    fourIsBreadcrumb: '开启面包屑',
+    fourIsBreadcrumbIcon: '开启面包屑图标',
+    fourIsTagsview: '开启标签页',
+    fourIsTagsviewIcon: '开启标签页图标',
+    fourIsCacheTagsView: '开启标签页缓存',
+    fourIsSortableTagsView: '开启标签页拖拽',
+    fourIsShareTagsView: '开启标签页共用',
     fourIsFooter: '开启 Footer',
     fourIsGrayscale: '灰色模式',
     fourIsInvert: '色弱模式',
@@ -167,7 +167,7 @@ export default {
     fourIsWartermark: '开启水印',
     fourWartermarkText: '水印文案',
     fiveTitle: '其它设置',
-    fiveTagsStyle: 'Tagsview 风格',
+    fiveTagsStyle: '标签页风格',
     fiveAnimation: '主页面切换动画',
     fiveColumnsAsideStyle: '分栏高亮风格',
     fiveColumnsAsideLayout: '分栏布局风格',

+ 8 - 8
src/i18n/lang/zh-tw.ts

@@ -108,7 +108,7 @@ export default {
     logOutCancel: '取消',
     logOutExit: '退出中',
   },
-  tagsView: {
+  標籤頁: {
     refresh: '重繪',
     close: '關閉',
     closeOther: '關閉其它',
@@ -153,13 +153,13 @@ export default {
     threeLockScreenTime: '自動鎖屏(s/秒)',
     fourTitle: '介面顯示',
     fourIsShowLogo: '側邊欄 Logo',
-    fourIsBreadcrumb: '開啟 Breadcrumb',
-    fourIsBreadcrumbIcon: '開啟 Breadcrumb 圖標',
-    fourIsTagsview: '開啟 Tagsview',
-    fourIsTagsviewIcon: '開啟 Tagsview 圖標',
-    fourIsCacheTagsView: '開啟 TagsView 緩存',
-    fourIsSortableTagsView: '開啟 TagsView 拖拽',
-    fourIsShareTagsView: '開啟 TagsView 共用',
+    fourIsBreadcrumb: '開啟麵包屑',
+    fourIsBreadcrumbIcon: '開啟麵包屑圖標',
+    fourIsTagsview: '開啟標籤頁',
+    fourIsTagsviewIcon: '開啟標籤頁圖標',
+    fourIsCacheTagsView: '開啟標籤頁緩存',
+    fourIsSortableTagsView: '開啟標籤頁拖拽',
+    fourIsShareTagsView: '開啟標籤頁共用',
     fourIsFooter: '開啟 Footer',
     fourIsGrayscale: '灰色模式',
     fourIsInvert: '色弱模式',

+ 8 - 1
src/layout/navBars/breadcrumb/setings.vue

@@ -323,7 +323,14 @@
         <div class="layout-breadcrumb-seting-bar-flex mt15">
           <div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fiveAnimation') }}</div>
           <div class="layout-breadcrumb-seting-bar-flex-value">
-            <el-select v-model="getThemeConfig.animation" placeholder="请选择" size="default" style="width: 90px" @change="setLocalThemeConfig">
+            <el-select
+              v-model="getThemeConfig.animation"
+              placeholder="请选择"
+              placement="bottom-end"
+              size="default"
+              style="width: 90px"
+              @change="setLocalThemeConfig"
+            >
               <el-option label="slide-right" value="slide-right"></el-option>
               <el-option label="slide-left" value="slide-left"></el-option>
               <el-option label="opacitys" value="opacitys"></el-option>

+ 9 - 1
src/layout/navBars/breadcrumb/user.vue

@@ -55,7 +55,7 @@
     </div>
     <el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
       <span class="layout-navbars-breadcrumb-user-link">
-        <img :src="userInfos.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
+        <img :src="avatar" class="layout-navbars-breadcrumb-user-link-photo mr5" />
         {{ userInfos.userName === '' ? 'common' : userInfos.userName }}
         <el-icon class="el-icon--right">
           <ele-ArrowDown />
@@ -105,6 +105,14 @@ const state = reactive({
   disabledSize: 'large',
 })
 
+// 头像地址
+const avatar = computed(() => {
+  return (
+    (userInfos.value.photo && `${import.meta.env.VITE_API_URL}upload/admin/avatar/${userInfos.value.photo}`) ||
+    'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500'
+  )
+})
+
 // 设置分割样式
 const layoutUserFlexNum = computed(() => {
   let num: string | number = ''

+ 1 - 1
src/stores/userInfo.ts

@@ -45,7 +45,7 @@ export const useUserInfo = defineStore('userInfo', {
               const user = res.data?.user
               const userInfos = {
                 userName: user?.nickName || user?.name,
-                photo: user?.avatar ? user?.avatar : '/favicon.ico',
+                photo: user?.avatar ? user?.avatar : '',
                 time: new Date().getTime(),
                 roles: [],
                 authBtnList: res.data?.permissions,

+ 126 - 0
src/views/admin/personal/components/change-password-form.vue

@@ -0,0 +1,126 @@
+<template>
+  <div>
+    <el-dialog v-model="state.showDialog" destroy-on-close :title="title" draggable width="375px">
+      <el-form ref="formRef" :model="form" size="default" label-width="80px" label-position="left">
+        <el-row :gutter="35">
+          <el-col :span="24">
+            <el-form-item label="旧密码" prop="oldPassword" :rules="[{ required: true, message: '请输入旧密码', trigger: ['blur', 'change'] }]">
+              <el-input v-model="form.oldPassword" show-password autocomplete="off" clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item
+              label="新密码"
+              prop="newPassword"
+              :rules="[
+                { required: true, message: '请输入新密码', trigger: ['blur', 'change'] },
+                { validator: testNewPassword, trigger: ['blur', 'change'] },
+              ]"
+            >
+              <el-input v-model="form.newPassword" show-password autocomplete="off" clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item
+              label="确认密码"
+              prop="confirmPassword"
+              :rules="[
+                { required: true, message: '请输入确认密码', trigger: ['blur', 'change'] },
+                { validator: testConfirmPassword, trigger: ['blur', 'change'] },
+              ]"
+            >
+              <el-input v-model="form.confirmPassword" show-password autocomplete="off" clearable />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="onCancel" size="default">取 消</el-button>
+          <el-button type="primary" @click="onSure" size="default" :loading="state.sureLoading">确 定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { reactive, toRefs, ref } from 'vue'
+import { UserChangePasswordInput } from '/@/api/admin/data-contracts'
+import { User as UserApi } from '/@/api/admin/User'
+
+defineProps({
+  title: {
+    type: String,
+    default: '',
+  },
+})
+
+const formRef = ref()
+const state = reactive({
+  showDialog: false,
+  sureLoading: false,
+  form: {} as UserChangePasswordInput,
+})
+const { form } = toRefs(state)
+
+// 新密码验证器
+const testNewPassword = (rule: any, value: any, callback: any) => {
+  if (value) {
+    if (state.form.confirmPassword !== '') {
+      formRef.value.validateField('confirmPassword')
+    }
+    callback()
+  }
+}
+
+// 确认密码验证器
+const testConfirmPassword = (rule: any, value: any, callback: any) => {
+  if (value) {
+    if (value !== state.form.newPassword) {
+      callback(new Error('新密码和确认密码不一致!'))
+    } else {
+      callback()
+    }
+  }
+}
+
+// 打开对话框
+const open = async () => {
+  state.showDialog = true
+}
+
+// 取消
+const onCancel = () => {
+  state.showDialog = false
+}
+
+// 确定
+const onSure = () => {
+  formRef.value.validate(async (valid: boolean) => {
+    if (!valid) return
+
+    state.sureLoading = true
+    const res = await new UserApi().changePassword(state.form, { showSuccessMessage: true }).catch(() => {
+      state.sureLoading = false
+    })
+    state.sureLoading = false
+
+    if (res?.success) {
+      state.showDialog = false
+    }
+  })
+}
+
+defineExpose({
+  open,
+})
+</script>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  name: 'admin/personal/change-password-form',
+})
+</script>

+ 77 - 6
src/views/admin/personal/index.vue

@@ -6,8 +6,21 @@
         <el-card shadow="hover" header="个人信息">
           <div class="personal-user">
             <div class="personal-user-left">
-              <el-upload class="h100 personal-user-left-upload" action="https://jsonplaceholder.typicode.com/posts/" multiple :limit="1">
-                <img src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" />
+              <el-upload
+                class="h100 personal-user-left-upload"
+                :action="avatarAction"
+                :headers="avatarHeaders"
+                :data="{ autoUpdate: true }"
+                :show-file-list="false"
+                :before-upload="
+                  () => {
+                    state.avatarLoading = true
+                  }
+                "
+                :on-success="onAvatarSuccess"
+                :on-error="onAvatarError"
+              >
+                <img :src="avatar" />
               </el-upload>
             </div>
             <div class="personal-user-right">
@@ -88,7 +101,7 @@
                 <div class="personal-edit-safe-item-left-value">当前密码强度:强</div>
               </div>
               <div class="personal-edit-safe-item-right">
-                <el-button text type="primary">立即修改</el-button>
+                <el-button text type="primary" @click="onChangePassword">立即修改</el-button>
               </div>
             </div>
           </div>
@@ -117,16 +130,24 @@
         </el-card>
       </el-col>
     </el-row>
+
+    <change-password-form ref="changePasswordFormRef" title="修改密码"></change-password-form>
   </div>
 </template>
 
 <script setup lang="ts" name="personal">
-import { reactive, computed, onMounted, toRefs, ref } from 'vue'
+import { reactive, computed, onMounted, toRefs, ref, getCurrentInstance } from 'vue'
 import { formatAxis } from '/@/utils/formatTime'
 import { User as UserApi } from '/@/api/admin/User'
 import { UserGetBasicOutput } from '/@/api/admin/data-contracts'
 import { useUserInfo } from '/@/stores/userInfo'
 import pinia from '/@/stores/index'
+import { storeToRefs } from 'pinia'
+import { getToken } from '/@/api/admin/http-client'
+import { AxiosResponse } from 'axios'
+import ChangePasswordForm from './components/change-password-form.vue'
+
+const { proxy } = getCurrentInstance() as any
 
 // 定义变量内容
 const state = reactive({
@@ -134,6 +155,7 @@ const state = reactive({
   newsInfoList: [] as any,
   recommendList: [] as any,
   personalInfo: {
+    avatar: '',
     mobile: '',
     email: '',
     name: '',
@@ -143,17 +165,39 @@ const state = reactive({
     name: '',
     nickName: '',
   },
+  avatarLoading: false,
   updateLoading: false,
 })
 
+const changePasswordFormRef = ref()
 const formRef = ref()
 const { personalInfo, personalForm } = toRefs(state)
+const storesUserInfo = useUserInfo(pinia)
+const { userInfos } = storeToRefs(storesUserInfo)
 
 // 当前时间提示语
 const currentTime = computed(() => {
   return formatAxis(new Date())
 })
 
+// 上传头像请求头部
+const avatarHeaders = computed(() => {
+  return { Authorization: 'Bearer ' + getToken() }
+})
+
+// 头像地址
+const avatar = computed(() => {
+  return (
+    (userInfos.value.photo && `${import.meta.env.VITE_API_URL}upload/admin/avatar/${userInfos.value.photo}`) ||
+    'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500'
+  )
+})
+
+// 上传头像请求url
+const avatarAction = computed(() => {
+  return import.meta.env.VITE_API_URL + 'api/admin/user/avatar-upload'
+})
+
 onMounted(() => {
   initData()
 })
@@ -170,6 +214,30 @@ const initData = async () => {
   state.loading = false
 }
 
+// 上传头像成功
+const onAvatarSuccess = (res: AxiosResponse) => {
+  state.avatarLoading = false
+  if (!res?.success) {
+    if (res.msg) {
+      proxy.$modal.msgError(res.msg)
+    }
+    return
+  }
+  state.personalInfo.avatar = res.data
+  storesUserInfo.setPhoto(res.data)
+}
+
+// 上传头像失败
+const onAvatarError = (err: any) => {
+  state.avatarLoading = false
+  if (err.message) {
+    const res = JSON.parse(err.message) as AxiosResponse
+    if (!res?.success && res.msg) {
+      proxy.$modal.msgError(res.msg)
+    }
+  }
+}
+
 // 更新个人信息
 const onUpdateBasic = async () => {
   formRef.value.validate(async (valid: boolean) => {
@@ -177,17 +245,20 @@ const onUpdateBasic = async () => {
 
     state.updateLoading = true
     const res = await new UserApi().updateBasic(state.personalForm, { showSuccessMessage: true })
-
     state.updateLoading = false
 
     if (res?.success) {
       state.personalInfo.nickName = state.personalForm.nickName
       state.personalInfo.name = state.personalForm.name
-      const storesUserInfo = useUserInfo(pinia)
       storesUserInfo.setUserName(state.personalForm.nickName || state.personalForm.name)
     }
   })
 }
+
+// 修改密码
+const onChangePassword = () => {
+  changePasswordFormRef.value.open()
+}
 </script>
 
 <style scoped lang="scss">

+ 0 - 102
src/views/viewtemplate/components/view-form.vue

@@ -1,102 +0,0 @@
-<template>
-  <div>
-    <el-dialog v-model="state.showDialog" destroy-on-close :title="title" :close-on-click-modal="false" draggable width="600px">
-      <el-form :model="form" ref="formRef" size="default" label-width="80px">
-        <el-row :gutter="35">
-          <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-            <el-form-item label="" prop="" :rules="[]"> </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-      <template #footer>
-        <span class="dialog-footer">
-          <el-button @click="onCancel" size="default">取 消</el-button>
-          <el-button type="primary" @click="onSure" size="default" :loading="state.sureLoading">确 定</el-button>
-        </span>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { getCurrentInstance, ref, reactive, toRefs } from 'vue'
-
-defineProps({
-  title: {
-    type: String,
-    default: '',
-  },
-})
-
-const { proxy } = getCurrentInstance() as any
-
-const formRef = ref()
-
-const state = reactive({
-  showDialog: false,
-  sureLoading: false,
-  form: {} as any,
-})
-
-const { form } = toRefs(state)
-
-// 打开对话框
-const open = async (row: any = {}) => {
-  proxy.$modal.loading()
-
-  if (row.id > 0) {
-    const res = {} as any
-
-    if (res?.success) {
-      state.form = res.data as any
-    }
-  } else {
-    state.form = {} as any
-  }
-
-  proxy.$modal.closeLoading()
-  state.showDialog = true
-}
-
-// 取消
-const onCancel = () => {
-  state.showDialog = false
-}
-
-// 确定
-const onSure = () => {
-  formRef.value.validate(async (valid: boolean) => {
-    if (!valid) return
-
-    state.sureLoading = true
-    let res = {} as any
-
-    if (state.form.id != undefined && state.form.id > 0) {
-      res = {} as any
-    } else {
-      res = {} as any
-    }
-
-    state.sureLoading = false
-
-    if (res?.success) {
-      proxy.eventBus.emit('refresh')
-      state.showDialog = false
-    }
-  })
-}
-
-defineExpose({
-  open,
-})
-</script>
-
-<script lang="ts">
-import { defineComponent } from 'vue'
-
-export default defineComponent({
-  name: '',
-})
-</script>
-
-<style lang="scss" scoped></style>

+ 0 - 131
src/views/viewtemplate/index.vue

@@ -1,131 +0,0 @@
-<template>
-  <div style="padding: 0px 0px 8px 8px">
-    <el-row :gutter="8" style="width: 100%">
-      <el-col :span="24" :xs="24">
-        <el-card shadow="never" :body-style="{ paddingBottom: '0' }" style="margin-top: 8px">
-          <el-form :inline="true">
-            <el-form-item>
-              <el-button type="primary" icon="ele-Search" @click="onQuery"> 查询 </el-button>
-              <el-button type="primary" icon="ele-Plus" @click="onAdd"> 新增 </el-button>
-            </el-form-item>
-          </el-form>
-        </el-card>
-
-        <el-card shadow="never" style="margin-top: 8px">
-          <el-table :data="state.listData" style="width: 100%" v-loading="state.loading" row-key="id">
-            <el-table-column label="" prop="" min-width="120" show-overflow-tooltip />
-            <el-table-column label="操作" width="160" fixed="right" header-align="center" align="center">
-              <template #default="{ row }">
-                <my-dropdown-more>
-                  <template #dropdown>
-                    <el-dropdown-menu>
-                      <el-dropdown-item @click="onEdit(row)">编辑</el-dropdown-item>
-                      <el-dropdown-item @click="onDelete(row)">删除</el-dropdown-item>
-                    </el-dropdown-menu>
-                  </template>
-                </my-dropdown-more>
-              </template>
-            </el-table-column>
-          </el-table>
-          <div class="my-flex my-flex-end view-info-page" style="margin-top: 20px">
-            <el-pagination
-              v-model:currentPage="state.pageInput.currentPage"
-              v-model:page-size="state.pageInput.pageSize"
-              :total="state.total"
-              :page-sizes="state.pageSize"
-              :layout="state.layout"
-              @size-change="onSizeChange"
-              @current-change="onCurrentChange"
-            ></el-pagination>
-          </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <view-form ref="viewFormRef" :title="state.viewFormTitle"></view-form>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { getCurrentInstance, onMounted, onUnmounted, reactive, ref } from 'vue'
-import ViewForm from './components/view-form.vue'
-import MyDropdownMore from '/@/components/my-dropdown-more/index.vue'
-
-const { proxy } = getCurrentInstance() as any
-
-const viewFormRef = ref()
-
-const state = reactive({
-  loading: false,
-  viewFormTitle: '',
-  total: 0,
-  pageSize: [10, 20, 50, 100],
-  layout: 'total, sizes, prev, pager, next',
-  pageInput: {
-    currentPage: 1,
-    pageSize: 10,
-  },
-  listData: [],
-})
-
-onMounted(() => {
-  onQuery()
-  proxy.eventBus.on('refresh', async () => {
-    onQuery()
-  })
-})
-
-onUnmounted(() => {
-  proxy.eventBus.off('refresh')
-})
-
-const onQuery = async () => {
-  state.loading = true
-
-  const res = {} as any
-
-  state.listData = res?.data?.list ?? []
-  state.total = res?.data?.total ?? 0
-
-  state.loading = false
-}
-
-const onAdd = () => {
-  state.viewFormTitle = '新增'
-  viewFormRef.value.open()
-}
-
-const onEdit = (row: any) => {
-  state.viewFormTitle = '编辑'
-  viewFormRef.value.open(row)
-}
-
-const onDelete = (row: any) => {
-  proxy.$modal
-    .confirmDelete(`确定要删除?`)
-    .then(async () => {
-      onQuery()
-    })
-    .catch(() => {})
-}
-
-const onSizeChange = (val: number) => {
-  state.pageInput.pageSize = val
-  onQuery()
-}
-
-const onCurrentChange = (val: number) => {
-  state.pageInput.currentPage = val
-  onQuery()
-}
-</script>
-
-<script lang="ts">
-import { defineComponent } from 'vue'
-
-export default defineComponent({
-  name: '',
-})
-</script>
-
-<style lang="scss" scoped></style>

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels