Compare commits
12
Commits
d2830c59f3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ccff41066 | ||
|
|
2f35e953df | ||
|
|
59fc90a880 | ||
|
|
eb7bce2289 | ||
|
|
e93946c971 | ||
|
|
4bf7b52918 | ||
|
|
348b3d80ac | ||
|
|
a4c29569bf | ||
|
|
681ba3d8f1 | ||
|
|
f499bf00dd | ||
|
|
0be51575be | ||
|
|
a71408eb9b |
No files matched your search
+1
-1
@@ -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
@@ -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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"appid": "wx23f86d809ae80259",
|
||||
"appid": "wx3b6667ba12eb2507",
|
||||
"projectname": "longli-mp",
|
||||
"description": "龙里文旅 · 微信小程序",
|
||||
"description": "云从居 · 微信小程序",
|
||||
"miniprogramRoot": "dist/build/mp-weixin/",
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "3.5.0",
|
||||
|
||||
@@ -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"/>',
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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
@@ -1,5 +1,5 @@
|
||||
/* ===========================================================================
|
||||
龙里文旅 · 样式总入口(Tailwind CSS v4)
|
||||
云从居 · 样式总入口(Tailwind CSS v4)
|
||||
---------------------------------------------------------------------------
|
||||
① @theme 设计 Token → 原子类(bg-brand / text-cap / rounded-card / shadow-elev2 …)
|
||||
② @utility 内置原子表达不了的复合样式(满屏渐变底 / 渐变填充 / 渐变字 / 伪元素装饰)
|
||||
|
||||
@@ -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>
|
||||
@@ -165,16 +197,15 @@ const onTap = () => {
|
||||
<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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -31,7 +31,7 @@ onLoad(async (options) => {
|
||||
});
|
||||
|
||||
onShareAppMessage(() => ({
|
||||
title: `${title.value} · 龙里文旅`,
|
||||
title: `${title.value} · 云从居`,
|
||||
path: `/pages-sub/agreement/index?type=${type.value}`,
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -68,7 +68,7 @@ const onCoverPreview = () => {
|
||||
};
|
||||
|
||||
onShareAppMessage(() => ({
|
||||
title: `${title.value} · 龙里文旅`,
|
||||
title: `${title.value} · 云从居`,
|
||||
path: `/pages-sub/catalog/index?section=${sectionKey.value}`,
|
||||
imageUrl: cover.value,
|
||||
}));
|
||||
|
||||
@@ -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";
|
||||
@@ -39,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 || "");
|
||||
@@ -116,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] || "",
|
||||
}));
|
||||
@@ -190,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 }}
|
||||
@@ -222,7 +226,7 @@ onShareAppMessage(() => ({
|
||||
到店权益
|
||||
</view>
|
||||
<BenefitCard v-for="benefit in shownBenefits" :key="benefit.id" :benefit="benefit" mode="detail"
|
||||
@action="onClaimBenefit" />
|
||||
: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"
|
||||
@@ -245,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>
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 |
@@ -72,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==")}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
/** 拨打电话 */
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
Reference in new issue
Block a user