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;