Переглянути джерело

refactor: admin-h5 多处UI优化

- 登录页新增记住密码功能,品牌名改为超级进化,按钮文字垂直居中
- 首页平均时长秒转分钟显示,主指标卡片补充图标,快捷功能新增用户管理入口
- 用户列表退款按钮移入卡片头部,不再独占一行

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 20 годин тому
батько
коміт
67b830293c

+ 25 - 2
admin-h5/src/pages/index/index.vue

@@ -75,10 +75,16 @@
       <!-- 主指标:订单 + 消费 -->
       <view class="metrics-primary">
         <view class="metric-card-lg">
+          <view class="metric-lg-icon">
+            <AppIcon name="clipboard" :size="20" color="#C6171E" />
+          </view>
           <text class="metric-lg-value">{{ todayOrders }}</text>
           <text class="metric-lg-label">今日订单</text>
         </view>
         <view class="metric-card-lg">
+          <view class="metric-lg-icon">
+            <AppIcon name="dollar" :size="20" color="#C6171E" />
+          </view>
           <text class="metric-lg-value">¥{{ formatAmount(todayConsumption) }}</text>
           <text class="metric-lg-label">消费总额</text>
         </view>
@@ -128,6 +134,12 @@
           </view>
           <text class="action-label">财务管理</text>
         </view>
+        <view class="action-item" @click="navigateTo('/pages/user/list')">
+          <view class="action-icon-wrap">
+            <AppIcon name="users" :size="26" color="#FFFFFF" />
+          </view>
+          <text class="action-label">用户管理</text>
+        </view>
       </view>
 
       <!-- 底栏信息 -->
@@ -248,7 +260,7 @@ const loadData = async (stationId) => {
       todayOrders.value = data.todayWashOrders || 0
       todayConsumption.value = data.todayConsumptionAmount || 0
       avgOrderPrice.value = data.avgOrderPrice || 0
-      avgDuration.value = data.avgOrderDuration || 0
+      avgDuration.value = Math.round((data.avgOrderDuration || 0) / 60 * 10) / 10
       todayMembers.value = data.todayRegisteredMembers || 0
     }
 
@@ -407,6 +419,15 @@ onPullDownRefresh(async () => {
   gap: 12rpx;
   transition: box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
 }
+.metric-lg-icon {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 18rpx;
+  background: #F5F7FA;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
 .metric-card-lg:active {
   transform: translateY(-2rpx);
   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
@@ -487,11 +508,12 @@ onPullDownRefresh(async () => {
 /* ===== Quick Actions ===== */
 .actions-row {
   display: flex;
+  flex-wrap: wrap;
   gap: 16rpx;
   margin-bottom: 28rpx;
 }
 .action-item {
-  flex: 1;
+  flex: 0 0 calc(50% - 8rpx);
   background: #FFFFFF;
   border-radius: 24rpx;
   padding: 32rpx 12rpx 24rpx;
@@ -499,6 +521,7 @@ onPullDownRefresh(async () => {
   flex-direction: column;
   align-items: center;
   gap: 18rpx;
+  box-sizing: border-box;
 }
 .action-icon-wrap {
   width: 88rpx;

+ 68 - 1
admin-h5/src/pages/login/login.vue

@@ -4,7 +4,7 @@
       <!-- Logo -->
       <view class="brand">
         <view class="brand-mark"></view>
-        <text class="brand-name">自助洗车</text>
+        <text class="brand-name">超级进化</text>
         <text class="brand-sub">运营管理平台</text>
       </view>
 
@@ -38,6 +38,13 @@
           />
         </view>
 
+        <view class="remember-row" @click="rememberMe = !rememberMe">
+          <view class="remember-check" :class="{ checked: rememberMe }">
+            <text v-if="rememberMe" class="remember-tick">✓</text>
+          </view>
+          <text class="remember-label">记住密码</text>
+        </view>
+
         <button class="submit-btn" @click="handleLogin" :disabled="loading">
           {{ loading ? '登录中...' : '登录' }}
         </button>
@@ -54,6 +61,7 @@ import { storage, showToast } from '../../utils/index.js'
 import JSEncrypt from 'jsencrypt'
 
 const loading = ref(false)
+const rememberMe = ref(false)
 const loginForm = ref({
   mobilePhone: '',
   password: ''
@@ -66,6 +74,17 @@ const encryptData = (str) => {
   return encryptor.encrypt(str)
 }
 
+const REMEMBER_KEY = 'remembered_login'
+
+onMounted(() => {
+  const saved = storage.get(REMEMBER_KEY)
+  if (saved) {
+    loginForm.value.mobilePhone = saved.mobilePhone || ''
+    loginForm.value.password = saved.password || ''
+    rememberMe.value = true
+  }
+})
+
 const loadDictionaries = async () => {
   await loadDicts()
 }
@@ -94,6 +113,15 @@ const handleLogin = async () => {
     const res = await login(phone, encryptedPassword)
 
     if (res && res.code === 200) {
+      if (rememberMe.value) {
+        storage.set(REMEMBER_KEY, {
+          mobilePhone: phone,
+          password: loginForm.value.password
+        })
+      } else {
+        storage.remove(REMEMBER_KEY)
+      }
+
       storage.set('token', res.data.accessToken)
       storage.set('userInfo', {
         id: res.data.id,
@@ -215,6 +243,42 @@ const handleLogin = async () => {
   color: #B0B0B0;
 }
 
+/* Remember Me */
+.remember-row {
+  display: flex;
+  align-items: center;
+  gap: 14rpx;
+  cursor: pointer;
+}
+
+.remember-check {
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 8rpx;
+  border: 2rpx solid #CCCCCC;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.2s;
+}
+
+.remember-check.checked {
+  background: #C6171E;
+  border-color: #C6171E;
+}
+
+.remember-tick {
+  font-size: 22rpx;
+  color: #FFFFFF;
+  font-weight: 700;
+  line-height: 1;
+}
+
+.remember-label {
+  font-size: 26rpx;
+  color: #666666;
+}
+
 /* Button */
 .submit-btn {
   width: 100%;
@@ -226,6 +290,9 @@ const handleLogin = async () => {
   font-size: 30rpx;
   font-weight: 600;
   margin-top: 16rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
   transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
 }
 

+ 13 - 13
admin-h5/src/pages/user/list.vue

@@ -26,7 +26,10 @@
             <text class="item-phone">{{ item.mobilePhone || '-' }}</text>
             <text class="item-id">UID: {{ item.userId || '-' }}</text>
           </view>
-          <text class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('User.status', item.status) }}</text>
+          <view class="item-header-right">
+            <text class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('User.status', item.status) }}</text>
+            <button class="refund-btn" @click.stop="handleApplyRefund(item)">退款</button>
+          </view>
         </view>
         <view class="item-content">
           <!-- 余额概览 -->
@@ -79,9 +82,6 @@
             </view>
           </view>
         </view>
-        <view class="item-footer">
-          <button class="refund-btn" @click.stop="handleApplyRefund(item)">申请退款</button>
-        </view>
       </view>
     </view>
 
@@ -334,6 +334,12 @@ onMounted(async () => {
   margin-top: 4rpx;
   display: block;
 }
+.item-header-right {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  flex-shrink: 0;
+}
 .status-tag {
   font-size: 22rpx;
   padding: 6rpx 16rpx;
@@ -422,20 +428,14 @@ onMounted(async () => {
   margin-top: 2rpx;
 }
 
-.item-footer {
-  display: flex;
-  justify-content: flex-end;
-  padding-top: 16rpx;
-  border-top: 1rpx solid #F0F0F0;
-  margin-top: 16rpx;
-}
 .refund-btn {
-  padding: 10rpx 28rpx;
+  padding: 6rpx 20rpx;
   background: #C6171E;
   color: #FFFFFF;
   border-radius: 16rpx;
-  font-size: 24rpx;
+  font-size: 22rpx;
   border: none;
+  line-height: 1.4;
 }
 
 .load-more {