Compare commits

...
15 Commits
24 changed files with 434 additions and 71 deletions

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>龙里文旅</title>
<title>云从居</title>
<script>
var coverSupport =
"CSS" in window &&
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "longli-mp",
"version": "1.0.0",
"description": "龙里文旅 · 微信小程序(uni-app + Vue3 + Vite + Tailwind)",
"description": "云从居 · 微信小程序(uni-app + Vue3 + Vite + Tailwind)",
"private": true,
"scripts": {
"postinstall": "node ./node_modules/weapp-tailwindcss/bin/weapp-tailwindcss.js patch",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"appid": "wx23f86d809ae80259",
"appid": "wx3b6667ba12eb2507",
"projectname": "longli-mp",
"description": "龙里文旅 · 微信小程序",
"description": "云从居 · 微信小程序",
"miniprogramRoot": "dist/build/mp-weixin/",
"compileType": "miniprogram",
"libVersion": "3.5.0",
+13 -1
View File
@@ -55,6 +55,14 @@ const STROKE_ICONS = {
food: '<path d="M6.6 3.6v5M9.4 3.6v5"/><path d="M8 3.6v16.8"/><path d="M6.6 8.6h2.8"/><path d="M16.4 3.6c1.7 0 2.8 1.3 2.8 3.1 0 1.9-1.1 3.1-2.8 3.1s-2.8-1.2-2.8-3.1c0-1.8 1.1-3.1 2.8-3.1z"/><path d="M16.4 9.8v10.6"/>',
bed: '<path d="M3.6 18.6V6.4"/><path d="M3.6 15h16.8v3.6"/><path d="M20.4 15v-2.6a1.6 1.6 0 0 0-1.6-1.6h-7V15"/><circle cx="7.6" cy="12.1" r="1.7"/>',
peak: '<path d="M2.8 19.2 9 8.4l3.6 6.2 2.2-3.4 6.4 8z"/>',
table:
'<ellipse cx="12" cy="8.4" rx="8.6" ry="3.2"/><path d="M12 11.6v8"/><path d="M8.6 19.6h6.8"/>',
hotspring:
'<path d="M3.4 15.2c1.7 0 1.7 1.5 3.4 1.5s1.7-1.5 3.4-1.5 1.7 1.5 3.4 1.5 1.7-1.5 3.4-1.5 1.7 1.5 3.4 1.5"/><path d="M3.4 19.3c1.7 0 1.7 1.5 3.4 1.5s1.7-1.5 3.4-1.5 1.7 1.5 3.4 1.5 1.7-1.5 3.4-1.5 1.7 1.5 3.4 1.5"/><path d="M8 11.8c-1.5-1.7 1-2.7.2-4.6"/><path d="M12 11.8c-1.5-1.7 1-2.7.2-4.6"/><path d="M16 11.8c-1.5-1.7 1-2.7.2-4.6"/>',
fuel:
'<rect x="3.2" y="3.6" width="9.8" height="16.8" rx="1.7"/><rect x="5.8" y="6.4" width="4.6" height="4" rx="1"/><path d="M13 10h2.4a2 2 0 0 1 2 2v6.4a1.7 1.7 0 0 0 3.4 0V12"/>',
cart:
'<path d="M3.2 4.6h2.4l2.7 9.9h9.5l2.1-7.3H6.5"/><circle cx="9.6" cy="19.2" r="1.6"/><circle cx="17" cy="19.2" r="1.6"/>',
people:
'<circle cx="9.2" cy="8.4" r="3.2"/><path d="M3.6 19.4a5.6 5.6 0 0 1 11.2 0"/><path d="M15.8 5.6a3.2 3.2 0 0 1 0 6"/><path d="M17.4 14.8a5.6 5.6 0 0 1 3 4.6"/>',
};
@@ -68,7 +76,7 @@ const FILL_ICONS = {
const MATRIX = {
pin: ["ink3", "brand", "white"],
arrow: ["white", "brandDeep", "ink"],
chev: ["ink3", "white", "brand", "ink2", "accentDeep"],
chev: ["ink3", "white", "brand", "ink2", "accentDeep", "brandDeep"],
back: ["ink", "white", "brand"],
phone: ["white", "brandDeep"],
copy: ["brandDeep", "brand"],
@@ -90,6 +98,10 @@ const MATRIX = {
food: ["brandDeep", "accent", "white", "brand"],
bed: ["accent", "brand", "white", "brandDeep"],
peak: ["accent", "brand", "white"],
table: ["white", "accent", "brandDeep"],
hotspring: ["white", "accent", "brandDeep"],
fuel: ["white", "accent", "brandDeep"],
cart: ["white", "accent", "brandDeep"],
people: ["ink3", "brand", "white", "brandDeep"],
wx: ["white"],
};
+17 -2
View File
@@ -1,12 +1,27 @@
<script setup>
import { onLaunch } from "@dcloudio/uni-app";
import { onLaunch, onShow } from "@dcloudio/uni-app";
import { useUserStore } from "@/store";
import userApi from "@/api/user";
import { handleEntry } from "@/utils/nav";
onLaunch(() => {
/**
* 启动/唤起参数 → 统一的入口对象。
* ⚠️ 只取 options.query:App 层的 options.scene 是「场景值」(数字),
* 小程序码真正的 scene 字符串在 options.query.scene 里,不能用前者覆盖后者。
*/
const entryParams = (options) => ({ ...((options && options.query) || {}) });
onLaunch((options) => {
useUserStore().restore();
// 本地若留有注销回执,查一次清理进度(无回执时不发请求),完成即丢弃
userApi.fetchErasureStatus();
// 扫码进入:冷启动参数在 onLaunch(页面 onLoad 也会带,双保险)
handleEntry(entryParams(options));
});
onShow((options) => {
// 后台被再次扫码唤起:页面 onLoad 不会重跑,参数只在这里
handleEntry(entryParams(options));
});
</script>
+17 -1
View File
@@ -8,7 +8,7 @@
* 2)公共内容(协议 / 隐私 / 关于 / 客服)
* GET /v1/content(列表,登录同意版本号取这里) + GET /v1/content/:key(单条详情)
*/
import { AGREEMENT_DATE, CUSTOMER_SERVICE_PHONE, CUSTOMER_SERVICE_TIME } from "@/constant";
import { AGREEMENT_DATE, CUSTOMER_SERVICE_PHONE, CUSTOMER_SERVICE_TIME, TICKET_ECONOMY_KEY } from "@/constant";
import { clone, cnDate, unique } from "@/utils";
import { toPublicCard } from "@/api/benefit";
import * as contentApi from "@/request/api/content";
@@ -166,6 +166,21 @@ export const getCustomerService = async () => {
return { phone: (info && info.phone) || CUSTOMER_SERVICE_PHONE };
};
/* ---------------------------------------------------------------------------
* 票根经济介绍页(GET /v1/content/ticket_economy)
* 口径与公共内容一致:title 作标题、body 作正文(纯文本按行拆段,`一、` 行作小标题)。
* 内容全部取自接口,接口无内容时正文为空(无本地兜底)。
* ------------------------------------------------------------------------- */
/** 票根经济介绍页:标题 + 正文段落 */
export const getTicketEconomy = async () => {
const info = await getContent(TICKET_ECONOMY_KEY);
return {
title: (info && info.title) || "票根经济",
blocks: toContentBlocks(info && info.body),
};
};
export default {
getHomepage,
getCategories,
@@ -178,4 +193,5 @@ export default {
getAgreement,
getAbout,
getCustomerService,
getTicketEconomy,
};
+1 -1
View File
@@ -1,5 +1,5 @@
/* ===========================================================================
龙里文旅 · 样式总入口(Tailwind CSS v4)
云从居 · 样式总入口(Tailwind CSS v4)
---------------------------------------------------------------------------
① @theme 设计 Token → 原子类(bg-brand / text-cap / rounded-card / shadow-elev2 …)
② @utility 内置原子表达不了的复合样式(满屏渐变底 / 渐变填充 / 渐变字 / 伪元素装饰)
+1 -1
View File
@@ -75,7 +75,7 @@ const onShare = () => emit("share");
</view>
</view>
<view class="flex items-center justify-center shrink-0 ml-auto" :class="btnCls">
<view v-if="showShare || $slots.right" class="flex items-center justify-center shrink-0 ml-auto" :class="btnCls">
<slot name="right">
<view v-if="showShare" class="w-full h-full flex items-center justify-center" hover-class="opacity-60" @click="onShare">
<AppIcon name="share" :tone="iconTone" :size="22" />
+54 -23
View File
@@ -3,19 +3,21 @@
* 权益卡(全站唯一组件 · 4 处复用)
* 使用位置:权益中心 / 我的权益 / 详情页「到店权益」
* 四行固定顺序:类型 / 价格 / 规格·适用 / 有效期·名额
* 价格两形态互斥:金额型 = 优惠价大字 + 划线原价 + 右端折扣标;
* 折扣型(price.rate)= 「门市价」小字 + 折扣大字,不再出划线原价与折扣标。
* 「进入商家」入口在权益中心与我的权益都出现(详情页本就在该商家内,不加)。
* 价格两形态互斥:金额型 = 优惠价大字 + 划线原价 + 右端折扣标;折扣型(price.rate)= 折扣大字。
* ①行=类型名(权益 `type_label` → 商家 `type_label` → 板块 2 字短名)+ 类型图标(`type_icon` → `type_label` 反查 → 板块图标)
* + 右端商家名胶囊(`merchant_name`,超 8 字截断),详情页不加胶囊。
*/
import { computed } from "vue";
import { BENEFIT_STATUS, BENEFIT_TYPE_ICON, BENEFIT_TYPE_LABEL } from "@/constant";
import { basisText, discountLabel, dotDate, rateText, yuanText } from "@/utils";
import { BENEFIT_ICON_ALIAS, BENEFIT_LABEL_ICON, BENEFIT_STATUS, BENEFIT_TYPE_ICON, BENEFIT_TYPE_SHORT } from "@/constant";
import { discountLabel, dotDate, rateText, yuanText } from "@/utils";
import { goDetail } from "@/utils/nav";
const props = defineProps({
benefit: { type: Object, required: true },
/** center 权益中心 | mine 我的权益 | detail 详情页到店权益 */
mode: { type: String, default: "center" },
/** 所属商家的 type_label(详情页由 merchant 传入,优先于板块短名) */
typeLabel: { type: String, default: "" },
});
const emit = defineEmits(["action", "tap"]);
@@ -30,15 +32,39 @@ const isClaimed = computed(() => status.value === BENEFIT_STATUS.CLAIMED);
/** 权益类型键:详情页走接口的 section_key,权益中心仍是本地 type,两者取其一 */
const typeKey = computed(() => b.value.section_key || b.value.type || "");
const typeLabel = computed(() => BENEFIT_TYPE_LABEL[typeKey.value] || "权益");
const typeIcon = computed(() => BENEFIT_TYPE_ICON[typeKey.value] || "gift");
/**
* ①行类型名:权益自带 type_label 最优先 —— 同一商家下权益类型可能不同
* (如云从朵花既有「住宿」又有「温泉」),不能只跟商家的分类走;
* 其次回落商家 type_label(详情页传入)、板块 2 字短名。
*/
const typeLabel = computed(() => {
const own = String(b.value.type_label || "").trim();
if (own) return own;
const fromMerchant = String(props.typeLabel || "").trim();
return fromMerchant || BENEFIT_TYPE_SHORT[typeKey.value] || "权益";
});
/** ①行图标:type_icon → 权益 type_label → 商家 type_label → 板块兜底 */
const typeIcon = computed(() => {
const byIcon = BENEFIT_ICON_ALIAS[String(b.value.type_icon || "").trim().toLowerCase()];
if (byIcon) return byIcon;
for (const label of [b.value.type_label, props.typeLabel]) {
const hit = BENEFIT_LABEL_ICON[String(label || "").trim()];
if (hit) return hit;
}
return BENEFIT_TYPE_ICON[typeKey.value] || "gift";
});
const deal = computed(() => yuanText(b.value.price?.deal));
const original = computed(() => yuanText(b.value.price?.original));
const pct = computed(() => discountLabel(b.value.price?.deal, b.value.price?.original));
const rate = computed(() => rateText(b.value.price));
const basis = computed(() => basisText(b.value.price));
const isRate = computed(() => !!rate.value);
/** 折扣型文案偏长(如「每升优惠0.20元」)时降字号,避免溢出信息区 */
const rateCls = computed(() =>
rate.value.length > 5
? "text-[17px] font-bold tracking-[-0.3px]"
: "text-display font-extrabold tracking-[-1px]"
);
const spec = computed(() => b.value.price?.spec || "");
const hasPrice = computed(() => isRate.value || !!deal.value);
@@ -48,8 +74,8 @@ const slogan = computed(() =>
const validText = computed(() => {
const { valid_until: until, valid_days: days } = b.value;
if (until) return `有效期至 ${dotDate(until)}`;
if (days) return `有效期:${days} 天`;
if (until) return `有效至 ${dotDate(until)}`;
if (days) return `有效期 ${days} 天`;
return "长期有效";
});
@@ -109,6 +135,13 @@ const goStateCls = computed(() =>
);
const goIconTone = computed(() => (isOff.value ? "ink3" : "accentDeep"));
/** 「进入商家」胶囊文案:取商家上架名称,超 8 字截断加省略号 */
const goLabel = computed(() => {
const name = String(b.value.merchant_name || "").trim();
if (!name) return "进入商家";
return name.length > 8 ? `${name.slice(0, 8)}…` : name;
});
/** 跳该权益所属商家详情 */
const onGoMerchant = () => {
if (b.value.merchant_id) goDetail(b.value.merchant_id);
@@ -126,15 +159,15 @@ const onTap = () => {
<!-- 左:信息区(四行固定顺序) -->
<view class="flex-1 min-w-0 rounded-l-card px-[14px] pt-[13px] pb-3 flex flex-col" :class="sideStateCls">
<!-- ① 类型(权益中心 / 我的权益在行右端带「进入商家」入口) -->
<view class="flex items-center gap-[6px] text-[13px] font-semibold whitespace-nowrap" :class="kindStateCls">
<view class="flex items-center gap-2 text-[13px] font-semibold whitespace-nowrap" :class="kindStateCls">
<view class="w-[21px] h-[21px] rounded-full shrink-0 flex items-center justify-center" :class="kindIconCls">
<AppIcon :name="typeIcon" tone="white" :size="12.5" />
</view>
<text>{{ typeLabel }}</text>
<view class="min-w-0 flex-1 line-clamp-1">{{ typeLabel }}</view>
<view v-if="mode !== 'detail'"
class="ml-auto shrink-0 h-5 px-2 rounded-full flex items-center gap-[2px] text-hint font-semibold border-[0.5px]"
class="ml-auto shrink-0 -mr-0.5 h-5 pl-2 pr-1.5 rounded-full flex items-center gap-[2px] text-hint font-semibold border-[0.5px]"
:class="goStateCls" @click.stop="onGoMerchant">
<text>进入商家</text>
<view class="min-w-0 line-clamp-1">{{ goLabel }}</view>
<AppIcon name="chev" :tone="goIconTone" :size="11" />
</view>
</view>
@@ -142,8 +175,7 @@ const onTap = () => {
<!-- ② 价格:金额型 = 优惠价 + 划线原价 + 折扣标;折扣型 = 门市价 + 折扣大字 -->
<view v-if="hasPrice" class="flex items-baseline gap-[6px] mt-2 leading-none whitespace-nowrap">
<template v-if="isRate">
<text v-if="basis" class="text-[11.5px] font-medium" :class="wasCls">{{ basis }}</text>
<text class="text-display font-extrabold tracking-[-1px]" :class="priceNumCls">{{ rate }}</text>
<text :class="[rateCls, priceNumCls]">{{ rate }}</text>
</template>
<template v-else>
<text class="text-[15px] font-bold" :class="priceNumCls">¥</text>
@@ -162,19 +194,18 @@ const onTap = () => {
:class="specCls">
{{ spec }}
</text>
<view class="min-w-0 flex-1 line-clamp-1">{{ b.special_notes || b.description || b.merchant_name || "" }}</view>
<view class="min-w-0 flex-1 line-clamp-1">{{ b.title || b.special_notes || b.description || b.merchant_name || "" }}</view>
</view>
<!-- ④ 有效期 + 第二项随态 -->
<!-- ④ 有效期 + 第二项随态(有效期恒完整;第二项空间不足时收缩截断,避免整行溢出) -->
<view class="flex items-center gap-2 mt-auto text-[10px] text-ink3">
<view v-if="validText" class="flex items-center gap-[3.5px] shrink-0 whitespace-nowrap">
<view class="shrink-0 flex items-center gap-[3.5px] whitespace-nowrap">
<AppIcon name="clock" tone="ink3" :size="12" />
<text>{{ validText }}</text>
</view>
<view class="flex items-center gap-[3.5px] shrink-0 whitespace-nowrap"
:class="secondMeta.ok ? 'text-[#00A46C]' : ''">
<AppIcon :name="secondMeta.icon" :tone="secondMeta.ok ? 'brand' : 'ink3'" :size="12" />
<text>{{ secondMeta.text }}</text>
<view class="min-w-0 flex-1 flex items-center gap-[3.5px]" :class="secondMeta.ok ? 'text-[#00A46C]' : ''">
<AppIcon :name="secondMeta.icon" :tone="secondMeta.ok ? 'brand' : 'ink3'" :size="12" class="shrink-0" />
<view class="min-w-0 line-clamp-1">{{ secondMeta.text }}</view>
</view>
</view>
</view>
+2 -2
View File
@@ -6,7 +6,7 @@
*/
import { computed } from "vue";
import { BENEFIT_TYPE_LABEL } from "@/constant";
import { basisText, dateRangeText, dotDateTime, rateText, yuanText } from "@/utils";
import { dateRangeText, dotDateTime, rateText, yuanText } from "@/utils";
const props = defineProps({
visible: { type: Boolean, default: false },
@@ -30,7 +30,7 @@ const typeLabel = computed(() => BENEFIT_TYPE_LABEL[typeKey.value] || "权益");
const priceLine = computed(() => {
const rate = rateText(b.value.price);
if (rate) return `${basisText(b.value.price)}${rate}`;
if (rate) return rate;
const deal = yuanText(b.value.price?.deal);
const original = yuanText(b.value.price?.original);
if (!deal) return b.value.scope_desc || "";
+92 -4
View File
@@ -47,7 +47,16 @@ export const BENEFIT_TYPE_LABEL = {
general: "通用权益",
};
/** 权益类型 → 卡片类型行图标 */
/** 权益类型 → 卡片类型行短名(权益卡①行空间有限,2 字上限;图标已承担类型辨识) */
export const BENEFIT_TYPE_SHORT = {
scenic: "景区",
stay: "住宿",
food: "餐饮",
specialty: "特产",
general: "通用",
};
/** 权益类型 → 卡片类型行图标(板块级兜底) */
export const BENEFIT_TYPE_ICON = {
scenic: "peak",
stay: "bed",
@@ -56,6 +65,73 @@ export const BENEFIT_TYPE_ICON = {
general: "gift",
};
/**
* 权益 `type_label`(中文,接口字段)→ 卡片类型行图标
* 同一商家下权益类型可能不同(如云从朵花既有「住宿」又有「温泉」),故按权益自己的标签取图标。
*/
export const BENEFIT_LABEL_ICON = {
住宿: "bed",
美食: "food",
餐饮: "food",
宴席: "table",
景区: "peak",
温泉: "hotspring",
加油: "fuel",
商超: "cart",
特产: "ticket",
通用: "gift",
};
/** 权益 `type_icon`(英文,接口字段)→ 本地图标名,兼容常见写法 */
export const BENEFIT_ICON_ALIAS = {
bed: "bed",
hotel: "bed",
room: "bed",
stay: "bed",
sleep: "bed",
lodging: "bed",
food: "food",
meal: "food",
restaurant: "food",
dining: "food",
eat: "food",
table: "table",
banquet: "table",
feast: "table",
dinner: "table",
peak: "peak",
scenic: "peak",
mountain: "peak",
view: "peak",
spot: "peak",
hotspring: "hotspring",
hot_spring: "hotspring",
"hot-spring": "hotspring",
"hot spring": "hotspring",
spring: "hotspring",
spa: "hotspring",
onsen: "hotspring",
fuel: "fuel",
gas: "fuel",
gas_station: "fuel",
"gas-station": "fuel",
oil: "fuel",
petrol: "fuel",
gasoline: "fuel",
station: "fuel",
cart: "cart",
shop: "cart",
store: "cart",
market: "cart",
supermarket: "cart",
super_market: "cart",
mall: "cart",
shopping: "cart",
ticket: "ticket",
specialty: "ticket",
gift: "gift",
};
/** 我的权益状态子分段(全部 / 可使用 / 已失效) */
export const BENEFIT_SUB_TABS = [
{ key: "all", label: "全部" },
@@ -63,7 +139,7 @@ export const BENEFIT_SUB_TABS = [
{ key: "invalid", label: "已失效" },
];
/** 板块 key → 详情页介绍卡标题(接口不提供,前端按 section_key 派生) */
/** 板块 key → 详情页介绍卡标题(type_label 缺失时的兜底;正常取 type_label + 介绍) */
export const SECTION_INTRO_TITLE = {
scenic: "景区介绍",
stay: "酒店介绍",
@@ -80,9 +156,11 @@ export const SECTION_TITLE = {
};
/** 运营信息 */
export const CUSTOMER_SERVICE_PHONE = "0854-5630880";
export const LOGO_URL =
"https://one-feel-image-bucket.oss-cn-chengdu.aliyuncs.com/mp/static/longli/logo.png";
export const CUSTOMER_SERVICE_PHONE = "0854-5631343";
export const CUSTOMER_SERVICE_TIME = "09:00 - 18:00";
export const OPERATOR_NAME = "智念科技";
export const OPERATOR_NAME = "贵州跃龙文化旅游发展有限公司";
export const APP_VERSION_FULL = "v1.0.0(Build 100)";
export const REGION_TEXT = "贵州省黔南布依族苗族自治州龙里县";
@@ -96,6 +174,12 @@ export const TABS = [
{ key: "mine", label: "我的", icon: "user" },
];
/**
* 入口固定值:二维码里的 id 等于它时不打开商家详情,只切到权益 Tab。
* 后端在 scene / query / 普通链接里传这个字符串即可(大小写不敏感)。
*/
export const ENTRY_BENEFIT_ONLY = "benefits";
/** 页面路径常量 */
export const ROUTES = {
HOME: "/pages/index/index",
@@ -104,5 +188,9 @@ export const ROUTES = {
DETAIL: "/pages-sub/detail/index",
AGREEMENT: "/pages-sub/agreement/index",
ABOUT: "/pages-sub/about/index",
INTRO: "/pages-sub/intro/index",
ACCOUNT_CANCEL: "/pages-sub/account/cancel",
};
/** 票根经济介绍页的内容 key(GET /v1/content/ticket_economy) */
export const TICKET_ECONOMY_KEY = "ticket_economy";
+3 -3
View File
@@ -1,5 +1,5 @@
{
"name": "龙里文旅",
"name": "云从居",
"appid": "",
"description": "龙里县全域文旅导览与票根权益小程序",
"versionName": "1.0.0",
@@ -26,7 +26,7 @@
},
"quickapp": {},
"mp-weixin": {
"appid": "wx23f86d809ae80259",
"appid": "wx3b6667ba12eb2507",
"setting": {
"urlCheck": false,
"es6": true,
@@ -41,7 +41,7 @@
"darkmode": false
},
"h5": {
"title": "龙里文旅",
"title": "云从居",
"router": {
"mode": "hash"
}
+5 -4
View File
@@ -9,6 +9,7 @@ import {
APP_VERSION_FULL,
CUSTOMER_SERVICE_PHONE,
CUSTOMER_SERVICE_TIME,
LOGO_URL,
OPERATOR_NAME,
} from "@/constant";
import contentApi from "@/api/content";
@@ -27,7 +28,7 @@ onLoad(async () => {
});
onShareAppMessage(() => ({
title: "龙里文旅 · 一图玩转龙里",
title: "云从居 · 一图玩转龙里",
path: "/pages/index/index",
}));
</script>
@@ -49,9 +50,9 @@ onShareAppMessage(() => ({
<view
class="w-[76px] h-[76px] rounded-card bg-white border-[0.5px] border-[#E6F8F1] flex items-center justify-center overflow-hidden shadow-[0_12px_24px_rgba(0,181,120,0.18),0_2px_6px_rgba(8,30,22,0.06)]"
>
<image class="w-full h-full" src="/static/logo.png" mode="aspectFill" />
<image class="w-full h-full" :src="LOGO_URL" mode="aspectFill" />
</view>
<text class="mt-3 text-[16px] font-semibold text-ink">龙里文旅</text>
<text class="mt-3 text-[16px] font-semibold text-ink">云从居</text>
<text class="mt-1 text-[11.5px] text-ink3">{{ APP_VERSION_FULL }}</text>
</view>
@@ -83,7 +84,7 @@ onShareAppMessage(() => ({
本小程序内营业时间、价格、联系方式等信息可能发生变动,请以商户现场公示为准。权益的适用范围与最终解释权归权益提供方所有。
</view>
<view class="mt-5 text-center text-[11px] leading-[1.8] text-ink3">© 2026 龙里文旅 保留所有权利</view>
<view class="mt-5 text-center text-[11px] leading-[1.8] text-ink3">© 2026 云从居 保留所有权利</view>
</view>
</view>
</template>
+1 -1
View File
@@ -31,7 +31,7 @@ onLoad(async (options) => {
});
onShareAppMessage(() => ({
title: `${title.value} · 龙里文旅`,
title: `${title.value} · 云从居`,
path: `/pages-sub/agreement/index?type=${type.value}`,
}));
</script>
+1 -1
View File
@@ -68,7 +68,7 @@ const onCoverPreview = () => {
};
onShareAppMessage(() => ({
title: `${title.value} · 龙里文旅`,
title: `${title.value} · 云从居`,
path: `/pages-sub/catalog/index?section=${sectionKey.value}`,
imageUrl: cover.value,
}));
+33 -10
View File
@@ -4,14 +4,14 @@
* 数据来自 GET /v1/merchants/:id/detail —— 一次返回「商家 + 到店权益 + 同类推荐」。
* 区块顺序:
* 头图 swiper → 白面板【名称 + 类型徽标 / 营业时间 / 地址条(内嵌复制钮)】
* → 介绍卡(标题按 section_key 命名)→ 联系方式 → 到店权益(品牌染色卡)
* → 介绍卡(标题取 type_label + 介绍)→ 联系方式 → 到店权益(品牌染色卡)
* → 附近同类推荐 → 底部留白
* 不展示:tags 标签、设施服务、地图导航(数据无经纬度)。
*/
import { computed, ref } from "vue";
import { onLoad, onPageScroll, onShareAppMessage } from "@dcloudio/uni-app";
import contentApi from "@/api/content";
import { BENEFIT_STATUS, SECTION_INTRO_TITLE } from "@/constant";
import { BENEFIT_STATUS } from "@/constant";
import { useBenefitStore } from "@/store";
import { copyText, goDetail, makeCall, previewImages, requireLogin } from "@/utils/nav";
import { unique } from "@/utils";
@@ -24,6 +24,14 @@ const benefits = ref([]);
const related = ref([]);
const current = ref(0);
/** 到店权益折叠:超过 3 张默认只展示前 3 张 */
const BENEFIT_VISIBLE = 3;
const benefitsExpanded = ref(false);
const collapsibleBenefit = computed(() => benefits.value.length > BENEFIT_VISIBLE);
const shownBenefits = computed(() =>
benefitsExpanded.value ? benefits.value : benefits.value.slice(0, BENEFIT_VISIBLE)
);
const images = computed(() => {
if (!merchant.value) return [];
return unique(
@@ -31,7 +39,11 @@ const images = computed(() => {
);
});
const badge = computed(() => merchant.value?.type_label || "");
const introTitle = computed(() => SECTION_INTRO_TITLE[merchant.value?.section_key] || "介绍");
/** 介绍卡标题:接口 intro_label + 「介绍」 */
const introTitle = computed(() => {
const label = String(merchant.value?.intro_label || "").trim();
return label ? `${label}介绍` : "介绍";
});
const addressText = computed(() => merchant.value?.address || "");
const phone = computed(() => merchant.value?.contact_phone || "");
const businessHours = computed(() => merchant.value?.business_hours || "");
@@ -108,7 +120,7 @@ const onClaimBenefit = async (benefit) => {
const goRelated = (rec) => goDetail(rec.id);
onShareAppMessage(() => ({
title: merchant.value?.name || "龙里文旅",
title: merchant.value?.name || "云从居",
path: `/pages-sub/detail/index?id=${itemId.value}`,
imageUrl: images.value[0] || "",
}));
@@ -182,7 +194,7 @@ onShareAppMessage(() => ({
</view>
</view>
<!-- 介绍卡(标题按板块命名) -->
<!-- 介绍卡(标题=接口 intro_label + 介绍) -->
<view v-if="merchant.description" :class="CARD">
<view class="flex items-center gap-2 text-[16px] font-bold tracking-[-0.3px] text-ink deco-titledot">
{{ introTitle }}
@@ -213,8 +225,17 @@ onShareAppMessage(() => ({
<view class="flex items-center gap-2 text-[16px] font-bold tracking-[-0.3px] text-ink deco-titledot">
到店权益
</view>
<BenefitCard v-for="benefit in benefits" :key="benefit.id" :benefit="benefit" mode="detail"
@action="onClaimBenefit" />
<BenefitCard v-for="benefit in shownBenefits" :key="benefit.id" :benefit="benefit" mode="detail"
:type-label="merchant.type_label" @action="onClaimBenefit" />
<view v-if="collapsibleBenefit"
class="mt-[11px] h-[34px] rounded-full flex items-center justify-center gap-[3px] text-cap font-semibold text-brand-deep bg-brand-soft border-[0.5px] border-brand-line"
hover-class="opacity-90" @click="benefitsExpanded = !benefitsExpanded">
<text>{{ benefitsExpanded ? "收起" : `展开全部权益 · 共 ${benefits.length} 项` }}</text>
<view class="flex" :class="benefitsExpanded ? '[rotate:-90deg]' : '[rotate:90deg]'">
<AppIcon name="chev" tone="brandDeep" :size="11" />
</view>
</view>
</view>
<!-- 附近同类推荐(图上压字卡) -->
@@ -228,9 +249,11 @@ onShareAppMessage(() => ({
@click="goRelated(rec)">
<image class="block w-[154px] h-[102px] rounded-[16px] shadow-[0_8px_20px_rgba(8,30,22,0.14)]"
:src="rec.main_image_url" mode="aspectFill" />
<view
class="absolute left-0 right-0 bottom-0 px-[10px] pt-[22px] pb-[9px] rounded-b-[16px] text-white text-[11.5px] font-semibold line-clamp-1 scrim-rec">
{{ rec.name }}
<!-- 名称最多两行:外层留渐变与内边距,内层 clamp + 硬裁 32px(2 × 16px 行高),绝不溢到卡片外 -->
<view class="absolute left-0 right-0 bottom-0 px-[10px] pt-[22px] pb-[9px] rounded-b-[16px] scrim-rec">
<view class="text-white text-[11.5px] leading-[16px] font-semibold max-h-[32px] overflow-hidden line-clamp-2">
{{ rec.name }}
</view>
</view>
</view>
</view>
+42
View File
@@ -0,0 +1,42 @@
<script setup>
/**
* 票根经济介绍页(首页品牌名同行小胶囊进入)
* ---------------------------------------------------------------------------
* 内容:GET /v1/content/ticket_economy —— 与公共内容同口径,title 作标题、body 作正文。
* 布局:根 `h-screen flex flex-col` —— 状态栏 + 导航栏(无底色)为固定区 `shrink-0`,
* 正文在导航栏下方的独立 scroll-view 里滚动,正文不会窜到导航栏底下。
*/
import { ref } from "vue";
import contentApi from "@/api/content";
const title = ref("票根经济");
const blocks = ref([]);
const load = async () => {
const res = await contentApi.getTicketEconomy();
title.value = res.title;
blocks.value = res.blocks || [];
};
load();
</script>
<template>
<view class="relative h-screen w-full flex flex-col skin-benefit">
<!-- 固定区:状态栏 + 导航栏(无底色、不随内容滚动),标题常显 -->
<view class="relative z-[60] shrink-0">
<AppStatusBar />
<AppNavBar bare :title="title" />
</view>
<!-- 滚动区:只有导航栏下方这一块滚动 -->
<scroll-view class="flex-1 min-h-0" scroll-y :show-scrollbar="false">
<view class="px-5 pt-4 pb-10">
<template v-for="(block, i) in blocks" :key="i">
<view v-if="block.type === 'h5'" class="text-[14px] font-semibold mt-5 mb-2 text-ink">{{ block.text }}</view>
<view v-else class="text-[13px] leading-[1.9] text-[#4B5563]">{{ block.text }}</view>
</template>
<view class="h-10" />
</view>
</scroll-view>
</view>
</template>
+10 -2
View File
@@ -19,7 +19,7 @@
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "龙里文旅",
"navigationBarTitleText": "云从居",
"enablePullDownRefresh": false
}
},
@@ -68,6 +68,14 @@
"enablePullDownRefresh": false
}
},
{
"path": "intro/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "票根经济",
"enablePullDownRefresh": false
}
},
{
"path": "account/cancel",
"style": {
@@ -87,7 +95,7 @@
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "龙里文旅",
"navigationBarTitleText": "云从居",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F5F7F9",
"backgroundColorTop": "#F5F7F9",
+38 -4
View File
@@ -2,11 +2,21 @@
/**
* 主包壳页面:主页 / 权益 / 我的 三块面板同屏共存,底部 Tab 栏只渲染一次
* ---------------------------------------------------------------------------
* 扫码入口:二维码指向本页并带商家 id(`merchant_id` / `scene` / `q` 三种来源都认),
* 进来自动切到权益 Tab 再打开商家详情 —— 取值见 utils/nav.js 的 pickMerchantId / handleEntry。
*/
import { onMounted, ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { TABS } from "@/constant";
import { consumePendingTab, registerTabController, setTabQuery } from "@/utils/nav";
import { nextTick, onMounted, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { ENTRY_BENEFIT_ONLY, TABS } from "@/constant";
import {
consumePendingMerchant,
consumePendingTab,
goDetail,
pickMerchantId,
registerTabController,
setPendingMerchant,
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";
@@ -32,17 +42,41 @@ const goTab = (key, query = "") => {
showPanel(key);
};
/** Tab 控制器是否已就绪(onShow 可能早于 onMounted,未就绪时不消费扫码参数) */
let shellReady = false;
/**
* 扫码进店:先切权益 Tab 再打开商家详情。
* 顺序不能反 —— 先切 Tab,「详情返回」才会落在权益页而不是主页。
* 入口 id 为固定值 ENTRY_BENEFIT_ONLY 时只切 Tab,不再打开详情。
*/
const runEntry = () => {
if (!shellReady) return;
const id = consumePendingMerchant();
if (!id) return;
goTab("benefit");
if (id.toLowerCase() === ENTRY_BENEFIT_ONLY) return;
nextTick(() => setTimeout(() => goDetail(id), 0));
};
onLoad((options) => {
setPendingMerchant(pickMerchantId(options));
});
onMounted(() => {
// 面板的 onMounted 早于本页 onMounted,此时 ref 已就绪
registerTabController({ go: goTab, getActive: () => active.value });
// 从子页 / 登录页切过来:nav 里可能压着待切 Tab(登录回跳、reLaunch 冷启)
const pending = consumePendingTab();
goTab(pending?.key || active.value, pending?.query || "");
shellReady = true;
runEntry();
});
onShow(() => {
// 从板块列表 / 详情 / 登录页等返回:让当前面板按需刷新(各家内部有幂等守卫)
showPanel(active.value);
runEntry();
});
</script>
+4 -3
View File
@@ -7,6 +7,7 @@
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useUserStore } from "@/store";
import { LOGO_URL } from "@/constant";
import { goAfterLogin, goAgreement, goBack } from "@/utils/nav";
const userStore = useUserStore();
@@ -87,9 +88,9 @@ const toggleAgree = () => {
<!-- 品牌 -->
<view
class="w-24 h-24 mx-auto mt-[58px] rounded-[24px] bg-white border-[0.5px] border-[#E6F8F1] overflow-hidden shadow-[0_16px_36px_rgba(0,181,120,0.24),0_4px_10px_rgba(8,30,22,0.08)]">
<image class="block w-full h-full" src="/static/logo.png" mode="aspectFill" />
<image class="block w-full h-full" :src="LOGO_URL" mode="aspectFill" />
</view>
<view class="mt-5 text-ink text-[26px] font-bold tracking-[4px] indent-[4px]">龙里文旅</view>
<view class="mt-5 text-ink text-[26px] font-bold tracking-[4px] indent-[4px]">云从居</view>
<view class="mt-[9px] text-cap text-ink2 tracking-[1px]">一图玩转龙里 · 领权益畅游全域</view>
<!-- 价值点条(无白卡底,直接浮在渐变上) -->
@@ -144,7 +145,7 @@ const toggleAgree = () => {
confirm-text="同意并登录" :mask-closable="false" @close="agreeVisible = false" @confirm="onAgreeConfirm">
<view class="mt-4 text-cap leading-[1.75] text-ink2">
<view class="mb-[13px] text-ink font-medium leading-[1.7]">
为向你提供龙里文旅导览与权益服务,我们需要收集你的微信头像、昵称与手机号。
为向你提供云从居导览与权益服务,我们需要收集你的微信头像、昵称与手机号。
</view>
<view v-for="(point, i) in AGREEMENT_POINTS" :key="i" class="flex items-start gap-[9px] mt-[9px]">
<view
Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

+13
View File
@@ -13,6 +13,7 @@
.ai-chev.t-brand{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDBCNTc4JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNOS41IDUuNSAxNiAxMmwtNi41IDYuNSIvPjwvc3ZnPg==")}
.ai-chev.t-ink2{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjNkI3MjgwJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNOS41IDUuNSAxNiAxMmwtNi41IDYuNSIvPjwvc3ZnPg==")}
.ai-chev.t-accentDeep{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjQzk1NTBDJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNOS41IDUuNSAxNiAxMmwtNi41IDYuNSIvPjwvc3ZnPg==")}
.ai-chev.t-brandDeep{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDA5QTY3JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNOS41IDUuNSAxNiAxMmwtNi41IDYuNSIvPjwvc3ZnPg==")}
.ai-back.t-ink{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMTQxODFDJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMTQuNSA1LjUgOCAxMmw2LjUgNi41Ii8+PC9zdmc+")}
.ai-back.t-white{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkZGRkZGJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMTQuNSA1LjUgOCAxMmw2LjUgNi41Ii8+PC9zdmc+")}
.ai-back.t-brand{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDBCNTc4JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMTQuNSA1LjUgOCAxMmw2LjUgNi41Ii8+PC9zdmc+")}
@@ -71,6 +72,18 @@
.ai-peak.t-accent{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkY4QTNEJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMi44IDE5LjIgOSA4LjRsMy42IDYuMiAyLjItMy40IDYuNCA4eiIvPjwvc3ZnPg==")}
.ai-peak.t-brand{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDBCNTc4JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMi44IDE5LjIgOSA4LjRsMy42IDYuMiAyLjItMy40IDYuNCA4eiIvPjwvc3ZnPg==")}
.ai-peak.t-white{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkZGRkZGJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMi44IDE5LjIgOSA4LjRsMy42IDYuMiAyLjItMy40IDYuNCA4eiIvPjwvc3ZnPg==")}
.ai-table.t-white{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkZGRkZGJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48ZWxsaXBzZSBjeD0iMTIiIGN5PSI4LjQiIHJ4PSI4LjYiIHJ5PSIzLjIiLz48cGF0aCBkPSJNMTIgMTEuNnY4Ii8+PHBhdGggZD0iTTguNiAxOS42aDYuOCIvPjwvc3ZnPg==")}
.ai-table.t-accent{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkY4QTNEJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48ZWxsaXBzZSBjeD0iMTIiIGN5PSI4LjQiIHJ4PSI4LjYiIHJ5PSIzLjIiLz48cGF0aCBkPSJNMTIgMTEuNnY4Ii8+PHBhdGggZD0iTTguNiAxOS42aDYuOCIvPjwvc3ZnPg==")}
.ai-table.t-brandDeep{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDA5QTY3JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48ZWxsaXBzZSBjeD0iMTIiIGN5PSI4LjQiIHJ4PSI4LjYiIHJ5PSIzLjIiLz48cGF0aCBkPSJNMTIgMTEuNnY4Ii8+PHBhdGggZD0iTTguNiAxOS42aDYuOCIvPjwvc3ZnPg==")}
.ai-hotspring.t-white{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkZGRkZGJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMy40IDE1LjJjMS43IDAgMS43IDEuNSAzLjQgMS41czEuNy0xLjUgMy40LTEuNSAxLjcgMS41IDMuNCAxLjUgMS43LTEuNSAzLjQtMS41IDEuNyAxLjUgMy40IDEuNSIvPjxwYXRoIGQ9Ik0zLjQgMTkuM2MxLjcgMCAxLjcgMS41IDMuNCAxLjVzMS43LTEuNSAzLjQtMS41IDEuNyAxLjUgMy40IDEuNSAxLjctMS41IDMuNC0xLjUgMS43IDEuNSAzLjQgMS41Ii8+PHBhdGggZD0iTTggMTEuOGMtMS41LTEuNyAxLTIuNy4yLTQuNiIvPjxwYXRoIGQ9Ik0xMiAxMS44Yy0xLjUtMS43IDEtMi43LjItNC42Ii8+PHBhdGggZD0iTTE2IDExLjhjLTEuNS0xLjcgMS0yLjcuMi00LjYiLz48L3N2Zz4=")}
.ai-hotspring.t-accent{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkY4QTNEJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMy40IDE1LjJjMS43IDAgMS43IDEuNSAzLjQgMS41czEuNy0xLjUgMy40LTEuNSAxLjcgMS41IDMuNCAxLjUgMS43LTEuNSAzLjQtMS41IDEuNyAxLjUgMy40IDEuNSIvPjxwYXRoIGQ9Ik0zLjQgMTkuM2MxLjcgMCAxLjcgMS41IDMuNCAxLjVzMS43LTEuNSAzLjQtMS41IDEuNyAxLjUgMy40IDEuNSAxLjctMS41IDMuNC0xLjUgMS43IDEuNSAzLjQgMS41Ii8+PHBhdGggZD0iTTggMTEuOGMtMS41LTEuNyAxLTIuNy4yLTQuNiIvPjxwYXRoIGQ9Ik0xMiAxMS44Yy0xLjUtMS43IDEtMi43LjItNC42Ii8+PHBhdGggZD0iTTE2IDExLjhjLTEuNS0xLjcgMS0yLjcuMi00LjYiLz48L3N2Zz4=")}
.ai-hotspring.t-brandDeep{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDA5QTY3JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMy40IDE1LjJjMS43IDAgMS43IDEuNSAzLjQgMS41czEuNy0xLjUgMy40LTEuNSAxLjcgMS41IDMuNCAxLjUgMS43LTEuNSAzLjQtMS41IDEuNyAxLjUgMy40IDEuNSIvPjxwYXRoIGQ9Ik0zLjQgMTkuM2MxLjcgMCAxLjcgMS41IDMuNCAxLjVzMS43LTEuNSAzLjQtMS41IDEuNyAxLjUgMy40IDEuNSAxLjctMS41IDMuNC0xLjUgMS43IDEuNSAzLjQgMS41Ii8+PHBhdGggZD0iTTggMTEuOGMtMS41LTEuNyAxLTIuNy4yLTQuNiIvPjxwYXRoIGQ9Ik0xMiAxMS44Yy0xLjUtMS43IDEtMi43LjItNC42Ii8+PHBhdGggZD0iTTE2IDExLjhjLTEuNS0xLjcgMS0yLjcuMi00LjYiLz48L3N2Zz4=")}
.ai-fuel.t-white{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkZGRkZGJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cmVjdCB4PSIzLjIiIHk9IjMuNiIgd2lkdGg9IjkuOCIgaGVpZ2h0PSIxNi44IiByeD0iMS43Ii8+PHJlY3QgeD0iNS44IiB5PSI2LjQiIHdpZHRoPSI0LjYiIGhlaWdodD0iNCIgcng9IjEiLz48cGF0aCBkPSJNMTMgMTBoMi40YTIgMiAwIDAgMSAyIDJ2Ni40YTEuNyAxLjcgMCAwIDAgMy40IDBWMTIiLz48L3N2Zz4=")}
.ai-fuel.t-accent{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkY4QTNEJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cmVjdCB4PSIzLjIiIHk9IjMuNiIgd2lkdGg9IjkuOCIgaGVpZ2h0PSIxNi44IiByeD0iMS43Ii8+PHJlY3QgeD0iNS44IiB5PSI2LjQiIHdpZHRoPSI0LjYiIGhlaWdodD0iNCIgcng9IjEiLz48cGF0aCBkPSJNMTMgMTBoMi40YTIgMiAwIDAgMSAyIDJ2Ni40YTEuNyAxLjcgMCAwIDAgMy40IDBWMTIiLz48L3N2Zz4=")}
.ai-fuel.t-brandDeep{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDA5QTY3JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cmVjdCB4PSIzLjIiIHk9IjMuNiIgd2lkdGg9IjkuOCIgaGVpZ2h0PSIxNi44IiByeD0iMS43Ii8+PHJlY3QgeD0iNS44IiB5PSI2LjQiIHdpZHRoPSI0LjYiIGhlaWdodD0iNCIgcng9IjEiLz48cGF0aCBkPSJNMTMgMTBoMi40YTIgMiAwIDAgMSAyIDJ2Ni40YTEuNyAxLjcgMCAwIDAgMy40IDBWMTIiLz48L3N2Zz4=")}
.ai-cart.t-white{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkZGRkZGJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMy4yIDQuNmgyLjRsMi43IDkuOWg5LjVsMi4xLTcuM0g2LjUiLz48Y2lyY2xlIGN4PSI5LjYiIGN5PSIxOS4yIiByPSIxLjYiLz48Y2lyY2xlIGN4PSIxNyIgY3k9IjE5LjIiIHI9IjEuNiIvPjwvc3ZnPg==")}
.ai-cart.t-accent{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkY4QTNEJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMy4yIDQuNmgyLjRsMi43IDkuOWg5LjVsMi4xLTcuM0g2LjUiLz48Y2lyY2xlIGN4PSI5LjYiIGN5PSIxOS4yIiByPSIxLjYiLz48Y2lyY2xlIGN4PSIxNyIgY3k9IjE5LjIiIHI9IjEuNiIvPjwvc3ZnPg==")}
.ai-cart.t-brandDeep{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDA5QTY3JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMy4yIDQuNmgyLjRsMi43IDkuOWg5LjVsMi4xLTcuM0g2LjUiLz48Y2lyY2xlIGN4PSI5LjYiIGN5PSIxOS4yIiByPSIxLjYiLz48Y2lyY2xlIGN4PSIxNyIgY3k9IjE5LjIiIHI9IjEuNiIvPjwvc3ZnPg==")}
.ai-people.t-ink3{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjOUNBM0FGJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48Y2lyY2xlIGN4PSI5LjIiIGN5PSI4LjQiIHI9IjMuMiIvPjxwYXRoIGQ9Ik0zLjYgMTkuNGE1LjYgNS42IDAgMCAxIDExLjIgMCIvPjxwYXRoIGQ9Ik0xNS44IDUuNmEzLjIgMy4yIDAgMCAxIDAgNiIvPjxwYXRoIGQ9Ik0xNy40IDE0LjhhNS42IDUuNiAwIDAgMSAzIDQuNiIvPjwvc3ZnPg==")}
.ai-people.t-brand{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDBCNTc4JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48Y2lyY2xlIGN4PSI5LjIiIGN5PSI4LjQiIHI9IjMuMiIvPjxwYXRoIGQ9Ik0zLjYgMTkuNGE1LjYgNS42IDAgMCAxIDExLjIgMCIvPjxwYXRoIGQ9Ik0xNS44IDUuNmEzLjIgMy4yIDAgMCAxIDAgNiIvPjxwYXRoIGQ9Ik0xNy40IDE0LjhhNS42IDUuNiAwIDAgMSAzIDQuNiIvPjwvc3ZnPg==")}
.ai-people.t-white{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkZGRkZGJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48Y2lyY2xlIGN4PSI5LjIiIGN5PSI4LjQiIHI9IjMuMiIvPjxwYXRoIGQ9Ik0zLjYgMTkuNGE1LjYgNS42IDAgMCAxIDExLjIgMCIvPjxwYXRoIGQ9Ik0xNS44IDUuNmEzLjIgMy4yIDAgMCAxIDAgNiIvPjxwYXRoIGQ9Ik0xNy40IDE0LjhhNS42IDUuNiAwIDAgMSAzIDQuNiIvPjwvc3ZnPg==")}
+60
View File
@@ -28,6 +28,8 @@ export const registerTabController = (ctl) => {
let tabQuery = "";
/** 壳尚未就绪时的待切 Tab */
let pendingTab = null;
/** 扫码 / 外链进入时待打开的商家 id */
let pendingMerchant = "";
export const setTabQuery = (query) => {
tabQuery = query || "";
@@ -47,6 +49,61 @@ export const consumePendingTab = () => {
return pending;
};
/** 记下待打开的商家 id(空值不覆盖已有的) */
export const setPendingMerchant = (id) => {
const value = String(id || "").trim();
if (value) pendingMerchant = value;
};
/** 取出并清空待打开的商家 id */
export const consumePendingMerchant = () => {
const value = pendingMerchant;
pendingMerchant = "";
return value;
};
/** 承载入口目标值的键名(query 直传、scene 内、q 解码后的 query 共用一套) */
const ID_KEYS = ["merchant_id", "merchantId", "id", "target"];
const pickByKeys = (source) => ID_KEYS.map((key) => pickParam(source, key)).find((v) => v) || "";
/**
* 从启动/唤起参数里取入口值:兼容三种来源
* - `merchant_id` / `merchantId` / `id` / `target`:页面 query 直传(小程序码 path、普通链接)
* - `scene`:小程序码 getUnlimited,值可能是 `merchant_id=xxx` / `target=xxx` 也可能是短码
* - `q`:普通链接二维码,值是 URL 编码过的原始链接,解码后再取 query
* 值可能是商家 id,也可能是保留值(如只切权益 Tab,见壳页 runEntry)
*/
export const pickMerchantId = (options) => {
const o = options || {};
const direct = ID_KEYS.map((key) => o[key]).find((v) => v);
if (direct) return decodeURIComponent(String(direct)).trim();
if (o.scene) {
const scene = decodeURIComponent(String(o.scene));
return pickByKeys(scene) || scene.trim();
}
if (o.q) {
const query = decodeURIComponent(String(o.q)).split("?")[1] || "";
return pickByKeys(query);
}
return "";
};
/**
* 入口参数处理(App.onLaunch / App.onShow 调用):记下商家 id;
* 栈顶不是壳页时 reLaunch 回壳 —— 由壳消费 pending 后切权益 Tab 并打开详情。
* 冷启动时页面栈还没建立(getCurrentPages 为空),此时只记 pending,不 reLaunch。
*/
export const handleEntry = (options) => {
const id = pickMerchantId(options);
if (!id) return;
setPendingMerchant(id);
const pages = getCurrentPages();
if (!pages.length) return;
const top = `/${pages[pages.length - 1].route}`;
if (top !== ROUTES.HOME) uni.reLaunch({ url: ROUTES.HOME });
};
/** query 串取参数 */
const pickParam = (query, key) => {
const matched = (query || "").match(new RegExp(`(?:^|&)${key}=([^&]*)`));
@@ -163,6 +220,9 @@ export const goAgreement = (type) => {
};
export const goAbout = () => uni.navigateTo({ url: ROUTES.ABOUT });
/** 跳票根经济介绍页 */
export const goIntro = () => uni.navigateTo({ url: ROUTES.INTRO });
export const goAccountCancel = () => uni.navigateTo({ url: ROUTES.ACCOUNT_CANCEL });
/** 拨打电话 */
+23 -4
View File
@@ -14,10 +14,12 @@
*/
import { computed, ref } from "vue";
import contentApi from "@/api/content";
import { goCatalog, previewImages } from "@/utils/nav";
import { goCatalog, goIntro, previewImages } from "@/utils/nav";
const home = ref({});
const featured = ref([]);
/** 票根经济入口文案:取接口 /v1/content/ticket_economy 的 title(与首页首屏并行前置请求) */
const introTitle = ref("什么是票根经济?");
/** 首屏数据是否已回来(骨架屏开关;请求失败也置真,避免骨架常驻) */
const loaded = ref(false);
@@ -44,9 +46,15 @@ const load = async () => {
if (loading) return;
loading = true;
try {
const data = await contentApi.getHomepage();
// 票根经济介绍内容与首屏一起前置请求:入口文案首屏即为接口值,
// 且二级页进入时命中内容缓存(不再等请求)。
const [data, intro] = await Promise.all([
contentApi.getHomepage(),
contentApi.getTicketEconomy(),
]);
home.value = data || {};
featured.value = data?.featured?.items || [];
if (intro?.title) introTitle.value = intro.title;
} catch (e) {
home.value = {};
featured.value = [];
@@ -140,8 +148,19 @@ const onMapTap = () => {
<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 class="flex items-center justify-between gap-2 mt-[9px]">
<view class="min-w-0 truncate text-display font-bold tracking-[-1px] leading-[1.1] text-ink">
{{ brandHead }}<text class="not-italic gradtext-brand">{{ brandTail }}</text>
</view>
<!-- 票根经济介绍入口:贴品牌名右端,半透明白底,不增加固定区高度;相对品牌名基线下移 8px -->
<view
class="shrink-0 relative top-2 flex items-center gap-[3px] h-[24px] px-[9px] rounded-full bg-[rgba(255,255,255,0.5)] border-[0.5px] border-brand-line"
hover-class="opacity-70"
@click="goIntro"
>
<text class="text-[11px] font-semibold text-brand-deep">{{ introTitle }}</text>
<AppIcon name="chev" tone="brandDeep" :size="11" />
</view>
</view>
<view class="mt-2 text-[11.5px] leading-[1.55] text-ink2 tracking-[0.1px]">
{{ brand.slogan }}