Kaynağa Gözat

feature(培训考试):微信jssdk认证对接

wanglj 9 ay önce
ebeveyn
işleme
9ad67f732f

+ 1 - 1
.env.development

@@ -11,7 +11,7 @@
 ENV = development
 # 本地环境接口地址
 VITE_API_URL = http://192.168.0.216:9954/
-VITE_API_COMMON = /wechat/access-token
+VITE_API_WECHAT = /wechat/
 VITE_TENANT = default
 
 # 微服务地址

+ 1 - 1
.env.production

@@ -12,7 +12,7 @@ ENV = production
 
 # 线上环境接口地址
 VITE_API_URL = /api/
-VITE_API_COMMON = /wechat/access-token
+VITE_API_WECHAT = /wechat/
 VITE_TENANT = default
 
 # 微服务地址

+ 0 - 1
components.d.ts

@@ -20,7 +20,6 @@ declare module 'vue' {
     VanForm: typeof import('vant/es')['Form']
     VanIcon: typeof import('vant/es')['Icon']
     VanList: typeof import('vant/es')['List']
-    VanNavBar: typeof import('vant/es')['NavBar']
     VanNotify: typeof import('vant/es')['Notify']
     VanPicker: typeof import('vant/es')['Picker']
     VanPickerGroup: typeof import('vant/es')['PickerGroup']

+ 6 - 2
src/App.vue

@@ -3,13 +3,17 @@
     <router-view />
     <van-notify v-model:show="show" type="success">
       <van-icon name="bell" style="margin-right: 4px" />
-      <span>通知内容</span>
+      <span>1234</span>
     </van-notify>
   </div>
 </template>
 <script setup lang="ts">
-  import { ref } from 'vue'
+  import { ref, onMounted } from 'vue'
+  import { useUserInfo } from '/@/stores/userInfo'
   const show = ref(false)
+  onMounted(() => {
+    useUserInfo().getSdkConfig()
+  })
 </script>
 <style>
   body {

+ 4 - 1
src/api/system/index.ts

@@ -18,7 +18,10 @@ export function useSystemApi() {
       return request.postRequest(basePath, 'User', 'GetUserByUserName', query)
     },
     getOpenId(query?: object) {
-      return requestCommon.post(commonPath, query)
+      return requestCommon.post('/access-token', query)
+    },
+    getSDKTicket(query?: object) {
+      return requestCommon.post('/ticket', query)
     }
   }
 }

+ 113 - 0
src/api/system/user.ts

@@ -0,0 +1,113 @@
+/*
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2023-07-19 13:42:40
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-03-19 09:54:45
+ * @Description: file content
+ * @FilePath: \labsop_meno\frontend\packages\base\src\api\system\user.ts
+ */
+import request from '/@/utils/micro_request.js'
+const basePath = import.meta.env.VITE_ADMIN
+const insrtPath = import.meta.env.VITE_INSTR_ADMIN
+// 日程管理
+export function useUserApi() {
+  return {
+    // 创建
+    createUser(query?: object) {
+      return request.postRequest(basePath, 'User', 'Create', query)
+    },
+    // 更新
+    updateUser(query?: object) {
+      return request.postRequest(basePath, 'User', 'UpdateById', query)
+    },
+    // 列表
+    getUserList(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetList', query)
+    },
+    // 删除
+    deleteUser(query?: object) {
+      return request.postRequest(basePath, 'User', 'DeleteByIds', query)
+    },
+    // 详情
+    getUserEntity(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetEntityById', query)
+    },
+    // 详情
+    getUserInfo(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetUserInfo', query)
+    },
+    // 修改密码
+    changePassword(query?: object) {
+      return request.postRequest(basePath, 'User', 'ChangePassword', query)
+    },
+    // 重置密码
+    resetPassword(query?: object) {
+      return request.postRequest(basePath, 'User', 'ResetPassword', query)
+    },
+    // 更新状态
+    setUserStatus(query?: object) {
+      return request.postRequest(basePath, 'User', 'SetStatus', query)
+    },
+    // 根据用户名获取用户信息
+    getUserByUserName(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetUserByUserName', query)
+    },
+    // 下载用户模板
+    downloadTmpl(query?: object) {
+      return request.postRequest(basePath, 'User', 'ExcelTemplate', query)
+    },
+    // 上传用户模板
+    uploadTmpl(query?: object) {
+      return request.postRequest(basePath, 'User', 'ExcelImport', query)
+    },
+    // 获取用户字典
+    getUserDict(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetDictList', query);
+    },
+    // 首页注册
+    indexRegister(query?: object) {
+      return request.postRequest(basePath, 'System', 'Register', query)
+    },
+    // 用户审批
+    registerUserApproval(query?: object) {
+      return request.postRequest(basePath, 'User', 'RegisterUserApproval', query)
+    },
+    // 发送邮件重置密码
+    sendResetPasswordEmail(query?: object) {
+      return request.postRequest(basePath, 'User', 'SendResetPasswordEmail', query)
+    },
+    // 邮箱链接重置密码
+    resetPasswordFromEmail(query?: object) {
+      return request.postRequest(basePath, 'User', 'ResetPasswordFromEmail', query)
+    },
+    // 根据字典类型获取字典项明细
+    getDictDataByType(str: string) {
+      return request.postRequest(basePath, 'Dict', 'GetDictDataByType', { dictType: str });
+    },
+    // 个人中心获取用户基础信息
+    getProfile(query?: object) {
+      return request.postRequest(basePath, 'User', 'GetProfile', query)
+    },
+    // 个人中心更新用户信息
+    updateProfile(query?: object) {
+      return request.postRequest(basePath, 'User', 'UpdateProfile', query)
+    },
+    // 个人中心用户修改头像
+    setAvatar(query?: object) {
+      return request.postRequest(basePath, 'User', 'SetAvatar', query)
+    },
+    // 个人中心信用分
+    getScore(query?: object) {
+      return request.postRequest(insrtPath, 'TusBlacklist', 'MyScore', query)
+    },
+    // 用户类型
+    getUserTypeList(query?: object) {
+      return request.postRequest(basePath, 'UserType', 'GetUserTypeList', query)
+    },
+     // 用户类型
+     getUserType10List(query?: object) {
+      return request.postRequest(basePath, 'UserType', 'GetUserType10List', query)
+    },
+  }
+}
+

+ 12 - 34
src/layout/index.vue

@@ -2,7 +2,7 @@
  * @Author: wanglj wanglijie@dashoo.cn
  * @Date: 2025-03-13 09:07:55
  * @LastEditors: wanglj wanglijie@dashoo.cn
- * @LastEditTime: 2025-03-18 15:13:14
+ * @LastEditTime: 2025-03-19 15:36:25
  * @FilePath: \labsop-h5\src\layout\index.vue
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 -->
@@ -16,7 +16,7 @@
         </div>
         首页
       </li>
-      <li class="service">
+      <li class="service" @click="onRouterPush('/service')">
         <div class="img-container">
           <img src="../assets/img/service-icon.png" alt="" />
         </div>
@@ -42,46 +42,24 @@
 </template>
 
 <script lang="ts" setup>
+  import to from 'await-to-js'
   import { showDialog } from 'vant'
-import { ref } from 'vue'
+  import { ref } from 'vue'
   import { useRouter } from 'vue-router'
-  import wx from 'weixin-js-sdk'
+  import { useUserInfo } from '/@/stores/userInfo'
+  import { Local } from '/@/utils/storage'
+
   const active = ref(0)
   const router = useRouter()
-  const initSDK = () => {
-    wx.config({
-      debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
-      appId: '', // 必填,公众号的唯一标识
-      timestamp: 0, // 必填,生成签名的时间戳
-      nonceStr: '', // 必填,生成签名的随机串
-      signature: '', // 必填,签名
-      jsApiList: ['scanQRCode'] // 必填,需要使用的JS接口列表
-    })
-    wx.ready(function () {
-      // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
-    })
-    wx.error(function (res: any) {
-      // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
-    })
-  }
 
   const onRouterPush = (val: string) => {
     router.push(val)
   }
-  const scan = () => {
-    wx.scanQRCode({
-      needResult: 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
-      scanType: ['qrCode', 'barCode'], // 可以指定扫二维码还是一维码,默认二者都有
-      success: function (res: any) {
-        var result = res.resultStr // 当needResult 为 1 时,扫码返回的结果
-        console.log(result, 'result')
-        showDialog({
-          title: '提示',
-          message: result,
-          showCancelButton: false
-        })
-      }
-    })
+  const scan = async () => {
+    const res = await useUserInfo().scanCode()
+    if (res) {
+      window.location.href = res as string
+    }
   }
 </script>
 

+ 34 - 3
src/router.ts

@@ -2,7 +2,7 @@
  * @Author: wanglj wanglijie@dashoo.cn
  * @Date: 2025-03-10 11:40:15
  * @LastEditors: wanglj wanglijie@dashoo.cn
- * @LastEditTime: 2025-03-18 18:12:41
+ * @LastEditTime: 2025-03-19 15:35:49
  * @FilePath: \vue3-ts\src\router.ts
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -46,6 +46,14 @@ const routes = [
           title: '首页'
         }
       },
+      {
+        name: 'service',
+        path: '/service',
+        component: () => import('/@/view/service/index.vue'),
+        meta: {
+          title: '服务'
+        }
+      },
       {
         name: 'todo',
         path: '/todo',
@@ -56,12 +64,20 @@ const routes = [
       },
       {
         name: 'approvalDetail',
-        path: '/approval-detail',
+        path: '/todo/approval-detail',
         component: () => import('/@/view/todo/approval-detail.vue'),
         meta: {
           title: '审批详情'
         }
       },
+      {
+        name: 'noticeDetail',
+        path: '/todo/notice-detail',
+        component: () => import('/@/view/todo/notice-detail.vue'),
+        meta: {
+          title: '通知详情'
+        }
+      },
       {
         name: 'user',
         path: '/user',
@@ -70,6 +86,14 @@ const routes = [
           title: '个人中心'
         }
       },
+      {
+        name: 'userEdit',
+        path: '/user/edit',
+        component: () => import('/@/view/user/edit.vue'),
+        meta: {
+          title: '个人信息编辑'
+        }
+      },
       {
         name: 'training',
         path: '/training',
@@ -112,6 +136,14 @@ const router = createRouter({
 })
 const whiteList = ['/login', '/register', '/training', '/training/enroll']
 router.beforeEach((to, from, next) => {
+  const storesUseUserInfo = useUserInfo()
+  // 微信授权码获取openId
+  const code = to.query.code
+  if (!storesUseUserInfo.openId && code) {
+    if (typeof code === 'string') {
+      storesUseUserInfo.setOpenId(code)
+    }
+  }
   const title = to?.meta?.title
   if (title) {
     document.title = title as string
@@ -123,7 +155,6 @@ router.beforeEach((to, from, next) => {
     if (!token) {
       next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`)
     } else {
-      const storesUseUserInfo = useUserInfo()
       if (!storesUseUserInfo.userInfos.id) {
         storesUseUserInfo.setUserInfos()
       }

+ 90 - 36
src/stores/userInfo.ts

@@ -2,16 +2,17 @@
  * @Author: wanglj wanglijie@dashoo.cn
  * @Date: 2025-03-17 14:46:02
  * @LastEditors: wanglj wanglijie@dashoo.cn
- * @LastEditTime: 2025-03-18 19:50:12
+ * @LastEditTime: 2025-03-19 15:18:54
  * @FilePath: \labsop-h5\src\view\stores\userInfo.ts
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
-import { defineStore } from 'pinia';
-import { useSystemApi } from '/@/api/system/index';
+import { defineStore } from 'pinia'
+import { useSystemApi } from '/@/api/system/index'
 
-import to from 'await-to-js';
-import { showNotify } from 'vant';
-const sysApi = useSystemApi();
+import to from 'await-to-js'
+import { showDialog, showNotify } from 'vant'
+import wx from 'weixin-js-sdk'
+const sysApi = useSystemApi()
 
 /**
  * 用户信息
@@ -21,22 +22,36 @@ export const useUserInfo = defineStore('userInfo', {
   state: (): UserInfosState => ({
     userInfos: {
       id: 0,
+      avatar: '',
+      userName: '',
       nickName: '',
       sex: '',
-      userName: '',
-      photo: '',
       phone: '',
+      photo: '',
       time: 0,
       roles: [],
       authBtnList: [],
+      deptName: '',
+      deptId: '',
+      userRoles: '',
+      userRoleNames: '',
+      userType: '',
+      creditScore: 0,
+      pgName: ''
     },
-    openId: ''
+    openId: '',
+    sdkConfig: {
+      app_id: '',
+      timestamp: 0,
+      nonce_str: '',
+      signature: ''
+    }
   }),
   actions: {
     async setUserInfos() {
       // 存储用户信息到浏览器缓存
-      const userInfos = <UserInfos>await this.getApiUserInfo();
-      this.userInfos = userInfos;
+      const userInfos = <UserInfos>await this.getApiUserInfo()
+      this.userInfos = userInfos
     },
     // 模拟接口数据
     // https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
@@ -44,32 +59,34 @@ export const useUserInfo = defineStore('userInfo', {
       // eslint-disable-next-line no-async-promise-executor
       return new Promise(async (resolve) => {
         // 模拟数据,请求接口时,记得删除多余代码及对应依赖的引入
-        const [err, res]: ToResponse = await to(sysApi.getUserByUserName());
-        if (err) return;
-        const { userInfo, perms, roles, projectGroupRes } = res?.data;
+        const [err, res]: ToResponse = await to(sysApi.getUserByUserName())
+        if (err) return
+        const { userInfo, perms, roles, projectGroupRes } = res?.data
         // 模拟数据
-        let defaultRoles: Array<string> = [];
-        let defaultAuthBtnList: Array<string> = [];
+        let defaultRoles: Array<string> = []
+        let defaultAuthBtnList: Array<string> = []
         // admin 页面权限标识,对应路由 meta.roles,用于控制路由的显示/隐藏
-        let adminRoles: Array<string> = ['admin'];
+        let adminRoles: Array<string> = ['admin']
         // admin 按钮权限标识
-        let adminAuthBtnList: Array<string> = ['btn.add', 'btn.del', 'btn.edit', 'btn.link'];
+        let adminAuthBtnList: Array<string> = ['btn.add', 'btn.del', 'btn.edit', 'btn.link']
         // test 页面权限标识,对应路由 meta.roles,用于控制路由的显示/隐藏
-        let testRoles: Array<string> = ['common'];
+        let testRoles: Array<string> = ['common']
         // test 按钮权限标识
-        let testAuthBtnList: Array<string> = ['btn.add', 'btn.link'];
+        let testAuthBtnList: Array<string> = ['btn.add', 'btn.link']
         // 不同用户模拟不同的用户权限
         if (userInfo.userName === 'admin') {
-          defaultRoles = adminRoles;
-          defaultAuthBtnList = adminAuthBtnList;
+          defaultRoles = adminRoles
+          defaultAuthBtnList = adminAuthBtnList
         } else {
-          defaultRoles = testRoles;
-          defaultAuthBtnList = testAuthBtnList;
+          defaultRoles = testRoles
+          defaultAuthBtnList = testAuthBtnList
         }
-        const userRoles = roles?.map((item: RowRoleType) => item.roleCode);
+        const userRoles = roles?.map((item: RowRoleType) => item.roleCode)
+        const userRoleNames = roles?.map((item: RowRoleType) => item.roleName)
         // 用户信息模拟数据
         const userInfos = {
           id: userInfo.id,
+          avatar: userInfo.avatar,
           userName: userInfo.userName,
           nickName: userInfo.nickName,
           sex: userInfo.sex,
@@ -81,20 +98,57 @@ export const useUserInfo = defineStore('userInfo', {
           deptName: userInfo.deptName,
           deptId: userInfo.deptId,
           userRoles: userRoles?.join() || '',
+          userRoleNames: userRoleNames.join('、'),
           userType: userInfo.userType,
-          projectGroupRes
-        };
-        resolve(userInfos);
-      });
+          creditScore: userInfo.creditScore,
+          pgName: projectGroupRes.pgName
+        }
+        resolve(userInfos)
+      })
     },
     async setOpenId(code: string) {
       const [err, res]: ToResponse = await to(sysApi.getOpenId({ code }))
-      if(err || res?.data?.code != '200') return
-      // showNotify({
-      //   message: code + JSON.stringify(res.data),
-      //   type: 'success'
-      // })
+      if (err || res?.data?.code != '200') return
       this.openId = res?.data?.data?.openid || ''
+    },
+    async getSdkConfig() {
+      const [err, res]: ToResponse = await to(sysApi.getSDKTicket({ url: window.location.href }))
+      if (err) return
+      this.sdkConfig = res?.data
+      wx.config({
+        debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
+        appId: this.sdkConfig.app_id, // 必填,公众号的唯一标识
+        timestamp: this.sdkConfig.timestamp, // 必填,生成签名的时间戳
+        nonceStr: this.sdkConfig.nonce_str, // 必填,生成签名的随机串
+        signature: this.sdkConfig.signature, // 必填,签名
+        jsApiList: ['scanQRCode'] // 必填,需要使用的JS接口列表
+      })
+      // wx.ready(function () {
+      //   showDialog({
+      //     title: '提示',
+      //     message: '初始化sdk成功',
+      //     showCancelButton: false
+      //   })
+      // })
+      // wx.error(function () {
+      //   showDialog({
+      //     title: '提示',
+      //     message: '初始化sdk失败',
+      //     showCancelButton: false
+      //   })
+      // })
+    },
+    scanCode() {
+      return new Promise((resolve, reject) => {
+        wx.scanQRCode({
+          needResult: 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
+          scanType: ['qrCode', 'barCode'], // 可以指定扫二维码还是一维码,默认二者都有
+          success: function (res: any) {
+            var result: string = res.resultStr // 当needResult 为 1 时,扫码返回的结果
+            return resolve(result)
+          }
+        })
+      })
     }
-  },
-});
+  }
+})

+ 1 - 1
src/theme/index.scss

@@ -18,7 +18,7 @@ body,
   display: flex;
   flex-direction: column;
   .app-container {
-    height: calc(100vh - 94px);
+    height: calc(100vh - 48px);
     padding: 0 10px;
     overflow-y: auto;
     background-color: #f7f8fa;

+ 41 - 27
src/types/index.d.ts

@@ -1,40 +1,54 @@
+/*
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-18 20:02:42
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-03-19 14:03:45
+ * @FilePath: \labsop_h5\src\types\index.d.ts
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
 
 // 系统字典
 declare interface RowDicDataType {
-  id?: number;
-  dictType: string;
-  dictLabel: string;
-  dictValue: string;
-  status: string;
-  isDefault: string;
-  sort: number;
+  id?: number
+  dictType: string
+  dictLabel: string
+  dictValue: string
+  status: string
+  isDefault: string
+  sort: number
 }
 // 用户角色
 declare interface RowRoleType {
-  id?: number;
-  roleName: string;
-  roleCode: string;
-  sort: number;
-  status: string;
-  createTime?: string;
-  dataScope?: string;
-  menuIds: number[];
+  id?: number
+  roleName: string
+  roleCode: string
+  sort: number
+  status: string
+  createTime?: string
+  dataScope?: string
+  menuIds: number[]
 }
 // 用户信息
 declare interface UserInfos<T = any> {
-  id: number;
-  nickName: string;
-  sex: string;
-  authBtnList: string[];
-  photo: string;
-  roles: string[];
-  time: number;
-  userName: string;
-  [key: string]: T;
+  id: number
+  nickName: string
+  sex: string
+  authBtnList: string[]
+  photo: string
+  roles: string[]
+  time: number
+  userName: string
+  [key: string]: T
 }
 declare interface UserInfosState {
-  userInfos: UserInfos;
-  openId: string;
+  userInfos: UserInfos
+  openId: string
+  sdkConfig: {
+    app_id: string
+    timestamp: number
+    nonce_str: string
+    signature: string
+  }
 }
 declare interface RowTraningApplyType {
   id: number | null
@@ -42,4 +56,4 @@ declare interface RowTraningApplyType {
   endTime: string
   title: string
   type: string
-}
+}

+ 25 - 7
src/utils/request.ts

@@ -1,10 +1,18 @@
+/*
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-18 20:02:42
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-03-19 15:02:34
+ * @FilePath: \labsop_h5\src\utils\request.ts
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
 import axios, { AxiosInstance } from 'axios'
 import { showNotify, showDialog } from 'vant'
 import { Local, Session } from '/@/utils/storage'
 
 // 配置新建一个 axios 实例
 const service: AxiosInstance = axios.create({
-  baseURL: import.meta.env.VITE_API_COMMON,
+  baseURL: import.meta.env.VITE_API_WECHAT,
   timeout: 50000,
   headers: { 'Content-Type': 'application/json' }
 })
@@ -12,9 +20,10 @@ const service: AxiosInstance = axios.create({
 // 添加请求拦截器
 service.interceptors.request.use(
   (config) => {
+    config.headers["Tenant"] = import.meta.env.VITE_TENANT;
     // 在发送请求之前做些什么 token
-    if (Local.get('token')) {
-      config.headers!['Authorization'] = `${Local.get('token')}`
+    if (Local.get('token') ) {
+      config.headers['Authorization'] = `Bearer ${Local.get('token')}`
     }
     return config
   },
@@ -29,18 +38,27 @@ service.interceptors.response.use(
   (response) => {
     // 对响应数据做点什么
     const res = response.data
-    if (res.code && res.code !== 0) {
+    if (res.code && res.code !== 0 && res.code != 200) {
       // `token` 过期或者账号已在别处登录
       if (res.code === 401 || res.code === 4001) {
         showDialog({
-          message: "登录状态已过期,请重新登录"
+          message: '登录状态已过期,请重新登录'
         }).then(() => {
           // / 清除缓存/token等
-          Session.clear();
+          Session.clear()
           Local.remove('token')
           // 使用 reload 时,不需要调用 resetRoute() 重置路由
-          window.location.reload();
+          window.location.reload()
         })
+      } else if (res.code === 500) {
+        showNotify({
+          message: res.message,
+          type: 'danger'
+        })
+        return Promise.reject(new Error(res.message))
+      } else if (res.code !== 200) {
+        showNotify({ message: res.message, type: 'danger' })
+        return Promise.reject('error')
       }
       return Promise.reject(service.interceptors.response)
     } else {

+ 0 - 1
src/view/exam/cover.vue

@@ -7,7 +7,6 @@
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 -->
 <template>
-  <van-nav-bar title="开始考试" right-text="返回" @click-right="onClickRight" />
   <div class="app-container">
     <h4 class="mt10 mb10">{{ state.form.name }}</h4>
     <van-cell-group>

+ 0 - 1
src/view/exam/index.vue

@@ -7,7 +7,6 @@
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 -->
 <template>
-  <van-nav-bar title="在线考试" />
   <div class="app-container">
     <header>
       <p>考试名称:{{ epName }}</p>

+ 45 - 9
src/view/home/index.vue

@@ -2,12 +2,11 @@
  * @Author: wanglj wanglijie@dashoo.cn
  * @Date: 2025-03-11 18:02:10
  * @LastEditors: wanglj wanglijie@dashoo.cn
- * @LastEditTime: 2025-03-14 15:34:01
+ * @LastEditTime: 2025-03-19 11:51:27
  * @FilePath: \vant-demo-master\vant\vue3-ts\src\view\login\index.vue
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 -->
 <template>
-  <van-nav-bar title="智慧实验室小助手" />
   <div class="app-container">
     <!-- <header>
       <div class="search">
@@ -59,24 +58,30 @@
       </ul>
     </div>
     <div class="card">
-      <h4>审批流程</h4>
+      <h4>
+        审批流程
+        <span class="link" @click="onRouterPush('/todo', { type: 'approval' })">全部审批>></span>
+      </h4>
       <ul class="approval">
-        <li v-for="item in approvalList">
+        <li v-for="item in approvalList" :key="item.id">
           <div class="flex justify-between">
             <van-text-ellipsis :content="`${item.instTitle}`" />
             <span class="time">{{ formatDate(new Date(item.createdTime), 'YYYY-mm-dd') }}</span>
           </div>
           <div class="flex justify-between">
             <van-text-ellipsis :content="`您提交的${item.defName}正在审核`" />
-            <a href="javascript:void(0);">查看详情</a>
+            <a href="javascript:void(0);" @click="onRouterPush('/todo/approval-detail', { id: item.id })">查看详情</a>
           </div>
         </li>
       </ul>
     </div>
     <div class="card">
-      <h4>通知公告</h4>
+      <h4>
+        通知公告
+        <span class="link" @click="onRouterPush('/todo', { type: 'notice' })">全部通知>></span>
+      </h4>
       <ul class="approval">
-        <li v-for="item in noticeList">
+        <li v-for="item in noticeList" :key="item.id" @click="onRouterPush('/todo/notice-detail', { id: item.id })">
           <div class="flex justify-between">
             <van-text-ellipsis :content="`【${item.noticeType === '10' ? '公告' : '通知'}】${item.noticeTitle}`" />
             <span class="time">{{ formatDate(new Date(item.noticeTime), 'YYYY-mm-dd') }}</span>
@@ -84,6 +89,20 @@
         </li>
       </ul>
     </div>
+    <div class="card mb20">
+      <h4>
+        培训通知
+        <span class="link" @click="onRouterPush('/training')">全部培训>></span>
+      </h4>
+      <ul class="approval">
+        <li v-for="item in trainingList" :key="item.id" @click="onRouterPush('/training/enroll', { state: item.id })">
+          <div class="flex justify-between">
+            <van-text-ellipsis :content="`${item.title}`" />
+            <span class="time">{{ formatDate(new Date(item.startTime), 'YYYY-mm-dd') }}</span>
+          </div>
+        </li>
+      </ul>
+    </div>
   </div>
 </template>
 
@@ -96,14 +115,17 @@
   import { useDeptApi } from '/@/api/system/dept'
   import { useExecutionApi } from '/@/api/execution'
   import { useNewsApi } from '/@/api/system/news'
+  import { useTrainingApi } from '/@/api/training'
   import { useRouter } from 'vue-router'
   const newsApi = useNewsApi()
   const dictApi = useDictApi()
   const proApi = useProApi()
   const deptApi = useDeptApi()
   const executionApi = useExecutionApi()
+  const trainingApi = useTrainingApi()
   const approvalList = ref<any[]>([])
   const noticeList = ref<any[]>([])
+  const trainingList = ref<any[]>([])
   const userTypeList = ref(<RowDicDataType[]>[])
   const userSexList = ref(<RowDicDataType[]>[])
   const userCertList = ref(<RowDicDataType[]>[])
@@ -143,14 +165,23 @@
     if (err) return
     noticeList.value = res?.data?.list || []
   }
+  const getTrainingList = async () => {
+    const [err, res]: ToResponse = await to(trainingApi.getList({ noPage: true }))
+    if (err) return
+    trainingList.value = res?.data?.list || []
+  }
   const onSearch = () => {}
-  const onRouterPush = (val: string) => {
-    router.push(val)
+  const onRouterPush = (val: string, params?: any) => {
+    router.push({
+      path: val,
+      query: { ...params }
+    })
   }
   onMounted(() => {
     getDicts()
     getApprovalList()
     getNotice()
+    getTrainingList()
   })
 </script>
 
@@ -212,6 +243,11 @@
       h4 {
         height: 18px;
         line-height: 18px;
+        display: flex;
+        span {
+          font-weight: normal;
+          margin-left: auto;
+        }
         &::before {
           display: inline-block;
           content: '';

+ 134 - 0
src/view/service/index.vue

@@ -0,0 +1,134 @@
+<!--
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-11 18:02:10
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-03-19 15:44:54
+ * @FilePath: \vant-demo-master\vant\vue3-ts\src\view\login\index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <div class="app-container">
+    <van-row class="pl10 pr10 pt10">
+      <van-button type="success" size="small" @click="changeType('instr')">仪器预约</van-button>
+      <van-button type="primary" size="small" @click="changeType('entry')">平台入室</van-button>
+      <van-button type="primary" size="small" @click="changeType('notice')">技术委托</van-button>
+    </van-row>
+    <van-list v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad">
+      <van-cell v-for="item in state.list" :key="item" @click="toDetail(item.id)">
+        <template #default>
+          <div v-if="state.queryParams.type == 'approval'" class="list">
+            <header class="flex justify-between">
+              <van-text-ellipsis class="title" :content="item.defName" />
+              <van-tag v-if="item.isFinish == '10'" type="warning">已完成</van-tag>
+              <van-tag v-else-if="item.isFinish == '20'" type="primary">未完成</van-tag>
+            </header>
+            <p class="inst-title">
+              <van-text-ellipsis :content="item.instTitle" />
+            </p>
+            <footer class="flex justify-between">
+              <van-text-ellipsis :content="item.candidate" />
+              <span class="time">{{ formatDate(new Date(item.createdTime), 'YYYY-mm-dd') }}</span>
+            </footer>
+          </div>
+          <div v-else class="flex justify-between">
+            <van-text-ellipsis class="inst-title" :content="`【${item.noticeType === '10' ? '公告' : '通知'}】${item.noticeTitle}`" />
+            <span class="time">{{ formatDate(new Date(item.noticeTime), 'YYYY-mm-dd') }}</span>
+          </div>
+        </template>
+      </van-cell>
+    </van-list>
+  </div>
+</template>
+
+<script name="home" lang="ts" setup>
+  import to from 'await-to-js'
+  import { formatDate } from '/@/utils/formatTime'
+  import { onMounted, reactive, ref } from 'vue'
+  import { useRouter, useRoute } from 'vue-router'
+  import { useExecutionApi } from '/@/api/execution'
+  import { useNewsApi } from '/@/api/system/news'
+  const executionApi = useExecutionApi()
+  const newsApi = useNewsApi()
+  const router = useRouter()
+  const route = useRoute()
+  const state = reactive({
+    queryParams: {
+      type: 'approval',
+      pageNum: 1,
+      pageSize: 10
+    },
+    finished: false,
+    loading: true,
+    list: [] as any[]
+  })
+  const onClickRight = () => {
+    router.go(-1)
+  }
+  const changeType = (str: string) => {
+    state.queryParams.type = str
+    state.queryParams.pageNum = 1
+    onLoad()
+  }
+  const onLoad = async () => {
+    const post = state.queryParams.type == 'approval' ? executionApi.getOwApproveList : newsApi.getNoticeList
+    const [err, res]: ToResponse = await to(post({ platformId: 1000103, noPage: true }))
+    if (err) return
+    state.list = res?.data?.list || []
+    state.loading = false
+    state.queryParams.pageNum++
+    if (state.list.length < state.queryParams.pageSize) {
+      state.finished = true
+    }
+  }
+  const toDetail = (id: number) => {
+    router.push({
+      path: state.queryParams.type === 'approval' ? '/todo/approval-detail' : '/todo/notice-detail',
+      query: {
+        id
+      }
+    })
+  }
+  onMounted(() => {
+    const type = route.query.type
+    if(type) {
+      state.queryParams.type = type as string
+    }
+    onLoad()
+  })
+</script>
+
+<style lang="scss" scoped>
+  .app-container {
+    .van-row .van-button {
+      flex: 1;
+      & + .van-button {
+        margin-left: 10px;
+      }
+    }
+    .van-list {
+      height: calc(100% - 62px);
+      background-color: #fff;
+      margin: 10px 0;
+      padding: 0 10px;
+      border-radius: 4px;
+      .van-cell {
+        background-color: #f9ffff;
+        margin-top: 10px;
+        header,
+        footer {
+          color: #333;
+        }
+        .title {
+          font-weight: bold;
+        }
+        .inst-title {
+          color: #333;
+          text-align: left;
+        }
+        .time {
+          color: #f69a4d;
+        }
+      }
+    }
+  }
+</style>

+ 2 - 3
src/view/todo/approval-detail.vue

@@ -2,12 +2,11 @@
  * @Author: wanglj wanglijie@dashoo.cn
  * @Date: 2025-03-11 18:02:10
  * @LastEditors: wanglj wanglijie@dashoo.cn
- * @LastEditTime: 2025-03-17 09:55:47
+ * @LastEditTime: 2025-03-19 13:46:04
  * @FilePath: \vant-demo-master\vant\vue3-ts\src\view\login\index.vue
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 -->
 <template>
-  <van-nav-bar title="审批详情" right-text="返回" @click-right="onClickRight" />
   <div class="app-container">
     <header>
       <h4 class="mb10">{{ state.form.startUserName }}发起的{{ state.form.defName }}</h4>
@@ -37,7 +36,7 @@
       </template>
     </van-cell>
     <van-steps direction="vertical" :active="0">
-      <van-step v-for="row in apprList">
+      <van-step v-for="row in apprList" :key="row.id">
         <div class="flex">
           <p>{{ getNodeModel(row.nodeType, row.nodeModel) }}</p>
           <p>{{ row.userName }}</p>

+ 10 - 6
src/view/todo/index.vue

@@ -2,19 +2,18 @@
  * @Author: wanglj wanglijie@dashoo.cn
  * @Date: 2025-03-11 18:02:10
  * @LastEditors: wanglj wanglijie@dashoo.cn
- * @LastEditTime: 2025-03-14 16:28:54
+ * @LastEditTime: 2025-03-19 12:00:44
  * @FilePath: \vant-demo-master\vant\vue3-ts\src\view\login\index.vue
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 -->
 <template>
-  <van-nav-bar title="待办" right-text="返回" @click-right="onClickRight" />
   <div class="app-container">
     <van-row class="pl10 pr10 pt10">
       <van-button type="success" size="small" @click="changeType('approval')">审批流程</van-button>
       <van-button type="primary" size="small" @click="changeType('notice')">系统公告</van-button>
     </van-row>
     <van-list v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad">
-      <van-cell v-for="item in state.list" :key="item" @click="toApprovalDetail(item.id)">
+      <van-cell v-for="item in state.list" :key="item" @click="toDetail(item.id)">
         <template #default>
           <div v-if="state.queryParams.type == 'approval'" class="list">
             <header class="flex justify-between">
@@ -44,12 +43,13 @@
   import to from 'await-to-js'
   import { formatDate } from '/@/utils/formatTime'
   import { onMounted, reactive, ref } from 'vue'
-  import { useRouter } from 'vue-router'
+  import { useRouter, useRoute } from 'vue-router'
   import { useExecutionApi } from '/@/api/execution'
   import { useNewsApi } from '/@/api/system/news'
   const executionApi = useExecutionApi()
   const newsApi = useNewsApi()
   const router = useRouter()
+  const route = useRoute()
   const state = reactive({
     queryParams: {
       type: 'approval',
@@ -79,15 +79,19 @@
       state.finished = true
     }
   }
-  const toApprovalDetail = (id: number) => {
+  const toDetail = (id: number) => {
     router.push({
-      path: '/approval-detail',
+      path: state.queryParams.type === 'approval' ? '/todo/approval-detail' : '/todo/notice-detail',
       query: {
         id
       }
     })
   }
   onMounted(() => {
+    const type = route.query.type
+    if(type) {
+      state.queryParams.type = type as string
+    }
     onLoad()
   })
 </script>

+ 91 - 0
src/view/todo/notice-detail.vue

@@ -0,0 +1,91 @@
+<!--
+ * @Author: wanglj wanglijie@dashoo.cn
+ * @Date: 2025-03-11 18:02:10
+ * @LastEditors: wanglj wanglijie@dashoo.cn
+ * @LastEditTime: 2025-03-19 15:10:14
+ * @FilePath: \vant-demo-master\vant\vue3-ts\src\view\login\index.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <div class="app-container">
+    <header>
+      <h4>【{{ state.form.noticeType }}】{{ state.form.noticeTitle }}</h4>
+    </header>
+    <h4>通知内容</h4>
+    <div class="content" v-html="state.form.noticeContent"></div>
+  </div>
+</template>
+
+<script name="home" lang="ts" setup>
+  import to from 'await-to-js'
+  import { formatDate } from '/@/utils/formatTime'
+  import { onMounted, reactive, ref } from 'vue'
+  import { useRouter, useRoute } from 'vue-router'
+  import { useNewsApi } from '/@/api/system/news'
+  const newsApi = useNewsApi()
+  const route = useRoute()
+  const state = reactive({
+    form: {
+      id: 0,
+      noticeTitle: '',
+      noticeTime: '',
+      noticeContent: '',
+      noticeStatus: '',
+      noticeType: '',
+      noticeLevel: '',
+      isTop: '',
+      redirectUrl: '',
+      fileType: '',
+      imageUrl: '',
+      platId: 0,
+      platName: ''
+    }
+  })
+  const getNoticeDetail = async () => {
+    const [err, res]: ToResponse = await to(newsApi.getNoticeEntity({ id: state.form.id }))
+    if (err) return
+    if (res?.data) {
+      state.form = res.data
+    }
+  }
+  onMounted(() => {
+    state.form.id = route.query.id ? +route.query.id : 0
+    getNoticeDetail()
+  })
+</script>
+
+<style lang="scss" scoped>
+  .app-container {
+    header {
+      padding: 14px;
+      background-color: #f9ffff;
+      border-radius: 4px;
+      margin-top: 10px;
+    }
+    h4 {
+      height: 18px;
+      line-height: 18px;
+      display: flex;
+      margin: 10px 0;
+      span {
+        font-weight: normal;
+        margin-left: auto;
+      }
+      &::before {
+        display: inline-block;
+        content: '';
+        width: 3px;
+        height: 18px;
+        background-color: #1c9bfd;
+        margin-right: 4px;
+        vertical-align: middle;
+      }
+    }
+    .content {
+      background-color: #fff;
+      padding: 10px;
+      border-radius: 8px;
+      word-break: break-all;
+    }
+  }
+</style>

+ 14 - 22
src/view/training/enroll.vue

@@ -1,21 +1,12 @@
 <!--
  * @Author: wanglj wanglijie@dashoo.cn
- * @Date: 2025-03-17 13:42:58
+ * @Date: 2025-03-18 20:02:42
  * @LastEditors: wanglj wanglijie@dashoo.cn
- * @LastEditTime: 2025-03-18 20:07:03
- * @FilePath: \labsop-h5\src\view\training\enroll.vue
- * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
--->
-<!--
- * @Author: wanglj wanglijie@dashoo.cn
- * @Date: 2025-03-11 18:02:10
- * @LastEditors: wanglj wanglijie@dashoo.cn
- * @LastEditTime: 2025-03-18 16:17:00
- * @FilePath: \vant-demo-master\vant\vue3-ts\src\view\login\index.vue
+ * @LastEditTime: 2025-03-19 15:23:08
+ * @FilePath: \labsop_h5\src\view\training\enroll.vue
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 -->
 <template>
-  <van-nav-bar title="培训报名" right-text="返回" @click-right="onClickRight" />
   <div class="app-container">
     <van-form ref="formRef" @submit="onSubmit" class="mt10" required="auto">
       <van-cell-group>
@@ -84,15 +75,15 @@
     })
   }
   const initForm = async () => {
-    // 微信授权码获取openId
-    const code = route.query.code
-    if (!openId.value && code) {
-      state.form.trainingNoticeId = route.query.state ? +route.query.state : 0
-      if (typeof code === 'string') {
-        await storesUseUserInfo.setOpenId(code)
-        state.form.wechatOpenId = openId.value
-      }
-    }
+    state.form.trainingNoticeId = route.query.state ? +route.query.state : 0
+    // // 微信授权码获取openId
+    // const code = route.query.code
+    // if (!openId.value && code) {
+    //   if (typeof code === 'string') {
+    //     await storesUseUserInfo.setOpenId(code)
+    //     state.form.wechatOpenId = openId.value
+    //   }
+    // }
     const date = new Date()
     state.form.trainingDate = [formatDate(date, 'YYYY'), formatDate(date, 'mm'), formatDate(date, 'dd')]
     if (userInfos.value.id) {
@@ -100,7 +91,7 @@
       state.form.name = userInfos.value.nickName
       state.form.telephone = userInfos.value.phone
       state.form.type = userInfos.value.userType
-      state.form.projectGroup = userInfos.value.projectGroupRes.pgName
+      state.form.projectGroup = userInfos.value.pgName
     }
     if(state.form.trainingNoticeId) {
       const [err, res]: ToResponse = await to(trainingApi.getEntity({ id: state.form.trainingNoticeId }))
@@ -120,6 +111,7 @@
   const onSubmit = async () => {
     const [errValid] = await to(formRef.value.validate())
     if (errValid) return
+    state.form.wechatOpenId = openId.value
     const [err]: ToResponse = await to(trainingApi.addTrainingApply(state.form))
     if (err) return
     showNotify({

+ 3 - 4
src/view/training/index.vue

@@ -2,17 +2,16 @@
  * @Author: wanglj wanglijie@dashoo.cn
  * @Date: 2025-03-17 13:36:58
  * @LastEditors: wanglj wanglijie@dashoo.cn
- * @LastEditTime: 2025-03-17 17:41:17
+ * @LastEditTime: 2025-03-19 11:37:11
  * @FilePath: \labsop-h5\src\view\training\index.vue
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 -->
 <template>
-  <van-nav-bar title="培训列表" />
   <div class="app-container">
     <van-list v-model:loading="state.loading" :finished="state.finished" finished-text="没有更多了" @load="onLoad" class="mt10">
       <van-cell v-for="(item, index) in state.list" :key="index" :center="true">
         <template #title>
-          <van-text-ellipsis :content="`${item.type}:${item.title}`" />
+          <van-text-ellipsis :content="`${item.title}`" />
         </template>
         <template #value><van-button type="primary" size="small" @click="onEnroll(item)">报名</van-button></template>
       </van-cell>
@@ -57,7 +56,7 @@
     router.push({
       path: '/training/enroll',
       query: {
-        id: row.id
+        state: row.id
       }
     })
   }

+ 141 - 0
src/view/user/edit.vue

@@ -0,0 +1,141 @@
+<template>
+  <div class="app-container">
+    <header>
+      <img :src="userInfos.avatar" alt="" />
+      <div class="content">
+        <p class="bold">
+          <van-text-ellipsis :content="`${userInfos.nickName}-${userInfos.userRoleNames}`" />
+        </p>
+        <p>
+          <van-text-ellipsis :content="userInfos.pgName" />
+        </p>
+        <p>
+          <van-text-ellipsis :content="userInfos.deptName" />
+        </p>
+      </div>
+    </header>
+    <van-form ref="formRef" @submit="onSubmit" class="mt10" required="auto">
+      <van-cell-group>
+        <van-field v-model="state.form.nickName" label="用户昵称" placeholder="用户昵称" :rules="[{ required: true, message: '请输入用户昵称' }]" />
+        <van-field v-model="state.form.phone" label="手机号" placeholder="手机号"> </van-field>
+        <van-field v-model="state.form.email" label="电子邮箱" placeholder="电子邮箱" />
+        <van-field v-model="state.form.sex" label="性别" placeholder="性别">
+          <template #input>
+            <van-radio-group v-model="state.form.sex" direction="horizontal">
+              <van-radio v-for="item in userSexList" :key="item.id" :name="item.dictValue">{{ item.dictLabel }}</van-radio>
+            </van-radio-group>
+          </template>
+        </van-field>
+        <van-field v-model="state.form.pgName" label="课题组" placeholder="课题组" readonly></van-field>
+      </van-cell-group>
+      <div style="margin: 16px">
+        <van-button round block type="primary" native-type="submit"> 保存 </van-button>
+      </div>
+    </van-form>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { storeToRefs } from 'pinia'
+  import { useUserInfo } from '/@/stores/userInfo'
+  import { Local } from '/@/utils/storage'
+  import { showConfirmDialog, showNotify } from 'vant'
+  import { useRouter } from 'vue-router'
+  import { onMounted, reactive, ref } from 'vue'
+  import { useUserApi } from '/@/api/system/user'
+  import to from 'await-to-js'
+  import { useDictApi } from '/@/api/system/dict'
+
+  const router = useRouter()
+  const storesUseUserInfo = useUserInfo()
+  const { userInfos } = storeToRefs(storesUseUserInfo)
+  const userApi = useUserApi()
+  const formRef = ref()
+  const dictApi = useDictApi()
+  const userSexList = ref(<RowDicDataType[]>[])
+  const state = reactive({
+    form: {
+      id: 0,
+      userId: 0, // 用户账号
+      nickName: '', // 用户昵称
+      phone: '', // 手机号
+      email: '', // 电子邮件
+      sex: '', // 性别
+      avatar: '', // 头像图片地址
+      pgName: ''
+    },
+    dialog: {
+      isShowDialog: false,
+      type: '',
+      title: '',
+      submitTxt: ''
+    }
+  })
+  const getDicts = () => {
+    Promise.all([dictApi.getDictDataByType('sys_com_sex')]).then(([sex]) => {
+      userSexList.value = sex.data.values || []
+    })
+  }
+  const onClickRight = () => {
+    router.go(-1)
+  }
+  const initForm = async () => {
+    const [err, res]: ToResponse = await to(userApi.getProfile())
+    if (err) return
+    state.form = res?.data
+  }
+  const onSubmit = async () => {
+    const [errValid] = await to(formRef.value.validate())
+    if (errValid) return
+    state.form.userId = state.form.id
+    const params = JSON.parse(JSON.stringify(state.form))
+    const [err]: ToResponse = await to(userApi.updateProfile(params))
+    if (err) return
+    showNotify({
+      type: 'success',
+      message: '修改成功'
+    })
+    storesUseUserInfo.setUserInfos()
+    router.push('/user')
+  }
+  onMounted(() => {
+    getDicts()
+    initForm()
+  })
+</script>
+
+<style lang="scss" scoped>
+  .app-container {
+    header {
+      background-color: #1c9bfd;
+      color: #fff;
+      padding: 10px;
+      border-radius: 8px;
+      margin-top: 10px;
+      display: flex;
+      img {
+        width: 100px;
+        height: 100px;
+        border-radius: 50%;
+        margin-right: 10px;
+      }
+      .content {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-around;
+        overflow: hidden;
+        .bold {
+          display: flex;
+          align-items: center;
+          font-weight: bold;
+          // flex-wrap: nowrap;
+          white-space: nowrap;
+          .van-text-ellipsis {
+            flex: 1;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 183 - 28
src/view/user/index.vue

@@ -1,40 +1,195 @@
 <template>
-  <van-nav-bar title="个人中心" />
   <div class="app-container">
-    <van-button @click="signOut">切换账号</van-button>
+    <header>
+      <img :src="userInfos.avatar" alt="" />
+      <div class="content">
+        <p class="bold">
+          <van-text-ellipsis :content="`${userInfos.nickName}-${userInfos.userRoleNames}`" />
+          <van-icon name="setting" size="20" @click="onRouterPush('/user/edit')" />
+        </p>
+        <p>
+          <van-text-ellipsis :content="userInfos.pgName" />
+        </p>
+        <p>
+          <van-text-ellipsis :content="userInfos.deptName" />
+        </p>
+      </div>
+    </header>
+    <div class="main">
+      <ul class="links">
+        <li @click="onRouterPush('/training')">培训通知</li>
+        <li>我的报名</li>
+        <li>我的学习</li>
+        <li>我的考试</li>
+      </ul>
+      <ul>
+        <li>使用指引</li>
+        <li>安全积分:{{ userInfos.creditScore }}分</li>
+      </ul>
+      <div class="card">
+        <h4>经费统计</h4>
+        <ul class="nav">
+          <li>
+            <span>123,000</span>
+            <p>账户余额(¥)</p>
+          </li>
+          <li>
+            <span>11</span>
+            <p>流水</p>
+          </li>
+          <li>
+            <span>6</span>
+            <p>账单</p>
+          </li>
+          <li>
+            <span>0</span>
+            <p>待支付</p>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <footer>
+      <van-button class="w100" @click="signOut" type="primary">切换账号</van-button>
+    </footer>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { Local } from '/@/utils/storage';
-const signOut = () => {
-  Local.clear();
-  window.location.reload()
-}
-</script>
-
-<style lang="scss">
-.user {
-  &-poster {
-    width: 100%;
-    height: 53vw;
-    display: block;
+  import { storeToRefs } from 'pinia'
+  import { useUserInfo } from '/@/stores/userInfo'
+  import { Local } from '/@/utils/storage'
+  import { showConfirmDialog } from 'vant'
+  import { useRouter } from 'vue-router'
+  const router = useRouter()
+  const storesUseUserInfo = useUserInfo()
+  const { userInfos, sdkConfig } = storeToRefs(storesUseUserInfo)
+  const onRouterPush = (val: string) => {
+    router.push(val)
   }
-
-  &-group {
-    margin-bottom: 15px;
+  const signOut = () => {
+    showConfirmDialog({
+      message: '确认切换账号?'
+    }).then(() => {
+      Local.clear()
+      window.location.reload()
+    })
   }
+</script>
 
-  &-links {
-    padding: 15px 0;
-    font-size: 12px;
-    text-align: center;
-    background-color: #fff;
-
-    .van-icon {
-      display: block;
-      font-size: 24px;
+<style lang="scss" scoped>
+  .app-container {
+    display: flex;
+    flex-direction: column;
+    header {
+      background-color: #1c9bfd;
+      color: #fff;
+      padding: 10px;
+      border-radius: 8px;
+      margin-top: 10px;
+      display: flex;
+      img {
+        width: 100px;
+        height: 100px;
+        border-radius: 50%;
+        margin-right: 10px;
+      }
+      .content {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-around;
+        overflow: hidden;
+        .bold {
+          display: flex;
+          align-items: center;
+          font-weight: bold;
+          // flex-wrap: nowrap;
+          white-space: nowrap;
+          .van-text-ellipsis {
+            flex: 1;
+          }
+        }
+      }
+    }
+    .main {
+      flex: 1;
+      overflow-y: auto;
+      > ul {
+        display: flex;
+        height: 60px;
+        margin-top: 10px;
+        li {
+          flex: 1;
+          background: #fff;
+          color: #333;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          border-radius: 4px;
+          & + li {
+            margin-left: 10px;
+          }
+        }
+        &.links {
+          li {
+            color: #fff;
+            background: #5eb7fc;
+            &:nth-child(2) {
+              background: #fd9c5e;
+            }
+            &:nth-child(3) {
+              background: #53e3a7;
+            }
+            &:nth-child(4) {
+              background: #888ac3;
+            }
+          }
+        }
+      }
+      .card {
+        margin-top: 10px;
+        border-radius: 4px;
+        background-color: #fff;
+        padding: 10px;
+        h4 {
+          height: 18px;
+          line-height: 18px;
+          &::before {
+            display: inline-block;
+            content: '';
+            width: 3px;
+            height: 18px;
+            background-color: #1c9bfd;
+            margin-right: 4px;
+            vertical-align: middle;
+          }
+        }
+        .nav {
+          display: flex;
+          margin: 10px 0;
+          flex-wrap: wrap;
+          li {
+            flex: 0 0 25%;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            span {
+              font-size: 20px;
+            }
+            p {
+              margin-top: 4px;
+            }
+            &:nth-child(n + 5) {
+              margin-top: 10px;
+            }
+          }
+        }
+      }
+    }
+    footer {
+      flex: 0 0 44px;
+      margin-bottom: 20px;
     }
   }
-}
 </style>