Kaynağa Gözat

add src/views/my/MyApproval.vue

reghao 1 hafta önce
ebeveyn
işleme
21f1e10218

+ 0 - 15
src/api/admin.js

@@ -5,9 +5,6 @@ const adminApi = {
   setAccountRegistryApi: '/api/admin/account/registry',
   setAccountCodeApi: '/api/admin/account/code',
   getUsersApi: '/api/admin/user/list',
-  getChargeReqApi: '/api/admin/user/charge',
-  approveChargeApi: '/api/admin/user/charge/approve',
-  declineChargeApi: '/api/admin/user/charge/decline',
   getUserAvatarsApi: '/api/admin/user/user/avatar',
   getVipPlanApi: '/api/admin/user/vip_plan',
   fileStoreConfigApi: '/api/admin/file/file_store',
@@ -33,18 +30,6 @@ export function getUserList(queryParams) {
   return get(adminApi.getUsersApi, queryParams)
 }
 
-export function getChargeReq(pn) {
-  return get(adminApi.getChargeReqApi + '?pn=' + pn)
-}
-
-export function approveCharge(payload) {
-  return post(adminApi.approveChargeApi + '/' + payload)
-}
-
-export function declineCharge(payload) {
-  return post(adminApi.declineChargeApi + '/' + payload)
-}
-
 export function getUserAvatarList() {
   return get(adminApi.getUserAvatarsApi)
 }

+ 17 - 4
src/api/user.js

@@ -13,8 +13,9 @@ const userApi = {
   searchUserApi: '/api/user/search',
   userMessageApi: '/api/message/user',
   walletApi: '/api/user/wallet',
-  chargeWalletApi: '/api/user/wallet/charge',
-  walletBillApi: '/api/user/wallet/bill'
+  walletBillApi: '/api/user/wallet/bill',
+  approvalApi: '/api/user/approval',
+  adminApprovalApi: '/api/admin/user/approval'
 }
 
 export function getUserInfo(userId) {
@@ -83,6 +84,18 @@ export function getWalletBill() {
   return get(userApi.walletBillApi)
 }
 
-export function chargeWallet(data) {
-  return post(userApi.chargeWalletApi, data)
+export function addApprovalRequest(payload) {
+  return post(userApi.approvalApi + '/add', payload)
+}
+
+export function getUserApprovalRequests(queryParams) {
+  return get(userApi.approvalApi + '/user_list', queryParams)
+}
+
+export function auditApprovalRequest(payload) {
+  return post(userApi.adminApprovalApi + '/audit', payload)
+}
+
+export function getApprovalRequests(queryParams) {
+  return get(userApi.adminApprovalApi + '/admin_list', queryParams)
 }

+ 61 - 4
src/permission.js

@@ -1,6 +1,8 @@
 import router from './router'
 import { getAccessToken, removeAll } from '@/utils/auth'
 import store from '@/store'
+import { addApprovalRequest } from '@/api/user'
+import { Message, MessageBox } from 'element-ui'
 
 router.beforeEach((to, from, next) => {
   // document.title = to.meta.title
@@ -12,7 +14,8 @@ router.beforeEach((to, from, next) => {
     } else {
       const hasRoles = store.getters.roles.length > 0
       if (hasRoles) {
-        next()
+        checkRolePermission(to, from, next)
+        // next()
       } else {
         const roles = store.dispatch('getUserRoles')
         const accessRoutes = store.dispatch('generateRoutes', roles)
@@ -32,12 +35,66 @@ router.beforeEach((to, from, next) => {
   }
 })
 
+function checkRolePermission(to, from, next) {
+  // 1. 如果路由配置了 roles 限制
+  if (to.meta && to.meta.roles && to.meta.roles.length > 0) {
+    const userRoles = store.getters.roles || []
+    const requiredRoles = to.meta.roles
+
+    // 2. 检查当前用户是否拥有该路由所需的任意一个角色
+    const hasPermission = userRoles.some(role => requiredRoles.includes(role))
+    if (!hasPermission) {
+      // 1. 🌟 立刻中断当前跳转,留在原地,防止异步弹窗导致路由穿透或页面空白
+      next(false)
+      // 2. 弹出确认对话框
+      MessageBox.confirm(`您当前无权访问 [${to.meta.title || '该模块'}], 是否立即申请特权角色?`, '权限提示', {
+        confirmButtonText: '立即申请',
+        cancelButtonText: '暂不申请',
+        type: 'warning',
+        center: true // 居中对齐,更符合现代 UI 风格
+      }).then(() => {
+        // 3. 用户点击了“立即申请”
+        manulApplyRole(requiredRoles[0])
+      }).catch(() => {
+        // 4. 用户点击了“暂不申请”或关闭了弹窗
+        Message.warning('用户取消了角色申请')
+      })
+
+      return
+    }
+  }
+
+  //  有权限,直接放行
+  next()
+}
+
+// 🌟 【新增辅助函数】:静默提交申请单
+function manulApplyRole(roleName) {
+  const requestData = {
+    bizType: 'applyRole',
+    // 将申请的目标角色包装进 payload 传给后端
+    bizPayload: JSON.stringify({ role: roleName, amount: 5 })
+  }
+  addApprovalRequest(requestData).then(resp => {
+    if (resp.code === 0) {
+      Message.info('特权角色申请已自动提交,请联系管理员审批!')
+    } else {
+      Message.warning('自动申请提交失败: ' + resp.msg)
+    }
+  }).catch(error => {
+    Message.error('网络异常,角色自动申请失败: ' + error.message)
+  }).finally(() => {
+    // 🌟 终极兜底:无论走 then 还是 catch,最终一定会在这里安全重定向
+    // next({ path: '/', replace: true })
+  })
+}
+
 router.afterEach((to) => {
   // 从路由配置的 meta 中获取 title
-  const title = to.meta && to.meta.title;
+  const title = to.meta && to.meta.title
   if (title) {
-    document.title = `${title}`;
+    document.title = `${title}`
   } else {
-    document.title = 'tnbapp';
+    document.title = 'tnbapp'
   }
 })

+ 9 - 0
src/router/background_account.js

@@ -6,6 +6,7 @@ const Background = () => import('views/admin/Background')
 // 用户后台主页
 const MyRecord = () => import('views/my/MyRecord')
 const MyMessage = () => import('views/my/MyMessage')
+const MyApproval = () => import('views/my/MyApproval')
 const MyContact = () => import('views/my/MyContact')
 const MyOAuth = () => import('views/my/MyOAuth')
 
@@ -34,6 +35,14 @@ export default {
       component: MyMessage,
       meta: { needAuth: true, roles: ['tnb_admin', 'tnb_user', 'tnb_disk'] }
     },
+    {
+      path: '/bg/account/approval',
+      name: 'MyApproval',
+      title: '我的审批',
+      icon: 'el-icon-user',
+      component: MyApproval,
+      meta: { needAuth: true, roles: ['tnb_admin', 'tnb_user', 'tnb_disk'] }
+    },
     {
       path: '/bg/account/contact',
       name: 'MyContact',

+ 5 - 5
src/router/background_admin.js

@@ -3,7 +3,7 @@
 const Background = () => import('views/admin/Background')
 
 // 后台管理
-const AdminGrant = () => import('views/admin/aaa/AdminGrant')
+const AdminApproval = () => import('views/admin/aaa/AdminApproval')
 const AdminUserList = () => import('views/admin/aaa/AdminUserList')
 const AdminVideoList = () => import('views/admin/aaa/AdminVideoList')
 
@@ -17,11 +17,11 @@ export default {
   meta: { needAuth: true, roles: ['tnb_admin'] },
   children: [
     {
-      path: '/bg/admin/grant',
-      name: 'AdminGrant',
-      title: '授权请求',
+      path: '/bg/admin/approval',
+      name: 'AdminApproval',
+      title: '审批',
       icon: 'el-icon-edit',
-      component: AdminGrant,
+      component: AdminApproval,
       meta: { needAuth: true, roles: ['tnb_admin'] }
     },
     {

+ 5 - 5
src/router/chat.js

@@ -9,31 +9,31 @@ export default {
   name: 'Chat',
   component: Chat,
   redirect: '/chat/list',
-  meta: { needAuth: false },
+  meta: { needAuth: true, roles: ['tnb_disk'] },
   children: [
     {
       path: '/chat/list',
       name: 'ChatList',
       component: ChatList,
-      meta: { title: '聊天', needAuth: false }
+      meta: { title: '聊天', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/chat/dialogue',
       name: 'ChatDialogue',
       component: ChatDialogue,
-      meta: { title: 'xxx', needAuth: false }
+      meta: { title: 'xxx', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/chat/contact',
       name: 'ChatAddressBook',
       component: ChatAddressBook,
-      meta: { title: '通讯录', needAuth: false }
+      meta: { title: '通讯录', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/chat/me',
       name: 'ChatMe',
       component: ChatMe,
-      meta: { title: '我', needAuth: false }
+      meta: { title: '我', needAuth: true, roles: ['tnb_disk'] }
     }
   ]
 }

+ 7 - 7
src/router/disk.js

@@ -11,43 +11,43 @@ export default {
   name: 'Disk',
   component: Disk,
   redirect: '/disk/file',
-  meta: { needAuth: true },
+  meta: { needAuth: true, roles: ['tnb_disk'] },
   children: [
     {
       path: '/disk/file',
       name: 'DiskFile',
       component: DiskFile,
-      meta: { title: '我的文件', needAuth: true }
+      meta: { title: '我的文件', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/disk/photo',
       name: 'DiskPhoto',
       component: DiskPhoto,
-      meta: { title: '我的相册', needAuth: true }
+      meta: { title: '我的相册', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/disk/cam',
       name: 'CamList',
       component: CamList,
-      meta: { title: '我的监控', needAuth: true }
+      meta: { title: '我的监控', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/disk/cam/detail',
       name: 'CamDetail',
       component: CamDetail,
-      meta: { needAuth: true }
+      meta: { needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/disk/share',
       name: 'DiskShare',
       component: DiskShare,
-      meta: { title: '我的分享', needAuth: true }
+      meta: { title: '我的分享', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/disk/me',
       name: 'DiskMe',
       component: DiskMe,
-      meta: { title: '我的', needAuth: true }
+      meta: { title: '我的', needAuth: true, roles: ['tnb_disk'] }
     }
   ]
 }

+ 7 - 7
src/router/diskm.js

@@ -11,43 +11,43 @@ export default {
   name: 'Disk',
   component: Disk,
   redirect: '/disk/file',
-  meta: { needAuth: true },
+  meta: { needAuth: true, roles: ['tnb_disk'] },
   children: [
     {
       path: '/disk/file',
       name: 'DiskFile',
       component: DiskFile,
-      meta: { title: '我的文件', needAuth: true }
+      meta: { title: '我的文件', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/disk/photo',
       name: 'DiskPhoto',
       component: DiskPhoto,
-      meta: { title: '我的相册', needAuth: true }
+      meta: { title: '我的相册', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/disk/cam',
       name: 'CamList',
       component: CamList,
-      meta: { title: '我的监控', needAuth: true }
+      meta: { title: '我的监控', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/disk/cam/detail',
       name: 'CamDetail',
       component: CamDetail,
-      meta: { needAuth: true }
+      meta: { needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/disk/share',
       name: 'DiskShare',
       component: DiskShare,
-      meta: { title: '我的分享', needAuth: true }
+      meta: { title: '我的分享', needAuth: true, roles: ['tnb_disk'] }
     },
     {
       path: '/disk/me',
       name: 'DiskMe',
       component: DiskMe,
-      meta: { title: '我的', needAuth: true }
+      meta: { title: '我的', needAuth: true, roles: ['tnb_disk'] }
     }
   ]
 }

+ 2 - 2
src/router/index.js

@@ -150,13 +150,13 @@ export const constantRoutes = [
     path: '/bg',
     name: 'BackgroundIndex',
     component: Background,
-    meta: { needAuth: true },
+    meta: { needAuth: true, roles: ['tnb_admin', 'tnb_user', 'tnb_disk'] },
     children: [
       {
         path: '',
         name: 'Dashboard',
         component: Dashboard,
-        meta: { needAuth: true, roles: ['admin'] }
+        meta: { needAuth: true, roles: ['tnb_admin', 'tnb_user', 'tnb_disk'] }
       }
     ]
   },

+ 16 - 0
src/views/admin/Dashboard.vue

@@ -182,6 +182,7 @@ import { userMixin } from 'assets/js/mixin'
 import { getAuthedUser, updateAuthedUser } from '@/utils/auth'
 import { updateAvatar } from '@/api/account' // 假设你的其它更新 API 也在 account 或对应模块里
 import { getAvatarChannelInfo } from '@/api/file'
+import { addApprovalRequest } from '@/api/user'
 
 export default {
   name: 'Dashboard',
@@ -251,6 +252,21 @@ export default {
       }
       this.$router.push(path)
     },
+    requireRole() {
+      const requestData = {
+        bizType: 'applyRole',
+        bizPayload: JSON.stringify({ amount: 5 })
+      }
+      addApprovalRequest(requestData).then(resp => {
+        if (resp.code === 0) {
+          this.$message.info('角色申请请求已提交')
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error('角色申请失败: ' + error.message)
+      })
+    },
 
     // ==================== 1. 常规单字段(显示名、邮箱、手机)修改业务 ====================
     openEditDialog(field, title, currentVal) {

+ 114 - 0
src/views/admin/aaa/AdminApproval.vue

@@ -0,0 +1,114 @@
+<template>
+  <el-row>
+    <el-row class="movie-list">
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>审批工作流</span>
+        </div>
+        <div class="text item">
+          <el-table
+            :data="approvalRequests"
+            border
+            style="width: 100%"
+          >
+            <el-table-column
+              prop="id"
+              label="ID"
+            />
+            <el-table-column
+              prop="createAt"
+              label="创建时间"
+            />
+            <el-table-column
+              prop="bizType"
+              label="业务类型"
+            />
+            <el-table-column
+              prop="userId"
+              label="请求用户"
+            />
+            <el-table-column
+              fixed="right"
+              label="操作"
+            >
+              <template slot-scope="scope">
+                <el-button
+                  size="mini"
+                  @click="onApprove(scope.row)"
+                >同意</el-button>
+                <el-button
+                  size="mini"
+                  type="danger"
+                  @click="onDecline(scope.row)"
+                >拒绝</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </el-card>
+    </el-row>
+  </el-row>
+</template>
+
+<script>
+import { userMixin } from 'assets/js/mixin'
+import { getVipPlan } from '@/api/admin'
+import { auditApprovalRequest, getApprovalRequests } from '@/api/user'
+
+export default {
+  name: 'AdminApproval',
+  mixins: [userMixin],
+  data() {
+    return {
+      approvalRequests: [],
+      vipPlanList: []
+    }
+  },
+  created() {
+    getVipPlan().then(resp => {
+      if (resp.code === 0) {
+        this.vipPlanList = resp.data
+      } else {
+        this.$message.error(resp.msg)
+      }
+    })
+
+    this.getData()
+  },
+  methods: {
+    getData() {
+      const queryParams = {}
+      queryParams.pn = 1
+      getApprovalRequests(queryParams).then(resp => {
+        if (resp.code === 0) {
+          this.approvalRequests = resp.data.list
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      })
+    },
+    onApprove(row) {
+      const payload = {}
+      payload.requestId = row.id
+      payload.action = 1
+      payload.remark = ''
+      this.audit(payload)
+    },
+    onDecline(row) {
+      const payload = {}
+      payload.requestId = row.id
+      payload.action = 2
+      payload.remark = ''
+      this.audit(payload)
+    },
+    audit(payload) {
+      auditApprovalRequest(payload).then(resp => {
+        this.$message.warning(resp.msg)
+      })
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 0 - 117
src/views/admin/aaa/AdminGrant.vue

@@ -1,117 +0,0 @@
-<template>
-  <el-row>
-    <el-row class="movie-list">
-      <el-col :md="16" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
-        <el-card class="box-card">
-          <div slot="header" class="clearfix">
-            <span>充值请求</span>
-          </div>
-          <div class="text item">
-            <el-table
-              :data="chargeReqList"
-              border
-              style="width: 100%"
-            >
-              <el-table-column
-                fixed="left"
-                label="No"
-                type="index"
-              />
-              <el-table-column
-                prop="chargeId"
-                label="充值 ID"
-              />
-              <el-table-column
-                prop="quantity"
-                label="金额"
-              />
-              <el-table-column
-                prop="owner"
-                label="用户"
-              />
-              <el-table-column
-                fixed="right"
-                label="操作"
-              >
-                <template slot-scope="scope">
-                  <el-button
-                    size="mini"
-                    @click="onApprove(scope.row)"
-                  >同意</el-button>
-                  <el-button
-                    size="mini"
-                    type="danger"
-                    @click="onDecline(scope.row)"
-                  >拒绝</el-button>
-                </template>
-              </el-table-column>
-            </el-table>
-          </div>
-        </el-card>
-      </el-col>
-    </el-row>
-  </el-row>
-</template>
-
-<script>
-import { userMixin } from 'assets/js/mixin'
-import {
-  approveCharge, declineCharge,
-  getChargeReq, getVipPlan
-} from '@/api/admin'
-
-export default {
-  name: 'AdminCharge',
-  mixins: [userMixin],
-  data() {
-    return {
-      chargeReqList: [],
-      vipPlanList: []
-    }
-  },
-  created() {
-    document.title = '站点配置'
-    getVipPlan().then(resp => {
-      if (resp.code === 0) {
-        this.vipPlanList = resp.data
-      } else {
-        this.$message.error(resp.msg)
-      }
-    })
-
-    this.getData()
-  },
-  methods: {
-    getData() {
-      getChargeReq(1).then(resp => {
-        if (resp.code === 0) {
-          this.chargeReqList = resp.data
-        } else {
-          this.$message.error(resp.msg)
-        }
-      })
-    },
-    onApprove(row) {
-      approveCharge(row.chargeId).then(resp => {
-        if (resp.code === 0) {
-          this.getData()
-        } else {
-          this.$message.error(resp.msg)
-        }
-      })
-    },
-    onDecline(row) {
-      declineCharge(row.chargeId).then(resp => {
-        if (resp.code === 0) {
-          this.getData()
-        } else {
-          this.$message.error(resp.msg)
-        }
-      })
-    }
-  }
-}
-</script>
-
-<style>
-</style>

+ 14 - 2
src/views/home/Home.vue

@@ -28,6 +28,10 @@
         <div v-if="noMore" class="no-more-bar">
           <span class="dot" /> 到底啦 <span class="dot" />
         </div>
+        <div v-if="isError" class="error-bar" style="text-align: center; padding: 15px; color: #909399;">
+          <span>加载失败,请检查网络 </span>
+          <el-button type="text" icon="el-icon-refresh" @click="retryLoad">点击重试</el-button>
+        </div>
       </div>
     </div>
   </div>
@@ -46,6 +50,7 @@ export default {
       dataList: [],
       loading: false,
       noMore: false,
+      isError: false,
       isFirstLoading: true,
       carouselList: [],
       carouselHeight: '340px',
@@ -54,7 +59,7 @@ export default {
   },
   computed: {
     disabled() {
-      return this.loading || this.noMore || this.isFirstLoading
+      return this.loading || this.noMore || this.isFirstLoading || this.isError
     }
   },
   created() {
@@ -91,7 +96,7 @@ export default {
       }
     },
     async videoRecommendWrapper(nextId) {
-      if (this.noMore) return
+      if (this.noMore || this.isError) return
       this.loading = true
       try {
         const resp = await videoRecommend(nextId)
@@ -103,13 +108,20 @@ export default {
             this.dataList = [...this.dataList, ...respData]
             this.nextId++
           }
+        } else {
+          this.isError = true // 标记错误
         }
       } catch (e) {
         console.error(e)
+        this.isError = true // 标记错误
       } finally {
         setTimeout(() => { this.loading = false }, 400)
       }
     },
+    retryLoad() {
+      this.isError = false
+      this.load()
+    },
     load() {
       this.videoRecommendWrapper(this.nextId)
     },

+ 98 - 0
src/views/my/MyApproval.vue

@@ -0,0 +1,98 @@
+<template>
+  <div>
+    <el-row class="movie-list">
+      <el-row>
+        <h2>我的审批</h2>
+        <el-button
+          size="mini"
+          type="danger"
+          @click="handleClear"
+        >清空</el-button>
+      </el-row>
+      <el-card>
+        <el-table
+          :data="dataList"
+          :show-header="true"
+          border
+          style="width: 100%"
+        >
+          <el-table-column
+            prop="id"
+            label="ID"
+          />
+          <el-table-column
+            prop="createAt"
+            label="创建时间"
+          />
+          <el-table-column
+            prop="bizType"
+            label="业务类型"
+          />
+          <el-table-column
+            prop="bizPayload"
+            label="业务数据"
+          />
+          <el-table-column
+            prop="status"
+            label="审批状态"
+          />
+        </el-table>
+      </el-card>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { getUserApprovalRequests } from '@/api/user'
+
+export default {
+  name: 'MyApproval',
+  data() {
+    return {
+      dataList: []
+    }
+  },
+  created() {
+    this.getData()
+  },
+  methods: {
+    getData() {
+      var queryParams = {}
+      queryParams.pn = 1
+      getUserApprovalRequests(queryParams).then(resp => {
+        if (resp.code === 0) {
+          this.dataList = resp.data.list
+        }
+      })
+    },
+    handleClear() {
+      this.$confirm('确定要清空所有消息?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.$message.info('clear')
+      }).catch(() => {
+        this.$message.info('已取消')
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+/*处于手机屏幕时*/
+@media screen and (max-width: 768px) {
+  .movie-list {
+    padding-top: 8px;
+    padding-left: 0.5%;
+    padding-right: 0.5%;
+  }
+}
+
+.movie-list {
+  padding-top: 15px;
+  padding-left: 6%;
+  padding-right: 6%;
+}
+</style>

+ 443 - 135
src/views/my/MyWallet.vue

@@ -1,122 +1,132 @@
 <template>
-  <el-main>
-    <el-row class="movie-list">
-      <el-col :md="6">
-        <el-card class="box-card">
-          <div slot="header" class="clearfix">
-            <span>我的小会员</span>
+  <el-main class="wallet-page-container">
+    <el-row :gutter="24" class="dashboard-grid">
+      <el-col :xs="24" :sm="12" :md="8" class="grid-col">
+        <el-card class="box-card-modern vip-card" shadow="never">
+          <div slot="header" class="card-header-custom">
+            <span class="header-title"><i class="el-icon-medal" /> 我的小会员</span>
           </div>
-          <div v-if="myVip !== null" class="text item">
-            <el-row v-if="!myVip.isVip">
-              您当前还不是小会员, <el-button type="text" @click="showPlanDialog">成为小会员</el-button>
-            </el-row>
-            <el-row v-else>
-              <el-row v-if="myVip.expired">
-                您的小会员已过期, <el-button type="text" @click="showPlanDialog">续费小会员</el-button>
-              </el-row>
-              <el-row v-else>
-                您的小会员还有 <span style="color: red">{{ myVip.expireIn }}</span> 天到期, <el-button type="text" @click="showPlanDialog">续费小会员</el-button>
-              </el-row>
-            </el-row>
+
+          <div v-if="myVip !== null" class="card-body-content">
+            <div v-if="!myVip.isVip" class="vip-status-box status-none">
+              <p class="status-tip">您当前还不是尊贵的小会员</p>
+              <el-button type="primary" size="medium" class="vip-action-btn" @click="showPlanDialog">立即开通</el-button>
+            </div>
+
+            <div v-else class="vip-status-box">
+              <div v-if="myVip.expired" class="status-expired">
+                <p class="status-tip">您的小会员已过期</p>
+                <el-button type="warning" size="medium" class="vip-action-btn" @click="showPlanDialog">立即续费</el-button>
+              </div>
+              <div v-else class="status-active">
+                <div class="days-remaining">
+                  <span class="num-highlight">{{ myVip.expireIn }}</span> <span class="unit">天到期</span>
+                </div>
+                <el-button type="text" size="small" class="renew-link-btn" icon="el-icon-refresh" @click="showPlanDialog">续费会员</el-button>
+              </div>
+            </div>
           </div>
         </el-card>
       </el-col>
-      <el-col :md="6" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
-        <el-card class="box-card">
-          <div slot="header" class="clearfix">
-            <span>我的钱包</span>
-            <el-button style="float: right; padding: 3px; color: red" type="text" @click="showTransferDialog">转帐</el-button>
-            <el-button style="float: right; padding: 3px; color: green" type="text" @click="showChargeDialog">充值</el-button>
+
+      <el-col :xs="24" :sm="12" :md="8" class="grid-col">
+        <el-card class="box-card-modern wallet-card" shadow="never">
+          <div slot="header" class="card-header-custom">
+            <span class="header-title"><i class="el-icon-wallet" /> 我的钱包</span>
+            <div class="header-actions">
+              <el-button size="mini" class="btn-charge" icon="el-icon-plus" @click="showChargeDialog">充值</el-button>
+              <el-button size="mini" class="btn-transfer" icon="el-icon-position" @click="showTransferDialog">转账</el-button>
+            </div>
           </div>
-          <div class="text item">
-            <el-row>
-              <h1>余额: <span style="color: green">¥{{ wallet.balance }} 元</span></h1>
-            </el-row>
+          <div class="card-body-content wallet-balance-box">
+            <span class="currency-symbol">¥</span>
+            <span class="balance-number">{{ wallet.balance }}</span>
+            <span class="balance-unit">元</span>
           </div>
         </el-card>
       </el-col>
-      <el-col :md="12" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
-        <el-card class="box-card">
-          <div slot="header" class="clearfix">
-            <span>我的账单</span>
+    </el-row>
+
+    <el-row class="bill-section-row">
+      <el-col :span="24">
+        <el-card class="box-card-modern bill-card" shadow="never">
+          <div slot="header" class="card-header-custom border-none">
+            <span class="header-title"><i class="el-icon-tickets" /> 账单明细</span>
           </div>
+
           <el-table
             :data="dataList"
             style="width: 100%"
+            class="custom-modern-table"
+            :header-cell-style="{background: '#f8fafc', color: '#475569', fontWeight: '600'}"
           >
-            <el-table-column
-              prop="createAt"
-              label="时间"
-            />
-            <el-table-column
-              prop="type"
-              label="类型"
-            >
+            <el-table-column prop="createAt" label="交易时间" min-width="160" />
+            <el-table-column prop="type" label="类型" width="120" align="center">
               <template slot-scope="scope">
-                <span v-if="scope.row.type === '支出'" style="color: red">
-                  {{ scope.row.type }}
-                </span>
-                <span v-else style="color: green">
+                <el-tag
+                  :type="scope.row.type === '支出' ? 'danger' : 'success'"
+                  size="small"
+                  effect="light"
+                  class="type-tag"
+                >
                   {{ scope.row.type }}
-                </span>
+                </el-tag>
               </template>
             </el-table-column>
-            <el-table-column
-              prop="quantity"
-              label="金额"
-            />
-            <el-table-column label="操作">
+            <el-table-column prop="amount" label="交易金额" min-width="120" align="right">
               <template slot-scope="scope">
-                <el-button
-                  size="mini"
-                  @click="handleDetail(scope.$index, scope.row)"
-                >详情</el-button>
+                <span :class="scope.row.type === '支出' ? 'amount-expense' : 'amount-income'">
+                  {{ scope.row.type === '支出' ? '-' : '+' }}¥{{ scope.row.quantity }}
+                </span>
               </template>
             </el-table-column>
           </el-table>
-          <el-pagination
-            :small="screenWidth <= 768"
-            layout="prev, pager, next"
-            :page-size="pageSize"
-            :current-page="currentPage"
-            :total="totalSize"
-            @current-change="handleCurrentChange"
-            @prev-click="handleCurrentChange"
-            @next-click="handleCurrentChange"
-          />
+
+          <div class="pagination-container">
+            <el-pagination
+              background
+              :small="screenWidth <= 768"
+              layout="total, prev, pager, next"
+              :page-size="pageSize"
+              :current-page="currentPage"
+              :total="totalSize"
+              @current-change="handleCurrentChange"
+            />
+          </div>
         </el-card>
       </el-col>
     </el-row>
 
     <el-dialog
-      title="充值"
+      title="账户充值"
       append-to-body
       :visible.sync="chargeDialog"
-      width="30%"
+      width="400px"
+      custom-class="custom-glass-dialog"
       center
     >
-      <el-form ref="form" :model="chargeForm">
-        <el-form-item label="充值金额">
-          <el-input-number v-model="chargeForm.quantity" :min="1" :max="100" style="margin-left: 5px" />
-        </el-form-item>
-        <el-form-item>
-          <el-button
-            type="primary"
-            @click="chargeWallet"
-          >确定</el-button>
+      <el-form ref="form" :model="chargeForm" label-position="top">
+        <el-form-item label="请输入充值金额 (元)">
+          <el-input-number v-model="chargeForm.amount" :min="1" :max="100" class="full-width-input" />
         </el-form-item>
       </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button size="medium" @click="chargeDialog = false">取 消</el-button>
+        <el-button type="primary" size="medium" class="btn-submit-glow" @click="onCharge">确认充值</el-button>
+      </div>
     </el-dialog>
+
     <el-dialog
-      title="转账"
+      title="安全转账"
       append-to-body
       :visible.sync="transferDialog"
-      width="30%"
+      width="420px"
+      custom-class="custom-glass-dialog"
       center
     >
-      <el-form ref="form" :model="transferForm">
-        <el-form-item label="转账给">
-          <el-select v-model="transferForm.userId" placeholder="请选择联系人">
+      <el-form ref="form" :model="transferForm" label-position="top">
+        <el-form-item label="选择收款联系人">
+          <el-select v-model="transferForm.userId" placeholder="请选择联系人" class="full-width-input">
             <el-option
               v-for="item in contactList"
               :key="item.value"
@@ -125,46 +135,54 @@
             />
           </el-select>
         </el-form-item>
-        <el-form-item label="转帐金额">
-          <el-input-number v-model="transferForm.quantity" :min="1" :max="10000" style="margin-left: 5px" />
-        </el-form-item>
-        <el-form-item>
-          <el-button
-            type="primary"
-            @click="transferTo"
-          >确定</el-button>
+        <el-form-item label="转账金额 (元)">
+          <el-input-number v-model="transferForm.amount" :min="1" :max="10000" class="full-width-input" />
         </el-form-item>
       </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button size="medium" @click="transferDialog = false">取 消</el-button>
+        <el-button type="success" size="medium" class="btn-submit-glow" @click="transferTo">确认转账</el-button>
+      </div>
     </el-dialog>
 
     <el-dialog
-      title="小会员计划"
+      title="开通 / 续费小会员"
       append-to-body
       :visible.sync="planDialog"
-      width="50%"
+      width="520px"
+      custom-class="custom-glass-dialog"
       center
     >
       <el-form ref="form" :model="planForm">
-        <el-form-item>
-          <el-radio-group v-for="(item, index) in vipPlans" :key="index" v-model="planForm.planId">
-            <el-radio style="margin-right: 5px" :label="item.planId" border>
-              {{ item.name }} (<span style="color: red">¥{{ item.price }}</span> 元)
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item>
-          <el-button
-            type="primary"
-            @click="buy"
-          >购买</el-button>
+        <el-form-item label="请选择订阅方案" label-position="top">
+          <div class="vip-plans-grid">
+            <el-radio-group v-model="planForm.planId" class="modern-radio-group">
+              <el-radio
+                v-for="(item, index) in vipPlans"
+                :key="index"
+                :label="item.planId"
+                border
+                class="plan-radio-item"
+              >
+                <div class="plan-radio-content">
+                  <span class="plan-name">{{ item.name }}</span>
+                  <span class="plan-price">¥{{ item.price }}</span>
+                </div>
+              </el-radio>
+            </el-radio-group>
+          </div>
         </el-form-item>
       </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button size="medium" @click="planDialog = false">取 消</el-button>
+        <el-button type="primary" size="medium" class="btn-submit-glow" @click="buy">立即支付</el-button>
+      </div>
     </el-dialog>
   </el-main>
 </template>
 
 <script>
-import { chargeWallet, getWallet, getWalletBill } from '@/api/user'
+import { addApprovalRequest, getWallet, getWalletBill } from '@/api/user'
 import { getMyVip, getVipPlans, vip } from '@/api/account'
 
 export default {
@@ -179,13 +197,13 @@ export default {
       dataList: [],
       chargeDialog: false,
       chargeForm: {
-        quantity: null
+        amount: null
       },
       contactList: [],
       transferDialog: false,
       transferForm: {
         userId: 0,
-        quantity: null
+        amount: null
       },
       wallet: {
         balance: '0.00'
@@ -195,7 +213,7 @@ export default {
       vipPlans: [],
       planForm: {
         planId: null,
-        quantity: null
+        amount: null
       },
       myVip: null
     }
@@ -222,34 +240,25 @@ export default {
     showChargeDialog() {
       this.chargeDialog = true
     },
-    chargeWallet() {
-      if (this.chargeForm.quantity === null) {
+    onCharge() {
+      if (this.chargeForm.amount === null) {
         this.$message('请填写充值金额')
         return
       }
-
       this.chargeDialog = false
-      chargeWallet(this.chargeForm).then(resp => {
+
+      const requestData = {
+        bizType: 'walletCharge',
+        bizPayload: JSON.stringify({ amount: 5 })
+      }
+      addApprovalRequest(requestData).then(resp => {
         if (resp.code === 0) {
-          this.$notify.info({
-            title: '充值请求已提交',
-            duration: 3000
-          })
+          this.$message.info('充值请求已提交')
         } else {
-          this.$notify({
-            title: '充值失败',
-            message: resp.msg,
-            type: 'error',
-            duration: 3000
-          })
+          this.$message.warning(resp.msg)
         }
       }).catch(error => {
-        this.$notify({
-          title: '充值失败',
-          message: error.message,
-          type: 'error',
-          duration: 3000
-        })
+        this.$message.error('充值失败: ' + error.message)
       })
     },
     showTransferDialog() {
@@ -266,9 +275,6 @@ export default {
         }
       })
     },
-    handleDetail(index, row) {
-      this.$message.info('账单详情')
-    },
     showPlanDialog() {
       this.planDialog = true
       getVipPlans().then(resp => {
@@ -299,13 +305,315 @@ export default {
 </script>
 
 <style scoped>
-.clearfix:before,
-.clearfix:after {
-  display: table;
-  content: "";
+/* ==================== 1. 基础布局及容器 ==================== */
+.wallet-page-container {
+  padding: 24px;
+  background-color: #f8fafc;
+  min-height: 100vh;
+}
+
+.dashboard-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  margin-bottom: 24px;
 }
 
-.clearfix:after {
-  clear: both;
+.grid-col {
+  flex: 1;
+  min-width: 320px;
+}
+
+/* ==================== 2. 现代通用卡片样式 ==================== */
+.box-card-modern {
+  background: #ffffff;
+  border-radius: 12px;
+  border: 1px solid #e2e8f0;
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px -1px rgba(0, 0, 0, 0.01);
+  transition: all 0.3s ease;
+}
+
+.card-header-custom {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  border-bottom: 1px solid #f1f5f9;
+}
+
+.header-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: #1e293b;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.header-title i {
+  font-size: 18px;
+  color: #475569;
+}
+
+.card-body-content {
+  padding: 24px;
+  min-height: 100px;
+  display: flex;
+  align-items: center;
+}
+
+/* ==================== 3. 会员卡片专属定制 ==================== */
+.vip-card {
+  border-left: 4px solid #6366f1;
+}
+
+.vip-card .header-title i {
+  color: #6366f1;
+}
+
+.vip-status-box {
+  width: 100%;
+}
+
+.status-none, .status-expired {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 100%;
+}
+
+.status-tip {
+  margin: 0;
+  font-size: 14px;
+  color: #64748b;
+}
+
+.vip-action-btn {
+  border-radius: 6px;
+  font-weight: 500;
+  box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
+}
+
+.status-active {
+  display: flex;
+  justify-content: space-between;
+  align-items: baseline;
+  width: 100%;
+}
+
+.days-remaining {
+  font-size: 14px;
+  color: #475569;
+}
+
+.num-highlight {
+  font-size: 32px;
+  font-weight: 700;
+  color: #ef4444;
+  font-family: 'Helvetica Neue', Arial, sans-serif;
+}
+
+.unit {
+  margin-left: 4px;
+  font-weight: 500;
+  color: #64748b;
+}
+
+.renew-link-btn {
+  color: #6366f1;
+  font-weight: 500;
+}
+
+/* ==================== 4. 钱包卡片专属定制 ==================== */
+.wallet-card {
+  border-left: 4px solid #10b981;
+}
+
+.wallet-card .header-title i {
+  color: #10b981;
+}
+
+.header-actions {
+  display: flex;
+  gap: 8px;
+}
+
+.btn-charge {
+  background: #f0fdf4;
+  color: #16a34a;
+  border-color: #bbf7d0;
+  border-radius: 6px;
+}
+.btn-charge:hover {
+  background: #16a34a !important;
+  color: #fff !important;
+}
+
+.btn-transfer {
+  background: #f8fafc;
+  color: #475569;
+  border-color: #cbd5e1;
+  border-radius: 6px;
+}
+.btn-transfer:hover {
+  background: #475569 !important;
+  color: #fff !important;
+}
+
+.wallet-balance-box {
+  display: flex;
+  align-items: baseline;
+  color: #0f172a;
+}
+
+.currency-symbol {
+  font-size: 20px;
+  font-weight: 600;
+  color: #10b981;
+  margin-right: 2px;
+}
+
+.balance-number {
+  font-size: 36px;
+  font-weight: 700;
+  font-family: 'Helvetica Neue', Arial, sans-serif;
+  letter-spacing: -0.5px;
+}
+
+.balance-unit {
+  font-size: 14px;
+  color: #64748b;
+  margin-left: 6px;
+  font-weight: 500;
+}
+
+/* ==================== 5. 账单明细板块 ==================== */
+.bill-section-row {
+  margin-top: 8px;
+}
+
+.border-none {
+  border: none !important;
+}
+
+.custom-modern-table {
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.type-tag {
+  border-radius: 4px;
+  font-weight: 500;
+  padding: 0 8px;
+}
+
+.amount-expense {
+  color: #ef4444;
+  font-weight: 600;
+  font-size: 15px;
+}
+
+.amount-income {
+  color: #10b981;
+  font-weight: 600;
+  font-size: 15px;
+}
+
+.pagination-container {
+  display: flex;
+  justify-content: center;
+  padding-top: 24px;
+}
+
+/* ==================== 6. 弹窗内控件与微调 ==================== */
+::v-deep .custom-glass-dialog {
+  border-radius: 12px !important;
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1) !important;
+}
+
+::v-deep .custom-glass-dialog .el-dialog__header {
+  padding: 20px 20px 10px;
+  border-bottom: 1px solid #f1f5f9;
+}
+
+::v-deep .custom-glass-dialog .el-dialog__title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1e293b;
+}
+
+.full-width-input {
+  width: 100% !important;
+}
+
+::v-deep .full-width-input.el-input-number .el-input__inner {
+  text-align: left;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+
+.btn-submit-glow {
+  box-shadow: 0 4px 10px rgba(0,0,0,0.05);
+}
+
+/* 会员选项排版美化 */
+.modern-radio-group {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  width: 100%;
+}
+
+.plan-radio-item {
+  width: 100% !important;
+  margin-right: 0 !important;
+  margin-left: 0 !important;
+  height: auto !important;
+  padding: 12px 16px !important;
+  border-radius: 8px !important;
+  border: 1px solid #e2e8f0 !important;
+}
+
+::v-deep .plan-radio-item .el-radio__label {
+  width: 100%;
+  padding-left: 8px;
+}
+
+.plan-radio-content {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 95%;
+}
+
+.plan-name {
+  font-size: 14px;
+  font-weight: 500;
+  color: #334155;
+}
+
+.plan-price {
+  font-size: 15px;
+  font-weight: 600;
+  color: #ef4444;
+}
+
+/* 响应式额外补丁 */
+@media screen and (max-width: 640px) {
+  .wallet-page-container {
+    padding: 12px;
+  }
+  .status-none, .status-expired {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 12px;
+  }
+  .vip-action-btn {
+    width: 100%;
+  }
 }
 </style>