diff --git a/pages_tool/card/list.vue b/pages_tool/card/list.vue
index c92d4a2..aa08294 100644
--- a/pages_tool/card/list.vue
+++ b/pages_tool/card/list.vue
@@ -63,9 +63,20 @@
— 没有更多了 —
-
-
-
+
+
+ 加载中…
+
+
+
+
+
+
+ {{ emptyScene.icon }}
+ {{ emptyScene.title }}
+ {{ emptyScene.sub }}
+
+
⏳
申请审核中
您的建卡申请正在等待商家审批
@@ -144,9 +155,34 @@ export default {
apply: null, // { status, status_text, note, has_card }
applyName: '',
applyMobile: '',
- applying: false
+ applying: false,
+ // 首次数据请求是否已结束: 未结束前不渲染空态,
+ // 否则重新编译/刷新时会先闪一下「还没有名片 + 申请表单」(列表其实有数据)
+ booted: false
};
},
+ computed: {
+ /**
+ * 筛选态下的空态文案。
+ * 重要: 只有「全部 + 无搜索 + 无收藏筛选」才代表"用户还没有名片"(这时才引导建卡申请);
+ * 待跟进/已完成/搜索/收藏 只是当下没有符合条件的卡, 不能弹申请表单。
+ */
+ emptyScene() {
+ if (this.followTab === 'due') {
+ return { icon: '📇', title: '暂无待跟进名片', sub: '有跟进中的名片会出现在这里' };
+ }
+ if (this.followTab === 'done') {
+ return { icon: '📇', title: '暂无已完成名片', sub: '标记完成的名片会出现在这里' };
+ }
+ if (this.onlyFav) {
+ return { icon: '☆', title: '暂无收藏的名片', sub: '点名片右侧的星标即可收藏' };
+ }
+ if (this.keyword.trim()) {
+ return { icon: '🔍', title: '未找到匹配的名片', sub: '换个关键词试试' };
+ }
+ return null;
+ }
+ },
onLoad(options) {
// 扫码带参预填(文档 §5): 二维码/链接带 claim_code 或 card_id 直接进入认领
if (options && (options.claim_code || options.card_id)) {
@@ -154,7 +190,12 @@ export default {
.toUpperCase().replace(/\s+/g, '');
this.claimMode = options.claim_code ? 'code' : 'id';
}
- this.refresh();
+ this.bootstrap();
+ },
+ onShow() {
+ // 首次 onShow 紧跟 onLoad(由 bootstrap 负责首屏加载), 这里只处理"返回本页"的刷新
+ if (this._entered) this.refresh();
+ this._entered = true;
},
onPullDownRefresh() {
this.refresh(() => uni.stopPullDownRefresh());
@@ -165,6 +206,45 @@ export default {
this.loadPage(this.page + 1);
},
methods: {
+ /**
+ * 首屏加载: 等 member token 就绪后再请求。
+ *
+ * 背景(为什么需要): App.vue 的 onLaunch 是 async, token 在多个 await
+ * (i18n / 主题 / 远端配置 / 隐私弹窗) 之后才从 storage 恢复到 store。
+ * 页面 onLoad 在 onLaunch 第一次 await 让出后就执行, 此时 store.state.token
+ * 仍是 null → card/lists 以未登录身份请求, 后端返回空列表;
+ * 本页原本又没有 onShow 重拉, 于是表现为「刷新/重新编译后没有名片,
+ * 必须退出页面重新进入才看得到」。
+ */
+ bootstrap() {
+ // 等 token 期间也处于"加载中", 否则首屏会先渲染空态
+ this.isLoading = true;
+ this.waitTokenReady().then(() => this.refresh());
+ },
+ /**
+ * 等 token 就绪:
+ * - store 已有 token → 直接放行;
+ * - storage 有 token 但 store 还没有 → onLaunch 还在跑, 轮询等待(最多 timeout);
+ * - storage 也没有 → 未登录/游客, 不等, 用空 token 请求(由后端决定返回)。
+ */
+ waitTokenReady(timeout = 3000) {
+ if (this.$store.state.token) return Promise.resolve(true);
+ let stored = '';
+ try { stored = uni.getStorageSync('token') || ''; } catch (e) { stored = ''; }
+ if (!stored) return Promise.resolve(false);
+ return new Promise(resolve => {
+ const start = Date.now();
+ const timer = setInterval(() => {
+ if (this.$store.state.token) {
+ clearInterval(timer);
+ resolve(true);
+ } else if (Date.now() - start >= timeout) {
+ clearInterval(timer);
+ resolve(false);
+ }
+ }, 50);
+ });
+ },
avatarText(name) {
return (name || '?').trim().charAt(0).toUpperCase();
},
@@ -310,7 +390,8 @@ export default {
this.page = pageNo;
this.list = pageNo === 1 ? rows : this.list.concat(rows);
// 空态(首页无卡): 拉取建卡申请状态, 驱动空态申请 UI(2.11.15)
- if (pageNo === 1 && rows.length === 0) this.loadApplyStatus();
+ // 仅「全部 + 无搜索 + 无收藏筛选」时才需要(其余筛选为空不代表用户没卡)
+ if (pageNo === 1 && rows.length === 0 && !this.emptyScene) this.loadApplyStatus();
this.emptyText = this.keyword.trim()
? '未找到匹配的名片'
: '暂无名片,可点击右上角「新建」或「认领」添加';
@@ -318,10 +399,13 @@ export default {
this.handleApiError(res);
}
this.isLoading = false;
+ // 首次请求结束(无论成功/失败)才允许渲染空态
+ this.booted = true;
typeof done === 'function' && done();
},
fail: () => {
this.isLoading = false;
+ this.booted = true;
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
typeof done === 'function' && done();
}
@@ -666,6 +750,14 @@ export default {
border-radius: 24rpx;
}
+/* 首屏加载占位(替代"先闪空态") */
+.first-loading {
+ text-align: center;
+ color: #999;
+ font-size: 26rpx;
+ padding: 160rpx 0;
+}
+
/* 空态: 建卡申请 */
.card-empty {
text-align: center;