diff --git a/src/components/AppTabBar/index.vue b/src/components/AppTabBar/index.vue
index 51541f9..209c4f1 100644
--- a/src/components/AppTabBar/index.vue
+++ b/src/components/AppTabBar/index.vue
@@ -1,10 +1,10 @@
/**
- * Tab 1 · 主页
- * 数据来自 GET /v1/homepage。
- * 纵向顺序:
- * 品牌开场区(吸顶固定)→ 主视觉大卡(180px)→ 玩转龙里(左一通高 + 右二上下)
- * → 全域导览 → 精选推荐 → 免责声明
- * 无顶栏、无搜索、不展示任何权益入口。
+ * 主包壳页面:主页 / 权益 / 我的 三块面板同屏共存,底部 Tab 栏只渲染一次
+ * ---------------------------------------------------------------------------
*/
-import { computed, ref } from "vue";
+import { onMounted, ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
-import contentApi from "@/api/content";
-import { goCatalog, previewImages } from "@/utils/nav";
+import { TABS } from "@/constant";
+import { consumePendingTab, registerTabController, setTabQuery } from "@/utils/nav";
+import HomePanel from "@/views/home/index.vue";
+import BenefitPanel from "@/views/benefit/index.vue";
+import MinePanel from "@/views/mine/index.vue";
-const home = ref({});
-const featured = ref([]);
+const active = ref("home");
+const homeRef = ref(null);
+const benefitRef = ref(null);
+const mineRef = ref(null);
-const brandName = computed(() => home.value?.brand_name || "");
-/** 品牌名末两字走品牌色(「发现龙里」→ 发现 + 龙里) */
-const brandHead = computed(() => brandName.value.slice(0, -2));
-const brandTail = computed(() => brandName.value.slice(-2));
-const brand = computed(() => home.value?.brand || {});
-const hero = computed(() => home.value?.hero || {});
-const entries = computed(() => home.value?.entries || []);
-const footerLines = computed(() => home.value?.footer_lines || []);
-/** 玩转龙里:有板块数据才展示整块 */
-const hasSections = computed(() => entries.value.length > 0);
-/** 全域导览:有图或有标题才展示整块 */
-const hasGuide = computed(() => !!(home.value?.guide_image_url || home.value?.guide_title));
-/** 精选推荐:有卡片才展示整块(含标题行与「更多」) */
-const hasFeatured = computed(() => featured.value.length > 0);
+const panelRefs = { home: homeRef, benefit: benefitRef, mine: mineRef };
-const entryOf = (key) => entries.value.find((e) => e.section_key === key) || {};
+/** 面板按需刷新(切到它、或页面 onShow 时) */
+const showPanel = (key) => panelRefs[key]?.value?.onPanelShow?.();
-let loading = false;
-
-const load = async () => {
- if (loading) return;
- loading = true;
- try {
- const data = await contentApi.getHomepage();
- home.value = data || {};
- featured.value = data?.featured?.items || [];
- } catch (e) {
- home.value = {};
- featured.value = [];
- } finally {
- loading = false;
- }
+/**
+ * 切 Tab 的唯一入口:Tab 栏点击、页面内 switchTabTo(key, query)、登录回跳都汇到这里。
+ * query 用 nav 的内存透传(如 `?seg=mine`),由目标面板 onPanelShow 消费。
+ */
+const goTab = (key, query = "") => {
+ if (!TABS.some((t) => t.key === key)) return;
+ setTabQuery(query);
+ active.value = key;
+ showPanel(key);
};
-onShow(() => {
- if (!home.value?.brand_name) load();
+onMounted(() => {
+ // 面板的 onMounted 早于本页 onMounted,此时 ref 已就绪
+ registerTabController({ go: goTab, getActive: () => active.value });
+ // 从子页 / 登录页切过来:nav 里可能压着待切 Tab(登录回跳、reLaunch 冷启)
+ const pending = consumePendingTab();
+ goTab(pending?.key || active.value, pending?.query || "");
});
-/** 主视觉大卡「开始探索」/ 精选「更多」→ hero 所属板块 */
-const onStartExplore = () => goCatalog(hero.value.section_key);
-const onMoreFeatured = () => goCatalog(hero.value.section_key);
-
-/** 导览图放大(接口已给图,不做独立页) */
-const onMapTap = () => {
- const url = home.value?.guide_image_url;
- if (!url) return;
- previewImages([url], url);
-};
+onShow(() => {
+ // 从板块列表 / 详情 / 登录页等返回:让当前面板按需刷新(各家内部有幂等守卫)
+ showPanel(active.value);
+});
-
-
-
-
-
-
-
- {{ brand.sub_name }}
-
-
- {{ brandHead }}{{ brandTail }}
-
-
- {{ brand.slogan }}
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
- {{ hero.eyebrow }}
-
- {{ hero.title }}
-
- {{ hero.subtitle }}
-
- {{ hero.button_text }}
-
-
-
-
-
-
-
-
- 玩转龙里
-
-
-
-
-
-
-
-
-
-
-
-
-
- 全域导览
-
-
-
-
-
-
- {{ home.guide_title }}
- {{ home.guide_subtitle }}
-
-
-
-
-
-
-
- 精选推荐
-
- 更多
-
-
-
-
-
-
-
-
-
- {{ line }}
-
-
-
-
-
-
-
+
diff --git a/src/utils/nav.js b/src/utils/nav.js
index 1fced96..84442de 100644
--- a/src/utils/nav.js
+++ b/src/utils/nav.js
@@ -1,9 +1,65 @@
/**
* 导航工具:登录回跳、Tab 切换、返回兜底
- * 本项目未使用原生 tabBar(Tab 栏为自绘组件),因此 Tab 之间用 redirectTo 切换。
+ * ---------------------------------------------------------------------------
+ * 三个 Tab 不是三个页面,而是主包壳页 pages/home/index.vue 里的三块面板(views/*):
+ * 同屏常驻、切 Tab 只切面板可见性 —— Tab 栏因此只渲染一次,切 Tab 不会闪烁、不重建、不重复请求。
+ * 壳在 onMounted 时用 registerTabController({ go, getActive }) 把切换器交给本模块,
+ * 页面内部(如「我的页 → 我的权益」)调的 switchTabTo 即转为调它,不再走路由。
+ *
+ * ⚠️ 面板不是页面,没有 query 可用 → Tab 间参数用内存透传(setTabQuery / consumeTabQuery)。
+ * ⚠️ 壳未挂载时切 Tab(子页 / 登录页 / 冷启)→ 记 pendingTab 并 reLaunch 回壳,由壳 onMounted 消费。
*/
import { ROUTES, STORAGE_KEYS } from "@/constant";
+/** 壳内面板 key */
+const TAB_KEYS = ["home", "benefit", "mine"];
+
+/** 壳注册进来的 Tab 切换器 */
+let tabCtl = { go: null, getActive: () => "home" };
+
+export const registerTabController = (ctl) => {
+ tabCtl = {
+ go: (ctl && ctl.go) || null,
+ getActive: (ctl && ctl.getActive) || (() => "home"),
+ };
+};
+
+/** Tab 间透传的参数 */
+let tabQuery = "";
+/** 壳尚未就绪时的待切 Tab */
+let pendingTab = null;
+
+export const setTabQuery = (query) => {
+ tabQuery = query || "";
+};
+
+/** 取出并清空透传参数(仅在目标面板 onPanelShow 调用一次) */
+export const consumeTabQuery = () => {
+ const query = tabQuery;
+ tabQuery = "";
+ return query;
+};
+
+/** 取出并清空待切 Tab(仅在壳 onMounted 调用一次) */
+export const consumePendingTab = () => {
+ const pending = pendingTab;
+ pendingTab = null;
+ return pending;
+};
+
+/** query 串取参数 */
+const pickParam = (query, key) => {
+ const matched = (query || "").match(new RegExp(`(?:^|&)${key}=([^&]*)`));
+ return matched ? matched[1] : "";
+};
+
+/** query 串去掉某个参数 */
+const dropParam = (query, key) =>
+ (query || "")
+ .split("&")
+ .filter((kv) => kv && !new RegExp(`^${key}=`).test(kv))
+ .join("&");
+
/** 当前页面路径(含参数) */
export const currentRoute = () => {
const pages = getCurrentPages();
@@ -18,6 +74,13 @@ export const currentRoute = () => {
return `/${page.route}${query ? `?${query}` : ""}`;
};
+/** 壳页地址补上当前 Tab(否则登录后只能落回主页) */
+const withTab = (url) => {
+ const [path, query = ""] = (url || "").split("?");
+ if (path !== ROUTES.HOME || /(?:^|&)tab=/.test(query)) return url;
+ return `${path}?tab=${tabCtl.getActive()}${query ? `&${query}` : ""}`;
+};
+
/** 是否已登录,未登录则跳登录页并带上 redirect */
export const requireLogin = (silent = false) => {
const token = uni.getStorageSync(STORAGE_KEYS.ACCESS_TOKEN) || "";
@@ -31,28 +94,28 @@ export const requireLogin = (silent = false) => {
/** 跳登录页,redirect 为登录后的回跳地址 */
export const goLogin = (redirect) => {
- const target = redirect || currentRoute();
+ const target = withTab(redirect || currentRoute());
const url = `${ROUTES.LOGIN}${target ? `?redirect=${encodeURIComponent(target)}` : ""}`;
uni.navigateTo({ url });
};
-/** 登录成功后的回跳 */
+/** 登录成功后的回跳:目标是壳页 → 切回原 Tab(含原分段);其余页面走 redirectTo */
export const goAfterLogin = (redirect) => {
- if (redirect) {
- const url = decodeURIComponent(redirect);
- const pages = getCurrentPages();
- const top = pages.length ? `/${pages[pages.length - 1].route}` : "";
- if (top && url.startsWith(top)) {
- uni.navigateBack();
- return;
- }
- uni.redirectTo({
- url,
+ if (!redirect) {
+ uni.navigateBack({
fail: () => uni.reLaunch({ url: ROUTES.HOME }),
});
return;
}
- uni.navigateBack({
+ const url = decodeURIComponent(redirect);
+ const [path, query = ""] = url.split("?");
+ if (path === ROUTES.HOME) {
+ const rest = dropParam(query, "tab");
+ switchTabTo(pickParam(query, "tab") || tabCtl.getActive(), rest ? `?${rest}` : "");
+ return;
+ }
+ uni.redirectTo({
+ url,
fail: () => uni.reLaunch({ url: ROUTES.HOME }),
});
};
@@ -67,18 +130,21 @@ export const goBack = () => {
}
};
-/** Tab 切换 */
+/**
+ * 切 Tab:已在壳页 → 交由壳切面板(不重建、不重复请求);
+ * 不在壳页(子页 / 登录页 / 冷启)→ 记待办并 reLaunch 回壳,由壳 onMounted 消费。
+ * extraQuery 形如 "?seg=mine",由目标面板 onPanelShow 取用。
+ */
export const switchTabTo = (key, extraQuery = "") => {
- const map = {
- home: ROUTES.HOME,
- benefit: ROUTES.BENEFIT,
- mine: ROUTES.MINE,
- };
- const url = `${map[key] || ROUTES.HOME}${extraQuery}`;
+ if (!TAB_KEYS.includes(key)) return;
const pages = getCurrentPages();
const top = pages.length ? `/${pages[pages.length - 1].route}` : "";
- if (top === url.split("?")[0]) return;
- uni.redirectTo({ url });
+ if (top === ROUTES.HOME && tabCtl.go) {
+ tabCtl.go(key, extraQuery);
+ return;
+ }
+ pendingTab = { key, query: extraQuery };
+ uni.reLaunch({ url: ROUTES.HOME });
};
/** 跳板块列表页 */
diff --git a/src/pages/benefit/index.vue b/src/views/benefit/index.vue
similarity index 92%
rename from src/pages/benefit/index.vue
rename to src/views/benefit/index.vue
index 6eec7db..0e50cb7 100644
--- a/src/pages/benefit/index.vue
+++ b/src/views/benefit/index.vue
@@ -1,6 +1,9 @@
+
+
+
+
+
+
+
+
+
+ {{ brand.sub_name }}
+
+
+ {{ brandHead }}{{ brandTail }}
+
+
+ {{ brand.slogan }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ hero.eyebrow }}
+
+ {{ hero.title }}
+
+ {{ hero.subtitle }}
+
+ {{ hero.button_text }}
+
+
+
+
+
+
+
+
+ 玩转龙里
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 全域导览
+
+
+
+
+
+
+ {{ home.guide_title }}
+ {{ home.guide_subtitle }}
+
+
+
+
+
+
+
+ 精选推荐
+
+ 更多
+
+
+
+
+
+
+
+
+
+ {{ line }}
+
+
+
+
+
+
+
+
diff --git a/src/pages/mine/index.vue b/src/views/mine/index.vue
similarity index 66%
rename from src/pages/mine/index.vue
rename to src/views/mine/index.vue
index 47500f2..867ead3 100644
--- a/src/pages/mine/index.vue
+++ b/src/views/mine/index.vue
@@ -1,12 +1,14 @@
-
+
-
-
- 我的
+
+
+
+ 我的
-
-
-
-
-
-
-
- {{ isLogin ? nickname : "点击登录" }}
-
-
- 登录后可领取权益、管理我的权益
-
-
- 绑定手机号
-
-
-
- {{ maskedPhone || "手机号已绑定" }}
-
-
-
-
-
- 已实名
-
-
- 未绑定
-
-
-
-
-
-
-
-
-
-
-
- {{ cell.label }}
-
-
+
+
+
+
+
+
+ {{ isLogin ? nickname : "点击登录" }}
+
+
+ 登录后可领取权益、管理我的权益
+
+
+ 绑定手机号
+
+
+
+ {{ maskedPhone || "手机号已绑定" }}
+
+
+
+
+
+ 已实名
- {{ cell.text }}
-
+
+ 未绑定
+
+
-
-
-
+
+
+
+
+
+
+ {{ cell.label }}
+
+
+ {{ cell.badge }}
+
+ {{ cell.text }}
+
+
+
+
+
+
+