feat: tab的调整
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 自绘底部 Tab 栏(未用原生 tabBar,以便还原玻璃拟态 + 渐变发丝线)
|
||||
* 切换方式:uni.redirectTo(三个 Tab 页为同级页面)
|
||||
* 自绘底部 Tab 栏
|
||||
* 三个 Tab 同屏共存(主包壳页面 pages/home/index.vue 里的三块面板),
|
||||
* 所以本组件只负责「画」+ 抛切换意图,不做任何路由跳转 —— 由壳处理 @change。
|
||||
*/
|
||||
import { TABS } from "@/constant";
|
||||
import { switchTabTo } from "@/utils/nav";
|
||||
|
||||
const props = defineProps({
|
||||
active: { type: String, default: "home" },
|
||||
@@ -12,15 +12,17 @@ const props = defineProps({
|
||||
badges: { type: Object, default: () => ({}) },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["change"]);
|
||||
|
||||
const onTap = (key) => {
|
||||
if (key === props.active) return;
|
||||
switchTabTo(key);
|
||||
emit("change", key);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="fixed left-0 right-0 bottom-0 h-[78px] pb-[22px] flex z-[120] deco-hairline bg-[rgba(255,255,255,0.94)] backdrop-blur-[26px] backdrop-saturate-[1.8] shadow-[0_-1px_0_rgba(20,24,28,0.03)]"
|
||||
class="fixed left-0 right-0 bottom-0 h-[78px] pb-[22px] flex z-[120] deco-hairline bg-[rgba(255,255,255,0.94)] shadow-[0_-1px_0_rgba(20,24,28,0.03)] [transform:translateZ(0)]"
|
||||
>
|
||||
<view
|
||||
v-for="tab in TABS"
|
||||
|
||||
@@ -89,18 +89,16 @@ export const REGION_TEXT = "贵州省黔南布依族苗族自治州龙里县";
|
||||
/** 协议页更新/生效日期 */
|
||||
export const AGREEMENT_DATE = "2026 年 9 月 11 日";
|
||||
|
||||
/** Tab 配置(自绘 Tab 栏) */
|
||||
/** Tab 配置(自绘 Tab 栏;三个 Tab 同属壳页 pages/home/index,面板与路径一一对应关系已不存在) */
|
||||
export const TABS = [
|
||||
{ key: "home", label: "主页", icon: "home", path: "/pages/home/index" },
|
||||
{ key: "benefit", label: "权益", icon: "ticket", path: "/pages/benefit/index" },
|
||||
{ key: "mine", label: "我的", icon: "user", path: "/pages/mine/index" },
|
||||
{ key: "home", label: "主页", icon: "home" },
|
||||
{ key: "benefit", label: "权益", icon: "ticket" },
|
||||
{ key: "mine", label: "我的", icon: "user" },
|
||||
];
|
||||
|
||||
/** 页面路径常量 */
|
||||
export const ROUTES = {
|
||||
HOME: "/pages/home/index",
|
||||
BENEFIT: "/pages/benefit/index",
|
||||
MINE: "/pages/mine/index",
|
||||
LOGIN: "/pages/login/index",
|
||||
CATALOG: "/pages-sub/catalog/index",
|
||||
DETAIL: "/pages-sub/detail/index",
|
||||
|
||||
@@ -23,22 +23,6 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/benefit/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "权益",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "我的",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
|
||||
@@ -1,171 +1,64 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="relative h-screen w-full flex flex-col skin-home">
|
||||
<view class="relative z-[60] shrink-0">
|
||||
<view class="px-4 pb-[10px]">
|
||||
<AppStatusBar />
|
||||
|
||||
<view class="flex items-center gap-[7px] mt-[6px] text-[10px] font-bold tracking-[2.4px] text-brand-deep">
|
||||
<view class="w-[5px] h-[5px] rounded-full bg-brand shadow-[0_0_0_4px_rgba(0,181,120,0.14)]" />
|
||||
<text>{{ brand.sub_name }}</text>
|
||||
</view>
|
||||
<view class="mt-[9px] text-display font-bold tracking-[-1px] leading-[1.1] text-ink">
|
||||
{{ brandHead }}<text class="not-italic gradtext-brand">{{ brandTail }}</text>
|
||||
</view>
|
||||
<view class="mt-2 text-[11.5px] leading-[1.55] text-ink2 tracking-[0.1px]">
|
||||
{{ brand.slogan }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="relative h-screen w-full overflow-hidden">
|
||||
<!-- 三块面板叠放同屏,靠 visibility 切换:见 script 头部的说明 -->
|
||||
<view :class="['absolute inset-0', active === 'home' ? 'visible' : 'invisible']">
|
||||
<HomePanel ref="homeRef" />
|
||||
</view>
|
||||
<view :class="['absolute inset-0', active === 'benefit' ? 'visible' : 'invisible']">
|
||||
<BenefitPanel ref="benefitRef" />
|
||||
</view>
|
||||
<view :class="['absolute inset-0', active === 'mine' ? 'visible' : 'invisible']">
|
||||
<MinePanel ref="mineRef" />
|
||||
</view>
|
||||
|
||||
<scroll-view class="flex-1 min-h-0" scroll-y :show-scrollbar="false">
|
||||
<view class="relative z-[1] px-4">
|
||||
<!-- 主视觉大卡:距固定区仅 2px(贴紧品牌区,让首屏更紧凑) -->
|
||||
<view class="relative h-[180px] mt-[2px] rounded-hero overflow-hidden shadow-hero" @click="onStartExplore">
|
||||
<image class="absolute inset-0 w-full h-full" :src="hero.image_url" mode="aspectFill" />
|
||||
<view class="absolute inset-0 scrim-hero" />
|
||||
<view
|
||||
class="pointer-events-none absolute inset-0 rounded-hero bg-[radial-gradient(210px_140px_at_98%_110%,rgba(63,223,168,0.44),transparent_74%)]" />
|
||||
|
||||
<!-- 白字加投影:hero 图片是亮绿山景,光靠遮罩中段不足以拉开对比(text-shadow 可继承) -->
|
||||
<view
|
||||
class="absolute inset-0 z-[2] flex flex-col justify-end items-start px-5 pb-[17px] [text-shadow:0_1px_3px_rgba(0,0,0,0.5),0_2px_12px_rgba(0,0,0,0.42)]">
|
||||
<view
|
||||
class="flex items-center gap-[7px] text-[9.5px] font-semibold tracking-[1.8px] text-[rgba(255,255,255,0.76)]">
|
||||
<view class="block w-[14px] h-px bg-brand-lite" />
|
||||
<text>{{ hero.eyebrow }}</text>
|
||||
</view>
|
||||
<view class="mt-[6px] text-[26px] font-bold text-white tracking-[0.6px] leading-[1.15]">{{ hero.title }}
|
||||
</view>
|
||||
<view class="mt-[7px] text-[12px] text-[rgba(255,255,255,0.9)] tracking-[0.3px]">{{ hero.subtitle }}</view>
|
||||
<view
|
||||
class="mt-[13px] h-[34px] px-[15px] rounded-full bg-white inline-flex items-center gap-[5px] text-[13px] font-semibold text-brand-deep shadow-[0_10px_24px_rgba(3,26,20,0.28)] [text-shadow:none]">
|
||||
<text>{{ hero.button_text }}</text>
|
||||
<AppIcon name="arrow" tone="brandDeep" :size="14" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 玩转龙里:左一通高 + 右二上下(无数据整块不展示) -->
|
||||
<template v-if="hasSections">
|
||||
<view class="flex items-baseline justify-between mt-5 mb-[10px] mx-[2px]">
|
||||
<text class="text-title font-semibold text-ink">玩转龙里</text>
|
||||
</view>
|
||||
<view class="flex gap-[10px] h-[226px] mt-[2px]">
|
||||
<SectionTile class="flex-[1.06] min-w-0" :section="entryOf('stay')" variant="tall" />
|
||||
<view class="flex-1 min-w-0 flex flex-col gap-[10px]">
|
||||
<SectionTile :section="entryOf('food')" variant="short" />
|
||||
<SectionTile :section="entryOf('specialty')" variant="short" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 全域导览(无数据整块不展示) -->
|
||||
<template v-if="hasGuide">
|
||||
<view class="flex items-baseline justify-between mt-5 mb-[10px] mx-[2px]">
|
||||
<text class="text-title font-semibold text-ink">全域导览</text>
|
||||
</view>
|
||||
<view class="relative h-[112px] rounded-tile overflow-hidden mt-[2px] shadow-elev2" @click="onMapTap">
|
||||
<image class="absolute inset-0 w-full h-full" :src="home.guide_image_url" mode="aspectFill" />
|
||||
<view class="absolute inset-0 scrim-map" />
|
||||
<view
|
||||
class="pointer-events-none absolute inset-0 rounded-tile bg-[radial-gradient(200px_130px_at_88%_112%,rgba(63,223,168,0.4),transparent_72%)]" />
|
||||
<view class="absolute inset-0 z-[2] flex flex-col justify-center items-start px-[18px]">
|
||||
<text class="text-white text-[16px] font-semibold">{{ home.guide_title }}</text>
|
||||
<text class="text-[rgba(255,255,255,0.85)] text-hint mt-1">{{ home.guide_subtitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 精选推荐(无卡片整块不展示;卡片自身完成跳转,页面不再重复绑定 tap) -->
|
||||
<template v-if="hasFeatured">
|
||||
<view class="flex items-baseline justify-between mt-5 mb-[10px] mx-[2px]">
|
||||
<text class="text-title font-semibold text-ink">精选推荐</text>
|
||||
<view class="flex items-center gap-[2px] text-[11.5px] text-ink3" @click="onMoreFeatured">
|
||||
<text>更多</text>
|
||||
<AppIcon name="chev" tone="ink3" :size="14" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-for="(item, i) in featured" :key="item.id" :class="i ? 'mt-[10px]' : ''">
|
||||
<ContentCard :item="item" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-for="(line, i) in footerLines" :key="i"
|
||||
:class="i ? 'mx-1 text-[10px] leading-[1.7] text-ink3 text-center' : 'mt-5 mx-1 text-[10px] leading-[1.7] text-ink3 text-center'">
|
||||
{{ line }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 = TabBar 高 78px + 18px 间隔,避免最后一条内容贴住 TabBar -->
|
||||
<view class="h-[96px]" />
|
||||
</scroll-view>
|
||||
<AppTabBar active="home" />
|
||||
<AppTabBar :active="active" @change="goTab" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
112
src/utils/nav.js
112
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 });
|
||||
};
|
||||
|
||||
/** 跳板块列表页 */
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup>
|
||||
/**
|
||||
* Tab 2 · 权益
|
||||
* Tab 2 · 权益(面板)
|
||||
* 由主包壳页面 pages/home/index.vue 承载,与主页 / 我的同屏共存;切 Tab 只切可见性。
|
||||
* 生命周期:组件里没有页面级 onShow,改由壳调 onPanelShow()(切到本面板 + 页面 onShow)。
|
||||
*
|
||||
* 分段一「权益中心」:页头 27px 渐变标题 + 数据行 + 白底分段控件 + 类别筛选 + 权益卡列表
|
||||
* 分段二「我的权益」:状态子分段(全部 / 可使用 / 已失效)+ 同款权益卡(点卡即使用确认弹窗)
|
||||
*
|
||||
@@ -11,11 +14,10 @@
|
||||
* 领取传 benefit_id(公开模板 id),使用传 user_benefit_id(已领取实例 id),写操作由接口层带幂等键。
|
||||
*/
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { BENEFIT_SUB_TABS, BENEFIT_TYPES, ROUTES } from "@/constant";
|
||||
import { getAccessToken } from "@/constant/token";
|
||||
import { useBenefitStore } from "@/store";
|
||||
import { goLogin, requireLogin } from "@/utils/nav";
|
||||
import { consumeTabQuery, goLogin, requireLogin } from "@/utils/nav";
|
||||
|
||||
/** 接口不可领原因 → 提示文案(login_required / phone_required 走登录与绑定引导,不在此列) */
|
||||
const CLAIM_REASON_TEXT = {
|
||||
@@ -55,21 +57,28 @@ const subTabs = computed(() =>
|
||||
BENEFIT_SUB_TABS.map((t) => ({ ...t, count: counts.value[t.key] ?? 0 }))
|
||||
);
|
||||
|
||||
onLoad((options) => {
|
||||
if (options?.seg === "mine") seg.value = "mine";
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
/** 壳在「切到本面板」与「页面 onShow」时调 */
|
||||
const onPanelShow = () => {
|
||||
// seg=mine 这类「切过来要落在哪个分段」的意图走 nav 的内存透传(不经过路由,故不用 query)
|
||||
// 未登录时强制回「权益中心」,否则退出登录后会停在空的「我的权益」分段。
|
||||
const query = consumeTabQuery();
|
||||
if (getAccessToken()) {
|
||||
if (/seg=mine/.test(query)) seg.value = "mine";
|
||||
} else if (seg.value === "mine") {
|
||||
seg.value = "center";
|
||||
}
|
||||
// 不传 force:store 内按缓存标记短路,切 Tab / 从详情页返回不再重拉
|
||||
// 领取与使用内部会显式 force 刷新,数据不会脏
|
||||
store.loadAll();
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({ onPanelShow });
|
||||
|
||||
const onSegChange = (key) => {
|
||||
// 我的权益需登录:未登录直接跳登录页且**不切分段**(用户没登录就退回权益中心),
|
||||
// redirect 带 seg,登录成功后回跳到「我的权益」并拉数据
|
||||
// redirect 带 tab/seg,登录成功后回壳并落到「我的权益」并拉数据
|
||||
if (key === "mine" && !getAccessToken()) {
|
||||
goLogin(`${ROUTES.BENEFIT}?seg=mine`);
|
||||
goLogin(`${ROUTES.HOME}?tab=benefit&seg=mine`);
|
||||
return;
|
||||
}
|
||||
seg.value = key;
|
||||
@@ -246,7 +255,6 @@ const onConfirmUse = async () => {
|
||||
<!-- 底部留白 = TabBar 高 78px + 18px 间隔,避免最后一条内容贴住 TabBar -->
|
||||
<view class="h-[96px]" />
|
||||
</scroll-view>
|
||||
<AppTabBar active="benefit" />
|
||||
|
||||
<!-- 使用弹窗:确认 / 成功 / 失败 三态 -->
|
||||
<BenefitDialog :visible="dialogVisible" :benefit="activeBenefit" :state="dialogState" :used-at="usedAt"
|
||||
179
src/views/home/index.vue
Normal file
179
src/views/home/index.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<script setup>
|
||||
/**
|
||||
* Tab 1 · 主页(面板)
|
||||
* 由主包壳页面 pages/home/index.vue 承载,与权益 / 我的同屏共存;
|
||||
* 切 Tab 只切可见性,本组件不重建、不重复请求。
|
||||
* 生命周期:不能再用页面级 onShow(组件里没有),改为壳在「显示本面板」与
|
||||
* 「页面 onShow」时调用 onPanelShow();首次加载由壳的 onMounted 派发。
|
||||
*
|
||||
* 数据来自 GET /v1/homepage。
|
||||
* 纵向顺序:
|
||||
* 品牌开场区(吸顶固定)→ 主视觉大卡(180px)→ 玩转龙里(左一通高 + 右二上下)
|
||||
* → 全域导览 → 精选推荐 → 免责声明
|
||||
* 无顶栏、无搜索、不展示任何权益入口。
|
||||
*/
|
||||
import { computed, ref } from "vue";
|
||||
import contentApi from "@/api/content";
|
||||
import { goCatalog, previewImages } from "@/utils/nav";
|
||||
|
||||
const home = ref({});
|
||||
const featured = ref([]);
|
||||
|
||||
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 entryOf = (key) => entries.value.find((e) => e.section_key === key) || {};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
/** 壳在「切到本面板」与「页面 onShow」时调;已有数据不重拉 */
|
||||
const onPanelShow = () => {
|
||||
if (!home.value?.brand_name) load();
|
||||
};
|
||||
|
||||
// 三块面板在壳里同时挂载,故这里**不自动加载**(否则一进 App 就连打三个接口);
|
||||
// 首次加载由壳的 onMounted 派发到当前面板。
|
||||
defineExpose({ onPanelShow });
|
||||
|
||||
/** 主视觉大卡「开始探索」/ 精选「更多」→ 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);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="relative h-screen w-full flex flex-col skin-home">
|
||||
<view class="relative z-[60] shrink-0">
|
||||
<view class="px-4 pb-[10px]">
|
||||
<AppStatusBar />
|
||||
|
||||
<view class="flex items-center gap-[7px] mt-[6px] text-[10px] font-bold tracking-[2.4px] text-brand-deep">
|
||||
<view class="w-[5px] h-[5px] rounded-full bg-brand shadow-[0_0_0_4px_rgba(0,181,120,0.14)]" />
|
||||
<text>{{ brand.sub_name }}</text>
|
||||
</view>
|
||||
<view class="mt-[9px] text-display font-bold tracking-[-1px] leading-[1.1] text-ink">
|
||||
{{ brandHead }}<text class="not-italic gradtext-brand">{{ brandTail }}</text>
|
||||
</view>
|
||||
<view class="mt-2 text-[11.5px] leading-[1.55] text-ink2 tracking-[0.1px]">
|
||||
{{ brand.slogan }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="flex-1 min-h-0" scroll-y :show-scrollbar="false">
|
||||
<view class="relative z-[1] px-4">
|
||||
<!-- 主视觉大卡:距固定区仅 2px(贴紧品牌区,让首屏更紧凑) -->
|
||||
<view class="relative h-[180px] mt-[2px] rounded-hero overflow-hidden shadow-hero" @click="onStartExplore">
|
||||
<image class="absolute inset-0 w-full h-full" :src="hero.image_url" mode="aspectFill" />
|
||||
<view class="absolute inset-0 scrim-hero" />
|
||||
<view
|
||||
class="pointer-events-none absolute inset-0 rounded-hero bg-[radial-gradient(210px_140px_at_98%_110%,rgba(63,223,168,0.44),transparent_74%)]" />
|
||||
|
||||
<!-- 白字加投影:hero 图片是亮绿山景,光靠遮罩中段不足以拉开对比(text-shadow 可继承) -->
|
||||
<view
|
||||
class="absolute inset-0 z-[2] flex flex-col justify-end items-start px-5 pb-[17px] [text-shadow:0_1px_3px_rgba(0,0,0,0.5),0_2px_12px_rgba(0,0,0,0.42)]">
|
||||
<view
|
||||
class="flex items-center gap-[7px] text-[9.5px] font-semibold tracking-[1.8px] text-[rgba(255,255,255,0.76)]">
|
||||
<view class="block w-[14px] h-px bg-brand-lite" />
|
||||
<text>{{ hero.eyebrow }}</text>
|
||||
</view>
|
||||
<view class="mt-[6px] text-[26px] font-bold text-white tracking-[0.6px] leading-[1.15]">{{ hero.title }}
|
||||
</view>
|
||||
<view class="mt-[7px] text-[12px] text-[rgba(255,255,255,0.9)] tracking-[0.3px]">{{ hero.subtitle }}</view>
|
||||
<view
|
||||
class="mt-[13px] h-[34px] px-[15px] rounded-full bg-white inline-flex items-center gap-[5px] text-[13px] font-semibold text-brand-deep shadow-[0_10px_24px_rgba(3,26,20,0.28)] [text-shadow:none]">
|
||||
<text>{{ hero.button_text }}</text>
|
||||
<AppIcon name="arrow" tone="brandDeep" :size="14" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 玩转龙里:左一通高 + 右二上下(无数据整块不展示) -->
|
||||
<template v-if="hasSections">
|
||||
<view class="flex items-baseline justify-between mt-5 mb-[10px] mx-[2px]">
|
||||
<text class="text-title font-semibold text-ink">玩转龙里</text>
|
||||
</view>
|
||||
<view class="flex gap-[10px] h-[226px] mt-[2px]">
|
||||
<SectionTile class="flex-[1.06] min-w-0" :section="entryOf('stay')" variant="tall" />
|
||||
<view class="flex-1 min-w-0 flex flex-col gap-[10px]">
|
||||
<SectionTile :section="entryOf('food')" variant="short" />
|
||||
<SectionTile :section="entryOf('specialty')" variant="short" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 全域导览(无数据整块不展示) -->
|
||||
<template v-if="hasGuide">
|
||||
<view class="flex items-baseline justify-between mt-5 mb-[10px] mx-[2px]">
|
||||
<text class="text-title font-semibold text-ink">全域导览</text>
|
||||
</view>
|
||||
<view class="relative h-[112px] rounded-tile overflow-hidden mt-[2px] shadow-elev2" @click="onMapTap">
|
||||
<image class="absolute inset-0 w-full h-full" :src="home.guide_image_url" mode="aspectFill" />
|
||||
<view class="absolute inset-0 scrim-map" />
|
||||
<view
|
||||
class="pointer-events-none absolute inset-0 rounded-tile bg-[radial-gradient(200px_130px_at_88%_112%,rgba(63,223,168,0.4),transparent_72%)]" />
|
||||
<view class="absolute inset-0 z-[2] flex flex-col justify-center items-start px-[18px]">
|
||||
<text class="text-white text-[16px] font-semibold">{{ home.guide_title }}</text>
|
||||
<text class="text-[rgba(255,255,255,0.85)] text-hint mt-1">{{ home.guide_subtitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 精选推荐(无卡片整块不展示;卡片自身完成跳转,页面不再重复绑定 tap) -->
|
||||
<template v-if="hasFeatured">
|
||||
<view class="flex items-baseline justify-between mt-5 mb-[10px] mx-[2px]">
|
||||
<text class="text-title font-semibold text-ink">精选推荐</text>
|
||||
<view class="flex items-center gap-[2px] text-[11.5px] text-ink3" @click="onMoreFeatured">
|
||||
<text>更多</text>
|
||||
<AppIcon name="chev" tone="ink3" :size="14" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-for="(item, i) in featured" :key="item.id" :class="i ? 'mt-[10px]' : ''">
|
||||
<ContentCard :item="item" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-for="(line, i) in footerLines" :key="i"
|
||||
:class="i ? 'mx-1 text-[10px] leading-[1.7] text-ink3 text-center' : 'mt-5 mx-1 text-[10px] leading-[1.7] text-ink3 text-center'">
|
||||
{{ line }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 = TabBar 高 78px + 18px 间隔,避免最后一条内容贴住 TabBar -->
|
||||
<view class="h-[96px]" />
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup>
|
||||
/**
|
||||
* Tab 3 · 我的
|
||||
* Tab 3 · 我的(面板)
|
||||
* 由主包壳页面 pages/home/index.vue 承载,与主页 / 权益同屏共存;切 Tab 只切可见性。
|
||||
* 生命周期:组件里没有页面级 onShow,改由壳调 onPanelShow()(切到本面板 + 页面 onShow)。
|
||||
*
|
||||
* 头部三态:未登录 / 已登录未绑手机号 / 已登录已绑手机号(手机号中间四位打码)
|
||||
* 功能列表未登录时照常可见(点击即引导登录)。
|
||||
* 列表用数组驱动渲染,避免 7 段结构重复。
|
||||
*/
|
||||
import { computed, ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { CUSTOMER_SERVICE_PHONE } from "@/constant";
|
||||
import contentApi from "@/api/content";
|
||||
import { useBenefitStore, useUserStore } from "@/store";
|
||||
@@ -47,16 +49,22 @@ const avaCls = computed(() =>
|
||||
: "w-[62px] h-[62px] rounded-full shrink-0 overflow-hidden flex items-center justify-center bg-alt"
|
||||
);
|
||||
|
||||
onShow(() => {
|
||||
/** 壳在「切到本面板」与「页面 onShow」时调 */
|
||||
const onPanelShow = () => {
|
||||
userStore.restore();
|
||||
// 资料以服务端为准(幂等:本会话已同步过则复用本地态,不再打接口)
|
||||
userStore.syncMe();
|
||||
// 未登录时「我的权益」必为空,不发请求;已登录才拉(store 内同样有缓存守卫)
|
||||
if (userStore.isLogin) benefitStore.loadMine();
|
||||
contentApi.getCustomerService().then((res) => {
|
||||
service.value = res;
|
||||
});
|
||||
});
|
||||
// 面板常驻,客服号只在首次进入时取一次(接口缺该 key 时失败不缓存,故用本地态挡重复请求)
|
||||
if (!service.value) {
|
||||
contentApi.getCustomerService().then((res) => {
|
||||
service.value = res;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ onPanelShow });
|
||||
|
||||
/** 头部点击:未登录 → 登录页;已登录未绑手机号 → 绑手机号抽屉;已绑定 → 修改昵称 */
|
||||
const onHeaderTap = () => {
|
||||
@@ -170,93 +178,94 @@ const saveNickname = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="relative min-h-screen w-full overflow-hidden">
|
||||
<view class="relative h-screen w-full flex flex-col overflow-hidden">
|
||||
<AppStatusBar />
|
||||
|
||||
<!-- 头部 -->
|
||||
<view class="bg-white px-5 pt-[6px] pb-[22px]">
|
||||
<view class="text-[20px] font-semibold pt-[6px] pb-5 text-ink">我的</view>
|
||||
<scroll-view scroll-y class="flex-1 min-h-0">
|
||||
<!-- 头部 -->
|
||||
<view class="bg-white px-5 pt-[6px] pb-[22px]">
|
||||
<view class="text-[20px] font-semibold pt-[6px] pb-5 text-ink">我的</view>
|
||||
|
||||
<view class="flex items-center gap-[14px]">
|
||||
<button
|
||||
v-if="isLogin"
|
||||
class="btn-reset w-[62px] h-[62px] shrink-0"
|
||||
open-type="chooseAvatar"
|
||||
@chooseavatar="onChooseAvatar"
|
||||
>
|
||||
<view :class="avaCls">
|
||||
<image v-if="avatar" :src="avatar" mode="aspectFill" class="w-full h-full" />
|
||||
<AppIcon v-else name="user" tone="brand" :size="30" />
|
||||
</view>
|
||||
</button>
|
||||
<view v-else :class="avaCls" @click="onHeaderTap">
|
||||
<AppIcon name="user" tone="ink3" :size="30" />
|
||||
</view>
|
||||
|
||||
<view class="flex-1 min-w-0" @click="onHeaderTap">
|
||||
<view class="text-[18px] font-semibold text-ink">{{ isLogin ? nickname : "点击登录" }}</view>
|
||||
<view class="mt-[5px] flex items-center gap-[5px] text-cap text-ink2">
|
||||
<template v-if="!isLogin">
|
||||
<text>登录后可领取权益、管理我的权益</text>
|
||||
</template>
|
||||
<template v-else-if="!userStore.phoneBound">
|
||||
<text>绑定手机号</text>
|
||||
<AppIcon name="chev" tone="ink3" :size="14" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<text>{{ maskedPhone || "手机号已绑定" }}</text>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text
|
||||
v-if="isLogin && userStore.phoneBound"
|
||||
class="text-[10px] font-semibold px-[7px] py-[2px] rounded-full bg-brand-soft text-brand-deep"
|
||||
>
|
||||
已实名
|
||||
</text>
|
||||
<text
|
||||
v-else-if="isLogin"
|
||||
class="text-[10px] font-semibold px-[7px] py-[2px] rounded-full bg-[#F1F3F5] text-ink3"
|
||||
>
|
||||
未绑定
|
||||
</text>
|
||||
<AppIcon v-else name="chev" tone="ink3" :size="18" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能列表:分组渲染,退出登录单独一组 -->
|
||||
<view
|
||||
v-for="group in groups"
|
||||
:key="group.key"
|
||||
class="mt-[14px] mx-[14px] bg-white rounded-card overflow-hidden border-[0.5px] border-line shadow-elev1"
|
||||
>
|
||||
<view
|
||||
v-for="(cell, i) in group.cells"
|
||||
:key="cell.key"
|
||||
class="flex items-center gap-3 h-14 px-[15px]"
|
||||
:class="i ? 'border-t-[0.5px] border-line' : ''"
|
||||
@click="cell.onTap"
|
||||
>
|
||||
<view class="w-[30px] h-[30px] rounded-[9px] bg-alt shrink-0 flex items-center justify-center">
|
||||
<AppIcon :name="cell.icon" tone="ink2" :size="18" />
|
||||
</view>
|
||||
<text class="flex-1 text-body text-ink">{{ cell.label }}</text>
|
||||
<view class="flex items-center gap-[6px] text-cap text-ink3">
|
||||
<text
|
||||
v-if="cell.badge"
|
||||
class="min-w-[18px] h-[18px] px-[5px] rounded-full bg-accent text-white text-hint font-semibold flex items-center justify-center"
|
||||
<view class="flex items-center gap-[14px]">
|
||||
<button
|
||||
v-if="isLogin"
|
||||
class="btn-reset w-[62px] h-[62px] shrink-0"
|
||||
open-type="chooseAvatar"
|
||||
@chooseavatar="onChooseAvatar"
|
||||
>
|
||||
{{ cell.badge }}
|
||||
<view :class="avaCls">
|
||||
<image v-if="avatar" :src="avatar" mode="aspectFill" class="w-full h-full" />
|
||||
<AppIcon v-else name="user" tone="brand" :size="30" />
|
||||
</view>
|
||||
</button>
|
||||
<view v-else :class="avaCls" @click="onHeaderTap">
|
||||
<AppIcon name="user" tone="ink3" :size="30" />
|
||||
</view>
|
||||
|
||||
<view class="flex-1 min-w-0" @click="onHeaderTap">
|
||||
<view class="text-[18px] font-semibold text-ink">{{ isLogin ? nickname : "点击登录" }}</view>
|
||||
<view class="mt-[5px] flex items-center gap-[5px] text-cap text-ink2">
|
||||
<template v-if="!isLogin">
|
||||
<text>登录后可领取权益、管理我的权益</text>
|
||||
</template>
|
||||
<template v-else-if="!userStore.phoneBound">
|
||||
<text>绑定手机号</text>
|
||||
<AppIcon name="chev" tone="ink3" :size="14" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<text>{{ maskedPhone || "手机号已绑定" }}</text>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text
|
||||
v-if="isLogin && userStore.phoneBound"
|
||||
class="text-[10px] font-semibold px-[7px] py-[2px] rounded-full bg-brand-soft text-brand-deep"
|
||||
>
|
||||
已实名
|
||||
</text>
|
||||
<text v-if="cell.text">{{ cell.text }}</text>
|
||||
<AppIcon name="chev" tone="ink3" :size="14" />
|
||||
<text
|
||||
v-else-if="isLogin"
|
||||
class="text-[10px] font-semibold px-[7px] py-[2px] rounded-full bg-[#F1F3F5] text-ink3"
|
||||
>
|
||||
未绑定
|
||||
</text>
|
||||
<AppIcon v-else name="chev" tone="ink3" :size="18" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="h-[78px]" />
|
||||
<AppTabBar active="mine" />
|
||||
<!-- 功能列表:分组渲染,退出登录单独一组 -->
|
||||
<view
|
||||
v-for="group in groups"
|
||||
:key="group.key"
|
||||
class="mt-[14px] mx-[14px] bg-white rounded-card overflow-hidden border-[0.5px] border-line shadow-elev1"
|
||||
>
|
||||
<view
|
||||
v-for="(cell, i) in group.cells"
|
||||
:key="cell.key"
|
||||
class="flex items-center gap-3 h-14 px-[15px]"
|
||||
:class="i ? 'border-t-[0.5px] border-line' : ''"
|
||||
@click="cell.onTap"
|
||||
>
|
||||
<view class="w-[30px] h-[30px] rounded-[9px] bg-alt shrink-0 flex items-center justify-center">
|
||||
<AppIcon :name="cell.icon" tone="ink2" :size="18" />
|
||||
</view>
|
||||
<text class="flex-1 text-body text-ink">{{ cell.label }}</text>
|
||||
<view class="flex items-center gap-[6px] text-cap text-ink3">
|
||||
<text
|
||||
v-if="cell.badge"
|
||||
class="min-w-[18px] h-[18px] px-[5px] rounded-full bg-accent text-white text-hint font-semibold flex items-center justify-center"
|
||||
>
|
||||
{{ cell.badge }}
|
||||
</text>
|
||||
<text v-if="cell.text">{{ cell.text }}</text>
|
||||
<AppIcon name="chev" tone="ink3" :size="14" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="h-[78px]" />
|
||||
</scroll-view>
|
||||
|
||||
<!-- 绑定手机号(底部抽屉) -->
|
||||
<view
|
||||
Reference in New Issue
Block a user