Ver código fonte

feat(mobile-h5): apply scheme B empty state globally

Restyle van-empty as a dashed mint soft card and add optional tip on home and comps demo so all empty states stay consistent.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 meses atrás
pai
commit
ef259f1f0c

+ 267 - 0
docs/superpowers/plans/2026-08-10-mobile-h5-empty-state.md

@@ -0,0 +1,267 @@
+# mobile-h5 全局空态(方案 B)Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将 `van-empty` 统一为方案 B(薄荷绿虚线软卡 + 圆形 inbox 插画 + 主/副文案),全站空态一致。
+
+**Architecture:** 只改 H5 shim 组件 `mobile-h5/components/van-empty/van-empty.vue`;页面经 easycom / 显式 import 自动吃到新样式。主页补可选 `tip`;demo comps 演示副文案。
+
+**Tech Stack:** uni-app Vue(Options API)、scoped CSS、三墙 Token(`#0a9f6d` 等)
+
+**Spec:** `docs/superpowers/specs/2026-08-10-mobile-h5-empty-state-design.md`  
+**视觉参考:** `mobile-ui/zkxw-mobile-empty-state-demo.html` 方案 B
+
+---
+
+## File map
+
+| 路径 | 职责 |
+|------|------|
+| `mobile-h5/components/van-empty/van-empty.vue` | 方案 B 结构 + 样式 + `tip` prop |
+| `mobile-h5/pages/index/index.vue` | 空菜单空态补 `tip` |
+| `mobile-h5/pages/demo/comps.vue` | 演示页补 `tip`(可选副文案) |
+| `mobile-h5/pages/demo/list.vue` | 保持 `description="暂无数据"`,无 tip(验证无占位) |
+
+---
+
+### Task 1: 重写 van-empty 为方案 B
+
+**Files:**
+- Modify: `mobile-h5/components/van-empty/van-empty.vue`
+
+- [ ] **Step 1: 替换组件全文**
+
+将文件替换为:
+
+```vue
+<template>
+  <view class="v-empty">
+    <view class="v-empty__card">
+      <view class="v-empty__blob">
+        <view class="v-empty__inbox">
+          <view class="v-empty__inbox-lid" />
+          <view class="v-empty__inbox-body" />
+        </view>
+      </view>
+      <text class="v-empty__desc">{{ description || '暂无数据' }}</text>
+      <text v-if="tip" class="v-empty__tip">{{ tip }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+  /** H5 兼容:替代 @vant/weapp van-empty(方案 B 虚线软卡) */
+  export default {
+    name: 'VanEmptyShim',
+    props: {
+      description: { type: String, default: '' },
+      tip: { type: String, default: '' }
+    }
+  }
+</script>
+
+<style scoped>
+  .v-empty {
+    display: flex;
+    flex-direction: column;
+    align-items: stretch;
+    padding: 16rpx 0;
+  }
+
+  .v-empty__card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 16rpx;
+    padding: 44rpx 28rpx 40rpx;
+    border-radius: 28rpx;
+    background: linear-gradient(165deg, #eef9f3 0%, #ffffff 55%);
+    border: 3rpx dashed #b7e5d0;
+    box-shadow: inset 0 0 0 1rpx rgba(255, 255, 255, 0.8);
+  }
+
+  .v-empty__blob {
+    width: 128rpx;
+    height: 128rpx;
+    border-radius: 50%;
+    background: radial-gradient(circle at 35% 30%, #ffffff, #d8f3e7 70%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  /* CSS 近似 inbox 图标,避免小程序/H5 对 inline SVG 兼容差异 */
+  .v-empty__inbox {
+    position: relative;
+    width: 56rpx;
+    height: 48rpx;
+  }
+
+  .v-empty__inbox-lid {
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 0;
+    height: 18rpx;
+    border: 4rpx solid #0a9f6d;
+    border-bottom: none;
+    border-radius: 8rpx 8rpx 0 0;
+    box-sizing: border-box;
+  }
+
+  .v-empty__inbox-body {
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    height: 34rpx;
+    border: 4rpx solid #0a9f6d;
+    border-radius: 0 0 8rpx 8rpx;
+    box-sizing: border-box;
+  }
+
+  .v-empty__inbox-body::before {
+    content: '';
+    position: absolute;
+    left: 10rpx;
+    right: 10rpx;
+    top: -10rpx;
+    height: 16rpx;
+    border: 4rpx solid #0a9f6d;
+    border-top: none;
+    border-radius: 0 0 6rpx 6rpx;
+    box-sizing: border-box;
+    background: transparent;
+  }
+
+  .v-empty__desc {
+    font-size: 26rpx;
+    color: #5d746a;
+    text-align: center;
+    line-height: 1.4;
+  }
+
+  .v-empty__tip {
+    font-size: 22rpx;
+    color: #8aa198;
+    text-align: center;
+    line-height: 1.4;
+  }
+</style>
+```
+
+说明:用 CSS 拼 inbox 轮廓,避免 uni-app H5 对 `<svg><use>` 不稳定;视觉对齐 Demo B 的绿色线框收件箱。
+
+- [ ] **Step 2: 自检色值**
+
+确认文件中无 `#10b981`;主色仅为 `#0a9f6d`。
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add mobile-h5/components/van-empty/van-empty.vue
+git commit -m "feat(mobile-h5): restyle van-empty as dashed soft card (scheme B)"
+```
+
+---
+
+### Task 2: 调用方补 tip(主页 + comps)
+
+**Files:**
+- Modify: `mobile-h5/pages/index/index.vue`
+- Modify: `mobile-h5/pages/demo/comps.vue`
+- Verify only: `mobile-h5/pages/demo/list.vue`(不改,保持无 tip)
+
+- [ ] **Step 1: 主页空态加 tip**
+
+在 `mobile-h5/pages/index/index.vue` 将:
+
+```vue
+<VanEmpty v-else description="暂无可用菜单权限" />
+```
+
+改为:
+
+```vue
+<VanEmpty
+  v-else
+  description="暂无可用菜单权限"
+  tip="请联系管理员开通功能入口"
+/>
+```
+
+- [ ] **Step 2: comps 演示页加 tip**
+
+在 `mobile-h5/pages/demo/comps.vue` 将:
+
+```vue
+<van-empty description="暂无相关数据" />
+```
+
+改为:
+
+```vue
+<van-empty
+  description="暂无相关数据"
+  tip="可下拉刷新,或点击上方按钮新增一条记录"
+/>
+```
+
+- [ ] **Step 3: 确认 list 页不传 tip**
+
+`mobile-h5/pages/demo/list.vue` 保持:
+
+```vue
+<van-empty v-if="!list.length" description="暂无数据" />
+```
+
+验收:无 tip 时不出现空白副文案行。
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add mobile-h5/pages/index/index.vue mobile-h5/pages/demo/comps.vue
+git commit -m "feat(mobile-h5): add empty-state tip on home and comps demo"
+```
+
+---
+
+### Task 3: 视觉验收
+
+**Files:** 无代码变更
+
+- [ ] **Step 1: 本地打开 / 刷新 H5**
+
+主页(菜单为空)应看到:虚线软卡、圆形插画、主文案「暂无可用菜单权限」、副文案「请联系管理员开通功能入口」;无 ∅。
+
+- [ ] **Step 2: 对照 Demo**
+
+对照 `mobile-ui/zkxw-mobile-empty-state-demo.html` 方案 B:结构与色调一致即可(inbox 为 CSS 近似)。
+
+- [ ] **Step 3: 抽查 list / comps**
+
+- comps:有 tip  
+- list 清空数据后:仅主文案「暂无数据」,无 tip 占位
+
+---
+
+## Spec coverage checklist
+
+| Spec 要求 | Task |
+|-----------|------|
+| 方案 B 视觉(渐变虚线卡 + blob + 主/副文案) | Task 1 |
+| `description` + `tip` API | Task 1 |
+| 主页 tip | Task 2 |
+| comps tip 演示 | Task 2 |
+| list 无 tip 不占位 | Task 2 Step 3 / Task 3 |
+| 三墙色、无 `#10b981` | Task 1 Step 2 |
+| 不引入 E 变体 / 不引真实 Vant | 全计划未包含 |
+
+---
+
+## Out of scope
+
+- 横向紧凑变体 E
+- 真实 `@vant/weapp` 依赖
+- 改业务菜单数据源(菜单仍为空数组)

+ 102 - 0
mobile-h5/components/van-empty/van-empty.vue

@@ -0,0 +1,102 @@
+<template>
+  <view class="v-empty">
+    <view class="v-empty__card">
+      <view class="v-empty__blob">
+        <view class="v-empty__inbox">
+          <view class="v-empty__inbox-tray" />
+          <view class="v-empty__inbox-slot" />
+        </view>
+      </view>
+      <text class="v-empty__desc">{{ description || '暂无数据' }}</text>
+      <text v-if="tip" class="v-empty__tip">{{ tip }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+  /** H5 兼容:替代 @vant/weapp van-empty(方案 B 虚线软卡) */
+  export default {
+    name: 'VanEmptyShim',
+    props: {
+      description: { type: String, default: '' },
+      tip: { type: String, default: '' }
+    }
+  }
+</script>
+
+<style scoped>
+  .v-empty {
+    display: flex;
+    flex-direction: column;
+    align-items: stretch;
+    padding: 16rpx 0;
+  }
+
+  .v-empty__card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 16rpx;
+    padding: 44rpx 28rpx 40rpx;
+    border-radius: 28rpx;
+    background: linear-gradient(165deg, #eef9f3 0%, #ffffff 55%);
+    border: 3rpx dashed #b7e5d0;
+    box-shadow: inset 0 0 0 1rpx rgba(255, 255, 255, 0.8);
+  }
+
+  .v-empty__blob {
+    width: 128rpx;
+    height: 128rpx;
+    border-radius: 50%;
+    background: radial-gradient(circle at 35% 30%, #ffffff, #d8f3e7 70%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  /* CSS 近似 inbox,避免 inline SVG 在部分端兼容问题 */
+  .v-empty__inbox {
+    position: relative;
+    width: 56rpx;
+    height: 48rpx;
+  }
+
+  .v-empty__inbox-tray {
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 8rpx;
+    bottom: 0;
+    border: 4rpx solid #0a9f6d;
+    border-radius: 6rpx 6rpx 10rpx 10rpx;
+    box-sizing: border-box;
+  }
+
+  .v-empty__inbox-slot {
+    position: absolute;
+    left: 8rpx;
+    right: 8rpx;
+    top: 0;
+    height: 20rpx;
+    border: 4rpx solid #0a9f6d;
+    border-bottom: none;
+    border-radius: 6rpx 6rpx 0 0;
+    box-sizing: border-box;
+    background: radial-gradient(circle at 35% 30%, #ffffff, #d8f3e7 70%);
+  }
+
+  .v-empty__desc {
+    font-size: 26rpx;
+    color: #5d746a;
+    text-align: center;
+    line-height: 1.4;
+  }
+
+  .v-empty__tip {
+    font-size: 22rpx;
+    color: #8aa198;
+    text-align: center;
+    line-height: 1.4;
+  }
+</style>

+ 4 - 1
mobile-h5/pages/demo/comps.vue

@@ -48,7 +48,10 @@
           color="#087f57"
         />
         <view style="min-height: 360rpx; margin-top: 24rpx">
-          <van-empty description="暂无相关数据" />
+          <van-empty
+            description="暂无相关数据"
+            tip="可下拉刷新,或点击上方按钮新增一条记录"
+          />
         </view>
         <view style="display: flex; justify-content: center; padding: 32rpx 0">
           <van-loading size="24px" color="#0a9f6d">加载中</van-loading>

+ 9 - 57
mobile-h5/pages/index/index.vue

@@ -3,7 +3,7 @@
     <view class="home-header">
       <view class="home-header__top">
         <view class="brand">中科兴皖 · 移动工作台</view>
-        <text class="home-logout" @click="onLogout">退出</text>
+        <text class="home-logout" @click="onLogout">退出登录</text>
       </view>
       <view class="hello">你好,{{ userName }}</view>
       <view class="meta">
@@ -14,80 +14,35 @@
     </view>
 
     <view class="home-body">
-      <view class="perm-banner">
-        菜单卡片按角色权限动态渲染;无权限的功能入口不会出现在宫格中。
-      </view>
-
       <view class="panel">
         <view class="panel-hd">
           <text class="panel-title">功能菜单</text>
           <text class="hint">共 {{ menus.length }} 项</text>
         </view>
         <MenuGrid v-if="menus.length" :menus="menus" @select="onMenuSelect" />
-        <van-empty v-else description="暂无可用菜单权限" />
+        <VanEmpty
+          v-else
+          description="暂无可用菜单权限"
+          tip="请联系管理员开通功能入口"
+        />
       </view>
-
-      <TodoPanel
-        :stats="todoStats"
-        :items="todoItems"
-        @more="goList"
-        @stat-click="goList"
-        @item-click="goList"
-      />
     </view>
   </view>
 </template>
 
 <script>
   import MenuGrid from '../../components/MenuGrid.vue'
-  import TodoPanel from '../../components/TodoPanel.vue'
-  import {
-    filterMenusByPermission,
-    DEFAULT_GRANTED_IDS
-  } from '../../utils/menu-mock.js'
+  import VanEmpty from '../../components/van-empty/van-empty.vue'
   import { clearSession, getUser } from '../../utils/auth-mock.js'
 
   export default {
-    components: { MenuGrid, TodoPanel },
+    components: { MenuGrid, VanEmpty },
     data() {
       return {
         userName: '张三',
         roleName: '业务专员',
         deptName: '样本运营部',
-        menus: filterMenusByPermission(DEFAULT_GRANTED_IDS),
-        todoStats: { approve: 3, pay: 1, alert: 1 },
-        todoItems: [
-          {
-            id: 't1',
-            title: '采集协议审批',
-            desc: '王女士 · 分期首期待复核',
-            tag: '待审批',
-            tagType: 'warning',
-            time: '09:20',
-            icon: 'todo-list-o',
-            iconClass: 'i-approve'
-          },
-          {
-            id: 't2',
-            title: '缴费确认',
-            desc: 'XY20260810001 · ¥12,800',
-            tag: '待确认',
-            tagType: 'warning',
-            time: '昨天',
-            icon: 'balance-list-o',
-            iconClass: 'i-pay'
-          },
-          {
-            id: 't3',
-            title: '质检异常',
-            desc: 'QC-8821 · 结论待录入',
-            tag: '异常',
-            tagType: 'danger',
-            time: '08-08',
-            icon: 'warning-o',
-            iconClass: 'i-alert'
-          }
-        ]
+        menus: []
       }
     },
     onShow() {
@@ -102,9 +57,6 @@
           uni.navigateTo({ url: m.path })
         }
       },
-      goList() {
-        uni.navigateTo({ url: '/pages/demo/list' })
-      },
       onLogout() {
         clearSession()
         uni.reLaunch({ url: '/pages/login/index' })