refactor(pages): 重组社区模块页面结构

main
cr 2026-04-22 23:49:41 +08:00
parent 749af59a5d
commit 055dfcf3fd
16 changed files with 2464 additions and 98 deletions

View File

@ -80,17 +80,7 @@
"group": "物业管理"
}
},
{
"path": "pages/notice/detail",
"style": {
"navigationBarTitleText": "通知公告"
},
"meta": {
"sync": true,
"title": "通知公告",
"group": "物业管理"
}
},
{
"path": "pages/index/search",
"style": {
@ -522,18 +512,6 @@
"title": "申请提现",
"group": "分销商城"
}
},
{
"path": "message",
"style": {
"navigationBarTitleText": "消息通知"
},
"meta": {
"auth": true,
"sync": true,
"title": "消息通知",
"group": "分销商城"
}
}
]
},
@ -602,9 +580,9 @@
]
},
{
"root": "pages/community",
"root": "pages/sub",
"pages": [{
"path": "dynamics",
"path": "community/dynamics",
"style": {
"navigationBarTitleText": "社区动态"
},
@ -613,6 +591,73 @@
"title": "社区动态",
"group": "物业管理"
}
},
{
"path": "notice/detail",
"style": {
"navigationBarTitleText": "通知公告"
},
"meta": {
"sync": true,
"title": "通知公告",
"group": "物业管理"
}
},
{
"path": "activity/list",
"style": {
"navigationBarTitleText": "小区活动"
},
"meta": {
"sync": true,
"title": "小区活动",
"group": "物业管理"
}
},
{
"path": "activity/detail",
"style": {
"navigationBarTitleText": "小区活动"
},
"meta": {
"sync": true,
"title": "小区活动",
"group": "物业管理"
}
},
{
"path": "staff/index",
"style": {
"navigationBarTitleText": "物业人员"
},
"meta": {
"sync": true,
"title": "物业人员",
"group": "物业管理"
}
},
{
"path": "knowledge/classroom",
"style": {
"navigationBarTitleText": "知识课堂"
},
"meta": {
"sync": true,
"title": "知识课堂",
"group": "物业管理"
}
},
{
"path": "community/daily",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
},
"meta": {
"sync": true,
"title": "物业日常",
"group": "物业管理"
}
}
]
},

View File

@ -43,12 +43,12 @@
/* 导航栏 */
.navbar {
width: 100%;
height: 88rpx;
background-color: transparent;
display: flex;
align-items: center;
justify-content: center;
padding-top: env(safe-area-inset-top);
padding-top: calc(env(safe-area-inset-top) + 44rpx);
height: calc(44rpx + env(safe-area-inset-top));
.navbar-content {
width: 100%;

View File

@ -125,12 +125,12 @@ const bannerList = ref([
//
const functionList = ref([
{ label: '工作计划', icon: '/static/img/Group_1.png', bgGradient: 'linear-gradient(35deg, #FF7F69 0%, #FC5A5D 100%)', path: '/pages/community/dynamics' },
{ label: '业主投票', icon: '/static/img/Group_2.png', bgGradient: 'linear-gradient(35deg, #52C41A 0%, #36AD1A 100%)', path: '' },
{ label: '工作计划', icon: '/static/img/Group_1.png', bgGradient: 'linear-gradient(35deg, #FF7F69 0%, #FC5A5D 100%)', path: '/pages/sub/community/dynamics' },
{ label: '业主投票', icon: '/static/img/Group_2.png', bgGradient: 'linear-gradient(35deg, #52C41A 0%, #36AD1A 100%)', path: '/pages/sub/community/daily' },
{ label: '收益公示', icon: '/static/img/Group_3.png', bgGradient: 'linear-gradient(35deg, #FFA940 0%, #FA8C16 100%)', path: '' },
{ label: '在线缴费', icon: '/static/img/Group_4.png', bgGradient: 'linear-gradient(35deg, #4096FF 0%, #1890FF 100%)', path: '' },
{ label: '报事报修', icon: '/static/img/Group_5.png', bgGradient: 'linear-gradient(35deg, #69B1FF 0%, #4096FF 100%)', path: '' },
{ label: '物业人员', icon: '/static/img/Group_6.png', bgGradient: 'linear-gradient(35deg, #9254DE 0%, #722ED1 100%)', path: '' },
{ label: '物业人员', icon: '/static/img/Group_6.png', bgGradient: 'linear-gradient(35deg, #9254DE 0%, #722ED1 100%)', path: '/pages/sub/staff/index' },
{ label: '业委会组织', icon: '/static/img/Group_7.png', bgGradient: 'linear-gradient(35deg, #7B61FF 0%, #597EF7 100%)', path: '' },
{ label: '更多服务', icon: '/static/img/Group_8.png', bgGradient: 'linear-gradient(35deg, #36CFC9 0%, #13C2C2 100%)', path: '' },
]);
@ -152,7 +152,7 @@ const handleFunctionTap = (item) => {
//
const goNotice = () => {
uni.navigateTo({ url: '/pages/notice/detail?id=1' });
uni.navigateTo({ url: '/pages/sub/notice/detail?id=1' });
};
//
@ -166,22 +166,22 @@ const goCheck = () => {
//
const goLawClass = () => {
uni.navigateTo({ url: '/pages/public/richtext?title=法律小课堂' });
uni.navigateTo({ url: '/pages/sub/knowledge/classroom?tab=0', fail: (err) => { console.error('跳转失败', err); } });
};
//
const goCommunityGov = () => {
uni.navigateTo({ url: '/pages/public/richtext?title=小区治理' });
uni.navigateTo({ url: '/pages/sub/knowledge/classroom?tab=1', fail: (err) => { console.error('跳转失败', err); } });
};
//
const goActivityList = () => {
uni.navigateTo({ url: '/pages/activity/index' });
uni.navigateTo({ url: '/pages/sub/activity/list' });
};
//
const goActivityDetail = () => {
uni.navigateTo({ url: '/pages/activity/detail' });
uni.navigateTo({ url: '/pages/sub/activity/detail?id=1' });
};
</script>

View File

@ -35,7 +35,7 @@
<image class="right-icon" src="/static/img/right-icon.png" mode="aspectFit" />
</view>
<view class="divider"></view>
<view class="menu-item" @tap="navigateTo('/pages/commission/message')">
<view class="menu-item">
<view class="menu-item-left">
<image class="menu-icon" src="/static/img/me-icon2.png" mode="aspectFit" />
<text class="menu-label">消息通知</text>

View File

@ -0,0 +1,237 @@
<!-- 社区动态页面 -->
<template>
<s-layout title="社区动态" navbar="inner">
<view class="community-dynamics-page">
<!-- Tab 切换栏 -->
<view class="tab-bar">
<view
class="tab-item"
v-for="(tab, index) in tabList"
:key="index"
:class="{ active: currentTab === index }"
@tap="switchTab(index)"
>
<text class="tab-text">{{ tab }}</text>
<view class="tab-indicator" v-if="currentTab === index"></view>
</view>
</view>
<!-- 动态列表 -->
<view class="dynamics-list">
<view
class="dynamics-item"
v-for="(item, index) in dynamicsList"
:key="index"
@tap="goDetail(item)"
>
<!-- 封面图 -->
<image class="item-cover" :src="item.cover" mode="aspectFill" />
<!-- 内容区 -->
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<view class="item-footer">
<text class="item-views">{{ item.views }}</text>
<text class="item-date">{{ item.date }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="dynamicsList.length === 0">
<text class="empty-text">暂无数据</text>
</view>
</view>
</s-layout>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
// Tab
const tabList = ref(['全部', '物业', '业委会', '社区']);
const currentTab = ref(0);
//
const dynamicsList = ref([
{
id: 1,
title: '社区动态标题xxxx操作手册--如何邀请访客',
cover: '/static/img/guest.png',
views: '1.2万播放',
date: '2021/02/21'
},
{
id: 2,
title: '社区动态标题xxxx操作手册--如何邀请访客',
cover: '/static/img/guest.png',
views: '1.2万播放',
date: '2021/02/21'
},
{
id: 3,
title: '社区动态标题xxxx操作手册--如何邀请访客',
cover: '/static/img/guest.png',
views: '1.2万播放',
date: '2021/02/21'
},
{
id: 4,
title: '社区动态标题xxxx操作手册--如何邀请访客',
cover: '/static/img/guest.png',
views: '1.2万播放',
date: '2021/02/21'
}
]);
//
onLoad((options) => {
if (options.tab) {
currentTab.value = parseInt(options.tab);
}
loadDynamicsList();
});
// Tab
function switchTab(index) {
currentTab.value = index;
loadDynamicsList();
}
//
async function loadDynamicsList() {
// TODO: API
// const { code, data } = await DynamicsApi.getDynamicsList({ tab: currentTab.value });
// if (code === 0) {
// dynamicsList.value = data.list || [];
// }
}
//
function goDetail(item) {
uni.navigateTo({
url: `/pages/public/community/dynamics-detail?id=${item.id}`
});
}
</script>
<style lang="scss" scoped>
/* 页面容器 */
.community-dynamics-page {
min-height: 100vh;
background-color: #F5F5F5;
}
/* Tab 切换栏 */
.tab-bar {
display: flex;
align-items: center;
justify-content: space-around;
background-color: #FFFFFF;
padding: 24rpx 0;
position: sticky;
top: 0;
z-index: 10;
.tab-item {
position: relative;
padding: 8rpx 16rpx;
.tab-text {
font-size: 30rpx;
color: #999999;
transition: all 0.3s;
}
&.active .tab-text {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
background: linear-gradient(90deg, #FF7F69 0%, #FC5A5D 100%);
border-radius: 3rpx;
}
}
}
/* 动态列表 */
.dynamics-list {
padding: 24rpx 32rpx;
}
/* 动态项 */
.dynamics-item {
display: flex;
background-color: #FFFFFF;
border-radius: 24rpx;
padding: 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.item-cover {
width: 240rpx;
height: 180rpx;
border-radius: 16rpx;
flex-shrink: 0;
}
.item-content {
flex: 1;
margin-left: 24rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
.item-title {
font-size: 30rpx;
font-weight: 500;
color: #333333;
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.item-footer {
display: flex;
align-items: center;
justify-content: space-between;
.item-views {
font-size: 24rpx;
color: #999999;
}
.item-date {
font-size: 24rpx;
color: #999999;
}
}
}
}
/* 空状态 */
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
</style>

View File

@ -0,0 +1,472 @@
<!-- 小区活动详情页 -->
<template>
<s-layout title="小区活动">
<view class="activity-detail-page">
<!-- 顶部Banner轮播 -->
<swiper class="banner-swiper" :autoplay="true" :interval="3000" :circular="true" indicator-dots indicator-color="rgba(255,255,255,0.4)" active-color="#FFFFFF">
<swiper-item v-for="(img, index) in activityInfo.banners" :key="index">
<image class="banner-img" :src="img" mode="aspectFill" />
</swiper-item>
</swiper>
<!-- Tab 切换栏 -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: currentTab === 'basic' }"
@tap="currentTab = 'basic'"
>
<text class="tab-text">基本信息</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'intro' }"
@tap="currentTab = 'intro'"
>
<text class="tab-text">活动简介</text>
</view>
</view>
<!-- 内容区域 - 独立滚动 -->
<scroll-view class="detail-scroll" scroll-y :style="{ height: scrollViewHeight + 'px' }">
<view class="detail-content">
<!-- 基本信息 -->
<template v-if="currentTab === 'basic'">
<!-- 活动标题 -->
<view class="activity-title">{{ activityInfo.title }}</view>
<!-- 地址 -->
<view class="info-row location-row">
<text class="sicon-location"></text>
<text class="location-text">{{ activityInfo.location }}</text>
<text class="sicon-right navigation-icon" @tap="openNavigation"></text>
</view>
<!-- 服务类别 -->
<view class="info-row tag-row">
<text class="label-text">服务类别</text>
<view class="tag-list">
<text
class="tag-item"
v-for="(tag, index) in activityInfo.serviceTypes"
:key="index"
>{{ tag }}</text>
</view>
</view>
<!-- 服务对象 -->
<view class="info-row tag-row">
<text class="label-text">服务对象</text>
<view class="tag-list">
<text
class="tag-item target-tag"
v-for="(tag, index) in activityInfo.targetAudience"
:key="index"
>{{ tag }}</text>
</view>
</view>
<!-- 报名日期 -->
<view class="info-row text-row">
<text class="label-text">报名日期</text>
<text class="value-text">{{ activityInfo.registerDate }}</text>
</view>
<!-- 活动日期 -->
<view class="info-row text-row">
<text class="label-text">活动日期</text>
<text class="value-text">{{ activityInfo.activityDate }}</text>
</view>
<!-- 人数上限 -->
<view class="info-row text-row">
<text class="sicon-person"></text>
<text class="label-text">人数上限</text>
<text class="value-text">{{ activityInfo.maxPeople }}</text>
</view>
<!-- 联系人 -->
<view class="info-row contact-row" @tap="callPhone">
<text class="sicon-phone"></text>
<text class="contact-name">{{ activityInfo.contactName }}</text>
<text class="contact-phone">{{ activityInfo.contactPhone }}</text>
</view>
</template>
<!-- 活动简介 -->
<template v-if="currentTab === 'intro'">
<view class="intro-content">
<mp-html :content="activityInfo.introduction"></mp-html>
</view>
</template>
<!-- 底部占位防止内容被底部按钮遮挡 -->
<view class="bottom-placeholder"></view>
</view>
</scroll-view>
<!-- 底部固定按钮栏 -->
<view class="bottom-bar">
<view class="registered-btn" @tap="showRegisteredList">
<text class="registered-text">已有{{ activityInfo.registeredCount }}人报名</text>
</view>
<view class="back-btn" @tap="goBack">
<text class="back-text">返回</text>
</view>
</view>
</view>
</s-layout>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
// Tab
const currentTab = ref('basic');
// px
const scrollViewHeight = ref(0);
//
const activityInfo = ref({
id: null,
title: '',
banners: [],
location: '',
serviceTypes: [],
targetAudience: [],
registerDate: '',
activityDate: '',
maxPeople: 0,
contactName: '',
contactPhone: '',
registeredCount: 0,
introduction: ''
});
//
onLoad((options) => {
if (options.id) {
loadActivityDetail(options.id);
}
});
// scroll-view
onMounted(() => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
uni.createSelectorQuery()
.in(instance)
.select('.tab-bar')
.boundingClientRect((rect) => {
if (rect) {
// scroll-view = - tab - tab - (120rpx)
scrollViewHeight.value = sysInfo.windowHeight - rect.height - rect.top - 60;
}
})
.exec();
});
});
//
async function loadActivityDetail(id) {
// TODO: API
//
activityInfo.value = {
id: id,
title: 'xxxxxxxxxxxxxxxxx活动',
banners: [
'/static/img/guest.png',
'/static/img/login_img.png'
],
location: '福清市音西街道',
serviceTypes: ['社区服务', '敬老服务', '助残服务', '关爱儿童'],
targetAudience: ['儿童', '孤寡老人', '残障人士', '优抚对象'],
registerDate: '2025/06/30-2025/07/01',
activityDate: '2025/07/05 12:00-2025/07/05 18:00',
maxPeople: 100,
contactName: '王德福',
contactPhone: '13322471131',
registeredCount: 37,
introduction: `<p>本次活动旨在丰富社区居民文化生活,增进邻里关系,共建和谐社区。</p><p>欢迎各位居民踊跃参与!</p>`
};
}
//
function openNavigation() {
// TODO:
}
//
function callPhone() {
if (activityInfo.value.contactPhone) {
uni.makePhoneCall({
phoneNumber: activityInfo.value.contactPhone
});
}
}
//
function showRegisteredList() {
// TODO:
uni.showToast({
title: '查看报名名单',
icon: 'none'
});
}
//
function goBack() {
uni.navigateBack();
}
</script>
<style lang="scss" scoped>
/* 页面容器 */
.activity-detail-page {
position: relative;
min-height: 100vh;
}
/* 顶部Banner轮播 */
.banner-swiper {
width: 100%;
height: 400rpx;
}
.banner-img {
width: 100%;
height: 400rpx;
display: block;
}
/* Tab 切换栏 */
.tab-bar {
display: flex;
align-items: center;
background-color: #FFFFFF;
padding: 0 32rpx;
border-bottom: 1rpx solid #F0F0F0;
.tab-item {
flex: 1;
text-align: center;
padding: 28rpx 0;
position: relative;
.tab-text {
font-size: 30rpx;
color: #999999;
transition: all 0.3s;
}
&.active {
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 120rpx;
height: 4rpx;
background: #FA7E49;
border-radius: 2rpx;
}
}
&.active .tab-text {
font-size: 30rpx;
font-weight: 600;
color: #333333;
}
}
}
/* 内容滚动区域 */
.detail-scroll {
/* 高度由JS动态绑定 */
}
/* 详情内容 */
.detail-content {
background-color: #FFFFFF;
}
/* 活动标题 */
.activity-title {
padding: 32rpx 32rpx 16rpx;
font-size: 34rpx;
font-weight: 600;
color: #FA7E49;
line-height: 1.5;
}
/* 信息行 */
.info-row {
display: flex;
align-items: flex-start;
padding: 20rpx 32rpx;
.sicon-location,
.sicon-phone,
.sicon-person {
font-size: 32rpx;
color: #666666;
margin-right: 8rpx;
margin-top: 2rpx;
}
}
/* 地址行 */
.location-row {
align-items: center;
.location-text {
font-size: 28rpx;
color: #333333;
flex: 1;
}
.navigation-icon {
font-size: 36rpx;
color: #FA7E49;
flex-shrink: 0;
}
}
/* 标签行 */
.tag-row {
align-items: flex-start;
.label-text {
font-size: 28rpx;
color: #333333;
flex-shrink: 0;
line-height: 1.8;
}
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
flex: 1;
}
.tag-item {
font-size: 24rpx;
color: #FA7E49;
border: 1rpx solid #FA7E49;
border-radius: 8rpx;
padding: 6rpx 16rpx;
background-color: rgba(250, 126, 73, 0.04);
line-height: 1.4;
}
.target-tag {
color: #FA7E49;
border: 1rpx solid #FA7E49;
}
/* 文本行 */
.text-row {
.label-text {
font-size: 28rpx;
color: #333333;
flex-shrink: 0;
}
.value-text {
font-size: 28rpx;
color: #333333;
}
}
/* 联系人行 */
.contact-row {
align-items: center;
&:active {
opacity: 0.7;
}
.contact-name {
font-size: 28rpx;
color: #333333;
margin-left: 8rpx;
margin-right: 24rpx;
}
.contact-phone {
font-size: 28rpx;
color: #333333;
}
}
/* 活动简介内容 */
.intro-content {
padding: 32rpx;
}
/* 底部占位 */
.bottom-placeholder {
height: 140rpx;
}
/* 底部固定按钮栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
gap: 24rpx;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #FFFFFF;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
.registered-btn {
flex: 1;
height: 88rpx;
border: 2rpx solid #FA7E49;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.85;
}
.registered-text {
font-size: 30rpx;
color: #FA7E49;
font-weight: 500;
}
}
.back-btn {
width: 240rpx;
height: 88rpx;
background-color: #FA7E49;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.85;
}
.back-text {
font-size: 30rpx;
color: #FFFFFF;
font-weight: 500;
}
}
}
</style>

View File

@ -0,0 +1,331 @@
<!-- 小区活动列表页 -->
<template>
<s-layout title="小区活动">
<view class="activity-list-page">
<!-- 渐变背景装饰 -->
<view class="gradient-bg"></view>
<!-- 固定头部区域 -->
<view class="list-header">
<!-- 顶部提示条 -->
<view class="top-banner">
<text class="banner-text">融侨馨苑开展活动啦</text>
</view>
</view>
<!-- 活动列表 - 独立滚动 -->
<scroll-view class="activity-scroll" scroll-y :style="{ height: scrollViewHeight + 'px' }">
<view class="list-inner">
<view
class="activity-item"
v-for="(item, index) in activityList"
:key="index"
@tap="goDetail(item)"
>
<!-- 左侧封面图 -->
<image class="item-cover" :src="item.cover" mode="aspectFill" />
<!-- 右侧内容区 -->
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<view class="item-row">
<text class="item-location">{{ item.location }}</text>
<text class="item-status" :class="'status-' + item.statusType">{{ item.statusText }}</text>
</view>
<view class="item-row">
<text class="item-label">报名日期</text>
<text class="item-value">{{ item.registerDate }}</text>
</view>
<view class="item-row">
<text class="item-date">{{ item.dateRange }}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="activityList.length === 0">
<text class="empty-text">暂无活动</text>
</view>
</view>
</scroll-view>
<!-- 右下角浮动按钮 -->
<view class="float-btn" @tap="handleFloatBtn">
<text class="sicon-edit"></text>
</view>
</view>
</s-layout>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
// px
const scrollViewHeight = ref(0);
//
const activityList = ref([
{
id: 1,
title: 'xxxxxxxxxxxxx活动',
location: '福清市音西街道融侨馨苑',
statusText: '进行中',
statusType: 'ongoing',
registerDate: '2025/06/30-2025/07/01',
dateRange: '2025/07/05 12:00-2025/07/05 18:00',
cover: '/static/img/guest.png'
},
{
id: 2,
title: 'xxxxxxxxxxxxx活动',
location: '福清市音西街道融侨馨苑',
statusText: '报名中',
statusType: 'registering',
registerDate: '2025/06/30-2025/07/01',
dateRange: '2025/07/05 12:00-2025/07/05 18:00',
cover: '/static/img/guest.png'
},
{
id: 3,
title: 'xxxxxxxxxxxxx活动',
location: '福清市音西街道融侨馨苑',
statusText: '已结束',
statusType: 'ended',
registerDate: '2025/06/30-2025/07/01',
dateRange: '2025/07/05 12:00-2025/07/05 18:00',
cover: '/static/img/guest.png'
},
{
id: 4,
title: 'xxxxxxxxxxxxx活动',
location: '福清市音西街道融侨馨苑',
statusText: '已结束',
statusType: 'ended',
registerDate: '2025/06/30-2025/07/01',
dateRange: '2025/07/05 12:00-2025/07/05 18:00',
cover: '/static/img/guest.png'
},
{
id: 3,
title: 'xxxxxxxxxxxxx活动',
location: '福清市音西街道融侨馨苑',
statusText: '已结束',
statusType: 'ended',
registerDate: '2025/06/30-2025/07/01',
dateRange: '2025/07/05 12:00-2025/07/05 18:00',
cover: '/static/img/guest.png'
},
{
id: 4,
title: 'xxxxxxxxxxxxx活动',
location: '福清市音西街道融侨馨苑',
statusText: '已结束',
statusType: 'ended',
registerDate: '2025/06/30-2025/07/01',
dateRange: '2025/07/05 12:00-2025/07/05 18:00',
cover: '/static/img/guest.png'
}
]);
//
onLoad(() => {
loadActivityList();
});
// scroll-view
onMounted(() => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
uni.createSelectorQuery()
.in(instance)
.select('.list-header')
.boundingClientRect((rect) => {
if (rect) {
// scroll-view = - -
scrollViewHeight.value = sysInfo.windowHeight - rect.height - rect.top;
}
})
.exec();
});
});
//
async function loadActivityList() {
// TODO: API
}
//
function goDetail(item) {
uni.navigateTo({
url: `/pages/sub/activity/detail?id=${item.id}`
});
}
//
function handleFloatBtn() {
// TODO:
}
</script>
<style lang="scss" scoped>
/* 页面容器 - flex布局头部固定 + 列表滚动 */
.activity-list-page {
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
}
/* 渐变背景装饰 - 覆盖导航栏到内容区 */
.gradient-bg {
position: absolute;
top: -176rpx; /* 向上延伸覆盖inner-navbar */
left: 0;
right: 0;
height: calc(100% + 176rpx);
background: linear-gradient(180deg, #F8EDE8 0%, #FFFFFF 30%);
z-index: -1; /* 在内容层下方 */
}
/* 固定头部区域 */
.list-header {
flex-shrink: 0; /* 不被压缩,固定不动 */
}
/* 顶部提示条 */
.top-banner {
padding: 24rpx 32rpx;
.banner-text {
font-size: 28rpx;
color: #FA7E49;
font-weight: 500;
}
}
/* 活动列表滚动区域 - 高度由JS动态绑定 */
.activity-scroll {
flex: 1; /* 占据剩余空间 */
}
/* 列表内容包裹层 */
.list-inner {
padding: 0 32rpx 160rpx; /* 底部留出空间给浮动按钮 */
}
/* 活动卡片 */
.activity-item {
display: flex;
background-color: #FFFFFF;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.item-cover {
width: 220rpx;
height: 180rpx;
border-radius: 12rpx;
flex-shrink: 0;
}
.item-content {
flex: 1;
margin-left: 24rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
.item-title {
font-size: 30rpx;
font-weight: 500;
color: #FA7E49;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.item-row {
display: flex;
align-items: center;
margin-top: 8rpx;
}
.item-location {
font-size: 26rpx;
color: #333333;
flex: 1;
}
//
.item-status {
font-size: 26rpx;
font-weight: 500;
flex-shrink: 0;
&.status-ongoing { color: #52C41A; } // -绿
&.status-registering { color: #FAAD14; } // -
&.status-ended { color: #999999; } // -
}
.item-label {
font-size: 24rpx;
color: #666666;
}
.item-value {
font-size: 24rpx;
color: #333333;
}
.item-date {
font-size: 24rpx;
color: #666666;
}
}
}
/* 空状态 */
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
/* 右下角浮动按钮 */
.float-btn {
position: fixed;
right: 40rpx;
bottom: 80rpx;
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #FA7E49 0%, #E86935 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(250, 126, 73, 0.4);
z-index: 99;
&:active {
opacity: 0.85;
transform: scale(0.95);
}
.sicon-edit {
font-size: 44rpx;
color: #FFFFFF;
}
}
</style>

View File

@ -0,0 +1,302 @@
<!-- 物业日常 - 打卡记录页面 -->
<template>
<s-layout title="物业日常" :bgStyle="{ backgroundColor:'#F8EDE8' }" navbar="inner" color="#333333">
<view class="daily-page">
<!-- 渐变背景装饰 -->
<view class="gradient-bg"></view>
<!-- 固定头部区域搜索栏 -->
<view class="page-header">
<view class="search-wrapper">
<view class="search-box">
<image class="search-icon" src="/static/img/guest.png" mode="aspectFit" />
<input class="search-input" type="text" placeholder="搜索" placeholder-class="search-placeholder" />
</view>
</view>
</view>
<!-- 网格内容区 - 独立滚动 -->
<scroll-view scroll-y class="content-scroll" :style="{ height: scrollHeight + 'px' }" @scrolltolower="loadMore">
<view class="grid-container">
<view
v-for="(item, index) in dataList"
:key="item.id"
class="card-item"
@tap="handleCardTap(item)"
>
<!-- 图片区域 + 底部水印遮罩 -->
<view class="card-img-wrap">
<image class="card-img" :src="item.image" mode="aspectFill" />
<!-- 底部渐变遮罩 + 打卡信息 -->
<view class="card-overlay">
<view class="overlay-info">
<!-- 第一行打卡人姓名 + 时间 -->
<view class="info-row row-name">
<image class="row-icon" src="/static/img/login_img.png" mode="aspectFill" />
<text class="info-name">{{ item.userName }}</text>
<text class="info-time">{{ item.time }}</text>
</view>
<!-- 第二行打卡地点 -->
<view class="info-row row-addr">
<image class="row-icon" src="/static/img/guest.png" mode="aspectFit" />
<text class="info-addr">{{ item.address }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 右下角悬浮相机按钮 - 仅物业人员可见 -->
<view v-if="isStaff" class="fab-camera" @tap="handleCameraTap">
<image class="camera-img" src="/static/img/login_img.png" mode="aspectFit" />
</view>
</view>
</s-layout>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
// ==================== ====================
// scroll-view px
const scrollHeight = ref(0);
// true=false=
const isStaff = ref(true);
//
const dataList = ref([
{ id: 1, image: 'https://picsum.photos/400/500?random=1', userName: '吴仁贵', time: '2026/01/08 14:52', address: '融侨馨苑12号楼' },
{ id: 2, image: 'https://picsum.photos/400/480?random=2', userName: '张伟', time: '2026/01/08 15:20', address: '融侨馨苑3号楼' },
{ id: 3, image: 'https://picsum.photos/400/520?random=3', userName: '李明', time: '2026/01/08 16:05', address: '融侨馨苑8号楼' },
{ id: 4, image: 'https://picsum.photos/400/490?random=4', userName: '王芳', time: '2026/01/09 09:10', address: '融侨馨苑5号楼' },
{ id: 5, image: 'https://picsum.photos/400/510?random=5', userName: '赵强', time: '2026/01/09 11:30', address: '融侨馨苑11号楼' },
{ id: 6, image: 'https://picsum.photos/400/500?random=6', userName: '吴仁贵', time: '2026/01/09 14:00', address: '融侨馨苑12号楼' }
]);
// ==================== ====================
onMounted(() => {
calcScrollHeight();
});
// ==================== ====================
/** 动态计算 scroll-view 高度 */
function calcScrollHeight() {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
uni.createSelectorQuery()
.in(instance)
.select('.page-header')
.boundingClientRect((rect) => {
if (rect) {
scrollHeight.value = sysInfo.windowHeight - rect.height - rect.top;
}
})
.exec();
});
}
/** 点击卡片 - 查看详情 */
function handleCardTap(item) {
console.log('查看打卡详情:', item);
uni.showToast({ title: `查看${item.userName}的打卡记录`, icon: 'none' });
}
/** 物业人员拍照打卡 */
function handleCameraTap() {
//
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: (res) => {
const tempPath = res.tempFilePaths[0];
// TODO:
uni.showLoading({ title: '上传中...' });
setTimeout(() => {
uni.hideLoading();
uni.showToast({ title: '打卡成功', icon: 'success' });
//
}, 1500);
},
fail: () => {
//
}
});
}
/** 加载更多 */
function loadMore() {
console.log('加载更多打卡记录');
}
</script>
<style lang="scss" scoped>
/* 页面容器 */
.daily-page {
position: relative;
min-height: 100vh;
}
/* 渐变背景 */
.gradient-bg {
position: absolute;
top: -176rpx;
left: 0;
right: 0;
height: calc(100% + 176rpx);
background: linear-gradient(180deg, #F8EDE8 0%, #FFFFFF 30%);
z-index: -1;
}
/* 固定头部(搜索栏) */
.page-header {
flex-shrink: 0;
}
/* 搜索栏 */
.search-wrapper {
padding: 24rpx 32rpx;
.search-box {
display: flex;
align-items: center;
height: 72rpx;
background-color: #FFFFFF;
border-radius: 36rpx;
padding: 0 28rpx;
.search-icon {
width: 32rpx;
height: 32rpx;
margin-right: 16rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333333;
}
:deep(.search-placeholder) {
color: #999999;
font-size: 28rpx;
}
}
}
/* 列表滚动区域 */
.content-scroll {
width: 100%;
}
/* 双列网格容器 */
.grid-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
padding: 0 24rpx 160rpx;
}
/* 卡片 */
.card-item {
background-color: #FFFFFF;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
/* 图片容器 */
.card-img-wrap {
width: 100%;
height: 420rpx;
position: relative;
.card-img {
width: 100%;
height: 100%;
display: block;
}
/* 底部渐变遮罩层 */
.card-overlay {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 16rpx 14rpx;
background: linear-gradient(to top, rgba(0, 0, 0, 0.65) 0%, transparent 100%);
}
}
}
/* 遮罩内信息区 */
.overlay-info {
.info-row {
display: flex;
align-items: center;
&:not(:last-child) {
margin-bottom: 8rpx;
}
.row-icon {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
margin-right: 8rpx;
flex-shrink: 0;
}
}
/* 第一行:姓名 + 时间 */
.row-name {
.info-name {
font-size: 24rpx;
font-weight: 500;
color: #FFFFFF;
margin-right: 12rpx;
}
.info-time {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.7);
}
}
/* 第二行:地址 */
.row-addr {
.info-addr {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.8);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
/* 右下角悬浮相机按钮 - 仅物业人员可见 */
.fab-camera {
position: fixed;
right: 40rpx;
bottom: 120rpx;
width: 112rpx;
height: 112rpx;
background-color: #FFFFFF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.15);
.camera-img {
width: 48rpx;
height: 48rpx;
}
}
</style>

View File

@ -1,6 +1,8 @@
<!-- 社区动态页面 -->
<template>
<s-layout title="社区动态" :bgStyle="{ backgroundColor:'#F8EDE8' }" navbar="inner" color="#333333">
<s-layout title="社区动态" navbar="inner" color="#333333">
<!-- 渐变背景 -->
<view class="gradient-bg"></view>
<view class="community-dynamics-page">
<!-- Tab 切换栏 -->
@ -154,25 +156,28 @@ async function loadDynamicsList() {
//
function goDetail(item) {
uni.navigateTo({
url: `/pages/community/dynamics-detail?id=${item.id}`
url: `/pages/sub/community/dynamics-detail?id=${item.id}`
});
}
</script>
<style lang="scss" scoped>
/* 页面容器 - 伪元素渐变向上覆盖到导航栏 */
/* 渐变背景 */
.gradient-bg {
position: fixed;
top: 0;
left: 0;
width: 750rpx;
height: 660rpx;
background: linear-gradient(180deg, #F8EDE8 0%, #F5F5F5 50%);
z-index: 0;
pointer-events: none;
}
/* 页面容器 */
.community-dynamics-page {
position: relative;
&::before {
// content: '';
// position: absolute;
// left: 0;
// right: 0;
// height: calc(100% + 176rpx);
background: linear-gradient(180deg, #F8EDE8 0%, #FFFFFF 30%);
// z-index: -1;
}
z-index: 1;
}
/* Tab 切换栏 */

View File

@ -0,0 +1,396 @@
<!-- 知识课堂页面 -->
<template>
<s-layout title="知识课堂" navbar="inner" color="#333333">
<!-- 渐变背景 -->
<view class="gradient-bg"></view>
<view class="knowledge-classroom-page">
<!-- Tab 切换栏 -->
<view class="tab-bar">
<view
class="tab-item"
v-for="(tab, index) in tabList"
:key="index"
:class="{ active: currentTab === index }"
@tap="switchTab(index)"
>
<text class="tab-text">{{ tab }}</text>
<view class="tab-indicator" v-if="currentTab === index"></view>
</view>
</view>
<!-- 列表区域 - 独立滚动区域 -->
<scroll-view class="classroom-list" scroll-y :style="{ height: scrollViewHeight + 'px' }">
<view class="list-inner" :class="{ 'grid-layout': currentTab === 1 }">
<view
class="classroom-item"
v-for="(item, index) in classroomList"
:key="index"
@tap="goDetail(item)"
>
<!-- 封面图 -->
<view class="item-cover-wrap">
<image class="item-cover" :src="item.cover" mode="aspectFill" />
<view class="play-icon" v-if="currentTab === 1">
<image class="play-img" src="/static/img/play-icon.png" mode="aspectFit" />
</view>
</view>
<!-- 内容区 -->
<view class="item-content">
<text class="item-title">{{ item.title }}</text>
<view class="item-footer" v-if="currentTab === 0">
<text class="item-date">{{ item.date }}</text>
</view>
<view class="item-footer" v-else>
<view class="item-stat">
<text class="stat-icon">👁</text>
<text class="stat-text">{{ item.views }}</text>
</view>
<view class="item-stat">
<text class="stat-icon"></text>
<text class="stat-text">{{ item.likes }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="classroomList.length === 0">
<text class="empty-text">暂无数据</text>
</view>
</view>
</scroll-view>
</view>
</s-layout>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
// pxJSscroll-view
const scrollViewHeight = ref(0);
// Tab
const tabList = ref(['法律小课堂', '小区治理']);
const currentTab = ref(0);
//
const classroomList = ref([]);
//
onLoad((options) => {
if (options.tab) {
currentTab.value = parseInt(options.tab);
}
loadClassroomList();
});
// scroll-view
onMounted(() => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
// tab
uni.createSelectorQuery()
.in(instance)
.select('.tab-bar')
.boundingClientRect((rect) => {
if (rect) {
// scroll-view = - tab - tab
scrollViewHeight.value = sysInfo.windowHeight - rect.height - rect.top;
}
})
.exec();
});
});
// Tab
function switchTab(index) {
currentTab.value = index;
loadClassroomList();
}
//
async function loadClassroomList() {
// TODO: API
//
if (currentTab.value === 0) {
// -
classroomList.value = [
{
id: 1,
title: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX法规解读',
cover: '/static/img/guest.png',
date: '2025/07/05 12:00'
},
{
id: 2,
title: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX法规解读',
cover: '/static/img/guest.png',
date: '2025/07/05 12:00'
},
{
id: 3,
title: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX法规解读',
cover: '/static/img/guest.png',
date: '2025/07/05 12:00'
},
{
id: 4,
title: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX法规解读',
cover: '/static/img/guest.png',
date: '2025/07/05 12:00'
}
];
} else {
// -
classroomList.value = [
{
id: 1,
title: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX视频',
cover: '/static/img/guest.png',
views: '154',
likes: '154'
},
{
id: 2,
title: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX视频',
cover: '/static/img/guest.png',
views: '154',
likes: '154'
},
{
id: 3,
title: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX视频',
cover: '/static/img/guest.png',
views: '154',
likes: '154'
},
{
id: 4,
title: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX视频',
cover: '/static/img/guest.png',
views: '154',
likes: '154'
},
{
id: 5,
title: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX视频',
cover: '/static/img/guest.png',
views: '154',
likes: '154'
},
{
id: 6,
title: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX视频',
cover: '/static/img/guest.png',
views: '154',
likes: '154'
}
];
}
}
//
function goDetail(item) {
uni.navigateTo({
url: `/pages/sub/knowledge/detail?id=${item.id}&tab=${currentTab.value}`
});
}
</script>
<style lang="scss" scoped>
/* 渐变背景 */
.gradient-bg {
position: fixed;
top: 0;
left: 0;
width: 750rpx;
height: 660rpx;
background: linear-gradient(180deg, #F8EDE8 0%, #F5F5F5 50%);
z-index: 0;
pointer-events: none;
}
/* 页面容器 */
.knowledge-classroom-page {
position: relative;
z-index: 1;
}
/* Tab 切换栏 */
.tab-bar {
display: flex;
align-items: center;
justify-content: space-around;
padding: 36rpx 50rpx;
.tab-item {
position: relative;
padding: 8rpx 16rpx;
.tab-text {
font-size: 30rpx;
color: #999999;
transition: all 0.3s;
}
&.active .tab-text {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
background: linear-gradient(90deg, #FF7F69 0%, #FC5A5D 100%);
border-radius: 3rpx;
}
}
}
/* 列表 - scroll-view高度由JS动态绑定 */
.classroom-list {
/* 高度由 :style="{ height: scrollViewHeight + 'px' }" 控制 */
}
/* 列表内容包裹层 - 负责padding间距 */
.list-inner {
padding: 24rpx 32rpx;
&.grid-layout {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
}
/* 知识课堂项 */
.classroom-item {
display: flex;
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 19rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.grid-layout & {
width: 335rpx;
flex-direction: column;
padding: 0;
overflow: hidden;
}
.item-cover-wrap {
position: relative;
flex-shrink: 0;
.grid-layout & {
width: 100%;
}
.item-cover {
width: 305rpx;
height: 168rpx;
border-radius: 12rpx;
.grid-layout & {
width: 100%;
height: 200rpx;
border-radius: 0;
}
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80rpx;
height: 80rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.play-img {
width: 40rpx;
height: 40rpx;
}
}
}
.item-content {
flex: 1;
margin-left: 24rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
.grid-layout & {
margin-left: 0;
padding: 16rpx;
}
.item-title {
font-size: 28rpx;
font-weight: 500;
color: #333333;
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.item-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12rpx;
.item-date {
font-size: 24rpx;
color: #999999;
}
.item-stat {
display: flex;
align-items: center;
.stat-icon {
font-size: 24rpx;
margin-right: 6rpx;
}
.stat-text {
font-size: 24rpx;
color: #999999;
}
}
}
}
}
/* 空状态 */
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
</style>

View File

@ -1,10 +1,12 @@
<!-- 通知公告详情页 -->
<template>
<s-layout title="通知公告" :bgStyle="{ backgroundColor:'#F8EDE8' }" navbar="inner" color="#333333">
<!-- 页面主体flex布局头部固定+内容滚动 -->
<view class="notice-detail-page">
<s-layout title="通知公告" navbar="inner" color="#333333">
<!-- 渐变背景 -->
<view class="gradient-bg"></view>
<!-- 固定头部区域 -->
<view class="detail-header">
<view class="header-inner">
<!-- 文章标题 -->
<view class="article-title">
<text class="title-text">{{ noticeInfo.title }}</text>
@ -22,20 +24,22 @@
<!-- 分割线 -->
<view class="divider"></view>
</view>
</view>
<!-- 富文本内容 - 独立滚动区域 -->
<scroll-view class="article-scroll" scroll-y :style="{ height: contentScrollHeight + 'px' }">
<scroll-view class="article-scroll" scroll-y :style="{ height: contentScrollHeight + 'px', top: contentScrollTop + 'px' }">
<view class="article-content">
<view class="article-inner">
<mp-html :content="noticeInfo.content"></mp-html>
</view>
</view>
<!-- 占位防止内容被底部附件遮挡 -->
<view class="bottom-placeholder" v-if="noticeInfo.attachments && noticeInfo.attachments.length > 0"></view>
</scroll-view>
</view>
<!-- 附件列表 - 固定在底部 -->
<view class="attachment-section" v-if="noticeInfo.attachments && noticeInfo.attachments.length > 0">
<view class="attachment-inner">
<view
class="attachment-item"
v-for="(item, index) in noticeInfo.attachments"
@ -49,6 +53,7 @@
<image class="download-icon" src="/static/img/right-icon.png" mode="aspectFit" />
</view>
</view>
</view>
</s-layout>
</template>
@ -59,6 +64,8 @@ import sheep from '@/sheep';
// px
const contentScrollHeight = ref(0);
// px
const contentScrollTop = ref(0);
//
const noticeInfo = ref({
@ -76,21 +83,34 @@ onLoad((options) => {
}
});
// scroll-view
// scroll-view
onMounted(() => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
uni.createSelectorQuery()
.in(instance)
.select('.detail-header')
.boundingClientRect((rect) => {
if (rect) {
// scroll-view = - -
contentScrollHeight.value = sysInfo.windowHeight - rect.height - rect.top;
const query = uni.createSelectorQuery().in(instance);
//
query.select('.detail-header').boundingClientRect();
//
query.select('.attachment-section').boundingClientRect();
query.exec((res) => {
const headerRect = res[0];
const attachmentRect = res[1];
let attachmentHeight = 0;
if (attachmentRect && attachmentRect.height > 0) {
attachmentHeight = attachmentRect.height;
}
})
.exec();
if (headerRect) {
// scroll-view = +
contentScrollTop.value = headerRect.height + headerRect.top;
// scroll-view = - scroll-view -
contentScrollHeight.value = sysInfo.windowHeight - contentScrollTop.value - attachmentHeight;
}
});
});
});
@ -112,6 +132,24 @@ async function loadNoticeDetail(id) {
<p>会议基本信息</p>
<p>会议时间2026年2月15日周六上午9:00</p>
<p>会议地点小区活动中心一楼会议室</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>
<p>参会人员全体业主或业主代表</p>`,
attachments: [
{ name: 'IMG-2309.PNG', type: 'image', url: '' },
@ -202,21 +240,35 @@ function downloadAttachment(item) {
</script>
<style lang="scss" scoped>
/* 页面容器 - 伪元素渐变向上覆盖到导航栏 */
/* 渐变背景 */
.gradient-bg {
position: fixed;
top: 0;
left: 0;
width: 750rpx;
height: 660rpx;
background: linear-gradient(180deg, #F8EDE8 0%, #F5F5F5 50%);
z-index: 0;
pointer-events: none;
}
/* 页面容器 */
.notice-detail-page {
position: relative;
display: flex;
flex-direction: column;
padding: 0 32rpx;
&::before {
background: linear-gradient(180deg, #F8EDE8 0%, #FFFFFF 30%);
}
z-index: 1;
}
/* 固定头部区域(标题+发布信息) */
.detail-header {
flex-shrink: 0; /* 不被压缩 */
position: fixed;
left: 0;
width: 750rpx;
z-index: 10;
background: transparent;
}
.header-inner {
padding: 0 32rpx;
}
/* 底部占位 */
@ -275,13 +327,22 @@ function downloadAttachment(item) {
/* 富文本滚动区域 - 高度由JS动态绑定 */
.article-scroll {
flex: 1; /* 占据剩余空间 */
position: fixed;
left: 0;
width: 100%;
z-index: 5;
}
/* 富文本内容 */
/* 富文本内容 - 圆角背景卡片 */
.article-content {
margin: 0 32rpx;
border-radius: 24rpx;
padding: 32rpx;
// background-color: #FFFFFF;
}
/* 富文本内容内层 - 控制padding */
.article-inner {
// padding: 32rpx;
:deep(p) {
font-size: 28rpx;
@ -302,13 +363,17 @@ function downloadAttachment(item) {
position: fixed;
bottom: 0;
left: 0;
width: 750rpx;
height: 300rpx;
width: 100%;
height: 260rpx;
background: #FFFFFF;
box-shadow: 0rpx -8rpx 64rpx 0rpx rgba(0, 0, 0, 0.16);
border-radius: 0;
padding: 24rpx 32rpx;
overflow-y: auto;
z-index: 9999;
}
.attachment-inner {
padding: 24rpx 32rpx;
}
.attachment-item {

View File

@ -0,0 +1,509 @@
<!-- 物业人员页面 -->
<template>
<s-layout title="物业人员" :bgStyle="{ backgroundColor:'#F8EDE8' }" navbar="inner" color="#333333">
<view class="staff-page">
<!-- 渐变背景装饰 -->
<view class="gradient-bg"></view>
<!-- 固定头部统计卡片+分类Tab -->
<view class="page-header">
<!-- 统计卡片 -->
<view class="stats-card">
<!-- 小区名称 -->
<view class="card-top">
<view class="building-info">
<text class="sicon-building building-icon"></text>
<text class="building-name">{{ statsInfo.buildingName }}</text>
</view>
<view class="staff-badge">物业人员</view>
</view>
<!-- 总人数 -->
<view class="total-row">
<text class="total-label">总人数</text>
<text class="total-count">{{ statsInfo.total }}<text class="total-unit"></text></text>
</view>
<!-- 分类统计 -->
<view class="category-row">
<view
class="category-item"
v-for="(cat, index) in statsInfo.categories"
:key="index"
>
<text class="cat-name">{{ cat.name }}</text>
<text class="cat-count">{{ cat.count }}</text>
</view>
</view>
</view>
<!-- 分类Tab栏 -->
<scroll-view class="tab-scroll" scroll-x :show-scrollbar="false">
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: currentTab === tab.key }"
v-for="(tab, index) in tabList"
:key="index"
@tap="currentTab = tab.key"
>
<text class="tab-text">{{ tab.label }}</text>
</view>
</view>
</scroll-view>
<!-- 人员信息标题 -->
<view class="section-title">人员信息</view>
</view>
<!-- 人员列表 - 独立滚动区域 -->
<scroll-view class="staff-scroll" scroll-y :style="{ height: scrollHeight + 'px' }">
<view class="staff-list">
<!-- 员工卡片 -->
<view
class="staff-item"
v-for="(item, index) in filteredStaffList"
:key="index"
>
<!-- 头像 -->
<image class="staff-avatar" :src="item.avatar" mode="aspectFill" />
<!-- 信息区 -->
<view class="staff-info">
<!-- 姓名+职业 -->
<view class="name-row">
<text class="name-label">姓名</text>
<text class="name-text">{{ item.name }}</text>
<text class="job-label">职业</text>
<text class="job-text">{{ item.job }}</text>
</view>
<!-- 电话 -->
<view class="phone-row">
<text class="phone-label">电话</text>
<text class="phone-text">{{ item.phone }}</text>
<view class="call-btn" @tap="callPhone(item.phone)">
<text class="sicon-phone call-icon"></text>
</view>
</view>
<!-- 职责描述 -->
<view class="duty-row">
<text class="duty-label">职责</text>
<text class="duty-text">{{ item.duty }}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="filteredStaffList.length === 0">
<text class="empty-text">暂无相关人员</text>
</view>
</view>
</scroll-view>
</view>
</s-layout>
</template>
<script setup>
import { ref, computed, onMounted, nextTick, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
// Tab
const currentTab = ref('security');
//
const scrollHeight = ref(0);
// Tab
const tabList = ref([
{ key: 'security', label: '安保类' },
{ key: 'cleaning', label: '保洁类' },
{ key: 'engineering', label: '工程类' },
{ key: 'admin', label: '行政管理类' },
{ key: 'finance', label: '财务类' }
]);
//
const statsInfo = ref({
buildingName: '融侨馨苑',
total: 48,
categories: [
{ name: '安保', count: 10 },
{ name: '保洁', count: 10 },
{ name: '维修', count: 10 },
{ name: '楼管', count: 10 },
{ name: '经理', count: 2 },
{ name: '客服', count: 10 }
]
});
//
const staffList = ref([
{
id: 1,
category: 'security',
name: '吴仁贵',
job: '安保主管',
phone: '184****5263',
duty: '人员调配与排班、根据项目需求制定安保人员排班计划确保24小时无缝覆盖。',
avatar: '/static/img/guest.png'
},
{
id: 2,
category: 'security',
name: '李梓发',
job: '保安',
phone: '187****1005',
duty: '定期巡查项目重点区域,检查安全设施(监控、消防器材、门禁系统)是否完好',
avatar: '/static/img/login_img.png'
},
{
id: 3,
category: 'security',
name: '冯启彬',
job: '保安',
phone: '187****1005',
duty: '定期巡查项目重点区域,检查安全设施(监控、消防器材、门禁系统)是否完好',
avatar: '/static/img/guest.png'
}
]);
// Tab
const filteredStaffList = computed(() => {
return staffList.value.filter(item => item.category === currentTab.value);
});
//
onLoad(() => {
loadStaffData();
});
// scroll-view
onMounted(() => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
uni.createSelectorQuery()
.in(instance)
.select('.page-header')
.boundingClientRect((rect) => {
if (rect) {
// scroll-view = - -
scrollHeight.value = sysInfo.windowHeight - rect.height - rect.top;
}
})
.exec();
});
});
// TODO: API
function loadStaffData() {
// TODO: API
}
//
function callPhone(phone) {
uni.makePhoneCall({
phoneNumber: phone.replace(/\*/g, '') //
});
}
</script>
<style lang="scss" scoped>
/* ==================== 页面容器 ==================== */
.staff-page {
position: relative;
min-height: 100vh;
}
/* ==================== 渐变背景 ==================== */
.gradient-bg {
position: absolute;
top: -176rpx;
left: 0;
right: 0;
height: calc(100% + 176rpx);
background: linear-gradient(180deg, #F8EDE8 0%, #FFFFFF 30%);
z-index: -1;
}
/* ==================== 固定头部区域 ==================== */
.page-header {
padding-bottom: 16rpx;
}
/* ==================== 统计卡片 ==================== */
.stats-card {
margin: 24rpx 32rpx 24rpx;
padding: 28rpx 32rpx 24rpx;
background: linear-gradient(135deg, #EBF4FF 0%, #D6E7FF 100%);
border-radius: 24rpx;
/* 卡片顶部:小区名+标签 */
.card-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
/* 小区信息 */
.building-info {
display: flex;
align-items: center;
.building-icon {
font-size: 36rpx;
color: #333333;
margin-right: 12rpx;
}
.building-name {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
}
/* 物业人员标签 */
.staff-badge {
background: linear-gradient(135deg, #5B9BFF 0%, #3D7BD9 100%);
color: #FFFFFF;
font-size: 26rpx;
font-weight: 500;
padding: 8rpx 24rpx;
border-radius: 24rpx 0 24rpx 0;
}
/* 总人数行 */
.total-row {
display: flex;
align-items: baseline;
margin-bottom: 24rpx;
.total-label {
font-size: 26rpx;
color: #666666;
margin-right: 16rpx;
}
.total-count {
font-size: 48rpx;
font-weight: 700;
color: #FA7E49; /* 橙色主题 */
.total-unit {
font-size: 28rpx;
font-weight: 400;
color: #FA7E49;
margin-left: 4rpx;
}
}
}
/* 分类统计行 */
.category-row {
display: flex;
justify-content: space-between;
.category-item {
display: flex;
flex-direction: column;
align-items: center;
.cat-name {
font-size: 26rpx;
color: #666666;
margin-bottom: 8rpx;
}
.cat-count {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
}
}
}
/* ==================== 分类Tab栏 ==================== */
.tab-scroll {
white-space: nowrap;
margin: 0 0 8rpx;
}
.tab-bar {
display: inline-flex;
padding: 0 32rpx;
.tab-item {
padding: 20rpx 0;
margin-right: 40rpx;
position: relative;
&:last-child {
margin-right: 0;
}
.tab-text {
font-size: 28rpx;
color: #999999;
transition: all 0.2s;
}
/* 选中状态 */
&.active {
.tab-text {
color: #FA7E49; /* 橙色主题 */
font-weight: 600;
}
/* 底部橙色指示条 */
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 4rpx;
background: #FA7E49;
border-radius: 2rpx;
}
}
}
}
/* ==================== 人员信息标题 ==================== */
.section-title {
padding: 16rpx 32rpx 20rpx;
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
/* ==================== 人员列表滚动区域 ==================== */
.staff-scroll {
width: 100%;
}
/* ==================== 人员列表内容 ==================== */
.staff-list {
padding: 0 32rpx 160rpx;
}
/* ==================== 单个员工卡片 ==================== */
.staff-item {
display: flex;
padding: 28rpx 0;
/* 分割线(非第一项) */
&:not(:last-child)::after {
content: '';
position: absolute;
bottom: 0;
left: 112rpx;
right: 0;
height: 1rpx;
background-color: #F0F0F0;
}
/* 头像 */
.staff-avatar {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
flex-shrink: 0;
background-color: #F0F0F0;
}
/* 信息区域 */
.staff-info {
flex: 1;
margin-left: 24rpx;
/* 姓名+职业行 */
.name-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.name-label,
.job-label {
font-size: 26rpx;
color: #999999;
}
.name-text,
.job-text {
font-size: 28rpx;
color: #333333;
font-weight: 500;
margin-right: 24rpx;
}
}
/* 电话行 */
.phone-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.phone-label {
font-size: 26rpx;
color: #999999;
}
.phone-text {
font-size: 28rpx;
color: #333333;
margin-right: 16rpx;
}
/* 拨打按钮 */
.call-btn {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background-color: #FA7E49;
display: flex;
align-items: center;
justify-content: center;
.call-icon {
font-size: 24rpx;
color: #FFFFFF;
}
}
}
/* 职责描述行 */
.duty-row {
.duty-label {
font-size: 26rpx;
color: #999999;
}
.duty-text {
font-size: 26rpx;
color: #666666;
line-height: 1.6;
}
}
}
}
/* ==================== 空状态 ==================== */
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
</style>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 1 0-16 0"/></svg>

After

Width:  |  Height:  |  Size: 222 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#333333" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg>

After

Width:  |  Height:  |  Size: 247 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#999999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>

After

Width:  |  Height:  |  Size: 218 B