add commiunity

This commit is contained in:
Daniel Kou 2026-07-08 16:17:50 +08:00
parent b770725a5d
commit 88632c52ed
35 changed files with 3324 additions and 160 deletions

View File

@ -1,6 +1,7 @@
export const apiArr = { export const apiArr = {
// 到家服务 // 到家服务
categoryList: "/api/v2/wechat/service/category/list", // 服务分类列表 categoryList: "/api/v2/wechat/service/category/list", // 服务分类列表
categorySubList: "/api/v2/wechat/service/category/sub-list", // 全部小类(到家+家政两大类合并,供金刚区)
serviceList: "/api/v2/wechat/service/list", // 服务商品列表 serviceList: "/api/v2/wechat/service/list", // 服务商品列表
serviceInfo: "/api/v2/wechat/service/info", // 服务详情(含SKU+师傅) serviceInfo: "/api/v2/wechat/service/info", // 服务详情(含SKU+师傅)
orderCreate: "/api/v2/wechat/service/order/create", // 下单(定金单) orderCreate: "/api/v2/wechat/service/order/create", // 下单(定金单)

View File

@ -47,7 +47,7 @@ export default {
photoAc: photoAc:
"https://static.hshuishang.com/property-img-file/footer_homeAc.png", "https://static.hshuishang.com/property-img-file/footer_homeAc.png",
nav_name: "智慧首页", nav_name: "智慧首页",
url: "/pages/index/index", url: "/pages/index2/index2",
}, },
{ {
photo: photo:
@ -55,7 +55,7 @@ export default {
photoAc: photoAc:
"https://static.hshuishang.com/property-img-file/footer_facilityAc.png", "https://static.hshuishang.com/property-img-file/footer_facilityAc.png",
nav_name: "智能设备", nav_name: "智能设备",
url: "/packages/smartDevice/index/index", url: "/packages/smartDevice/home/index",
}, },
{ {
photo: photo:
@ -70,7 +70,7 @@ export default {
"https://static.hshuishang.com/property-img-file/footer_community.png", "https://static.hshuishang.com/property-img-file/footer_community.png",
photoAc: photoAc:
"https://static.hshuishang.com/property-img-file/footer_communityAc.png", "https://static.hshuishang.com/property-img-file/footer_communityAc.png",
nav_name: "我的区", nav_name: "我的区",
url: "/packages/community/index/index", url: "/packages/community/index/index",
}, },
// { // {
@ -200,9 +200,10 @@ export default {
const ctoken = uni.getStorageSync("ctoken"); const ctoken = uni.getStorageSync("ctoken");
const authen = [ const authen = [
"/pages/index2/index2",
"/pages/index/index", "/pages/index/index",
"/pages/user/index", "/pages/user/index",
"/packages/smartDevice/index/index", "/packages/smartDevice/home/index",
]; ];
// ctoken tab访 // ctoken tab访
if (authen.includes(url)) { if (authen.includes(url)) {

View File

@ -138,9 +138,9 @@ export default {
icon: "none", icon: "none",
duration: 1500, duration: 1500,
}); });
// //
setTimeout(() => { setTimeout(() => {
NavgateTo("/packages/community/myCommunity/index"); uni.reLaunch({ url: "/pages/user/index" });
}, 1500); }, 1500);
}); });
}, },

View File

@ -53,14 +53,21 @@ image {
min-height: calc(100vh - 120rpx); min-height: calc(100vh - 120rpx);
width: 100%; width: 100%;
position: relative; position: relative;
/* 去掉渐变续色,页面纯白底 */
} }
.searchBox { .searchBox {
display: flex; display: flex;
align-items: center; align-items: center;
position: absolute; /* 不要红头,保持页面白底 */
left: 0; background: #ffffff;
z-index: 2; }
.back-btn {
display: flex;
align-items: center;
padding-left: 20rpx;
padding-right: 4rpx;
} }
.searchBox_add { .searchBox_add {
@ -73,7 +80,7 @@ image {
width: auto; width: auto;
height: 44rpx; height: 44rpx;
padding: 5rpx 15rpx; padding: 5rpx 15rpx;
background: rgba(255, 255, 255, 0.8); background: #f4f5f7;
border-radius: 100rpx 100rpx 100rpx 100rpx; border-radius: 100rpx 100rpx 100rpx 100rpx;
display: flex; display: flex;
align-items: center; align-items: center;
@ -103,7 +110,11 @@ image {
.swiperBox1 { .swiperBox1 {
height: 500rpx; height: 500rpx;
width: 750rpx; width: 710rpx;
/* 与首页 banner 一致:内缩圆角卡片,两侧留白让 .container 续色透出 */
margin: 20rpx auto 0;
border-radius: 16rpx;
overflow: hidden;
position: relative; position: relative;
} }
@ -111,8 +122,8 @@ image {
height: 300rpx; height: 300rpx;
width: 710rpx; width: 710rpx;
position: relative; position: relative;
margin-top: 170rpx; /* 头区已改普通流,去掉原为绝对定位 searchBox 预留的 170rpx 顶部空隙 */
margin: 170rpx auto 0; margin: 20rpx auto 0;
} }
.swiperBox_no_img{ .swiperBox_no_img{
@ -123,15 +134,9 @@ image {
height: 100%; height: 100%;
} }
/* B方案去掉 banner 底部的半透明→白 遮罩(会截断红色续色、造成不连续) */
.swiperBox1::after { .swiperBox1::after {
content: ''; content: none;
width: 100%;
position: absolute;
left: 0;
bottom: 0;
height: 120rpx;
/* 从上到下 透明渐变成白色 */
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
} }
.dot { .dot {
@ -163,14 +168,14 @@ image {
.swiperBox2 { .swiperBox2 {
height: 150rpx; height: 150rpx;
width: 710rpx; width: 710rpx;
margin: 0 auto; /* 与金刚区互换后位于其下方,去掉原上叠 banner 的 -96rpx改正常上间距 */
margin-top: -96rpx; margin: 30rpx auto 0;
} }
.swiperBox_no2 { .swiperBox_no2 {
height: 150rpx; height: 150rpx;
width: 710rpx; width: 710rpx;
margin: 0 auto;; margin: 30rpx auto 0;
} }
.funcList { .funcList {
@ -284,6 +289,55 @@ image {
overflow: hidden; overflow: hidden;
} }
/* 宣传栏:图左 + 右侧标题与内容预览 */
.newsItemL {
display: flex;
align-items: center;
padding: 30rpx 0;
margin: 0 20rpx;
box-sizing: border-box;
border-bottom: 1rpx solid #EBEBEB;
}
.newsItemL:last-child {
border-bottom: none;
}
.newsItemL_pic {
width: 220rpx;
height: 160rpx;
border-radius: 16rpx;
overflow: hidden;
flex-shrink: 0;
margin-right: 24rpx;
}
.newsItemL_body {
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: center;
}
.newsItemL_tit {
font-size: 32rpx;
font-weight: 600;
color: #222222;
margin-bottom: 14rpx;
}
.newsItemL_desc {
font-size: 26rpx;
color: #999999;
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.tips { .tips {
font-size: 26rpx; font-size: 26rpx;
color: #999999; color: #999999;

View File

@ -8,6 +8,9 @@
<view v-if="communityList.length == 0"> <view v-if="communityList.length == 0">
<view class="searchBox" :style="{ height: localHeight + 'px', paddingTop: top + 'px' }"> <view class="searchBox" :style="{ height: localHeight + 'px', paddingTop: top + 'px' }">
<view class="back-btn" v-if="showBack" @tap="goBack">
<u-icon name="arrow-left" color="#333" size="40"></u-icon>
</view>
<view class="searchBox_add"> <view class="searchBox_add">
<view class="emptyCommunity" @click="addCommunity"> <view class="emptyCommunity" @click="addCommunity">
{{ communityVal }} {{ communityVal }}
@ -28,13 +31,7 @@
</view> </view>
</view> </view>
<view class="swiperBox_no2"> <!-- 金刚区与消费送物业费广告互换金刚区在上广告在下与已绑定状态一致 -->
<swiper @animationfinish="swipers" autoplay circular>
<image src="https://static.hshuishang.com/community_no_image2.png" mode="aspectFill"
class="swiperBox_no_img" @click="addCommunity" />
</swiper>
</view>
<view class="funcList"> <view class="funcList">
<u-grid :col="5" :border="false"> <u-grid :col="5" :border="false">
<u-grid-item v-for="(item, index) in noValFunctionList" @click="addCommunity" :key="index"> <u-grid-item v-for="(item, index) in noValFunctionList" @click="addCommunity" :key="index">
@ -44,6 +41,13 @@
</u-grid> </u-grid>
</view> </view>
<view class="swiperBox_no2">
<swiper @animationfinish="swipers" autoplay circular>
<image src="https://static.hshuishang.com/community_no_image2.png" mode="aspectFill"
class="swiperBox_no_img" @click="addCommunity" />
</swiper>
</view>
<view class="tabs" v-if="defaultCategoryList.length > 0"> <view class="tabs" v-if="defaultCategoryList.length > 0">
<view v-for="(item, index) in defaultCategoryList" :key="index" <view v-for="(item, index) in defaultCategoryList" :key="index"
:class="['tabItem', selectedTab === index ? 'active2' : '']" @click="selectTab(index, item)"> :class="['tabItem', selectedTab === index ? 'active2' : '']" @click="selectTab(index, item)">
@ -51,21 +55,25 @@
</view> </view>
</view> </view>
<!-- 宣传栏图片在左标题在右标题下方展示内容预览 -->
<view class="newsList" v-if="defaultCategoryList.length > 0"> <view class="newsList" v-if="defaultCategoryList.length > 0">
<view class="newsItem" v-for="item in defaultInfoList" @click="defaultDetail(item)" :key="item.id"> <view class="newsItemL" v-for="item in defaultInfoList" @click="defaultDetail(item)" :key="item.id">
<view class="newsItem_left"> <view class="newsItemL_pic">
<view class="newsItem_left_tit">{{ item.title }}</view>
<view class="newsItem_left_sub">{{ item.author }}</view>
</view>
<view class="newsItem_right">
<image :src="item.pic" mode="aspectFill" /> <image :src="item.pic" mode="aspectFill" />
</view> </view>
<view class="newsItemL_body">
<view class="newsItemL_tit">{{ item.title }}</view>
<view class="newsItemL_desc">{{ contentPreview(item.id) }}</view>
</view>
</view> </view>
</view> </view>
</view> </view>
<view v-else> <view v-else>
<view class="searchBox" :style="{ height: localHeight + 'px', paddingTop: top + 'px' }"> <view class="searchBox" :style="{ height: localHeight + 'px', paddingTop: top + 'px' }">
<view class="back-btn" v-if="showBack" @tap="goBack">
<u-icon name="arrow-left" color="#333" size="40"></u-icon>
</view>
<view class="searchBox_add"> <view class="searchBox_add">
<view class="emptyCommunity" @click="addCommunity"> <view class="emptyCommunity" @click="addCommunity">
{{ communityVal }} {{ communityVal }}
@ -100,6 +108,16 @@
</view> </view>
</view> </view>
<!-- 金刚区消费送物业费广告互换金刚区在上广告在下 -->
<view class="funcList">
<u-grid :col="rowNum" :border="false">
<u-grid-item v-for="(item, index) in functionList" @click="jump(item.mini_program_url)" :key="index">
<image class="grid_Pic" :src="item.nav_icon" mode=""></image>
<text class="grid_Text">{{ item.nav_name }}</text>
</u-grid-item>
</u-grid>
</view>
<view class="swiperBox2" v-if="streamerList.length > 0"> <view class="swiperBox2" v-if="streamerList.length > 0">
<swiper @animationfinish="swipers" autoplay circular> <swiper @animationfinish="swipers" autoplay circular>
<swiper-item v-for="(item, index) in streamerList" :key="index" @click="headerServerClick(item)"> <swiper-item v-for="(item, index) in streamerList" :key="index" @click="headerServerClick(item)">
@ -115,15 +133,6 @@
</swiper> </swiper>
</view> </view>
<view class="funcList">
<u-grid :col="rowNum" :border="false">
<u-grid-item v-for="(item, index) in functionList" @click="jump(item.mini_program_url)" :key="index">
<image class="grid_Pic" :src="item.nav_icon" mode=""></image>
<text class="grid_Text">{{ item.nav_name }}</text>
</u-grid-item>
</u-grid>
</view>
<view v-for="(item, index) in tileList" :key="index" :class="['ads', index == 0 ? 'ads_first' : '']" <view v-for="(item, index) in tileList" :key="index" :class="['ads', index == 0 ? 'ads_first' : '']"
@click="headerServerClick(item)"> @click="headerServerClick(item)">
<image :src="item.ad_picture" mode="aspectFill" /> <image :src="item.ad_picture" mode="aspectFill" />
@ -143,27 +152,28 @@
</view> </view>
</view> </view>
<!-- 已绑定临时通知列表图左 + 右侧标题与内容预览 -->
<view class="newsList" v-if="categoryList.length > 0"> <view class="newsList" v-if="categoryList.length > 0">
<view class="newsItem" v-for="item in infoList" @click="detail(item)" :key="item.id"> <view class="newsItemL" v-for="item in infoList" @click="detail(item)" :key="item.id">
<view class="newsItem_left"> <view class="newsItemL_pic">
<view class="newsItem_left_tit">{{ item.title }}</view>
<view class="newsItem_left_sub">{{ item.author }}</view>
</view>
<view class="newsItem_right">
<image :src="item.list_image" mode="aspectFill" /> <image :src="item.list_image" mode="aspectFill" />
</view> </view>
<view class="newsItemL_body">
<view class="newsItemL_tit">{{ item.title }}</view>
<view class="newsItemL_desc">{{ noticePreview(item) }}</view>
</view>
</view> </view>
</view> </view>
<view class="newsList" v-else> <view class="newsList" v-else>
<view class="newsItem" v-for="item in defaultInfoList" @click="defaultDetail(item)" :key="item.id"> <view class="newsItemL" v-for="item in defaultInfoList" @click="defaultDetail(item)" :key="item.id">
<view class="newsItem_left"> <view class="newsItemL_pic">
<view class="newsItem_left_tit">{{ item.title }}</view>
<view class="newsItem_left_sub">{{ item.author }}</view>
</view>
<view class="newsItem_right">
<image :src="item.pic" mode="aspectFill" /> <image :src="item.pic" mode="aspectFill" />
</view> </view>
<view class="newsItemL_body">
<view class="newsItemL_tit">{{ item.title }}</view>
<view class="newsItemL_desc">{{ contentPreview(item.id) }}</view>
</view>
</view> </view>
</view> </view>
@ -227,6 +237,7 @@ export default {
picUrl, picUrl,
top: "", top: "",
localHeight: "", localHeight: "",
showBack: false, // ()tab
queryPage: { queryPage: {
page_num: 1, page_num: 1,
page_size: 10, page_size: 10,
@ -308,6 +319,7 @@ export default {
// JSON // JSON
defaultCategoryList: dataJson.defaultCategoryList, defaultCategoryList: dataJson.defaultCategoryList,
defaultInfoList: dataJson.defaultInfoList, defaultInfoList: dataJson.defaultInfoList,
defaultNoticeList: dataJson.defaultNoticeList, //
// defaultInfoList // defaultInfoList
originalDefaultInfoList: null originalDefaultInfoList: null
}; };
@ -330,6 +342,7 @@ export default {
this.top = meun.top; this.top = meun.top;
// this.top = meun.height + meun.top; // this.top = meun.height + meun.top;
this.localHeight = meun.height; this.localHeight = meun.height;
this.showBack = getCurrentPages().length > 1;
this.defaultFunctionList = [ this.defaultFunctionList = [
{ {
create_time: "", create_time: "",
@ -514,6 +527,9 @@ export default {
addCommunity() { addCommunity() {
NavgateTo("/packages/community/myCommunity/index"); NavgateTo("/packages/community/myCommunity/index");
}, },
goBack() {
uni.navigateBack({ delta: 1 });
},
swipers(event) { swipers(event) {
// //
@ -826,6 +842,39 @@ export default {
console.log(e); console.log(e);
NavgateTo("../defaultNotice/index?id=" + e.id); NavgateTo("../defaultNotice/index?id=" + e.id);
}, },
// id //
contentPreview(id) {
const n = (this.defaultNoticeList || []).find((x) => x.id === id);
if (!n || !n.content) return "";
const text = n.content
.replace(/&nbsp;/g, " ")
.replace(/<[^>]+>/g, "")
.replace(/\s+/g, " ")
.trim();
return text.length > 50 ? text.slice(0, 50) + "…" : text;
},
// +
noticePreview(item) {
if (!item) return "";
const raw =
item.content ||
item.summary ||
item.describe ||
item.intro ||
item.sub_title ||
"";
if (raw) {
const text = String(raw)
.replace(/&nbsp;/g, " ")
.replace(/<[^>]+>/g, "")
.replace(/\s+/g, " ")
.trim();
if (text) return text.length > 50 ? text.slice(0, 50) + "…" : text;
}
return item.author || "";
},
}, },
}; };
</script> </script>

View File

@ -0,0 +1,144 @@
<!-- 未来社区 - 活动详情 -->
<template>
<view class="ad-page">
<view class="fc-nav" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="fc-nav-bar">
<view class="fc-back" @tap="goBack"><u-icon name="arrow-left" color="#1a1a1a" size="38"></u-icon></view>
<text class="fc-nav-title">活动详情</text>
<view class="ad-star" @tap="onFollow" v-if="act"><u-icon :name="followed ? 'star-fill' : 'star'" :color="followed ? '#ff9a12' : '#9098a0'" size="40"></u-icon></view>
</view>
</view>
<view class="ad-body" v-if="act">
<!-- 封面 -->
<view class="ad-banner">
<image v-if="act.image" :src="act.image" class="ad-banner-img" mode="aspectFill" />
<view v-else class="ad-banner-img" :style="{ background: sceneBg(act.scene) }"></view>
<text class="ad-badge" :style="statusStyle(act.status)">{{ statusLabel(act.status) }}</text>
</view>
<!-- 摘要 -->
<view class="ad-summary">
<text class="ad-title">{{ act.title }}</text>
<view class="ad-sum-row">
<view class="ad-dist"><u-icon name="map" color="#9098a0" size="24"></u-icon><text>{{ act.distance || '社区内' }}</text></view>
<view class="ad-call" v-if="act.phone" @tap="onCall"><u-icon name="phone" color="#e9272d" size="24"></u-icon><text>联系电话</text></view>
</view>
<text class="ad-addr">{{ act.address || act.place }}</text>
</view>
<!-- 活动说明 -->
<view class="ad-card">
<text class="ad-card-tt">活动说明</text>
<view class="ad-info-row"><u-icon name="account" color="#9098a0" size="26"></u-icon><text class="ad-lb">报名人数</text><text class="ad-vl">{{ act.enrolled || act.quota }}</text></view>
<view class="ad-info-row"><u-icon name="clock" color="#9098a0" size="26"></u-icon><text class="ad-lb">报名时间</text><text class="ad-vl">{{ act.signupRange || act.deadline }}</text></view>
<view class="ad-info-row"><u-icon name="calendar" color="#9098a0" size="26"></u-icon><text class="ad-lb">活动时间</text><text class="ad-vl">{{ act.time }}</text></view>
<view class="ad-info-row"><u-icon name="integral" color="#9098a0" size="26"></u-icon><text class="ad-lb">报名费用</text><text class="ad-vl">{{ act.fee || '免费' }}</text></view>
<view class="ad-info-row"><u-icon name="server-man" color="#9098a0" size="26"></u-icon><text class="ad-lb">组织方</text><text class="ad-vl">{{ act.organizer || '九九社区' }}</text></view>
<view class="ad-info-row"><u-icon name="phone" color="#9098a0" size="26"></u-icon><text class="ad-lb">联系电话</text><text class="ad-vl">{{ act.phone || '暂无' }}</text></view>
</view>
<!-- 活动介绍 -->
<view class="ad-card">
<text class="ad-card-tt">活动介绍</text>
<rich-text class="ad-content" :nodes="act.content"></rich-text>
<view class="ad-points" v-if="act.points && act.points.length">
<view class="ad-point" v-for="(p, i) in act.points" :key="i"><text class="ad-dot"></text><text class="ad-pt">{{ p }}</text></view>
</view>
</view>
<!-- 参与须知 -->
<view class="ad-card">
<text class="ad-card-tt">参与须知</text>
<view class="ad-point" v-for="(p, i) in ACTIVITY_REGISTER_NOTICE" :key="i"><text class="ad-dot"></text><text class="ad-pt">{{ p }}</text></view>
</view>
<view class="fc-spacer"></view>
</view>
<view v-else class="fc-empty">活动不存在</view>
<view class="ad-foot" v-if="act">
<view class="ad-foot-fee"><text class="ad-foot-lb">报名费用</text><text class="ad-foot-vl">{{ act.fee || '免费' }}</text></view>
<text class="ad-join" :class="{ done: joined, ended: act.status === 'ended' }" @tap="onJoin">{{ joinLabel }}</text>
</view>
</view>
</template>
<script>
import { findActivity, ACTIVITY_STATUS_META, ACTIVITY_SCENE_BG, ACTIVITY_REGISTER_NOTICE, isActivityJoined, toggleActivityJoin, isActivityFollowed, toggleActivityFollow } from '../data.js'
export default {
data() { return { statusBarTop: 20, act: null, joined: false, followed: false, ACTIVITY_REGISTER_NOTICE } },
onLoad(options) {
const sys = uni.getSystemInfoSync()
this.statusBarTop = sys.statusBarHeight || 20
this.act = findActivity(options && options.id) || null
if (this.act) { this.joined = isActivityJoined(this.act.id); this.followed = isActivityFollowed(this.act.id) }
},
computed: {
joinLabel() {
if (!this.act) return ''
if (this.act.status === 'ended') return '活动已结束'
return this.joined ? '已报名 · 点击取消' : '立即报名'
}
},
methods: {
statusLabel(s) { return (ACTIVITY_STATUS_META[s] || {}).label || '' },
statusStyle(s) { const m = ACTIVITY_STATUS_META[s] || {}; return { color: m.color, background: m.bg } },
sceneBg(s) { return ACTIVITY_SCENE_BG[s] || ACTIVITY_SCENE_BG.handicraft },
goBack() { uni.navigateBack({ delta: 1 }) },
onCall() { uni.showToast({ title: '联系电话:' + this.act.phone, icon: 'none' }) },
onFollow() { this.followed = toggleActivityFollow(this.act.id); uni.showToast({ title: this.followed ? '已关注活动' : '已取消关注', icon: 'none' }) },
onJoin() {
if (this.act.status === 'ended') { uni.showToast({ title: '活动已结束', icon: 'none' }); return }
this.joined = toggleActivityJoin(this.act.id)
uni.showToast({ title: this.joined ? '报名成功' : '已取消报名', icon: 'none' })
}
}
}
</script>
<style scoped>
page { background: #f5f6f7; }
.ad-page { min-height: 100vh; background: #f5f6f7; padding-bottom: 140rpx; }
.fc-nav { position: sticky; top: 0; z-index: 100; background: #fff; padding: 0 24rpx; }
.fc-nav-bar { position: relative; display: flex; align-items: center; justify-content: center; height: 88rpx; }
.fc-back { position: absolute; left: 0; top: 0; height: 88rpx; display: flex; align-items: center; }
.fc-nav-title { font-size: 34rpx; font-weight: 700; color: #1a1a1a; }
.ad-star { position: absolute; right: 0; top: 0; height: 88rpx; display: flex; align-items: center; }
.ad-body { padding: 20rpx; }
.ad-banner { position: relative; border-radius: 20rpx; overflow: hidden; }
.ad-banner-img { width: 100%; height: 360rpx; display: block; }
.ad-badge { position: absolute; left: 20rpx; top: 20rpx; font-size: 22rpx; padding: 4rpx 16rpx; border-radius: 6rpx; }
.ad-summary { background: #fff; border-radius: 20rpx; padding: 26rpx; margin-top: 16rpx; }
.ad-title { font-size: 38rpx; font-weight: 800; color: #1a1a1a; }
.ad-sum-row { display: flex; align-items: center; justify-content: space-between; margin-top: 16rpx; }
.ad-dist { display: flex; align-items: center; font-size: 24rpx; color: #6b7078; }
.ad-dist text { margin-left: 8rpx; }
.ad-call { display: flex; align-items: center; font-size: 24rpx; color: #e9272d; }
.ad-call text { margin-left: 6rpx; }
.ad-addr { display: block; margin-top: 14rpx; font-size: 24rpx; color: #9098a0; }
.ad-card { background: #fff; border-radius: 20rpx; padding: 26rpx; margin-top: 16rpx; }
.ad-card-tt { display: block; font-size: 30rpx; font-weight: 700; color: #1a1a1a; margin-bottom: 18rpx; }
.ad-info-row { display: flex; align-items: center; padding: 14rpx 0; }
.ad-lb { width: 130rpx; margin-left: 14rpx; font-size: 26rpx; color: #9098a0; }
.ad-vl { flex: 1; font-size: 26rpx; color: #333; }
.ad-content { display: block; font-size: 28rpx; color: #333; line-height: 1.8; }
.ad-points { margin-top: 12rpx; }
.ad-point { display: flex; margin-top: 12rpx; }
.ad-dot { color: #e9272d; margin-right: 12rpx; font-size: 28rpx; line-height: 1.6; }
.ad-pt { flex: 1; font-size: 26rpx; color: #555; line-height: 1.6; }
.ad-foot { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; display: flex; align-items: center; padding: 14rpx 24rpx calc(14rpx + env(safe-area-inset-bottom)); box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.04); }
.ad-foot-fee { flex: 1; display: flex; flex-direction: column; }
.ad-foot-lb { font-size: 20rpx; color: #9098a0; }
.ad-foot-vl { font-size: 34rpx; color: #e9272d; font-weight: 700; }
.ad-join { flex-shrink: 0; background: linear-gradient(90deg, #ff5b5b 0%, #ef111b 100%); color: #fff; font-size: 30rpx; font-weight: 700; padding: 20rpx 70rpx; border-radius: 44rpx; }
.ad-join.done { background: #fff0ee; color: #ef111b; }
.ad-join.ended { background: #eef0f3; color: #9aa0a8; }
.fc-empty { text-align: center; color: #9aa0a8; font-size: 26rpx; padding: 120rpx 0; }
.fc-spacer { height: 20rpx; }
</style>

View File

@ -0,0 +1,99 @@
<!-- 未来社区 - 社区活动列表 -->
<template>
<view class="al-page">
<view class="fc-nav" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="fc-nav-bar">
<view class="fc-back" @tap="goBack"><u-icon name="arrow-left" color="#1a1a1a" size="38"></u-icon></view>
<text class="fc-nav-title">社区活动</text>
</view>
<view class="al-search">
<u-icon name="search" color="#9098a0" size="28"></u-icon>
<input class="al-search-input" v-model="keyword" placeholder="搜索活动名称、地点" placeholder-class="al-ph" />
</view>
<scroll-view class="al-tabs" scroll-x>
<text class="al-tab" :class="{ on: activeTab === t.key }" v-for="t in ACTIVITY_TABS" :key="t.key" @tap="activeTab = t.key">{{ t.label }}</text>
</scroll-view>
</view>
<view class="al-body">
<view class="al-card" v-for="a in filtered" :key="a.id" @tap="goDetail(a)">
<view class="al-cover">
<image v-if="a.image" :src="a.image" class="al-img" mode="aspectFill" />
<view v-else class="al-img" :style="{ background: sceneBg(a.scene) }"></view>
<text class="al-badge" :style="statusStyle(a.status)">{{ statusLabel(a.status) }}</text>
</view>
<view class="al-info">
<text class="al-title">{{ a.title }}</text>
<view class="al-row"><u-icon name="map" color="#9098a0" size="22"></u-icon><text>{{ a.place }}</text></view>
<view class="al-row"><u-icon name="clock" color="#9098a0" size="22"></u-icon><text>{{ a.time }}</text></view>
<view class="al-row"><u-icon name="account" color="#9098a0" size="22"></u-icon><text>{{ a.enrolled || a.quota }}</text></view>
<view class="al-foot">
<view class="al-row"><u-icon name="map" color="#c2c7ce" size="20"></u-icon><text class="al-dist">{{ a.distance || '社区内' }}</text></view>
<text class="al-fee">{{ a.fee || '免费' }}</text>
</view>
</view>
</view>
<view class="fc-empty" v-if="!filtered.length">暂无相关活动</view>
</view>
</view>
</template>
<script>
import { NavgateTo } from '@/utils'
import { activities, ACTIVITY_TABS, ACTIVITY_STATUS_META, ACTIVITY_SCENE_BG } from '../data.js'
export default {
data() { return { statusBarTop: 20, ACTIVITY_TABS, activeTab: 'all', keyword: '', activities } },
onLoad(options) {
const sys = uni.getSystemInfoSync()
this.statusBarTop = sys.statusBarHeight || 20
if (options && options.tab && ACTIVITY_TABS.some(t => t.key === options.tab)) this.activeTab = options.tab
},
computed: {
filtered() {
const kw = this.keyword.trim()
return this.activities.filter(a => {
const okTab = this.activeTab === 'all' || a.status === this.activeTab
const okKw = !kw || (a.title + a.subtitle + a.place).indexOf(kw) >= 0
return okTab && okKw
})
}
},
methods: {
statusLabel(s) { return (ACTIVITY_STATUS_META[s] || {}).label || '' },
statusStyle(s) { const m = ACTIVITY_STATUS_META[s] || {}; return { color: m.color, background: m.bg } },
sceneBg(s) { return ACTIVITY_SCENE_BG[s] || ACTIVITY_SCENE_BG.handicraft },
goBack() { uni.navigateBack({ delta: 1 }) },
goDetail(a) { NavgateTo('/packages/futureCommunity/activityDetail/index?id=' + a.id, { isLogin: false }) }
}
}
</script>
<style scoped>
page { background: #f5f6f7; }
.al-page { min-height: 100vh; background: #f5f6f7; }
.fc-nav { position: sticky; top: 0; z-index: 100; background: #fff; padding: 0 24rpx 12rpx; }
.fc-nav-bar { position: relative; display: flex; align-items: center; justify-content: center; height: 88rpx; }
.fc-back { position: absolute; left: 0; top: 0; height: 88rpx; display: flex; align-items: center; }
.fc-nav-title { font-size: 34rpx; font-weight: 700; color: #1a1a1a; }
.al-search { display: flex; align-items: center; height: 68rpx; background: #f4f5f7; border-radius: 34rpx; padding: 0 24rpx; }
.al-search-input { flex: 1; margin-left: 12rpx; font-size: 26rpx; color: #333; }
.al-ph { color: #9098a0; }
.al-tabs { white-space: nowrap; padding: 14rpx 0 4rpx; }
.al-tab { display: inline-block; font-size: 28rpx; color: #6b7078; padding: 8rpx 6rpx; margin-right: 40rpx; }
.al-tab.on { color: #e9272d; font-weight: 700; }
.al-body { padding: 20rpx; }
.al-card { background: #fff; border-radius: 22rpx; overflow: hidden; margin-bottom: 20rpx; }
.al-cover { position: relative; width: 100%; height: 300rpx; }
.al-img { width: 100%; height: 300rpx; display: block; }
.al-badge { position: absolute; left: 16rpx; top: 16rpx; font-size: 20rpx; padding: 4rpx 14rpx; border-radius: 6rpx; }
.al-info { padding: 22rpx 24rpx; display: flex; flex-direction: column; }
.al-title { font-size: 32rpx; font-weight: 700; color: #1a1a1a; }
.al-row { display: flex; align-items: center; margin-top: 12rpx; font-size: 24rpx; color: #6b7078; }
.al-row text { margin-left: 8rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.al-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 16rpx; }
.al-dist { color: #9098a0; }
.al-fee { font-size: 28rpx; color: #e9272d; font-weight: 700; }
.fc-empty { text-align: center; color: #9aa0a8; font-size: 26rpx; padding: 120rpx 0; }
</style>

View File

@ -0,0 +1,66 @@
<!-- 未来社区 - 便民电话 -->
<template>
<view class="cp-page">
<view class="fc-nav" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="fc-nav-bar">
<view class="fc-back" @tap="goBack"><u-icon name="arrow-left" color="#1a1a1a" size="38"></u-icon></view>
<text class="fc-nav-title">便民电话</text>
</view>
</view>
<view class="cp-body">
<view class="cp-tip"><u-icon name="info-circle" color="#f53d42" size="28"></u-icon><text>{{ phoneTip }}</text></view>
<view class="cp-card" v-for="item in phoneList" :key="item.id">
<view class="cp-icon" :style="{ background: item.iconBg }"><u-icon :name="item.icon" color="#fff" size="44"></u-icon></view>
<view class="cp-main">
<text class="cp-name">{{ item.name }}</text>
<text class="cp-phone">{{ item.phone }}</text>
<text class="cp-time">{{ item.time }}</text>
</view>
<view class="cp-dial" @tap="onDial(item)"><u-icon name="phone" color="#f53d42" size="40"></u-icon></view>
</view>
<view class="fc-spacer"></view>
</view>
</view>
</template>
<script>
import { phoneTip, phoneList } from '../data.js'
export default {
data() { return { statusBarTop: 20, phoneTip, phoneList } },
onLoad() {
const sys = uni.getSystemInfoSync()
this.statusBarTop = sys.statusBarHeight || 20
},
methods: {
goBack() { uni.navigateBack({ delta: 1 }) },
onDial(item) {
const tel = item.phone.replace(/\s/g, '')
uni.makePhoneCall({ phoneNumber: tel, fail: () => uni.showToast({ title: '拨打 ' + item.name + '' + item.phone, icon: 'none' }) })
}
}
}
</script>
<style scoped>
page { background: #f5f6f7; }
.cp-page { min-height: 100vh; background: #f5f6f7; }
.fc-nav { position: sticky; top: 0; z-index: 100; background: #fff; padding: 0 24rpx 16rpx; }
.fc-nav-bar { position: relative; display: flex; align-items: center; justify-content: center; height: 88rpx; }
.fc-back { position: absolute; left: 0; top: 0; height: 88rpx; display: flex; align-items: center; }
.fc-nav-title { font-size: 34rpx; font-weight: 700; color: #1a1a1a; }
.cp-body { padding: 20rpx; }
.cp-tip { display: flex; align-items: center; background: #fff2ee; border-radius: 25rpx; padding: 22rpx 28rpx; }
.cp-tip text { margin-left: 12rpx; font-size: 24rpx; color: #676c74; }
.cp-card { display: flex; align-items: center; background: #fff; border-radius: 20rpx; border: 1rpx solid #eeeef0; box-shadow: 0 5rpx 16rpx rgba(0,0,0,0.055); padding: 28rpx; margin-top: 20rpx; }
.cp-icon { flex-shrink: 0; width: 96rpx; height: 96rpx; border-radius: 22rpx; display: flex; align-items: center; justify-content: center; margin-right: 20rpx; }
.cp-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.cp-name { font-size: 30rpx; font-weight: 700; color: #111; }
.cp-phone { font-size: 26rpx; color: #666b72; margin-top: 8rpx; }
.cp-time { font-size: 22rpx; color: #9098a0; margin-top: 8rpx; }
.cp-dial { flex-shrink: 0; width: 84rpx; height: 84rpx; border-radius: 999rpx; background: #fff0f0; display: flex; align-items: center; justify-content: center; }
.fc-spacer { height: 40rpx; }
</style>

View File

@ -0,0 +1,194 @@
/**
* 未来社区九九社区静态内容数据 1:1 还原设计稿 mock
* 来源liuyuanxitong-yidong/src/config/community-data.jscommunity-content-data.jsconvenience-phone-data.js
* 全部为固定数据关注/报名等可变状态用本地存储维护
* 图标名为 uview u-icon 名称已由 Vant 名映射而来
*/
/** 顶部定位 */
export const locationInfo = { community: '上河坊·九九社区' }
export const searchPlaceholder = '搜索公告、活动、便民信息'
/** 5 个快捷入口(彩色渐变圆形图标) */
export const shortcuts = [
{ key: 'notice', label: '社区公告', icon: 'volume', bg: 'linear-gradient(135deg, #FF9A12 0%, #FF7900 100%)' },
{ key: 'activity', label: '社区活动', icon: 'calendar', bg: 'linear-gradient(135deg, #FF4752 0%, #EF2231 100%)' },
{ key: 'phone', label: '便民电话', icon: 'phone', bg: 'linear-gradient(135deg, #21C86A 0%, #08A84E 100%)' },
{ key: 'house', label: '我的房屋', icon: 'home', bg: 'linear-gradient(135deg, #3C82FF 0%, #2164F0 100%)' },
{ key: 'goodshop', label: '周边好店', icon: 'shopping-cart', bg: 'linear-gradient(135deg, #B04AFF 0%, #913EE7 100%)' }
]
/** 顶部轮播(用渐变+文案,不依赖外链图片) */
export const heroSlides = [
{ key: 'community-life', title: '九九社区美好生活', subtitle: '邻里相伴,生活更暖心', bg: 'linear-gradient(120deg, #ff4752 0%, #ff8a3d 100%)' },
{ key: 'community-activity', title: '邻里活动精彩不断', subtitle: '一起参与,共享社区好时光', bg: 'linear-gradient(120deg, #ff6a3d 0%, #ffb14a 100%)' },
{ key: 'community-service', title: '便民服务就在身边', subtitle: '公告、报修、生活服务一站直达', bg: 'linear-gradient(120deg, #2164f0 0%, #4aa8ff 100%)' }
]
/** 公告分类标签样式 */
export const NOTICE_TAG_META = {
announcement: { label: '公告', color: '#ffffff', bg: '#df3c3f' },
urgent: { label: '紧急', color: '#ffffff', bg: '#e9272d' },
notice: { label: '通知', color: '#e46d25', bg: '#ffe3cb' },
service: { label: '服务', color: '#1f8a4c', bg: '#dcf5e5' },
activity: { label: '活动', color: '#2164f0', bg: '#e2ecff' }
}
export const NOTICE_TABS = [
{ key: 'all', label: '全部' },
{ key: 'urgent', label: '紧急' },
{ key: 'notice', label: '通知' },
{ key: 'service', label: '服务' }
]
/** 活动状态样式 */
export const ACTIVITY_STATUS_META = {
upcoming: { label: '即将开始', color: '#e46d25', bg: '#ffe9d6' },
ongoing: { label: '进行中', color: '#ffffff', bg: '#e9272d' },
ended: { label: '已结束', color: '#9aa0a8', bg: '#eef0f3' }
}
export const ACTIVITY_TABS = [
{ key: 'all', label: '全部' },
{ key: 'upcoming', label: '即将开始' },
{ key: 'ongoing', label: '进行中' },
{ key: 'ended', label: '已结束' }
]
export const ACTIVITY_REGISTER_NOTICE = [
'请准时到场,活动开始后 15 分钟未到视为放弃名额',
'如需取消报名,请提前 1 天在「邻里关注」中操作',
'活动现场请听从工作人员安排,注意自身与财物安全',
'亲子类活动需家长全程陪同,确保儿童安全'
]
/** 活动场景 → 兜底渐变(无封面图时用) */
export const ACTIVITY_SCENE_BG = {
handicraft: 'linear-gradient(135deg, #ffd9c0 0%, #ffb088 100%)',
market: 'linear-gradient(135deg, #ffe6a3 0%, #ffc24d 100%)',
garden: 'linear-gradient(135deg, #c8efb0 0%, #7cc36a 100%)',
fitness: 'linear-gradient(135deg, #bfe3ff 0%, #6db4f0 100%)',
festival: 'linear-gradient(135deg, #ffc1bb 0%, #f37c6e 100%)'
}
/** 社区公告 */
export const notices = [
{
id: 'n1', icon: 'volume', tag: 'announcement', title: '关于五一劳动节期间服务安排的通知',
summary: '五一期间物业服务中心值班安排及服务时间调整…', date: '2025-04-28', source: '物业服务中心',
readCount: 1286, pinned: true,
content: '<p>尊敬的业主:</p><p>五一劳动节期间,物业服务中心将安排工作人员轮值。前台服务时间调整为每日 9:00-17:0024 小时报修及应急电话保持畅通。</p><p>如需办理装修登记、门禁卡等业务,建议提前预约。祝各位业主节日愉快。</p>'
},
{
id: 'n2', icon: 'bell', tag: 'notice', title: '电梯例行维护保养通知',
summary: '本周将对小区部分楼栋电梯进行例行维护保养…', date: '2025-04-27', source: '物业服务中心',
readCount: 942, pinned: false,
content: '<p>尊敬的业主:</p><p>本周将对小区部分楼栋电梯进行例行维护保养。维护期间电梯将分时段暂停运行,请根据现场提示错峰出行。</p><p>给您带来的不便敬请谅解,感谢您的理解与配合。</p>'
},
{
id: 'n3', icon: 'account', tag: 'notice', title: '物业服务中心工作时间调整',
summary: '自5月1日起物业服务中心工作时间调整如下…', date: '2025-04-25', source: '社区服务站',
readCount: 658, pinned: false,
content: '<p>自 5 月 1 日起,物业服务中心工作时间调整为周一至周日 8:30-18:00午间照常提供咨询服务。</p><p>非工作时间如遇紧急情况,请拨打物业 24 小时值班电话。</p>'
},
{
id: 'n4', icon: 'account', tag: 'activity', title: '邻里议事会:小区充电桩加装意见征集',
summary: '诚邀业主代表参与议事会,就充电桩选址与方案建言。', date: '2026-06-12', source: '社区居委会',
readCount: 473, pinned: false,
content: '<p>为缓解小区新能源车充电难题,社区拟在地面停车区加装公共充电桩。现面向全体业主征集选址与方案意见,欢迎参加本月邻里议事会,共同决定社区事务。</p>'
},
{
id: 'n5', icon: 'gift', tag: 'notice', title: '端午粽香·社区敬老慰问活动回顾',
summary: '社区联合志愿者为高龄独居老人送上粽子与节日问候。', date: '2026-06-09', source: '社区志愿队',
readCount: 805, pinned: false,
content: '<p>端午节前夕,社区志愿者走访慰问了 30 余位高龄独居老人,送上亲手包的粽子与节日祝福。邻里互助、守望相助,是九九社区一直倡导的温暖底色。</p>'
}
]
/** 社区活动image 为本地封面,空串则用 sceneBg 渐变兜底) */
export const activities = [
{
id: 'a6', scene: 'festival', status: 'ended', title: '端午竹编小龙舟手工活动', subtitle: '粽香艾草间,一起把龙舟装进端午记忆',
image: 'https://static.hshuishang.com/static/act-dragonboat.jpg', time: '2026-06-18 14:30 至 16:00',
place: '五常街道海汇社区党群服务中心', address: '五常街道海汇社区党群服务中心一楼活动室', distance: '4.08km',
quota: '限 25 人', enrolled: '25/25人', signupRange: '2026-06-16 10:00 至 2026-06-18 14:00', deadline: '2026-06-18 14:00',
fee: '免费', phone: '0571-8888 0618', organizer: '海汇社区联合九九社区',
points: ['认识端午节、龙舟与竹编手作的传统文化', '在老师指导下完成竹编龙舟骨架与端午挂饰', '活动材料由社区统一提供,成品可带回家留念'],
content: '<p>五月五,过端午。粽子香、艾草芳,龙舟划进手掌上。</p><p>本次活动将传统端午文化与亲子手作结合,老师会从竹编基础、龙舟结构和端午纹样讲起,带领居民一步步完成属于自己的竹编小龙舟。</p><p>活动现场提供竹篾、彩绳、粽子挂饰等材料,完成作品后还可参与社区端午作品展示与合影。</p>'
},
{
id: 'a1', scene: 'handicraft', status: 'upcoming', title: '亲子手工课堂', subtitle: '一起动手,创意无限',
image: 'https://static.hshuishang.com/static/act-handicraft.jpg', time: '2025-05-10 10:00',
place: '橘园社区活动室', address: '橘园社区党群服务中心二楼手工教室', distance: '320m',
quota: '限 20 人', enrolled: '12/20人', signupRange: '2025-05-01 09:00 至 2025-05-09 18:00', deadline: '2025-05-09 18:00',
fee: '免费', phone: '021-5666 0510', organizer: '橘园社区',
points: ['亲子协作完成手工制作,增进亲子感情', '培养孩子动手能力与创造力,激发想象力', '作品展示与分享,收获成就感与快乐'],
content: '<p>本周末邀请专业老师带领小朋友们体验黏土手工,材料由社区免费提供。完成的作品可带回家,也可参与社区作品展。</p><p>适合 3-10 岁儿童及家长共同参与,名额有限,报满即止。</p>'
},
{
id: 'a2', scene: 'market', status: 'upcoming', title: '邻里集市 · 乐享生活', subtitle: '好物共享,邻里更亲',
image: 'https://static.hshuishang.com/static/act-market.jpg', time: '2025-05-17 09:00',
place: '小区中心广场', address: '上河坊小区中心广场东侧', distance: '560m',
quota: '限 50 人', enrolled: '31/50人', signupRange: '2025-05-08 09:00 至 2025-05-16 20:00', deadline: '2025-05-16 20:00',
fee: '免费', phone: '021-5666 0517', organizer: '九九社区邻里中心',
points: ['闲置好物以物换物或低价转让', '现场分区:儿童玩具 / 二手书 / 生活用品', '报名后可领取专属摊位号'],
content: '<p>把家里的闲置好物带到市集,与邻居以物换物或低价转让。现场设有儿童玩具区、二手书区、生活用品区,欢迎全家一起来逛。</p><p>报名后可领取专属摊位号。</p>'
},
{
id: 'a3', scene: 'garden', status: 'ongoing', title: '社区花园认养计划', subtitle: '一平米花园,邻里共养',
image: '', time: '2026-06-01 至 2026-08-31', place: '小区中央花园', address: '', distance: '',
quota: '剩余 8 个地块', enrolled: '16/24人', signupRange: '', deadline: '认养至 2026-08-31',
fee: '免费', phone: '', organizer: '',
points: ['认养小块菜地,自由种植花草蔬果', '社区统一提供工具与水源', '共建绿意社区,结识园艺邻居'],
content: '<p>社区中央花园开放小块菜地认养,认养后可自行种植花草蔬果,社区提供工具与水源。让我们一起把社区装扮得更绿意盎然。</p>'
},
{
id: 'a4', scene: 'fitness', status: 'ongoing', title: '清晨广场健身操', subtitle: '动起来,遇见更好的邻居',
image: '', time: '每日 07:00 · 长期', place: '中心广场', address: '', distance: '',
quota: '无需报名亦可参加', enrolled: '不限人数', signupRange: '', deadline: '长期开放',
fee: '免费', phone: '', organizer: '',
points: ['专业志愿者带操,节奏舒缓适合各年龄', '报名后加入晨练群,接收每日提醒', '坚持打卡有机会获得社区健康好礼'],
content: '<p>社区健身志愿者每天清晨带操,舒缓的有氧节奏适合各年龄段邻居。报名后可加入晨练群,接收每日提醒。</p>'
},
{
id: 'a5', scene: 'festival', status: 'ended', title: '端午包粽子邻里大赛', subtitle: '巧手包粽,温情端午',
image: '', time: '2026-06-08 周日 10:00', place: '社区活动中心', address: '', distance: '',
quota: '活动已结束', enrolled: '40/40人', signupRange: '', deadline: '报名已截止',
fee: '免费', phone: '', organizer: '',
points: ['邻里比拼包粽手艺,现场欢声笑语', '本次共 40 组家庭参与', '感谢参与,下次活动再见'],
content: '<p>端午佳节,邻里齐聚一堂比拼包粽手艺,现场欢声笑语不断。本次活动共有 40 组家庭参与,感谢大家的热情参与,我们下次活动再见。</p>'
}
]
/** 便民电话 */
export const phoneTip = '社区常用便民电话,点击即可一键拨打'
export const phoneList = [
{ id: 'P01', name: '物业服务中心', phone: '021-8266 1234', time: '服务时间 08:00-20:00', icon: 'server-man', iconBg: '#f53d42' },
{ id: 'P02', name: '社区卫生服务站', phone: '021-8266 5678', time: '服务时间 08:30-17:30', icon: 'plus', iconBg: '#24bd6c' },
{ id: 'P03', name: '社区警务室', phone: '021-8266 0110', time: '24 小时值班', icon: 'account', iconBg: '#397eef' },
{ id: 'P04', name: '水电报修热线', phone: '021-8266 9000', time: '服务时间 07:00-22:00', icon: 'home', iconBg: '#ff8a24' },
{ id: 'P05', name: '社区便民服务台', phone: '021-8266 2233', time: '服务时间 09:00-18:00', icon: 'server-man', iconBg: '#ff4164' }
]
// ===== 查找辅助 =====
export function findNotice(id) { return notices.find(n => n.id === id) }
export function findActivity(id) { return activities.find(a => a.id === id) }
// ===== 关注 / 报名状态(本地存储,初始种子与设计稿一致)=====
const K_FN = 'fc_followed_notices'
const K_JA = 'fc_joined_acts'
const K_FA = 'fc_followed_acts'
function getArr(key, seed) {
const v = uni.getStorageSync(key)
if (v === '' || v === undefined || v === null) { uni.setStorageSync(key, seed); return seed.slice() }
return Array.isArray(v) ? v : seed.slice()
}
function toggle(key, seed, id) {
const arr = getArr(key, seed)
const i = arr.indexOf(id)
if (i >= 0) arr.splice(i, 1); else arr.push(id)
uni.setStorageSync(key, arr)
return i < 0
}
export function isNoticeFollowed(id) { return getArr(K_FN, ['n1']).includes(id) }
export function toggleNoticeFollow(id) { return toggle(K_FN, ['n1'], id) }
export function isActivityJoined(id) { return getArr(K_JA, ['a3', 'a6']).includes(id) }
export function toggleActivityJoin(id) { return toggle(K_JA, ['a3', 'a6'], id) }
export function isActivityFollowed(id) { return getArr(K_FA, ['a1']).includes(id) }
export function toggleActivityFollow(id) { return toggle(K_FA, ['a1'], id) }

View File

@ -0,0 +1,218 @@
<!--
页面未来社区九九社区主页 还原设计稿 liuyuanxitong-yidong/pages/community/community.vue
内容为静态 mock ../data.js挂到首页金刚区未来社区跳转目标
-->
<template>
<view class="fc-page">
<!-- 头部 -->
<view class="fc-hd" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="fc-hd-bar">
<view class="fc-back" @tap="goBack"><u-icon name="arrow-left" color="#1a1a1a" size="38"></u-icon></view>
<text class="fc-hd-title">九九社区</text>
</view>
<view class="fc-loc" @tap="goSelect">
<u-icon name="map" color="#e9272d" size="26"></u-icon>
<text class="fc-loc-txt">{{ locationInfo.community }}</text>
<u-icon name="arrow-down" color="#9098a0" size="20"></u-icon>
</view>
</view>
<view class="fc-body">
<!-- 轮播 -->
<swiper class="fc-hero" :autoplay="true" :interval="4000" :duration="500" circular indicator-dots
indicator-active-color="#fff" indicator-color="rgba(255,255,255,0.5)">
<swiper-item v-for="s in heroSlides" :key="s.key">
<view class="fc-hero-slide" :style="{ background: s.bg }">
<view class="fc-hero-mask"></view>
<view class="fc-hero-copy">
<text class="fc-hero-title">{{ s.title }}</text>
<text class="fc-hero-sub">{{ s.subtitle }}</text>
</view>
</view>
</swiper-item>
</swiper>
<!-- 快捷入口 -->
<view class="fc-shortcut">
<view class="fc-sc-item" v-for="s in shortcuts" :key="s.key" @tap="onShortcut(s)">
<view class="fc-sc-icon" :style="{ background: s.bg }">
<u-icon :name="s.icon" color="#fff" size="40"></u-icon>
</view>
<text class="fc-sc-label">{{ s.label }}</text>
</view>
</view>
<!-- 社区公告 -->
<view class="fc-sec-head">
<view class="fc-sec-left"><text class="fc-sec-bar"></text><text class="fc-sec-title">社区公告</text></view>
<text class="fc-sec-more" @tap="goNoticeList">查看更多 </text>
</view>
<view class="fc-notice-panel">
<view class="fc-notice" v-for="n in topNotices" :key="n.id" @tap="goNoticeDetail(n)">
<view class="fc-notice-icon"><u-icon :name="n.icon" color="#e43f43" size="34"></u-icon></view>
<view class="fc-notice-main">
<text class="fc-notice-title">{{ n.title }}</text>
<text class="fc-notice-sum">{{ n.summary }}</text>
</view>
<view class="fc-notice-aside">
<text class="fc-tag" :style="tagStyle(n.tag)">{{ tagLabel(n.tag) }}</text>
<text class="fc-notice-date">{{ n.date }}</text>
</view>
</view>
</view>
<!-- 社区活动 -->
<view class="fc-sec-head">
<view class="fc-sec-left"><text class="fc-sec-bar"></text><text class="fc-sec-title">社区活动</text></view>
<text class="fc-sec-more" @tap="goActivityList">查看更多 </text>
</view>
<view class="fc-act-list">
<view class="fc-act" v-for="a in topActivities" :key="a.id" @tap="goActivityDetail(a)">
<view class="fc-act-cover">
<image v-if="a.image" :src="a.image" class="fc-act-img" mode="aspectFill" />
<view v-else class="fc-act-img" :style="{ background: sceneBg(a.scene) }"></view>
<text class="fc-act-badge" :style="statusStyle(a.status)">{{ statusLabel(a.status) }}</text>
</view>
<view class="fc-act-info">
<text class="fc-act-title">{{ a.title }}</text>
<text class="fc-act-fee">{{ a.fee || '免费' }}</text>
<text class="fc-act-sub">{{ a.subtitle }}</text>
<view class="fc-act-row"><u-icon name="clock" color="#9098a0" size="22"></u-icon><text>{{ a.time }}</text></view>
<view class="fc-act-row"><u-icon name="map" color="#9098a0" size="22"></u-icon><text>{{ a.place }}</text></view>
<view class="fc-act-foot">
<view class="fc-act-row"><u-icon name="account" color="#9098a0" size="22"></u-icon><text>{{ a.enrolled }}</text></view>
<text class="fc-act-btn" :class="{ done: isJoined(a.id) }" @tap.stop="onJoin(a)">{{ isJoined(a.id) ? '已报名' : '立即报名' }}</text>
</view>
</view>
</view>
</view>
<view class="fc-spacer"></view>
</view>
</view>
</template>
<script>
import { NavgateTo } from '@/utils'
import {
locationInfo, shortcuts, heroSlides, notices, activities,
NOTICE_TAG_META, ACTIVITY_STATUS_META, ACTIVITY_SCENE_BG,
isActivityJoined, toggleActivityJoin
} from '../data.js'
export default {
data() {
return {
statusBarTop: 20,
locationInfo, shortcuts, heroSlides,
topNotices: notices.slice(0, 3),
topActivities: activities.filter(a => a.status !== 'ended').slice(0, 2),
joinedIds: []
}
},
onLoad() {
const sys = uni.getSystemInfoSync()
this.statusBarTop = sys.statusBarHeight || 20
},
onShow() {
this.joinedIds = activities.filter(a => isActivityJoined(a.id)).map(a => a.id)
},
methods: {
tagLabel(t) { return (NOTICE_TAG_META[t] || {}).label || '' },
tagStyle(t) { const m = NOTICE_TAG_META[t] || {}; return { color: m.color, background: m.bg } },
statusLabel(s) { return (ACTIVITY_STATUS_META[s] || {}).label || '' },
statusStyle(s) { const m = ACTIVITY_STATUS_META[s] || {}; return { color: m.color, background: m.bg } },
sceneBg(s) { return ACTIVITY_SCENE_BG[s] || ACTIVITY_SCENE_BG.handicraft },
isJoined(id) { return this.joinedIds.includes(id) },
goBack() { uni.navigateBack({ delta: 1, fail: () => NavgateTo('/pages/index2/index2') }) },
goSelect() { uni.showToast({ title: '当前社区:' + this.locationInfo.community, icon: 'none' }) },
onShortcut(s) {
const map = {
notice: '/packages/futureCommunity/noticeList/index',
activity: '/packages/futureCommunity/activityList/index',
phone: '/packages/futureCommunity/conveniencePhone/index',
house: '/packages/community/index/index',
goodshop: '/packages/localLife/index/index'
}
if (map[s.key]) NavgateTo(map[s.key], { isLogin: false })
else uni.showToast({ title: s.label + '功能开发中', icon: 'none' })
},
goNoticeList() { NavgateTo('/packages/futureCommunity/noticeList/index', { isLogin: false }) },
goActivityList() { NavgateTo('/packages/futureCommunity/activityList/index', { isLogin: false }) },
goNoticeDetail(n) { NavgateTo('/packages/futureCommunity/noticeDetail/index?id=' + n.id, { isLogin: false }) },
goActivityDetail(a) { NavgateTo('/packages/futureCommunity/activityDetail/index?id=' + a.id, { isLogin: false }) },
onJoin(a) {
if (a.status === 'ended') { uni.showToast({ title: '活动已结束', icon: 'none' }); return }
const joined = toggleActivityJoin(a.id)
this.joinedIds = activities.filter(x => isActivityJoined(x.id)).map(x => x.id)
uni.showToast({ title: joined ? '报名成功' : '已取消报名', icon: 'none' })
}
}
}
</script>
<style scoped>
page { background: #f5f6f7; }
.fc-page { min-height: 100vh; background: #f5f6f7; }
/* 头部 */
.fc-hd { position: sticky; top: 0; z-index: 100; background: #fff; padding: 0 24rpx 16rpx; }
.fc-hd-bar { position: relative; display: flex; align-items: center; justify-content: center; height: 80rpx; }
.fc-back { position: absolute; left: 0; top: 0; height: 80rpx; display: flex; align-items: center; }
.fc-hd-title { font-size: 34rpx; font-weight: 700; color: #1a1a1a; }
.fc-loc { display: flex; align-items: center; }
.fc-loc-txt { margin: 0 8rpx; font-size: 26rpx; color: #333; }
.fc-body { padding: 20rpx; }
/* 轮播 */
.fc-hero { width: 100%; height: 286rpx; border-radius: 24rpx; overflow: hidden; }
.fc-hero-slide { position: relative; width: 100%; height: 286rpx; }
.fc-hero-mask { position: absolute; inset: 0; background: linear-gradient(90deg, rgba(40,25,20,0.5) 0%, rgba(40,25,20,0.04) 72%); }
.fc-hero-copy { position: absolute; left: 36rpx; bottom: 46rpx; z-index: 2; display: flex; flex-direction: column; }
.fc-hero-title { font-size: 40rpx; font-weight: 800; color: #fff; }
.fc-hero-sub { margin-top: 10rpx; font-size: 24rpx; color: rgba(255,255,255,0.92); }
/* 快捷入口 */
.fc-shortcut { display: flex; justify-content: space-between; background: #fff; border-radius: 22rpx; padding: 28rpx 16rpx; margin-top: 20rpx; }
.fc-sc-item { display: flex; flex-direction: column; align-items: center; }
.fc-sc-icon { width: 92rpx; height: 92rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.fc-sc-label { margin-top: 14rpx; font-size: 24rpx; color: #333; }
/* 分区标题 */
.fc-sec-head { display: flex; align-items: center; justify-content: space-between; margin: 32rpx 4rpx 18rpx; }
.fc-sec-left { display: flex; align-items: center; }
.fc-sec-bar { width: 6rpx; height: 30rpx; background: #e9272d; border-radius: 3rpx; margin-right: 14rpx; }
.fc-sec-title { font-size: 32rpx; font-weight: 700; color: #1a1a1a; }
.fc-sec-more { font-size: 24rpx; color: #9aa0a8; }
/* 公告 */
.fc-notice-panel { background: #fff; border-radius: 22rpx; padding: 6rpx 24rpx; }
.fc-notice { display: flex; align-items: center; padding: 24rpx 0; border-bottom: 1rpx solid #f2f3f5; }
.fc-notice:last-child { border-bottom: none; }
.fc-notice-icon { flex-shrink: 0; width: 64rpx; height: 64rpx; border-radius: 50%; background: #fff1f1; display: flex; align-items: center; justify-content: center; margin-right: 18rpx; }
.fc-notice-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.fc-notice-title { font-size: 28rpx; font-weight: 600; color: #1a1a1a; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.fc-notice-sum { margin-top: 8rpx; font-size: 22rpx; color: #9098a0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.fc-notice-aside { flex-shrink: 0; margin-left: 16rpx; display: flex; flex-direction: column; align-items: flex-end; }
.fc-tag { font-size: 20rpx; padding: 2rpx 12rpx; border-radius: 6rpx; }
.fc-notice-date { margin-top: 10rpx; font-size: 20rpx; color: #b6bcc4; }
/* 活动 */
.fc-act-list { display: flex; flex-direction: column; }
.fc-act { display: flex; background: #fff; border-radius: 22rpx; overflow: hidden; margin-bottom: 20rpx; }
.fc-act-cover { position: relative; width: 260rpx; flex-shrink: 0; }
.fc-act-img { width: 260rpx; height: 100%; min-height: 220rpx; display: block; }
.fc-act-badge { position: absolute; left: 12rpx; top: 12rpx; font-size: 20rpx; padding: 2rpx 12rpx; border-radius: 6rpx; }
.fc-act-info { position: relative; flex: 1; min-width: 0; padding: 18rpx 20rpx; display: flex; flex-direction: column; }
.fc-act-title { font-size: 30rpx; font-weight: 700; color: #1a1a1a; max-width: 280rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.fc-act-fee { position: absolute; right: 20rpx; top: 18rpx; font-size: 24rpx; color: #e9272d; font-weight: 700; }
.fc-act-sub { margin-top: 8rpx; font-size: 22rpx; color: #9098a0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.fc-act-row { display: flex; align-items: center; margin-top: 10rpx; font-size: 22rpx; color: #6b7078; }
.fc-act-row text { margin-left: 8rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.fc-act-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 14rpx; }
.fc-act-btn { flex-shrink: 0; background: linear-gradient(90deg, #ff5b5b 0%, #ef111b 100%); color: #fff; font-size: 22rpx; padding: 8rpx 24rpx; border-radius: 26rpx; }
.fc-act-btn.done { background: #eef0f3; color: #9aa0a8; }
.fc-spacer { height: 40rpx; }
</style>

View File

@ -0,0 +1,78 @@
<!-- 未来社区 - 公告详情 -->
<template>
<view class="nd-page">
<view class="fc-nav" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="fc-nav-bar">
<view class="fc-back" @tap="goBack"><u-icon name="arrow-left" color="#1a1a1a" size="38"></u-icon></view>
<text class="fc-nav-title">公告详情</text>
</view>
</view>
<view class="nd-body" v-if="notice">
<view class="nd-card">
<text class="fc-tag" :style="tagStyle(notice.tag)">{{ tagLabel(notice.tag) }}</text>
<text class="nd-title">{{ notice.title }}</text>
<view class="nd-meta">
<u-icon name="clock" color="#b6bcc4" size="22"></u-icon>
<text>{{ notice.date }}</text>
<text class="nd-meta-dot">·</text>
<text>上河坊·九九社区</text>
</view>
<rich-text class="nd-content" :nodes="notice.content"></rich-text>
</view>
<view class="fc-spacer"></view>
</view>
<view v-else class="fc-empty">公告不存在</view>
<view class="nd-foot" v-if="notice">
<text class="nd-share" @tap="onShare">分享</text>
<text class="nd-follow" :class="{ on: followed }" @tap="onFollow">{{ followed ? '已关注' : '关注公告' }}</text>
</view>
</view>
</template>
<script>
import { findNotice, NOTICE_TAG_META, isNoticeFollowed, toggleNoticeFollow } from '../data.js'
export default {
data() { return { statusBarTop: 20, notice: null, followed: false } },
onLoad(options) {
const sys = uni.getSystemInfoSync()
this.statusBarTop = sys.statusBarHeight || 20
this.notice = findNotice(options && options.id) || null
if (this.notice) this.followed = isNoticeFollowed(this.notice.id)
},
methods: {
tagLabel(t) { return (NOTICE_TAG_META[t] || {}).label || '' },
tagStyle(t) { const m = NOTICE_TAG_META[t] || {}; return { color: m.color, background: m.bg } },
goBack() { uni.navigateBack({ delta: 1 }) },
onShare() { uni.showToast({ title: '分享功能开发中', icon: 'none' }) },
onFollow() { this.followed = toggleNoticeFollow(this.notice.id); uni.showToast({ title: this.followed ? '已关注' : '已取消关注', icon: 'none' }) }
}
}
</script>
<style scoped>
page { background: #f5f6f7; }
.nd-page { min-height: 100vh; background: #f5f6f7; padding-bottom: 130rpx; }
.fc-nav { position: sticky; top: 0; z-index: 100; background: #fff; padding: 0 24rpx 16rpx; }
.fc-nav-bar { position: relative; display: flex; align-items: center; justify-content: center; height: 88rpx; }
.fc-back { position: absolute; left: 0; top: 0; height: 88rpx; display: flex; align-items: center; }
.fc-nav-title { font-size: 34rpx; font-weight: 700; color: #1a1a1a; }
.nd-body { padding: 20rpx; }
.nd-card { background: #fff; border-radius: 20rpx; padding: 30rpx; }
.fc-tag { display: inline-block; font-size: 22rpx; padding: 4rpx 16rpx; border-radius: 6rpx; }
.nd-title { display: block; font-size: 38rpx; font-weight: 800; color: #1a1a1a; margin-top: 18rpx; line-height: 1.4; }
.nd-meta { display: flex; align-items: center; margin-top: 18rpx; font-size: 22rpx; color: #b6bcc4; }
.nd-meta text { margin-left: 6rpx; }
.nd-meta-dot { margin: 0 10rpx; }
.nd-content { display: block; margin-top: 24rpx; font-size: 28rpx; color: #333; line-height: 1.8; }
.nd-foot { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; display: flex; align-items: center; gap: 20rpx; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.04); }
.nd-share { flex: 1; text-align: center; font-size: 30rpx; color: #333; border: 1rpx solid #e5e5e5; border-radius: 44rpx; padding: 18rpx 0; }
.nd-follow { flex: 1; text-align: center; font-size: 30rpx; color: #fff; background: linear-gradient(90deg, #ff5b5b 0%, #ef111b 100%); border-radius: 44rpx; padding: 18rpx 0; }
.nd-follow.on { background: #eef0f3; color: #9aa0a8; }
.fc-empty { text-align: center; color: #9aa0a8; font-size: 26rpx; padding: 120rpx 0; }
.fc-spacer { height: 20rpx; }
</style>

View File

@ -0,0 +1,109 @@
<!-- 未来社区 - 社区公告列表 -->
<template>
<view class="nl-page">
<view class="fc-nav" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="fc-nav-bar">
<view class="fc-back" @tap="goBack"><u-icon name="arrow-left" color="#1a1a1a" size="38"></u-icon></view>
<view class="fc-nav-tt"><text class="fc-nav-title">社区公告</text><text class="fc-nav-sub">上河坊·九九社区</text></view>
</view>
<scroll-view class="nl-tabs" scroll-x>
<text class="nl-tab" :class="{ on: activeTab === t.key }" v-for="t in NOTICE_TABS" :key="t.key" @tap="activeTab = t.key">{{ t.label }}</text>
</scroll-view>
</view>
<view class="nl-body">
<!-- 置顶全部tab -->
<view class="nl-pinned" v-if="activeTab === 'all' && pinned" @tap="goDetail(pinned)">
<view class="nl-pin-corner">置顶</view>
<view class="nl-pin-head">
<view class="nl-icon" :style="{ background: '#fff1f1' }"><u-icon :name="pinned.icon" color="#e43f43" size="34"></u-icon></view>
<view class="nl-pin-tt">
<text class="nl-title">{{ pinned.title }}</text>
<text class="nl-meta">{{ pinned.date }} · {{ pinned.source }}</text>
</view>
</view>
<text class="nl-sum">{{ pinned.summary }}</text>
<text class="nl-read">{{ pinned.readCount }} 次阅读</text>
</view>
<view class="nl-card" v-for="n in listNotices" :key="n.id" @tap="goDetail(n)">
<view class="nl-icon" :style="{ background: tagBg(n.tag, true) }"><u-icon :name="n.icon" :color="tagColor(n.tag)" size="34"></u-icon></view>
<view class="nl-main">
<view class="nl-row1">
<text class="nl-title">{{ n.title }}</text>
<text class="fc-tag" :style="tagStyle(n.tag)">{{ tagLabel(n.tag) }}</text>
</view>
<text class="nl-meta">{{ n.date }} · {{ n.source }}</text>
<text class="nl-sum">{{ n.summary }}</text>
<text class="nl-read">{{ n.readCount }} 次阅读</text>
</view>
</view>
<view class="fc-empty" v-if="!listNotices.length && !(activeTab === 'all' && pinned)">暂无公告</view>
</view>
</view>
</template>
<script>
import { NavgateTo } from '@/utils'
import { notices, NOTICE_TABS, NOTICE_TAG_META } from '../data.js'
export default {
data() {
return { statusBarTop: 20, NOTICE_TABS, activeTab: 'all', notices }
},
onLoad(options) {
const sys = uni.getSystemInfoSync()
this.statusBarTop = sys.statusBarHeight || 20
if (options && options.tab && NOTICE_TABS.some(t => t.key === options.tab)) this.activeTab = options.tab
},
computed: {
pinned() { return this.notices.find(n => n.pinned) },
listNotices() {
if (this.activeTab === 'all') return this.notices.filter(n => !n.pinned)
return this.notices.filter(n => n.tag === this.activeTab)
}
},
methods: {
tagLabel(t) { return (NOTICE_TAG_META[t] || {}).label || '' },
tagColor(t) { return (NOTICE_TAG_META[t] || {}).color || '#666' },
tagBg(t) { return (NOTICE_TAG_META[t] || {}).bg || '#eee' },
tagStyle(t) { const m = NOTICE_TAG_META[t] || {}; return { color: m.color, background: m.bg } },
goBack() { uni.navigateBack({ delta: 1 }) },
goDetail(n) { NavgateTo('/packages/futureCommunity/noticeDetail/index?id=' + n.id, { isLogin: false }) }
}
}
</script>
<style scoped>
page { background: #f5f6f7; }
.nl-page { min-height: 100vh; background: #f5f6f7; }
.fc-nav { position: sticky; top: 0; z-index: 100; background: #fff; padding: 0 24rpx; }
.fc-nav-bar { position: relative; display: flex; align-items: center; justify-content: center; min-height: 88rpx; }
.fc-back { position: absolute; left: 0; top: 0; height: 88rpx; display: flex; align-items: center; }
.fc-nav-tt { display: flex; flex-direction: column; align-items: center; }
.fc-nav-title { font-size: 34rpx; font-weight: 700; color: #1a1a1a; }
.fc-nav-sub { font-size: 20rpx; color: #9098a0; margin-top: 2rpx; }
.nl-tabs { white-space: nowrap; padding: 8rpx 0 14rpx; }
.nl-tab { display: inline-block; font-size: 28rpx; color: #6b7078; padding: 8rpx 6rpx; margin-right: 40rpx; }
.nl-tab.on { color: #e9272d; font-weight: 700; }
.nl-body { padding: 20rpx; }
.fc-tag { font-size: 20rpx; padding: 2rpx 12rpx; border-radius: 6rpx; flex-shrink: 0; }
.nl-icon { flex-shrink: 0; width: 64rpx; height: 64rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
.nl-title { font-size: 28rpx; font-weight: 600; color: #1a1a1a; }
.nl-meta { font-size: 20rpx; color: #b6bcc4; margin-top: 6rpx; }
.nl-sum { font-size: 24rpx; color: #6b7078; margin-top: 10rpx; }
.nl-read { font-size: 20rpx; color: #b6bcc4; margin-top: 10rpx; }
.nl-pinned { position: relative; background: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; box-shadow: 0 6rpx 20rpx rgba(30,34,44,0.06); display: flex; flex-direction: column; }
.nl-pin-corner { position: absolute; right: 0; top: 0; background: #e9272d; color: #fff; font-size: 20rpx; padding: 4rpx 16rpx; border-radius: 0 20rpx 0 16rpx; }
.nl-pin-head { display: flex; align-items: center; }
.nl-pin-tt { margin-left: 18rpx; display: flex; flex-direction: column; }
.nl-card { display: flex; background: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 18rpx; }
.nl-main { flex: 1; min-width: 0; margin-left: 18rpx; display: flex; flex-direction: column; }
.nl-row1 { display: flex; align-items: center; justify-content: space-between; }
.nl-row1 .nl-title { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin-right: 12rpx; }
.fc-empty { text-align: center; color: #9aa0a8; font-size: 26rpx; padding: 100rpx 0; }
</style>

View File

@ -0,0 +1,157 @@
<!--
页面未来到家 - 服务目录金刚区图标点击进入
格式顶部服务标题 + 搜索栏左侧小类竖向导航选中红条高亮
右侧按小类分组的服务网格3封面图 + 名称点左侧定位到对应分组
-->
<template>
<view class="sc-page">
<!-- 头部 -->
<view class="sc-hd" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="sc-nav">
<view class="sc-back" @tap="goBack"><u-icon name="arrow-left" color="#1a1a1a" size="38"></u-icon></view>
<text class="sc-title">服务</text>
</view>
<view class="sc-search">
<text class="sc-city">{{ cityName }}</text>
<text class="sc-city-arrow"></text>
<view class="sc-search-line"></view>
<u-icon name="search" color="#9098a0" size="30"></u-icon>
<input class="sc-search-input" v-model="keyword" placeholder="搜索您需要的服务" placeholder-class="sc-ph" />
</view>
</view>
<!-- 主体左侧分类导航 + 右侧分组商品 -->
<view class="sc-body">
<scroll-view class="sc-side" scroll-y>
<view class="sc-side-item" :class="{ on: activeCate === cat.id }" v-for="cat in categoryList" :key="cat.id"
@tap="selectCate(cat)">
<text>{{ cat.category_name }}</text>
</view>
</scroll-view>
<!-- 右侧直接显示当前选中分类的商品3列网格 -->
<scroll-view class="sc-main" scroll-y :scroll-top="mainScrollTop">
<view class="sc-sec">
<text class="sc-sec-tt">{{ activeCateName }}</text>
<view class="sc-grid">
<view class="sc-item" v-for="s in activeItems" :key="s.id" @tap="toDetail(s)">
<!-- 与详情页轮播一致固定高度铺满格子超出部分居中裁剪(aspectFill) -->
<image class="sc-item-img" :src="s.showImg" mode="aspectFill" />
<text class="sc-item-name">{{ s.service_name }}</text>
</view>
</view>
</view>
<view class="sc-empty" v-if="!activeItems.length">该分类暂无服务</view>
<view class="sc-pad"></view>
</scroll-view>
</view>
</view>
</template>
<script>
import { request, picUrl, NavgateTo } from '@/utils'
import { apiArr } from '@/api/homeService'
export default {
data() {
return {
statusBarTop: 20,
cityName: '定位中',
keyword: '',
categoryList: [],
activeCate: 0,
mainScrollTop: 0,
groups: [] // [{ id, name, items: [service] }]
}
},
onLoad(options) {
const sys = uni.getSystemInfoSync()
this.statusBarTop = sys.statusBarHeight || 20
const loc = uni.getStorageSync('location') || {}
this.cityName = loc.cityName || '定位中'
this.initData(options && options.cate_id ? Number(options.cate_id) : 0)
},
computed: {
activeCateName() {
const c = this.categoryList.find(x => x.id === this.activeCate)
return c ? c.category_name : ''
},
// 3
activeItems() {
const grp = this.groups.find(g => g.id === this.activeCate)
const items = grp ? grp.items : []
const kw = this.keyword.trim()
return kw ? items.filter(s => (s.service_name || '').indexOf(kw) >= 0) : items
}
},
methods: {
async initData(cateId) {
try {
const [cateRes, listRes] = await Promise.all([
request(apiArr.categorySubList, 'POST', {}, {}, false),
//
request(apiArr.serviceList, 'POST', { service_type: 0, category_id: 0, page_num: 1, page_size: 500 }, {}, false)
])
this.categoryList = (cateRes && cateRes.rows) ? cateRes.rows : []
const services = ((listRes && listRes.rows) ? listRes.rows : []).map(s => {
const cover0 = s.cover_pic ? String(s.cover_pic).split(',')[0] : ''
s.showImg = cover0 ? picUrl + cover0 : ''
return s
})
this.groups = this.categoryList.map(c => ({
id: c.id, name: c.category_name,
items: services.filter(s => s.category_id === c.id)
}))
this.activeCate = cateId || (this.categoryList[0] ? this.categoryList[0].id : 0)
} catch (e) {
console.warn('服务目录加载失败:', e)
}
},
selectCate(cat) {
this.activeCate = cat.id
//
this.mainScrollTop = this.mainScrollTop === 0 ? 0.01 : 0
},
toDetail(s) {
NavgateTo('/packages/homeService/detail/index?id=' + s.id)
},
goBack() {
uni.navigateBack({ delta: 1, fail: () => NavgateTo('/packages/homeService/index/index', { isLogin: false }) })
}
}
}
</script>
<style scoped>
page { background: #fff; }
.sc-page { height: 100vh; display: flex; flex-direction: column; background: #fff; }
/* 头部 */
.sc-hd { flex-shrink: 0; background: #fff; padding: 0 24rpx 16rpx; }
.sc-nav { position: relative; display: flex; align-items: center; justify-content: center; height: 88rpx; }
.sc-back { position: absolute; left: 0; top: 0; height: 88rpx; display: flex; align-items: center; }
.sc-title { font-size: 36rpx; font-weight: 700; color: #1a1a1a; }
.sc-search { display: flex; align-items: center; height: 76rpx; background: #f4f5f7; border-radius: 16rpx; padding: 0 24rpx; }
.sc-city { flex-shrink: 0; font-size: 28rpx; color: #111; max-width: 160rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.sc-city-arrow { margin: 0 10rpx 0 6rpx; font-size: 22rpx; color: #666; }
.sc-search-line { width: 2rpx; height: 32rpx; background: #d9d9d9; margin-right: 18rpx; }
.sc-search-input { flex: 1; margin-left: 12rpx; font-size: 28rpx; color: #333; }
.sc-ph { color: #a0a0a0; }
/* 主体双栏 */
.sc-body { flex: 1; min-height: 0; display: flex; }
.sc-side { flex-shrink: 0; width: 200rpx; height: 100%; background: #f5f6f7; }
.sc-side-item { position: relative; padding: 34rpx 16rpx; font-size: 28rpx; color: #333; text-align: center; }
.sc-side-item.on { background: #fff; color: #e9272d; font-weight: 700; }
.sc-side-item.on::before { content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 8rpx; height: 40rpx; background: #e9272d; border-radius: 0 4rpx 4rpx 0; }
.sc-main { flex: 1; min-width: 0; height: 100%; background: #fff; }
.sc-sec { padding: 24rpx 20rpx 6rpx; }
.sc-sec-tt { display: block; font-size: 32rpx; font-weight: 700; color: #1a1a1a; margin-bottom: 20rpx; }
.sc-grid { display: flex; flex-wrap: wrap; }
.sc-item { width: 33.33%; display: flex; flex-direction: column; align-items: center; margin-bottom: 28rpx; padding: 0 8rpx; box-sizing: border-box; }
.sc-item-img { width: 100%; height: 84px; border-radius: 16rpx; background: #f4f5f7; display: block; }
.sc-item-name { margin-top: 12rpx; font-size: 26rpx; color: #333; text-align: center; line-height: 1.35; }
.sc-empty { text-align: center; color: #9aa0a8; font-size: 26rpx; padding: 120rpx 0; }
.sc-pad { height: 40rpx; }
</style>

View File

@ -2,10 +2,10 @@
<view class="hsd-page"> <view class="hsd-page">
<swiper class="hsd-swiper" autoplay circular v-if="carousel.length"> <swiper class="hsd-swiper" autoplay circular v-if="carousel.length">
<swiper-item v-for="(p, i) in carousel" :key="i"> <swiper-item v-for="(p, i) in carousel" :key="i">
<image :src="p" mode="aspectFill" class="hsd-swiper-img" /> <image :src="p" mode="aspectFit" class="hsd-swiper-img" />
</swiper-item> </swiper-item>
</swiper> </swiper>
<image v-else :src="picUrl + (info.cover_pic || '')" class="hsd-swiper-img" mode="aspectFill" /> <image v-else :src="picUrl + (info.cover_pic || '')" class="hsd-swiper-img" mode="aspectFit" />
<view class="hsd-main"> <view class="hsd-main">
<view class="hsd-name">{{ info.service_name }}</view> <view class="hsd-name">{{ info.service_name }}</view>
@ -38,10 +38,10 @@
</view> </view>
</view> </view>
<!-- 详情图 --> <!-- 服务流程图可多张按序展示 -->
<view class="hsd-block" v-if="detailPics.length"> <view class="hsd-block" v-if="flowChartList.length">
<view class="hsd-block-tit">服务详情</view> <view class="hsd-block-tit">服务流程</view>
<image v-for="(p, i) in detailPics" :key="i" :src="p" mode="widthFix" class="hsd-detail-img" /> <image v-for="(p, i) in flowChartList" :key="i" :src="p" mode="widthFix" class="hsd-detail-img" />
</view> </view>
<view class="hsd-bottom"> <view class="hsd-bottom">
@ -70,6 +70,7 @@ export default {
info: {}, info: {},
carousel: [], carousel: [],
detailPics: [], detailPics: [],
flowChartList: [],
skuList: [], skuList: [],
workerList: [], workerList: [],
currentSku: null, currentSku: null,
@ -90,8 +91,12 @@ export default {
loadInfo() { loadInfo() {
request(apiArr.serviceInfo, 'POST', { id: this.id }, {}, false).then(res => { request(apiArr.serviceInfo, 'POST', { id: this.id }, {}, false).then(res => {
this.info = res || {} this.info = res || {}
this.carousel = (res.carousel ? res.carousel.split(',') : []).filter(Boolean).map(p => p.startsWith('http') ? p : this.picUrl + p) const toUrls = (str) => (str ? String(str).split(',') : []).filter(Boolean).map(p => p.startsWith('http') ? p : this.picUrl + p)
this.detailPics = (res.detail_pic ? res.detail_pic.split(',') : []).filter(Boolean).map(p => p.startsWith('http') ? p : this.picUrl + p) // () carousel
const coverArr = toUrls(res.cover_pic)
this.carousel = coverArr.length ? coverArr : toUrls(res.carousel)
this.detailPics = toUrls(res.detail_pic)
this.flowChartList = toUrls(res.flow_chart) //
this.skuList = res.sku_list || [] this.skuList = res.sku_list || []
this.workerList = res.worker_list || [] this.workerList = res.worker_list || []
if (this.skuList.length) this.currentSku = this.skuList[0] if (this.skuList.length) this.currentSku = this.skuList[0]
@ -128,7 +133,8 @@ export default {
<style scoped> <style scoped>
.hsd-page { background: #f5f5f5; min-height: 100vh; padding-bottom: 130rpx; } .hsd-page { background: #f5f5f5; min-height: 100vh; padding-bottom: 130rpx; }
.hsd-swiper, .hsd-swiper-img { width: 100%; height: 520rpx; } /* 轮播按正方形完整显示封面(1:1)aspectFit 不裁剪;非方图留白居中 */
.hsd-swiper, .hsd-swiper-img { width: 100%; height: 750rpx; background: #f2f2f2; }
.hsd-main { background: #fff; padding: 24rpx; } .hsd-main { background: #fff; padding: 24rpx; }
.hsd-name { font-size: 36rpx; font-weight: 600; color: #222; } .hsd-name { font-size: 36rpx; font-weight: 600; color: #222; }
.hsd-pricedesc { font-size: 26rpx; color: #FF370B; margin-top: 14rpx; } .hsd-pricedesc { font-size: 26rpx; color: #FF370B; margin-top: 14rpx; }

View File

@ -1,124 +1,461 @@
<!--
页面未来到家到家服务按设计稿重做
说明顶部红头品牌 + 城市 + 关键词搜索二级分类以金刚区展示
下面商品用瀑布流与首页好店一致两列图片按原始宽高比
大类到家服务 / 家政服务 tab 忽略金刚区改为铺开展示两大类下的全部小类
后端 /service/category/sub-list默认瀑布流取金刚区第一个小类的商品列表
-->
<template> <template>
<view class="hs-page"> <view class="fh-page">
<!-- 到家/家政 Tab --> <!-- 顶部红头 -->
<view class="hs-tabs"> <view class="fh-hd" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="hs-tab" :class="{ active: serviceType === 1 }" @tap="switchType(1)">到家服务</view> <view class="fh-hd-top">
<view class="hs-tab" :class="{ active: serviceType === 2 }" @tap="switchType(2)">家政服务</view> <view class="fh-back" @tap="goBack">
<u-icon name="arrow-left" color="#fff" size="36"></u-icon>
</view>
<view class="fh-brand">
<text class="fh-brand-title">未来到家</text>
<text class="fh-brand-sub">专业服务 · 品质到家</text>
</view>
</view>
<!-- 搜索栏 -->
<view class="fh-search">
<view class="fh-search-city">
<text class="fh-city-name">{{ cityName }}</text>
<text class="fh-city-arrow"></text>
</view>
<input class="fh-search-input" v-model="keyword" placeholder="空调清洗" placeholder-class="fh-search-ph"
confirm-type="search" @confirm="doSearch" />
<view class="fh-search-btn" @tap="doSearch">搜索</view>
</view>
</view> </view>
<view class="hs-body"> <view class="fh-body">
<!-- 左侧分类 --> <!-- 金刚区二级分类 -->
<scroll-view class="hs-cate" scroll-y> <view class="fh-grid" v-if="categoryList.length">
<view class="hs-cate-item" :class="{ active: currentCate === 0 }" @tap="selectCate(0)">全部</view> <view class="fh-grid-item" v-for="cat in categoryList" :key="cat.id" @tap="selectCate(cat)">
<view class="hs-cate-item" :class="{ active: currentCate === item.id }" <image class="fh-grid-icon" :src="picUrl + cat.category_pic" mode="aspectFit" />
v-for="item in categoryList" :key="item.id" @tap="selectCate(item.id)"> <text class="fh-grid-txt" :class="{ on: currentCate === cat.id }">{{ cat.category_name }}</text>
{{ item.category_name }}
</view> </view>
</scroll-view> </view>
<!-- 右侧服务列表 --> <!-- 商品瀑布流与首页好店一致两列图片按原始宽高比 -->
<scroll-view class="hs-list" scroll-y @scrolltolower="loadMore"> <view class="wf">
<view class="hs-card" v-for="item in serviceList" :key="item.id" @tap="toDetail(item)"> <view class="wf-col">
<image class="hs-card-pic" :src="picUrl + item.cover_pic" mode="aspectFill" /> <view class="wf-card" v-for="item in leftCol" :key="item.id" @tap="toDetail(item)">
<view class="hs-card-info"> <image class="wf-img" :src="item.showImg" :style="{ height: item.imgH + 'rpx' }" mode="aspectFill" />
<view class="hs-card-name">{{ item.service_name }}</view> <view class="wf-meta">
<view class="hs-card-desc">{{ item.price_desc }}</view> <view class="wf-name">{{ item.service_name }}</view>
<view class="hs-card-foot"> <view class="wf-desc" v-if="item.intro">{{ item.intro }}</view>
<text class="hs-card-tag">{{ serviceType === 1 ? '到家' : '家政' }}</text> <text class="wf-tag" v-if="cateMap[item.category_id]">{{ cateMap[item.category_id] }}</text>
<text class="hs-card-btn">查看</text> <view class="wf-foot">
<text class="wf-price" v-if="item.price_desc">{{ item.price_desc }}</text>
<text class="wf-book">去预约</text>
</view>
</view> </view>
</view> </view>
</view> </view>
<view v-if="serviceList.length === 0" class="hs-empty">暂无服务</view> <view class="wf-col">
</scroll-view> <view class="wf-card" v-for="item in rightCol" :key="item.id" @tap="toDetail(item)">
<image class="wf-img" :src="item.showImg" :style="{ height: item.imgH + 'rpx' }" mode="aspectFill" />
<view class="wf-meta">
<view class="wf-name">{{ item.service_name }}</view>
<view class="wf-desc" v-if="item.intro">{{ item.intro }}</view>
<text class="wf-tag" v-if="cateMap[item.category_id]">{{ cateMap[item.category_id] }}</text>
<view class="wf-foot">
<text class="wf-price" v-if="item.price_desc">{{ item.price_desc }}</text>
<text class="wf-book">去预约</text>
</view>
</view>
</view>
</view>
</view>
<view class="wf-more" v-if="hasMore && (leftCol.length || rightCol.length)">上拉加载更多</view>
<view class="wf-more" v-else-if="leftCol.length || rightCol.length">没有更多了</view>
<view class="fh-empty" v-if="!leftCol.length && !rightCol.length && !loadingMore">暂无服务</view>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
import { request, picUrl, NavgateTo } from '@/utils' import { request, picUrl, NavgateTo, menuButtonInfo } from '@/utils'
import { apiArr } from '@/api/homeService' import { apiArr } from '@/api/homeService'
// rpx (750-60)/2 = 345
const WF_COL_W = 345
//
const WF_META_H = 150
export default { export default {
data() { data() {
return { return {
picUrl, picUrl,
serviceType: 1, statusBarTop: 0,
cityName: '定位中',
keyword: '',
categoryList: [], categoryList: [],
cateMap: {}, // id
currentCate: 0, currentCate: 0,
serviceList: [], //
pageNum: 1, leftCol: [],
pageSize: 10, rightCol: [],
hasMore: true colHeights: [0, 0],
page_num: 1,
page_size: 10,
hasMore: true,
loadingMore: false
} }
}, },
onLoad() { onLoad() {
this.loadCategory() const menu = menuButtonInfo()
this.loadList(true) this.statusBarTop = menu.top || 0
const loc = uni.getStorageSync('location') || {}
this.cityName = loc.cityName || '定位中'
this.initData()
},
onPullDownRefresh() {
this.initData().finally(() => uni.stopPullDownRefresh())
},
onReachBottom() {
if (this.hasMore && !this.loadingMore) this.loadList(false)
}, },
methods: { methods: {
switchType(t) { async initData() {
if (this.serviceType === t) return await this.loadCategory()
this.serviceType = t //
this.currentCate = 0 this.currentCate = this.categoryList.length ? this.categoryList[0].id : 0
this.loadCategory() this.keyword = ''
this.loadList(true) await this.loadList(true)
}, },
selectCate(id) { async loadCategory() {
this.currentCate = id try {
this.loadList(true) // (1+2)
}, const res = await request(apiArr.categorySubList, 'POST', {}, {}, false)
loadCategory() { this.categoryList = (res && res.rows) ? res.rows : []
request(apiArr.categoryList, 'POST', { service_type: this.serviceType }, {}, false).then(res => { const map = {}
this.categoryList = res.rows || [] this.categoryList.forEach(c => { map[c.id] = c.category_name })
}) this.cateMap = map
}, } catch (e) {
loadList(reset) { this.categoryList = []
if (reset) { this.cateMap = {}
this.pageNum = 1
this.hasMore = true
this.serviceList = []
} }
if (!this.hasMore) return
request(apiArr.serviceList, 'POST', {
service_type: this.serviceType,
category_id: this.currentCate,
page_num: this.pageNum,
page_size: this.pageSize
}, {}, false).then(res => {
const rows = res.rows || []
this.serviceList = this.serviceList.concat(rows)
this.hasMore = rows.length === this.pageSize
this.pageNum++
})
}, },
loadMore() { selectCate(cat) {
this.loadList(false) // +
NavgateTo('/packages/homeService/catalog/index?cate_id=' + cat.id, { isLogin: false })
},
doSearch() {
//
this.currentCate = 0
this.loadList(true)
},
async loadList(reset) {
if (this.loadingMore) return
this.loadingMore = true
if (reset) {
this.page_num = 1
this.hasMore = true
this.leftCol = []
this.rightCol = []
this.colHeights = [0, 0]
}
try {
const res = await request(apiArr.serviceList, 'POST', {
// service_type 0() category_id
service_type: 0,
category_id: this.currentCate,
keyword: this.keyword,
page_num: this.page_num,
page_size: this.page_size
}, {}, false)
const rows = (res && res.rows) ? res.rows : []
rows.forEach((item) => {
//
const cover0 = item.cover_pic ? String(item.cover_pic).split(',')[0] : ''
item.showImg = cover0 ? picUrl + cover0 : ''
})
this.hasMore = rows.length === this.page_size
if (this.hasMore) this.page_num++
await this.appendMasonry(rows)
} catch (e) {
console.warn('服务列表加载失败:', e)
} finally {
this.loadingMore = false
}
},
//
async appendMasonry(rows) {
for (const item of rows) {
let ratio = 0.8
if (item.showImg) {
try {
const info = await new Promise((resolve, reject) => {
uni.getImageInfo({ src: item.showImg, success: resolve, fail: reject })
})
if (info && info.width) ratio = info.height / info.width
} catch (e) { /* 用默认比例 */ }
}
item.imgH = Math.round(WF_COL_W * ratio)
const target = this.colHeights[0] <= this.colHeights[1] ? 0 : 1
if (target === 0) this.leftCol.push(item)
else this.rightCol.push(item)
this.colHeights[target] += item.imgH + WF_META_H
}
}, },
toDetail(item) { toDetail(item) {
NavgateTo('/packages/homeService/detail/index?id=' + item.id) NavgateTo('/packages/homeService/detail/index?id=' + item.id)
},
goBack() {
uni.navigateBack({ delta: 1, fail: () => { NavgateTo('/pages/index2/index2') } })
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.hs-page { display: flex; flex-direction: column; height: 100vh; background: #f5f5f5; } page { background: #f5f6f8; }
.hs-tabs { display: flex; background: #fff; } .fh-page { min-height: 100vh; background: #f5f6f8; }
.hs-tab { flex: 1; text-align: center; padding: 26rpx 0; font-size: 28rpx; color: #666; }
.hs-tab.active { color: #FF370B; font-weight: 600; border-bottom: 4rpx solid #FF370B; } /* ===== 顶部红头(吸顶:搜索栏不随页面滚动) ===== */
.hs-body { flex: 1; display: flex; overflow: hidden; } .fh-hd {
.hs-cate { width: 180rpx; background: #fafafa; height: 100%; } position: sticky;
.hs-cate-item { padding: 28rpx 16rpx; font-size: 26rpx; color: #555; text-align: center; } top: 0;
.hs-cate-item.active { background: #fff; color: #FF370B; font-weight: 600; } z-index: 100;
.hs-list { flex: 1; padding: 20rpx; box-sizing: border-box; } /* 设计稿:横向红色渐变 90deg #FF1A1F → #FF634A */
.hs-card { display: flex; background: #fff; border-radius: 12rpx; padding: 16rpx; margin-bottom: 16rpx; } background: linear-gradient(90deg, #ff1a1f 0%, #ff634a 100%);
.hs-card-pic { width: 200rpx; height: 200rpx; border-radius: 8rpx; flex-shrink: 0; background: #f0f0f0; } padding-left: 28rpx;
.hs-card-info { flex: 1; margin-left: 18rpx; display: flex; flex-direction: column; min-width: 0; } padding-right: 28rpx;
.hs-card-name { font-size: 30rpx; color: #222; font-weight: 600; } padding-bottom: 28rpx;
.hs-card-desc { font-size: 24rpx; color: #999; margin-top: 12rpx; flex: 1; } }
.hs-card-foot { display: flex; align-items: center; justify-content: space-between; } .fh-hd-top {
.hs-card-tag { font-size: 20rpx; color: #FF370B; border: 1rpx solid #FFD9CD; border-radius: 6rpx; padding: 2rpx 10rpx; } position: relative;
.hs-card-btn { background: linear-gradient(91deg, #FF7658, #FF370B); color: #fff; font-size: 24rpx; padding: 8rpx 28rpx; border-radius: 30rpx; } display: flex;
.hs-empty { text-align: center; color: #999; font-size: 26rpx; padding: 80rpx 0; } align-items: center;
height: 80rpx;
}
.fh-back {
position: absolute;
left: 0;
top: 0;
height: 80rpx;
display: flex;
align-items: center;
padding-right: 16rpx;
}
.fh-brand {
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 56rpx;
}
.fh-brand-title {
/* 设计稿 33px ≈ 58rpx */
font-size: 58rpx;
font-weight: 700;
color: #fff;
letter-spacing: 2rpx;
}
.fh-brand-sub {
margin-top: 8rpx;
/* 设计稿 12px ≈ 21rpx */
font-size: 21rpx;
color: rgba(255, 255, 255, 0.92);
letter-spacing: 4rpx;
}
/* 搜索栏 */
.fh-search {
display: flex;
align-items: center;
height: 77rpx;
margin-top: 22rpx;
background: #fff;
border-radius: 42rpx;
padding: 0 8rpx 0 26rpx;
}
.fh-search-city {
display: flex;
align-items: center;
flex-shrink: 0;
/* 设计稿城市 16px ≈ 28rpx#111111 */
font-size: 28rpx;
color: #111;
max-width: 180rpx;
}
.fh-city-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.fh-city-arrow {
margin-left: 6rpx;
font-size: 22rpx;
color: #666;
}
.fh-search-city::after {
content: '';
width: 2rpx;
height: 34rpx;
background: #d9d9d9;
margin: 0 20rpx;
}
.fh-search-input {
flex: 1;
/* 设计稿搜索词 15px ≈ 26rpx */
font-size: 26rpx;
color: #333;
}
.fh-search-ph { color: #a0a0a0; }
.fh-search-btn {
flex-shrink: 0;
/* 设计稿搜索按钮 64×36px ≈ 112×63rpx#FF3929radius 18px≈31rpx */
height: 63rpx;
line-height: 63rpx;
min-width: 112rpx;
text-align: center;
padding: 0 26rpx;
background: #ff3929;
color: #fff;
font-size: 26rpx;
font-weight: 700;
border-radius: 31rpx;
}
/* ===== 主体 ===== */
.fh-body {
position: relative;
margin-top: -12rpx;
padding: 20rpx 20rpx 40rpx;
}
/* 金刚区 */
.fh-grid {
display: flex;
flex-wrap: wrap;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 6rpx 4rpx;
}
.fh-grid-item {
width: 20%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 26rpx;
}
.fh-grid-icon {
/* 设计稿分类图标底 46×46px ≈ 80rpx浅暖底 #FFF4F0 */
width: 80rpx;
height: 80rpx;
border-radius: 18rpx;
background: #fff4f0;
box-sizing: border-box;
padding: 8rpx;
}
.fh-grid-txt {
margin-top: 12rpx;
/* 设计稿分类文字 14px ≈ 24rpx#111111 */
font-size: 24rpx;
color: #111;
text-align: center;
}
.fh-grid-txt.on {
color: #ff3929;
font-weight: 600;
}
/* ===== 瀑布流(与首页好店一致) ===== */
.wf {
display: flex;
align-items: flex-start;
margin-top: 20rpx;
}
.wf-col {
width: 345rpx;
}
.wf-col:first-child {
margin-right: 20rpx;
}
.wf-card {
background: #fff;
/* 设计稿卡片1px #EDEDED 描边radius 10px≈17rpx */
border: 1rpx solid #ededed;
border-radius: 17rpx;
overflow: hidden;
margin-bottom: 20rpx;
}
.wf-img {
width: 100%;
display: block;
background: #eee;
}
.wf-meta {
padding: 16rpx 14rpx;
}
.wf-name {
/* 设计稿标题 15px≈26rpx#111111 */
font-size: 26rpx;
font-weight: 700;
color: #111;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.wf-desc {
margin-top: 8rpx;
/* 设计稿描述 11px≈19rpx#777777 */
font-size: 19rpx;
color: #777;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 服务标签:浅底红框红字胶囊 */
.wf-tag {
display: inline-block;
margin-top: 12rpx;
background: #fff6f4;
border: 1rpx solid #ff4a38;
border-radius: 7rpx;
color: #ff3929;
font-size: 18rpx;
line-height: 1.4;
padding: 2rpx 10rpx;
}
.wf-foot {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 14rpx;
}
.wf-price {
/* 设计稿价格 16px≈28rpx#FF3929 */
font-size: 28rpx;
font-weight: 700;
color: #ff3929;
}
.wf-book {
flex-shrink: 0;
/* 设计稿预约按钮:纯红 #FF3929radius 15px≈26rpx */
background: #ff3929;
color: #fff;
font-size: 22rpx;
font-weight: 700;
padding: 10rpx 24rpx;
border-radius: 26rpx;
}
.wf-more {
text-align: center;
color: #999;
font-size: 24rpx;
padding: 24rpx 0 20rpx;
}
.fh-empty {
text-align: center;
color: #999;
font-size: 26rpx;
padding: 100rpx 0;
}
</style> </style>

View File

@ -0,0 +1,323 @@
/* ============ 智能设备聚合首页 样式(还原设计稿 device 页) ============ */
page {
background: #f5f6f7;
}
.device-page {
min-height: 100vh;
background: #f5f6f7;
}
/* 内容体:去掉渐变续色;保留 12rpx 顶部间距Hero 与页头之间的空隙,纯页面底色)。 */
.device-body {
padding-top: 12rpx;
}
/* ===== 头部:居中标题 ===== */
.device-header {
padding-left: 30rpx;
padding-right: 30rpx;
/* 不要红头,保持页面底色 */
background: #f5f6f7;
}
.title-bar {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
}
.page-title {
font-size: 36rpx;
font-weight: 700;
color: #1a1a1a;
letter-spacing: 1rpx;
}
/* ===== Hero红橙渐变底 + 叠加文案 ===== */
.device-hero {
position: relative;
/* margin-top 移到 .device-body 的 padding-top避免 margin 塌陷露出灰底白条 */
margin: 0 30rpx;
height: 360rpx;
border-radius: 24rpx;
overflow: hidden;
/* 图片加载失败时露出的红橙渐变兜底 */
background: linear-gradient(120deg, #f4141c 0%, #ff8758 100%);
box-shadow: 0 12rpx 30rpx rgba(244, 20, 28, 0.18);
}
/* 轮播底图 */
.hero-swiper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.hero-img {
width: 100%;
height: 100%;
display: block;
}
/* 文字可读性遮罩:左深右透 */
.hero-mask {
position: absolute;
inset: 0;
z-index: 1;
background: linear-gradient(100deg, rgba(120, 20, 14, 0.7) 0%, rgba(180, 40, 24, 0.34) 52%, rgba(255, 135, 88, 0.06) 100%);
}
.hero-overlay {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 40rpx 32rpx;
}
.hero-greeting {
font-size: 30rpx;
font-weight: 700;
color: #fff;
}
.hero-greeting-sub {
margin-top: 6rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.85);
}
.hero-title {
margin-top: 12rpx;
font-size: 46rpx;
font-weight: 800;
color: #fff;
letter-spacing: -1rpx;
}
.hero-sub {
margin-top: 12rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
}
/* 登录前双按钮 */
.hero-actions {
display: flex;
margin-top: 28rpx;
}
.hero-btn {
height: 64rpx;
padding: 0 36rpx;
border-radius: 999rpx;
font-size: 26rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.hero-btn.primary {
background: #fff;
color: #ef111b;
}
.hero-btn.ghost {
margin-left: 20rpx;
border: 2rpx solid rgba(255, 255, 255, 0.9);
color: #fff;
}
/* 登录后 4 特性标签 */
.hero-tags {
display: flex;
margin-top: 28rpx;
}
.hero-tag {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 36rpx;
}
.hero-tag-icon {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.22);
display: flex;
align-items: center;
justify-content: center;
}
.hero-tag-label {
margin-top: 10rpx;
font-size: 20rpx;
color: #fff;
}
/* ===== 设备分类卡2 列网格 ===== */
.cat-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 24rpx 30rpx 0;
}
.cat-card {
position: relative;
overflow: hidden;
width: 336rpx;
height: 240rpx;
margin-bottom: 18rpx;
border-radius: 20rpx;
padding: 24rpx;
background: linear-gradient(135deg, #fbf2f0 0%, #fdf8f7 100%);
box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.04);
box-sizing: border-box;
}
/* 右下角产品几何块 */
.cat-pic {
position: absolute;
right: 14rpx;
bottom: 14rpx;
width: 130rpx;
height: 130rpx;
z-index: 0;
border-radius: 16rpx;
overflow: hidden;
}
.cat-pic-img {
width: 100%;
height: 100%;
display: block;
}
.illust-access { background: linear-gradient(180deg, #2c3440 0%, #4a5566 100%); }
.illust-parking { background: linear-gradient(180deg, #9ee7cf 0%, #62c99b 100%); }
.illust-guard { background: linear-gradient(180deg, #eef1f4 0%, #c9d2dc 100%); }
.illust-environment { background: linear-gradient(180deg, #2a2f3a 0%, #525a6b 100%); }
.illust-monitor { background: linear-gradient(180deg, #f0f2f5 0%, #cdd4dd 100%); }
.illust-water { background: linear-gradient(180deg, #eaf4ff 0%, #bcd8f5 100%); }
.illust-lighting { background: linear-gradient(180deg, #ffe9b0 0%, #f4c45a 100%); }
.illust-scene { background: linear-gradient(180deg, #ffe2e0 0%, #ffb9b3 100%); }
.cat-text {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
max-width: 220rpx;
}
.cat-title {
font-size: 30rpx;
font-weight: 700;
color: #1a1a1a;
letter-spacing: 1rpx;
}
.cat-desc {
margin-top: 8rpx;
font-size: 22rpx;
line-height: 1.4;
color: #9a9a9a;
}
/* 底部行:锁 / 链接 */
.cat-foot {
position: absolute;
left: 24rpx;
bottom: 24rpx;
z-index: 1;
display: flex;
align-items: center;
}
.foot-lock {
margin-left: 6rpx;
font-size: 21rpx;
color: #b8b8b8;
}
.foot-action {
margin-left: 6rpx;
font-size: 22rpx;
font-weight: 600;
color: #ef111b;
}
/* ===== 登录后底部统计条 ===== */
.stat-bar {
display: flex;
align-items: center;
margin: 24rpx 30rpx 0;
padding: 24rpx 28rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #fff3f1 0%, #fff9f8 100%);
box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.04);
}
.stat-icon {
flex-shrink: 0;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: linear-gradient(135deg, #ff5e61 0%, #ef111b 100%);
display: flex;
align-items: center;
justify-content: center;
}
.stat-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
margin-left: 18rpx;
}
.stat-title {
font-size: 26rpx;
font-weight: 700;
color: #1a1a1a;
}
.stat-sub {
margin-top: 6rpx;
font-size: 20rpx;
color: #999;
}
.stat-num {
flex-shrink: 0;
display: flex;
align-items: baseline;
}
.stat-value {
font-size: 48rpx;
font-weight: 800;
color: #ef111b;
}
.stat-unit {
margin-left: 6rpx;
font-size: 20rpx;
color: #999;
}
/* 底栏占位 */
.tabbar-spacer {
height: 160rpx;
}

View File

@ -0,0 +1,193 @@
<!--
页面智能设备聚合首页
说明按设计稿 liuyuanxitong-yidong/pages/device 整体还原到 uniapp
顶部固定标题智能设备登录前引导态Hero 双按钮 + 8 卡锁文案
登录后总览态问候 + 4 特性标签 + 8 卡操作链接 + 底部统计条
停车充电卡挂到现有停车页 /packages/smartDevice/index/index其余卡占位
-->
<template>
<view class="device-page">
<!-- 头部固定标题智能设备 -->
<view class="device-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="title-bar">
<text class="page-title">智能设备</text>
</view>
</view>
<!-- 内容体承接页头渐变往下续色渐变加在此从页头下方 0 处起与页头 #ff4848 无缝 -->
<view class="device-body">
<!-- Hero图片轮播底图 + 叠加文案登录前双按钮 / 登录后问候 + 4 特性标签 -->
<view class="device-hero">
<swiper class="hero-swiper" :autoplay="true" :interval="4000" :duration="500" circular>
<swiper-item v-for="(img, idx) in heroImages" :key="idx">
<image class="hero-img" :src="img" mode="aspectFill" />
</swiper-item>
</swiper>
<view class="hero-mask"></view>
<view class="hero-overlay">
<!-- 登录后问候 -->
<block v-if="isLogin">
<text class="hero-greeting">您好{{ nickName || '业主' }}</text>
<text class="hero-greeting-sub">欢迎查看您所在社区的智能设备</text>
<text class="hero-title">社区智能设备总览</text>
<text class="hero-sub">一站式浏览社区设备智慧生活触手可及</text>
<view class="hero-tags">
<view class="hero-tag" v-for="tag in heroTags" :key="tag.key">
<view class="hero-tag-icon">
<u-icon :name="tag.icon" color="#fff" size="30"></u-icon>
</view>
<text class="hero-tag-label">{{ tag.label }}</text>
</view>
</view>
</block>
<!-- 登录前引导 -->
<block v-else>
<text class="hero-title">连接社区智能设备</text>
<text class="hero-sub">门禁停车安防环境一页轻松查看</text>
<view class="hero-actions">
<view class="hero-btn primary" @click="goLogin">立即登录</view>
<view class="hero-btn ghost" @click="handlePreview">先看看</view>
</view>
</block>
</view>
</view>
<!-- 设备分类卡2 列网格 -->
<view class="cat-grid">
<view class="cat-card" v-for="cat in deviceCategories" :key="cat.key" @click="handleCardTap(cat)">
<!-- 右下角产品图还原设计稿渐变块为兜底底色 -->
<view class="cat-pic" :class="'illust-' + cat.illust">
<image class="cat-pic-img" :src="cat.image" mode="aspectFill" />
</view>
<!-- 文案 -->
<view class="cat-text">
<text class="cat-title">{{ cat.title }}</text>
<text class="cat-desc">{{ cat.desc[0] }}</text>
<text class="cat-desc">{{ cat.desc[1] }}</text>
</view>
<!-- 底部登录前锁文案 / 登录后红色链接 -->
<view class="cat-foot">
<block v-if="isLogin">
<u-icon name="play-circle" color="#ef111b" size="24"></u-icon>
<text class="foot-action">{{ cat.actionText }}</text>
</block>
<block v-else>
<u-icon name="lock" color="#b8b8b8" size="22"></u-icon>
<text class="foot-lock">{{ cat.lockText }}</text>
</block>
</view>
</view>
</view>
<!-- 登录后底部统计条 -->
<view class="stat-bar" v-if="isLogin">
<view class="stat-icon">
<u-icon name="home" color="#fff" size="30"></u-icon>
</view>
<view class="stat-text">
<text class="stat-title">社区已接入多类智能设备</text>
<text class="stat-sub">持续升级中为您带来更智慧的社区体验</text>
</view>
<view class="stat-num">
<text class="stat-value">128+</text>
<text class="stat-unit">设备类型</text>
</view>
</view>
<view class="tabbar-spacer"></view>
</view>
<nav-footer :current="1" />
</view>
</template>
<script>
import { request, NavgateTo } from '../../../utils';
import { apiArr } from '../../../api/v2User';
export default {
data() {
return {
statusBarHeight: 20,
isLogin: false,
nickName: '',
// 稿 4
heroImages: [
'/static/smartDevice/hero1.jpg',
'/static/smartDevice/hero2.jpg',
'/static/smartDevice/hero3.jpg',
'/static/smartDevice/hero4.jpg'
],
// 4 uview
heroTags: [
{ key: 'rich', label: '设备丰富', icon: 'grid' },
{ key: 'safe', label: '安全可靠', icon: 'checkmark-circle' },
{ key: 'smart', label: '智慧便捷', icon: 'star-fill' },
{ key: 'life', label: '美好生活', icon: 'heart' }
],
// 8 稿 device-data.js
deviceCategories: [
{ key: 'access', title: '门禁通行', desc: ['安全有序出入', '无忧生活体验'], illust: 'access', image: '/static/smartDevice/cat-access.jpg', lockText: '登录后远程开门', actionText: '设备展示' },
{ key: 'parking', title: '停车充电', desc: ['社区停车便捷', '充电设备丰富'], illust: 'parking', image: '/static/smartDevice/cat-parking.jpg', lockText: '登录后查看车位与充电桩', actionText: '进入停车' },
{ key: 'guard', title: '家庭安防', desc: ['守护家庭安全', '安心每一天'], illust: 'guard', image: '/static/smartDevice/cat-guard.jpg', lockText: '登录后可控制', actionText: '设备简介' },
{ key: 'environment', title: '智慧环境', desc: ['空气与温湿度监测', '营造舒适环境'], illust: 'environment', image: '/static/smartDevice/cat-environment.jpg', lockText: '登录后可查看', actionText: '运行概览' },
{ key: 'monitor', title: '视频监控', desc: ['高清实时监控', '守护社区安全'], illust: 'monitor', image: '/static/smartDevice/cat-monitor.jpg', lockText: '登录后查看摄像头', actionText: '仅供查看' },
{ key: 'water', title: '净水设备', desc: ['优质净水体验', '健康饮水每一天'], illust: 'water', image: '/static/smartDevice/cat-water.jpg', lockText: '登录后可查看', actionText: '设备展示' },
{ key: 'lighting', title: '智能照明', desc: ['灯光随心营造', '温馨舒适氛围'], illust: 'lighting', image: '/static/smartDevice/cat-lighting.jpg', lockText: '登录后可控制', actionText: '场景展示' },
{ key: 'scene', title: '场景联动', desc: ['多设备协同', '智慧生活体验'], illust: 'scene', image: '/static/smartDevice/cat-scene.jpg', lockText: '登录后可使用', actionText: '场景展示' }
]
}
},
onLoad() {
const sys = uni.getSystemInfoSync();
this.statusBarHeight = sys.statusBarHeight || 20;
},
onShow() {
this.refreshLogin();
},
methods: {
refreshLogin() {
const ctoken = uni.getStorageSync('ctoken');
this.isLogin = !!ctoken;
if (this.isLogin) {
this.getUserInfo();
} else {
this.nickName = '';
}
},
async getUserInfo() {
try {
const res = await request(apiArr.getUserInfo, 'POST', {}, { silent: false });
this.nickName = (res && res.nick_name) ? res.nick_name : '';
} catch (e) {
this.nickName = '';
}
},
goLogin() {
uni.redirectTo({ url: '/pages/login/login' });
},
handlePreview() {
uni.showToast({ title: '浏览模式,登录后可使用完整功能', icon: 'none' });
},
//
handleCardTap(item) {
if (item.key === 'parking') {
// NavgateTo
NavgateTo('/packages/smartDevice/index/index');
return;
}
if (!this.isLogin) {
uni.showToast({ title: '登录后可查看设备详情', icon: 'none' });
return;
}
uni.showToast({ title: item.title + '功能开发中', icon: 'none' });
}
}
}
</script>
<style>
@import url("./index.css");
</style>

View File

@ -1,5 +1,16 @@
{ {
"pages": [ "pages": [
{
"path": "pages/index2/index2",
"style": {
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#FFF",
"enablePullDownRefresh": true,
"usingComponents": {
"nav-footer": "/components/nav/nav"
}
}
},
{ {
"path": "pages/index/index", "path": "pages/index/index",
"style": { "style": {
@ -139,7 +150,11 @@
"pages": [ "pages": [
{ {
"path": "index/index", "path": "index/index",
"style": { "navigationBarTitleText": "到家服务", "navigationBarBackgroundColor": "#fff" } "style": { "navigationBarTitleText": "未来到家", "navigationStyle": "custom", "navigationBarBackgroundColor": "#ff3838", "enablePullDownRefresh": true }
},
{
"path": "catalog/index",
"style": { "navigationBarTitleText": "服务", "navigationStyle": "custom" }
}, },
{ {
"path": "detail/index", "path": "detail/index",
@ -1194,6 +1209,17 @@
{ {
"root": "packages/smartDevice", "root": "packages/smartDevice",
"pages": [ "pages": [
{
"path": "home/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "智能设备",
"navigationBarBackgroundColor": "#f5f6f7",
"usingComponents": {
"nav-footer": "/components/nav/nav"
}
}
},
{ {
"path": "index/index", "path": "index/index",
"style": { "style": {
@ -1206,6 +1232,35 @@
} }
] ]
}, },
{
"root": "packages/futureCommunity",
"pages": [
{
"path": "index/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "九九社区", "enablePullDownRefresh": false }
},
{
"path": "noticeList/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "社区公告" }
},
{
"path": "noticeDetail/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "公告详情" }
},
{
"path": "activityList/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "社区活动" }
},
{
"path": "activityDetail/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "活动详情" }
},
{
"path": "conveniencePhone/index",
"style": { "navigationStyle": "custom", "navigationBarTitleText": "便民电话" }
}
]
},
{ {
"root": "packages/advertising", "root": "packages/advertising",
"pages": [ "pages": [

View File

@ -102,11 +102,11 @@
</view> </view>
<view class="white_container margin_top20 padding_bottom32"> <view class="white_container margin_top20 padding_bottom32">
<view v-if="!currentCity.district" class="Tit">定位错误 未知区域</view> <!--view v-if="!currentCity.district" class="Tit">定位错误 未知区域</view>
<view v-else class="Tit"> <view v-else class="Tit">
<view class="local">{{ currentCity.district }}</view>附近 <view class="local">{{ currentCity.district }}</view>附近
<view class="Tit_desc">一刻钟便利生活圈</view> <view class="Tit_desc">一刻钟便利生活圈</view>
</view> </view-->
<view class="serverList"> <view class="serverList">
<view class="serverList_left"> <view class="serverList_left">
<swiper> <swiper>

382
pages/index2/index2.css Normal file
View File

@ -0,0 +1,382 @@
/* ============ 新首页 index2 样式 ============ */
page {
background: #f5f6f8;
}
.home2 {
min-height: 100vh;
}
/* ===== 顶部红色头区(吸顶固定:滚动/切 tab 时搜索栏与分类条保持不变) ===== */
.hd {
position: sticky;
top: 0;
z-index: 100;
/* 不要红头,保持页面底色 */
background: #f5f6f8;
padding: 0 24rpx 20rpx;
}
.hd-loc {
display: flex;
align-items: center;
height: 64rpx;
color: #333;
}
.hd-loc-icon {
width: 30rpx;
margin-right: 6rpx;
}
.hd-loc-txt {
font-size: 30rpx;
font-weight: 600;
max-width: 420rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.hd-loc-arrow {
font-size: 24rpx;
margin-left: 6rpx;
}
/* 搜索框 */
.hd-search {
display: flex;
align-items: center;
height: 72rpx;
background: #fff;
border: 2rpx solid #ef111b;
border-radius: 40rpx;
padding: 0 8rpx 0 24rpx;
margin-top: 8rpx;
}
.hd-search-icon {
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.hd-search-input {
flex: 1;
font-size: 26rpx;
color: #333;
}
.hd-search-input::-webkit-input-placeholder {
color: #b0b3b8;
}
.hd-search-btn {
flex-shrink: 0;
height: 56rpx;
line-height: 56rpx;
padding: 0 28rpx;
/* 与源码搜索按钮一致:纯红 #ff2b2f */
background: #ff2b2f;
color: #fff;
font-size: 26rpx;
border-radius: 32rpx;
}
/* 分类条移到瀑布上方并吸顶top 由内联样式给页头高度);仅瀑布在其下滚动 */
.hd-cate {
position: sticky;
z-index: 50;
white-space: nowrap;
/* 抵消 .bd 的 20rpx 左右内边距,吸顶时整宽铺满、盖住下方瀑布 */
margin: 0 -20rpx;
padding: 16rpx 20rpx;
background: #f5f6f8;
}
.hd-cate-item {
display: inline-block;
color: #666;
font-size: 28rpx;
padding: 6rpx 4rpx;
margin-right: 34rpx;
position: relative;
}
.hd-cate-item.on {
color: #ff4848;
font-weight: 700;
font-size: 30rpx;
}
.hd-cate-item.on::after {
content: '';
position: absolute;
left: 50%;
bottom: -4rpx;
transform: translateX(-50%);
width: 36rpx;
height: 6rpx;
border-radius: 3rpx;
background: #ff4848;
}
/* ===== 主体 ===== */
.bd {
position: relative;
margin-top: -8rpx;
background: #f5f6f8;
border-radius: 24rpx 24rpx 0 0;
padding: 20rpx 20rpx 180rpx;
}
/* 轮播 */
.banner {
position: relative;
border-radius: 20rpx;
overflow: hidden;
}
.banner-swiper {
width: 100%;
height: 300rpx;
}
.banner-img {
width: 100%;
height: 300rpx;
display: block;
}
.banner-dots {
position: absolute;
right: 20rpx;
bottom: 16rpx;
display: flex;
}
.banner-dots .dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
margin-left: 8rpx;
}
.banner-dots .dot.on {
width: 24rpx;
border-radius: 6rpx;
background: #fff;
}
/* 金刚区(自绘 flex上下留白 = .grid 的 margin各 24rpx无组件内建空间 */
.grid {
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 24rpx 0;
}
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
}
.grid-icon {
width: 84rpx;
height: 84rpx;
border-radius: 24rpx;
display: block;
}
.grid-txt {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
line-height: 1;
color: #333;
text-align: center;
}
/* 广告栏 */
.ads {
display: flex;
/* 与金刚区间距由 .grid 的 padding-bottom 5px 控制 */
margin-top: 0;
/* 按原主页广告位尺寸放大:整体约 450rpx 高 */
height: 450rpx;
}
.ads-left {
width: 50%;
margin-right: 16rpx;
border-radius: 20rpx;
overflow: hidden;
}
.ads-left image {
width: 100%;
height: 450rpx;
display: block;
}
.ads-right {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.ads-right-item {
height: 217rpx;
border-radius: 20rpx;
overflow: hidden;
}
.ads-right-item image {
width: 100%;
height: 217rpx;
display: block;
}
/* 空广告位占位(招商中) */
.ad-ph {
width: 100%;
height: 100%;
min-height: 217rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #fff3ee, #ffe9e0);
color: #ff9a76;
font-size: 24rpx;
letter-spacing: 2rpx;
}
.ads-left .ad-ph {
height: 450rpx;
}
/* 好店标题 */
.wf-title {
display: flex;
align-items: center;
/* 内容已注释为空,去掉多余留白 */
margin: 0;
}
.wf-title-main {
font-size: 34rpx;
font-weight: 700;
color: #222;
}
.wf-title-loc {
color: #ff4d4f;
}
.wf-title-tag {
margin-left: 16rpx;
font-size: 22rpx;
color: #ff7a45;
background: #fff0e8;
border-radius: 20rpx;
padding: 4rpx 16rpx;
}
/* 瀑布 */
.wf {
display: flex;
align-items: flex-start;
}
.wf-col {
width: 345rpx;
}
.wf-col:first-child {
margin-right: 20rpx;
}
.wf-card {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
margin-bottom: 20rpx;
}
.wf-img {
width: 345rpx;
display: block;
background: #eee;
}
.wf-meta {
padding: 16rpx;
}
.wf-name {
font-size: 28rpx;
font-weight: 600;
color: #222;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.wf-sub {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10rpx;
}
.wf-stars {
display: flex;
}
.wf-stars image {
width: 22rpx;
height: 22rpx;
margin-right: 2rpx;
}
.wf-dist {
font-size: 22rpx;
color: #999;
}
.wf-tags {
display: flex;
flex-wrap: wrap;
margin-top: 10rpx;
}
.wf-tag {
font-size: 20rpx;
color: #ff6a3d;
border: 1rpx solid #ffcbb3;
background: #fff5f0;
border-radius: 8rpx;
padding: 2rpx 10rpx;
margin-right: 8rpx;
margin-bottom: 6rpx;
}
.wf-addr {
margin-top: 8rpx;
font-size: 22rpx;
color: #999;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.wf-more {
text-align: center;
color: #999;
font-size: 24rpx;
padding: 24rpx 0 40rpx;
}

691
pages/index2/index2.vue Normal file
View File

@ -0,0 +1,691 @@
<template>
<view class="home2" v-if="loading">
<!-- ===== 顶部红色头区状态栏 + 定位 + 搜索 + 分类条 ===== -->
<view class="hd" :style="{ paddingTop: top + 'px' }">
<!-- 定位 -->
<view class="hd-loc" @tap="map">
<image class="hd-loc-icon" src="https://static.hshuishang.com/Index_add.png" mode="widthFix" />
<text class="hd-loc-txt">{{ currentCity.cityName || '选择城市' }}</text>
<text class="hd-loc-arrow"></text>
</view>
<!-- 搜索框新增本轮仅 UI + 占位后台好店搜索接口回头接入 handleSearch -->
<view class="hd-search">
<image class="hd-search-icon"
src="https://static.hshuishang.com/property-img-file/com_communitySearchIcon.png" mode="aspectFill" />
<input class="hd-search-input" v-model="keyword" :placeholder="searchPlaceholder"
confirm-type="search" @confirm="handleSearch" />
<view class="hd-search-btn" @tap="handleSearch">搜索</view>
</view>
</view>
<!-- ===== 主体白底圆角上浮 ===== -->
<view class="bd">
<!-- 轮播图 -->
<view class="banner" v-if="isRecommend && bannerList.length">
<swiper class="banner-swiper" @animationfinish="swipers" autoplay circular>
<swiper-item v-for="(item, index) in bannerList" :key="index" @click="handleAdClick(item)">
<image class="banner-img" :src="item.pic_src" mode="aspectFill" />
</swiper-item>
</swiper>
<view class="banner-dots">
<view :class="['dot', currentIdx == index ? 'on' : '']" v-for="(item, index) in bannerList" :key="index">
</view>
</view>
</view>
<!-- 金刚区自绘 flex 布局间距完全可控不用 u-grid 以免组件自带底部空间 -->
<view class="grid" v-if="isRecommend && tabList.length">
<view class="grid-item" :style="{ width: (100 / (rowNum || 5)) + '%' }"
v-for="(item, index) in tabList" :key="index" @tap="headerServerClick(item)">
<image class="grid-icon" :src="item.icon_src" mode="aspectFill"></image>
<text class="grid-txt">{{ item.title }}</text>
</view>
</view>
<!-- 广告栏固定三格左大 + 右上 + 右下空位显示招商占位 -->
<view class="ads" v-if="isRecommend">
<view class="ads-left">
<image v-if="isRealAd(serverLeftList[0])" :src="serverLeft" mode="aspectFill"
@tap="handleAdClick(serverLeftList[0])" />
<view v-else class="ad-ph">广告招商中</view>
</view>
<view class="ads-right">
<view class="ads-right-item" v-for="(item, index) in serverRightList" :key="index">
<image v-if="isRealAd(item)" :src="item.pic_src" mode="aspectFill" @tap="handleAdClick(item)" />
<view v-else class="ad-ph">广告招商中</view>
</view>
</view>
</view>
<!-- 好店列表标题 -->
<view class="wf-title" v-if="isRecommend">
<!--view class="wf-title-main">
<text class="wf-title-loc" v-if="currentCity.district">{{ currentCity.district }}</text>
附近好店
</view>
<view class="wf-title-tag">一刻钟便利生活圈</view-->
</view>
<!-- 分类条移到瀑布上方上滑时吸顶(top=页头高度)仅瀑布在其下滚动 -->
<scroll-view scroll-x class="hd-cate" v-if="lifeCircleList.length" :style="{ top: cateTop + 'px' }">
<view class="hd-cate-item" :class="{ on: currentTab === 'recommend' }" @tap="selectRecommend">推荐</view>
<view class="hd-cate-item" v-for="(item, index) in lifeCircleList" :key="index"
:class="{ on: currentTab === item.title }" @tap="selectCircle(item)">
{{ item.title }}
</view>
</scroll-view>
<!-- 好店瀑布两列图片按原始宽高比制造瀑布效果 -->
<view class="wf">
<view class="wf-col">
<view class="wf-card" v-for="item in leftCol" :key="item.id" @click="Info(item)">
<image class="wf-img" :src="item.showImg" :style="{ height: item.imgH + 'rpx' }" mode="aspectFill" />
<view class="wf-meta">
<view class="wf-name">{{ item.merchant_name }}</view>
<view class="wf-sub">
<view class="wf-stars">
<image v-for="i in 5" :key="i" :src="i < item.rating
? 'https://static.hshuishang.com/property-img-file/local_start1.png'
: 'https://static.hshuishang.com/property-img-file/local_start2.png'" mode="aspectFill"></image>
</view>
<text class="wf-dist">{{ item.distances }}</text>
</view>
<view class="wf-tags">
<text class="wf-tag" v-if="item.refund_property_fee_ratio">买单返岁银</text>
<text class="wf-tag" v-if="item.refund_user_points_ratio">买单返积分</text>
</view>
<view class="wf-addr">{{ item.comAddress }}</view>
</view>
</view>
</view>
<view class="wf-col">
<view class="wf-card" v-for="item in rightCol" :key="item.id" @click="Info(item)">
<image class="wf-img" :src="item.showImg" :style="{ height: item.imgH + 'rpx' }" mode="aspectFill" />
<view class="wf-meta">
<view class="wf-name">{{ item.merchant_name }}</view>
<view class="wf-sub">
<view class="wf-stars">
<image v-for="i in 5" :key="i" :src="i < item.rating
? 'https://static.hshuishang.com/property-img-file/local_start1.png'
: 'https://static.hshuishang.com/property-img-file/local_start2.png'" mode="aspectFill"></image>
</view>
<text class="wf-dist">{{ item.distances }}</text>
</view>
<view class="wf-tags">
<text class="wf-tag" v-if="item.refund_property_fee_ratio">买单返岁银</text>
<text class="wf-tag" v-if="item.refund_user_points_ratio">买单返积分</text>
</view>
<view class="wf-addr">{{ item.comAddress }}</view>
</view>
</view>
</view>
</view>
<view class="wf-more" v-if="merchantHasMore">上拉加载更多</view>
<view class="wf-more" v-else-if="leftCol.length || rightCol.length">没有更多了</view>
<nav-footer :current="0" />
</view>
</view>
</template>
<script>
import {
NavgateTo,
calculateDistance,
} from '../../utils/index';
import {
util,
picUrl,
request,
menuButtonInfo
} from '../../utils/index.js';
import {
apiArr
} from '../../api/community.js';
import {
apiArr as apiArr2
} from '../../api/v2Home.js'
import nav from '../../components/nav/nav';
// 广 home_banner_region.ad_position
const AD_POS = {
TOP_BANNER: 1, //
CARD_LEFT: 2, // 广
CARD_RIGHT_TOP: 3, // 广
CARD_RIGHT_BOTTOM: 4, // 广
MID_LEFT: 5, // 广
MID_CENTER_TOP: 6, //
MID_CENTER: 7, //
MID_CENTER_BOTTOM: 8, //
MID_RIGHT_TOP: 9, //
MID_RIGHT_CENTER: 10, //
MID_RIGHT_BOTTOM: 11, //
POPUP: 12,
BOTTOM_RECOMMEND: 13,
};
// rpx 20rpx + 20rpx (750-60)/2 = 345
const WF_COL_W = 345;
// rpx
const WF_META_H = 150;
export default {
components: { nav },
data() {
return {
loading: false,
top: 0,
localHeight: 44,
cateTop: 0, // = (.hd)(px)
currentCity: '', //
qqmap_key: '',
keyword: '', //
hotWord: [], // placeholder
currentIdx: 0,
bannerList: [], // 1
tabList: [], //
rowNum: 0,
colNum: 0,
// 广2 / 3 / 4
serverLeft: '',
serverLeftList: [],
serverRightList: [],
// 6-11
lifeCircleList: [],
currentTab: 'recommend', // 'recommend'
//
categoryList: [],
currentCategoryId: null,
//
page_num: 1,
page_size: 10,
leftCol: [],
rightCol: [],
colHeights: [0, 0],
merchantHasMore: false,
loadingMore: false,
_bannerGroups: null,
}
},
computed: {
searchPlaceholder() {
return this.hotWord && this.hotWord.length ? this.hotWord[0] : '搜索好店 / 商家 / 优惠'
},
// //广
isRecommend() {
return this.currentTab === 'recommend'
}
},
methods: {
/* ---------- 顶部/通用 ---------- */
map() {
uni.navigateTo({ url: '/pages/shopcity/shopcity' });
},
// keyword getMechantList
handleSearch() {
const kw = (this.keyword || '').trim()
// TODO: getMerchantList keyword
uni.showToast({
title: kw ? `搜索「${kw}」功能即将开放` : '请输入关键字',
icon: 'none'
})
},
swipers(e) {
this.currentIdx = e.detail.current
},
NotOpen() {
uni.showModal({ title: '提示', content: '此功能暂未开通!', showCancel: false, complete: () => { } })
},
// 广 promotion_type
handleAdClick(item) {
if (!item) return
switch (item.promotion_type) {
case 3:
if (!item.link_url) return this.NotOpen()
if (item.link_url === '/packages/shopEnter/index/index') return this.headershopEnterClick()
return NavgateTo(item.link_url, { isLogin: false })
case 4:
if (!item.appid) return this.NotOpen()
return uni.navigateToMiniProgram({ appId: item.appid, path: item.link_url || '', envVersion: 'release' })
case 5:
return NavgateTo('/packages/advertising/index/index?id=' + item.id, { isLogin: false })
case 1:
default:
return this.NotOpen()
}
},
// headerServerClick
headerServerClick(e) {
if (!e.link_url) return this.NotOpen()
if (e.link_url === '/packages/shopEnter/index/index') return this.headershopEnterClick()
// #ifdef APP-PLUS
uni.navigateTo({ url: '/pages/webview/webview?url=' + encodeURIComponent(e.link_url) });
// #endif
// #ifdef H5
window.open(e.link_url, '_blank');
// #endif
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU
if (e.appid) {
uni.navigateToMiniProgram({ appId: e.appid, path: e.link_url, extraData: { data1: 'test' } })
} else {
NavgateTo(e.link_url, { isLogin: false })
}
// #endif
},
// id
recommendCateId() {
const target = this.lifeCircleList.find(c => c.title === '美食圈') || this.lifeCircleList[0]
if (target) {
const cate = this.categoryList.find(c => c.cate_name === target.title)
if (cate) return cate.id
}
return this.categoryList.length ? this.categoryList[0].id : null
},
//
async selectRecommend() {
this.currentTab = 'recommend'
const id = this.recommendCateId()
if (id) await this.switchCategory(id)
},
//
async selectCircle(item) {
if (item.title == '更多') {
return NavgateTo('/packages/localLife/index/index', { isLogin: false })
}
const cate = this.categoryList.find(c => c.cate_name === item.title)
if (cate && cate.id) {
this.currentTab = item.title
await this.switchCategory(cate.id)
} else {
uni.showToast({ title: '暂无「' + item.title + '」相关好店', icon: 'none' })
}
},
async headershopEnterClick() {
if (!uni.getStorageSync('userId')) {
return NavgateTo('/packages/shopEnter/choose/index', { isLogin: false })
}
const res = await request(apiArr2.statusQuery, 'POST', {}, { silent: false })
if (res.status) {
NavgateTo('/packages/shopEnter/auditStatus/index?itemObj=' + JSON.stringify(res))
} else {
NavgateTo('/packages/shopEnter/choose/index')
}
},
/* ---------- 定位 + 首屏取数 ---------- */
init() {
uni.showLoading({ title: '加载中', mask: true })
this.qqmap_key = '55NBZ-MUQYW-EAJRL-YIWPA-ZXCR6-4NBPP'
this.getUserLocation()
},
getUserLocation() {
const that = this
const { qqmap_key } = this
const userlocat = uni.getStorageSync('location')
uni.getLocation({
type: 'wgs84',
success: (res) => {
const latitude = userlocat ? userlocat.lat : res.latitude
const longitude = userlocat ? userlocat.lng : res.longitude
uni.request({
url: `https://apis.map.qq.com/ws/geocoder/v1/?location=${latitude},${longitude}&key=${qqmap_key}&get_poi=0`,
success: async (r) => {
const { data: { result: { address_component, ad_info } } } = r
const preciseLocation = {
cityName: address_component.city,
region: address_component.province + address_component.city + address_component.district,
district: address_component.district + `${address_component.street_number ? address_component.street_number : address_component.street}`,
address: address_component.city + address_component.district + address_component.street_number,
lat: latitude,
lng: longitude,
}
that.currentCity = preciseLocation
uni.setStorageSync('location', preciseLocation)
uni.setStorageSync('ad_code', ad_info.adcode)
// 广1-11 getHomeXxx
await this.prefetchBannerGroups()
const [bannerList, serverLeft, serverRightList, lifeCircleList, tabList] = await Promise.all([
this.getHomeBanner(),
this.getServerLeft(),
this.getServerRight(),
this.getLifeCircle(), // 6-11
this.getButtonNum(), //
])
this.bannerList = bannerList
this.serverLeft = serverLeft
this.serverRightList = serverRightList
this.lifeCircleList = lifeCircleList
this.tabList = tabList
// //广/
// loading=true
this.loading = true
uni.hideLoading()
this.$nextTick(() => setTimeout(() => this.measureLayout(), 60)) // +
this.getCateList().catch(e => console.warn('好店分类/瀑布加载失败:', e)) // +
},
fail: (err) => {
console.log('地图API解析地址失败:', err)
uni.showToast({ title: '定位失败,请检查网络', icon: 'none' })
uni.hideLoading()
}
})
},
fail: (err) => {
console.log('获取位置信息失败:', err)
if (err.errMsg && err.errMsg.indexOf('auth deny') >= 0) {
uni.showModal({
title: '提示', content: '需要您授权位置信息才能使用此功能', confirmText: '去授权',
success: (res) => {
if (res.confirm) {
// #ifdef MP-WEIXIN
wx.openSetting();
// #endif
// #ifdef APP-PLUS
uni.openSetting();
// #endif
}
uni.hideLoading()
}
})
} else {
uni.showToast({ title: '定位失败,请重试', icon: 'none' })
uni.hideLoading()
}
}
})
},
/* ---------- 广告位取数(沿用旧首页逻辑) ---------- */
async prefetchBannerGroups(positions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) {
try {
const res = await request(apiArr2.getHomeBannerMulti, 'POST', {
ad_code: Number(uni.getStorageSync('ad_code')),
ad_positions: positions,
longitude: uni.getStorageSync('location').lng,
latitude: uni.getStorageSync('location').lat,
page_size: 50
}, { silent: false })
this._bannerGroups = (res && res.groups) ? res.groups : {}
} catch (e) {
console.warn('批量拉取首页广告失败,回退逐个请求:', e)
this._bannerGroups = null
}
},
async getBannerRows(position) {
if (this._bannerGroups) {
return this._bannerGroups[String(position)] || []
}
const res = await request(apiArr2.getHomeBanner, 'POST', {
ad_code: uni.getStorageSync('ad_code'),
ad_position: position,
longitude: uni.getStorageSync('location').lng,
latitude: uni.getStorageSync('location').lat,
page_num: 1,
page_size: 50
}, { silent: false })
return (res && res.rows) ? res.rows : []
},
withPicUrl(list) {
list.forEach(item => { item.pic_src = picUrl + item.pic_src })
return list
},
filterShowList(list, type) {
if (list && list.length) return list.filter(item => item.show_status == type)
return []
},
async getBannerAt(position, { placeholder = false } = {}) {
const rows = await this.getBannerRows(position)
const [first] = this.withPicUrl(this.filterShowList(rows, 1))
if (first) return first
return placeholder ? { ad_position: position, pic_src: '' } : null
},
async getBannersAt(positions, opts) {
return Promise.all(positions.map(p => this.getBannerAt(p, opts)))
},
async getHomeBanner() {
const rows = await this.getBannerRows(AD_POS.TOP_BANNER)
return this.withPicUrl(this.filterShowList(rows, 1))
},
// 广(2) + (3) + (4)
// 广稿
async getServerLeft() {
const item = await this.getBannerAt(AD_POS.CARD_LEFT, { placeholder: true })
this.serverLeftList = [item]
return item ? item.pic_src : ''
},
async getServerRight() {
// placeholder:true pic_src
return this.getBannersAt([AD_POS.CARD_RIGHT_TOP, AD_POS.CARD_RIGHT_BOTTOM], { placeholder: true })
},
// 广
isRealAd(item) {
return !!(item && item.pic_src)
},
// 6-11
async getLifeCircle() {
const list = await this.getBannersAt([
AD_POS.MID_CENTER_TOP, AD_POS.MID_CENTER, AD_POS.MID_CENTER_BOTTOM,
AD_POS.MID_RIGHT_TOP, AD_POS.MID_RIGHT_CENTER, AD_POS.MID_RIGHT_BOTTOM,
])
return list.filter(Boolean).filter(item => item.title)
},
/* ---------- 金刚区 ---------- */
async getButtonNum() {
const res = await request(apiArr2.getButtonNum, 'POST', {}, { slice: false })
this.rowNum = res.nav_row_num
this.colNum = res.nav_row_total
return await this.getHomeButton()
},
async getHomeButton() {
const res = await request(apiArr2.getHomeButton, 'POST', {
ad_code: Number(uni.getStorageSync('ad_code')),
page_num: 1,
page_size: 50
}, { slice: false })
const filterRes = this.filterShowList(res?.rows, 1)
const newList = filterRes.slice(0, this.rowNum * this.colNum)
if (newList.length) {
newList.forEach(item => { item.icon_src = picUrl + item.icon_src })
return newList
}
return []
},
/* ---------- 分类 + 好店瀑布 ---------- */
async getCateList() {
const res = await request(apiArr2.getCateList, 'POST', { isShop: 1 }, { slice: false })
if (res.rows && res.rows.length) {
this.categoryList = res.rows
//
this.currentTab = 'recommend'
this.currentCategoryId = this.recommendCateId() || res.rows[0].id
await this.getMechantList(true)
} else {
this.categoryList = []
}
},
async switchCategory(id) {
if (this.currentCategoryId === id) return
this.currentCategoryId = id
await this.getMechantList(true)
//
},
// reset=true /
async getMechantList(reset = false) {
if (this.loadingMore) return
this.loadingMore = true
if (reset) {
this.page_num = 1
this.leftCol = []
this.rightCol = []
this.colHeights = [0, 0]
}
try {
const res = await request(apiArr2.getMerchantList, 'POST', {
merchant_cate_id: this.currentCategoryId,
page_num: this.page_num,
page_size: this.page_size,
})
const latitude = uni.getStorageSync('location').lat
const longitude = uni.getStorageSync('location').lng
const rows = (res && res.rows) ? res.rows : []
rows.forEach((item) => {
item.bigImg = item.album_images?.split(',')
item.showImg = item.album_images ? picUrl + item.album_images.split(',')[0] : ''
const distanceInKm = calculateDistance(latitude, longitude, item.latitude, item.longitude)
item.distances = distanceInKm >= 1
? `${distanceInKm.toFixed(1)}km`
: `${(distanceInKm * 1000).toFixed(1)}m`
item.comAddress = item.ad ? item.ad.ad_name.replace(/,/g, '') + item.address : item.address
})
//
rows.sort((a, b) => {
const va = parseFloat(a.distances) * (a.distances.includes('km') ? 1000 : 1)
const vb = parseFloat(b.distances) * (b.distances.includes('km') ? 1000 : 1)
return va - vb
})
this.merchantHasMore = rows.length === this.page_size
if (this.merchantHasMore) this.page_num++
await this.appendMasonry(rows)
} catch (e) {
console.warn('好店列表加载失败:', e)
} finally {
this.loadingMore = false
}
},
//
// mode="widthFix"
async appendMasonry(rows) {
const ratios = await Promise.all(rows.map(item => new Promise(resolve => {
if (!item.showImg) return resolve(0.8)
uni.getImageInfo({
src: item.showImg,
success: info => resolve(info && info.width ? info.height / info.width : 0.8),
fail: () => resolve(0.8)
})
})))
rows.forEach((item, i) => {
item.imgH = Math.round(WF_COL_W * ratios[i]) //
const target = this.colHeights[0] <= this.colHeights[1] ? 0 : 1
if (target === 0) this.leftCol.push(item)
else this.rightCol.push(item)
this.colHeights[target] += item.imgH + WF_META_H
})
},
//
measureLayout() {
uni.createSelectorQuery().in(this)
.select('.hd').boundingClientRect(rect => {
if (rect && rect.height) this.cateTop = Math.round(rect.height)
}).exec()
},
Info(e) {
uni.setStorageSync('merchantInfo', e)
NavgateTo('/packages/localLife/detail/index', { isLogin: false })
},
async getSearchVal() {
try {
const res = await request(apiArr.hotWord, 'POST', {}, { slice: false })
this.hotWord = res.search_hot_word ? res.search_hot_word.split(/[,]/) : []
} catch (e) { this.hotWord = [] }
},
},
onLoad() {
const menu = menuButtonInfo()
this.top = menu.top
this.localHeight = menu.height
// = top(px) + (64+8+72+20=164rpx) px
const sys = uni.getSystemInfoSync()
this.cateTop = Math.round(this.top + 164 * sys.windowWidth / 750)
this.init()
this.getSearchVal()
},
onReady() {
//
this.$nextTick(() => {
uni.createSelectorQuery().in(this).select('.hd').boundingClientRect(rect => {
if (rect && rect.height) this.cateTop = Math.round(rect.height)
}).exec()
})
},
onShow() {
const storageLocation = uni.getStorageSync('location')
if (storageLocation && this.loading) {
// /
// currentCity.address storageLocation.district
//
const changed = !this.currentCity ||
this.currentCity.cityName !== storageLocation.cityName ||
this.currentCity.district !== storageLocation.district
if (changed) {
this.currentCity = storageLocation
this.init()
}
}
},
onPullDownRefresh() {
this.init()
uni.stopPullDownRefresh()
},
async onReachBottom() {
if (this.merchantHasMore && !this.loadingMore) {
await this.getMechantList(false)
}
}
}
</script>
<style>
@import url("./index2.css");
</style>

View File

@ -7,8 +7,13 @@ page {
/* height: 578rpx; */ /* height: 578rpx; */
/* background: linear-gradient(to bottom, #FFE7E1, #FFFFFF); */ /* background: linear-gradient(to bottom, #FFE7E1, #FFFFFF); */
/* background: linear-gradient( 359deg, rgba(255,255,255,0) 0%, #FDD9CA 100%); */ /* background: linear-gradient( 359deg, rgba(255,255,255,0) 0%, #FDD9CA 100%); */
background: url('https://static.hshuishang.com/property-img-file/page_user_Group_1584.png'); /* 与设计稿个人中心一致:顶部柔和五彩渐变(薄荷绿/粉/淡紫低饱和叠加)向下淡出到灰底 */
background-size: contain; background:
radial-gradient(120% 56% at 12% 0%, rgba(150, 222, 210, 0.62) 0%, rgba(150, 222, 210, 0) 52%),
radial-gradient(110% 52% at 52% 2%, rgba(255, 188, 210, 0.55) 0%, rgba(255, 188, 210, 0) 56%),
radial-gradient(120% 58% at 96% 0%, rgba(200, 176, 242, 0.6) 0%, rgba(200, 176, 242, 0) 56%),
linear-gradient(180deg, rgba(244, 244, 246, 0) 0%, #f4f4f6 540rpx),
#f4f4f6;
background-repeat: no-repeat; background-repeat: no-repeat;
background-attachment: fixed; background-attachment: fixed;
} }
@ -85,10 +90,12 @@ page {
.login-btn { .login-btn {
font-size: 36rpx; font-size: 36rpx;
pointer-events: auto; pointer-events: auto;
color: #1a1a1a;
} }
.avater_mobile { .avater_mobile {
margin-top: 10rpx; margin-top: 10rpx;
color: #666;
} }
.center { .center {

View File

@ -192,17 +192,17 @@ export default {
], ],
visitList: [{ visitList: [{
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1572.png", image: "https://static.hshuishang.com/property-img-file/page_user_Group_1572.png",
name: "到家服务订单", name: "服务订单",
url: "/packages/homeService/myOrders/index", url: "/packages/homeService/myOrders/index",
}, },
{ {
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1573.png", image: "https://static.hshuishang.com/property-img-file/page_user_Group_1573.png",
name: "我的月账单", name: "月账单",
url: "/packages/homeService/myMonthlyBills/index", url: "/packages/homeService/myMonthlyBills/index",
}, },
{ {
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1574.png", image: "https://static.hshuishang.com/property-img-file/page_user_Group_1574.png",
name: "我的家政合同", name: "家政合同",
url: "/packages/homeService/myContracts/index", url: "/packages/homeService/myContracts/index",
} }
], ],

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB