feat(home): 动态加载首页数据并新增详情页

main
cr 2026-04-25 01:02:01 +08:00
parent 1582d6e193
commit bd1c6a9596
15 changed files with 1258 additions and 236 deletions

1
.env
View File

@ -7,6 +7,7 @@ SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development
#SHOPRO_DEV_BASE_URL=http://127.0.0.1:48080
SHOPRO_DEV_BASE_URL=http://1.12.53.43:9999
#SHOPRO_DEV_BASE_URL=http://192.168.1.105:48080
### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc

View File

@ -605,7 +605,18 @@
}
},
{
"path": "notice/detail",
"path": "community/dynamics-detail",
"style": {
"navigationBarTitleText": "社区动态"
},
"meta": {
"sync": true,
"title": "社区动态详情",
"group": "物业管理"
}
},
{
"path": "notice/list",
"style": {
"navigationBarTitleText": "通知公告"
},
@ -615,6 +626,17 @@
"group": "物业管理"
}
},
{
"path": "notice/detail",
"style": {
"navigationBarTitleText": "通知详情"
},
"meta": {
"sync": true,
"title": "通知详情",
"group": "物业管理"
}
},
{
"path": "activity/list",
"style": {
@ -670,6 +692,17 @@
"group": "物业管理"
}
},
{
"path": "knowledge/detail",
"style": {
"navigationBarTitleText": "知识课堂"
},
"meta": {
"sync": true,
"title": "知识课堂详情",
"group": "物业管理"
}
},
{
"path": "community/daily",
"style": {
@ -681,6 +714,17 @@
"title": "物业日常",
"group": "物业管理"
}
},
{
"path": "service/more",
"style": {
"navigationBarTitleText": "更多服务"
},
"meta": {
"sync": true,
"title": "更多服务",
"group": "物业管理"
}
}
]
},

View File

@ -28,7 +28,7 @@
indicator-color="rgba(255, 127, 105, 0.3)"
indicator-active-color="#FF7F69"
>
<swiper-item v-for="(item, index) in bannerList" :key="index">
<swiper-item v-for="(item, index) in bannerList" :key="index" @tap="handleBannerTap(item)">
<view class="banner-content">
<view class="banner-text">
<text class="banner-title">{{ item.title }}</text>
@ -58,7 +58,7 @@
<!-- 通知栏 -->
<view class="notice-bar" @tap="goNotice">
<view class="notice-tag">通知</view>
<text class="notice-text">关于做好小区消防安全管理的通知...</text>
<text class="notice-text">{{ noticeTitle || '暂无通知' }}</text>
<image class="right-icon" src="/static/img/right-icon.png" mode="aspectFit" />
</view>
@ -111,6 +111,7 @@
import { ref, onMounted, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import MemberHouseApi from '@/sheep/api/community/memberHouse';
import NoticeApi from '@/sheep/api/community/notice';
import sheep from '@/sheep';
//
@ -145,22 +146,37 @@ const houseAddress = computed(() => {
return userInfo.value.currentHouseAddress || '';
});
// Banner
const bannerList = ref([
{
title: '智慧社区',
subtitle: '让生活更简单',
icon: '/static/img/home-banner-icon.png',
},
{
title: '智慧社区',
subtitle: '让生活更简单',
icon: '/static/img/home-banner-icon.png',
},
]);
//
const noticeTitle = ref('');
//
const functionList = ref([
//
const fetchNotice = async () => {
const { code, data } = await NoticeApi.getPage({ pageNo: 1, pageSize: 1 });
if (code === 0 && data && data.list && data.list.length > 0) {
noticeTitle.value = data.list[0].title;
}
};
// Banner
const bannerList = ref([]);
// Banner
const fetchBannerList = async () => {
const { code, data } = await NoticeApi.getBannerList(1);
if (code === 0 && data && data.length > 0) {
bannerList.value = data.map((item) => ({
id: item.id,
title: item.name,
subtitle: '',
icon: item.picUrl,
url: item.url,
}));
}
};
//
/*
const functionListStatic = [
{ 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: '/pages/sub/income/index' },
@ -169,7 +185,34 @@ const functionList = ref([
{ 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: '' },
]);
];
*/
const functionList = ref([]);
// 使
const bgGradientPresets = [
'linear-gradient(35deg, #FF7F69 0%, #FC5A5D 100%)',
'linear-gradient(35deg, #52C41A 0%, #36AD1A 100%)',
'linear-gradient(35deg, #FFA940 0%, #FA8C16 100%)',
'linear-gradient(35deg, #4096FF 0%, #1890FF 100%)',
'linear-gradient(35deg, #69B1FF 0%, #4096FF 100%)',
'linear-gradient(35deg, #9254DE 0%, #722ED1 100%)',
'linear-gradient(35deg, #7B61FF 0%, #597EF7 100%)',
'linear-gradient(35deg, #36CFC9 0%, #13C2C2 100%)',
];
//
const fetchFunctionList = async () => {
const { code, data } = await NoticeApi.getMiniAppConfigList(1);
if (code === 0 && data && data.length > 0) {
functionList.value = data.map((item, index) => ({
label: item.name,
icon: item.icon,
path: item.url,
bgGradient: bgGradientPresets[index % bgGradientPresets.length],
}));
}
};
onLoad(() => {
//
@ -177,6 +220,12 @@ onLoad(() => {
statusBarHeight.value = systemInfo.statusBarHeight || 0;
//
fetchCommunityTree();
//
fetchNotice();
// Banner
fetchBannerList();
//
fetchFunctionList();
});
onShow(() => {
@ -239,8 +288,9 @@ const showCommunityPicker = () => {
//
const handleFunctionTap = (item) => {
if (item.path) {
uni.navigateTo({ url: item.path });
const path = item.path ? item.path.trim() : '';
if (path && path !== '/') {
uni.navigateTo({ url: path });
} else {
uni.showToast({ title: `${item.label}功能开发中`, icon: 'none' });
}
@ -248,7 +298,7 @@ const handleFunctionTap = (item) => {
//
const goNotice = () => {
uni.navigateTo({ url: '/pages/sub/notice/detail?id=1' });
uni.navigateTo({ url: '/pages/sub/notice/list' });
};
//
@ -285,6 +335,18 @@ const goActivityList = () => {
const goActivityDetail = () => {
uni.navigateTo({ url: '/pages/sub/activity/detail?id=1' });
};
// Banner
const handleBannerTap = (item) => {
if (!item.url) return;
// webview
if (item.url.startsWith('http')) {
uni.navigateTo({ url: `/pages/public/webview?url=${encodeURIComponent(item.url)}` });
} else {
//
uni.navigateTo({ url: item.url });
}
};
</script>
<style lang="less">

View File

@ -29,7 +29,6 @@
<input
class="form-input"
type="number"
maxlength="4"
placeholder="请输入验证码"
placeholder-class="input-placeholder"
v-model="formData.code"

View File

@ -0,0 +1,210 @@
<!-- 社区动态详情页 -->
<template>
<s-layout title="社区动态" navbar="inner" color="#333333">
<!-- 渐变背景 -->
<view class="gradient-bg"></view>
<!-- 固定头部区域标题+发布信息 -->
<view class="detail-header">
<view class="header-inner">
<!-- 标题 -->
<view class="detail-title">
<text class="title-text">{{ detailInfo.title }}</text>
</view>
<!-- 发布信息 -->
<view class="publish-info">
<text class="community-name">{{ detailInfo.communityName }}</text>
<text class="publish-date">{{ detailInfo.publishDate ? sheep.$helper.timeFormat(detailInfo.publishDate, 'yyyy年mm月dd日 hh:MM') : '' }}</text>
</view>
<!-- 分割线 -->
<view class="divider"></view>
</view>
</view>
<!-- 富文本内容 - 固定滚动区域 -->
<scroll-view class="content-scroll" scroll-y :style="{ height: scrollHeight + 'px', top: scrollTop + 'px' }">
<view class="content-card">
<view class="content-inner">
<mp-html :content="detailInfo.content"></mp-html>
</view>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
</scroll-view>
</s-layout>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import DynamicsApi from '@/sheep/api/community/dynamics';
import sheep from '@/sheep';
//
const detailInfo = ref({
title: '',
communityName: '',
publishDate: '',
content: '',
});
//
const scrollHeight = ref(0);
//
const scrollTop = ref(0);
//
onLoad((options) => {
if (options.id) {
loadDetail(options.id);
}
});
// scroll-view
const calcScrollHeight = () => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
const query = uni.createSelectorQuery().in(instance);
//
query.select('.detail-header').boundingClientRect();
query.exec((res) => {
const headerRect = res[0];
if (headerRect) {
const safeBottom = sysInfo.safeAreaInsets?.bottom || 0;
scrollTop.value = headerRect.height + headerRect.top;
scrollHeight.value = sysInfo.windowHeight - scrollTop.value - safeBottom;
}
});
});
};
onMounted(() => {
calcScrollHeight();
});
//
async function loadDetail(id) {
const { code, data } = await DynamicsApi.getDetail(id);
if (code === 0 && data) {
const communityName = data.author || '';
detailInfo.value = {
title: data.title || '',
communityName,
publishDate: data.publishTime || '',
content: data.content || '',
};
setTimeout(calcScrollHeight, 100);
}
}
</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;
}
/* 固定头部区域 */
.detail-header {
position: fixed;
left: 0;
width: 100%;
z-index: 10;
background: transparent;
}
.header-inner {
padding: 0 32rpx;
}
/* 文章标题 */
.detail-title {
padding: 32rpx 0 24rpx;
.title-text {
font-size: 40rpx;
font-weight: 600;
color: #333333;
line-height: 1.5;
font-family: 'PingFang SC', sans-serif;
}
}
/* 发布信息 */
.publish-info {
display: flex;
align-items: center;
gap: 16rpx;
padding-bottom: 24rpx;
justify-content: space-between;
.community-name {
font-size: 28rpx;
color: #666666;
font-family: 'PingFang SC', sans-serif;
}
.publish-date {
font-size: 26rpx;
color: #999999;
font-family: 'PingFang SC', sans-serif;
}
}
/* 分割线 */
.divider {
height: 1rpx;
background-color: #E5E5E5;
margin-bottom: 32rpx;
}
/* 内容滚动区域 - 固定定位 */
.content-scroll {
position: fixed;
left: 0;
width: 100%;
z-index: 5;
}
/* 内容卡片 */
.content-card {
border-radius: 24rpx;
padding: 32rpx;
}
/* 富文本内容 */
.content-inner {
:deep(p) {
font-size: 30rpx;
color: #333333;
line-height: 1.8;
margin-bottom: 24rpx;
font-family: 'PingFang SC', sans-serif;
}
:deep(img) {
width: 100%;
border-radius: 16rpx;
margin: 16rpx 0;
}
}
/* 底部占位 */
.bottom-placeholder {
height: 40rpx;
}
</style>

View File

@ -35,7 +35,7 @@
<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-views">{{ item.views||0 }}人浏览</text>
<text class="item-date">{{ item.date }}</text>
</view>
</view>
@ -52,8 +52,10 @@
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
import { ref, onMounted, nextTick, getCurrentInstance, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import DynamicsApi from '@/sheep/api/community/dynamics';
import sheep from '@/sheep';
// pxJSscroll-view
const scrollViewHeight = ref(0);
@ -63,57 +65,36 @@ 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'
},
{
id: 5,
title: '社区动态标题xxxx操作手册--如何邀请访客',
cover: '/static/img/guest.png',
views: '1.2万播放',
date: '2021/02/21'
},
{
id: 6,
title: '社区动态标题xxxx操作手册--如何邀请访客',
cover: '/static/img/guest.png',
views: '1.2万播放',
date: '2021/02/21'
},
{
id: 7,
title: '社区动态标题xxxx操作手册--如何邀请访客',
cover: '/static/img/guest.png',
views: '1.2万播放',
date: '2021/02/21'
const dynamicsList = ref([]);
//
const pageNo = ref(1);
const pageSize = ref(10);
const total = ref(0);
const loading = ref(false);
const noMore = ref(false);
// ID
const communityId = computed(() => {
return sheep.$store('user').userInfo?.currentCommunityId || null;
});
// Tab
const tabTypeMap = {
0: null, //
1: 1, //
2: 2, //
3: 3, //
};
//
function formatViewCount(count) {
if (!count && count !== 0) return '0';
if (count >= 10000) {
return (count / 10000).toFixed(1) + '万';
}
]);
return String(count);
}
//
onLoad((options) => {
@ -145,12 +126,57 @@ onMounted(() => {
// Tab
function switchTab(index) {
currentTab.value = index;
pageNo.value = 1;
noMore.value = false;
loadDynamicsList();
}
//
async function loadDynamicsList() {
// TODO: API
if (loading.value) return;
loading.value = true;
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
};
const postType = tabTypeMap[currentTab.value];
if (postType) {
params.postType = postType;
}
if (communityId.value) {
params.communityId = communityId.value;
}
const { code, data } = await DynamicsApi.getPage(params);
loading.value = false;
if (code === 0 && data) {
const list = (data.list || []).map((item) => ({
id: item.id,
title: item.title,
cover: item.coverImage || '/static/img/guest.png',
views: formatViewCount(item.viewCount),
date: item.publishTime ? sheep.$helper.timeFormat(item.publishTime, 'yyyy/mm/dd') : '',
}));
if (pageNo.value === 1) {
dynamicsList.value = list;
} else {
dynamicsList.value = dynamicsList.value.concat(list);
}
total.value = data.total || 0;
noMore.value = dynamicsList.value.length >= total.value;
}
}
//
function loadMore() {
if (noMore.value || loading.value) return;
pageNo.value += 1;
loadDynamicsList();
}
//

View File

@ -20,7 +20,7 @@
</view>
<!-- 列表区域 - 独立滚动区域 -->
<scroll-view class="classroom-list" scroll-y :style="{ height: scrollViewHeight + 'px' }">
<scroll-view class="classroom-list" scroll-y :style="{ height: scrollViewHeight + 'px' }" v-if="classroomList.length > 0">
<view class="list-inner" :class="{ 'grid-layout': currentTab === 1 }">
<view
class="classroom-item"
@ -31,9 +31,6 @@
<!-- 封面图 -->
<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>
<!-- 内容区 -->
@ -54,20 +51,22 @@
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="classroomList.length === 0">
<text class="empty-text">暂无数据</text>
</view>
</view>
</scroll-view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<text class="empty-text">暂无数据</text>
</view>
</view>
</s-layout>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
import { ref, onMounted, nextTick, getCurrentInstance, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import KnowledgeApi from '@/sheep/api/community/knowledge';
import sheep from '@/sheep';
// pxJSscroll-view
const scrollViewHeight = ref(0);
@ -79,6 +78,33 @@ const currentTab = ref(0);
//
const classroomList = ref([]);
//
const pageNo = ref(1);
const pageSize = ref(10);
const total = ref(0);
const loading = ref(false);
const noMore = ref(false);
// ID
const communityId = computed(() => {
return sheep.$store('user').userInfo?.currentCommunityId || null;
});
// Tab
const tabTypeMap = {
0: 1, //
1: 2, //
};
//
function formatCount(count) {
if (!count && count !== 0) return '0';
if (count >= 10000) {
return (count / 10000).toFixed(1) + '万';
}
return String(count);
}
//
onLoad((options) => {
if (options.tab) {
@ -109,90 +135,60 @@ onMounted(() => {
// Tab
function switchTab(index) {
currentTab.value = index;
pageNo.value = 1;
noMore.value = false;
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'
if (loading.value) return;
loading.value = true;
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
};
const classType = tabTypeMap[currentTab.value];
if (classType) {
params.classType = classType;
}
];
if (communityId.value) {
params.communityId = communityId.value;
}
const { code, data } = await KnowledgeApi.getPage(params);
loading.value = false;
if (code === 0 && data) {
const list = (data.list || []).map((item) => ({
id: item.id,
title: item.title,
cover: item.coverImage || '/static/img/guest.png',
views: formatCount(item.viewCount),
likes: formatCount(item.likeCount),
date: item.createTime ? sheep.$helper.timeFormat(item.createTime, 'yyyy/mm/dd hh:MM') : '',
}));
if (pageNo.value === 1) {
classroomList.value = list;
} 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'
classroomList.value = classroomList.value.concat(list);
}
];
total.value = data.total || 0;
noMore.value = classroomList.value.length >= total.value;
}
}
//
function loadMore() {
if (noMore.value || loading.value) return;
pageNo.value += 1;
loadClassroomList();
}
//
function goDetail(item) {
uni.navigateTo({

View File

@ -0,0 +1,209 @@
<!-- 知识课堂详情页 -->
<template>
<s-layout title="知识课堂" navbar="inner" color="#333333">
<!-- 渐变背景 -->
<view class="gradient-bg"></view>
<!-- 固定头部区域标题+发布信息 -->
<view class="detail-header">
<view class="header-inner">
<!-- 标题 -->
<view class="detail-title">
<text class="title-text">{{ detailInfo.title }}</text>
</view>
<!-- 发布信息 -->
<view class="publish-info">
<text class="community-name">{{ detailInfo.viewCount || 0 }}次浏览</text>
<text class="publish-date">{{ detailInfo.publishDate ? sheep.$helper.timeFormat(detailInfo.publishDate, 'yyyy年mm月dd日 hh:MM') : '' }}</text>
</view>
<!-- 分割线 -->
<view class="divider"></view>
</view>
</view>
<!-- 富文本内容 - 固定滚动区域 -->
<scroll-view class="content-scroll" scroll-y :style="{ height: scrollHeight + 'px', top: scrollTop + 'px' }">
<view class="content-card">
<view class="content-inner">
<mp-html :content="detailInfo.content"></mp-html>
</view>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
</scroll-view>
</s-layout>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import KnowledgeApi from '@/sheep/api/community/knowledge';
import sheep from '@/sheep';
//
const detailInfo = ref({
title: '',
viewCount: '',
publishDate: '',
content: '',
});
//
const scrollHeight = ref(0);
//
const scrollTop = ref(0);
//
onLoad((options) => {
if (options.id) {
loadDetail(options.id);
}
});
// scroll-view
const calcScrollHeight = () => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
const query = uni.createSelectorQuery().in(instance);
//
query.select('.detail-header').boundingClientRect();
query.exec((res) => {
const headerRect = res[0];
if (headerRect) {
const safeBottom = sysInfo.safeAreaInsets?.bottom || 0;
scrollTop.value = headerRect.height + headerRect.top;
scrollHeight.value = sysInfo.windowHeight - scrollTop.value - safeBottom;
}
});
});
};
onMounted(() => {
calcScrollHeight();
});
//
async function loadDetail(id) {
const { code, data } = await KnowledgeApi.getDetail(id);
if (code === 0 && data) {
detailInfo.value = {
title: data.title || '',
viewCount: data.viewCount || '',
publishDate: data.createTime || '',
content: data.content || '',
};
setTimeout(calcScrollHeight, 100);
}
}
</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;
}
/* 固定头部区域 */
.detail-header {
position: fixed;
left: 0;
width: 100%;
z-index: 10;
background: transparent;
}
.header-inner {
padding: 0 32rpx;
}
/* 文章标题 */
.detail-title {
padding: 32rpx 0 24rpx;
.title-text {
font-size: 40rpx;
font-weight: 600;
color: #333333;
line-height: 1.5;
font-family: 'PingFang SC', sans-serif;
}
}
/* 发布信息 */
.publish-info {
display: flex;
align-items: center;
gap: 16rpx;
padding-bottom: 24rpx;
justify-content: space-between;
.community-name {
font-size: 28rpx;
color: #666666;
font-family: 'PingFang SC', sans-serif;
}
.publish-date {
font-size: 26rpx;
color: #999999;
font-family: 'PingFang SC', sans-serif;
}
}
/* 分割线 */
.divider {
height: 1rpx;
background-color: #E5E5E5;
margin-bottom: 32rpx;
}
/* 内容滚动区域 - 固定定位 */
.content-scroll {
position: fixed;
left: 0;
width: 100%;
z-index: 5;
}
/* 内容卡片 */
.content-card {
background-color: #FFF8F0;
border-radius: 24rpx;
padding: 32rpx;
margin: 0 32rpx;
}
/* 富文本内容 */
.content-inner {
:deep(p) {
font-size: 30rpx;
color: #333333;
line-height: 1.8;
margin-bottom: 24rpx;
font-family: 'PingFang SC', sans-serif;
}
:deep(img) {
width: 100%;
border-radius: 16rpx;
margin: 16rpx 0;
}
}
/* 底部占位 */
.bottom-placeholder {
height: 40rpx;
}
</style>

View File

@ -15,10 +15,10 @@
<!-- 发布信息 -->
<view class="publish-info">
<view class="publisher">
<image class="publisher-avatar" src="/static/img/login_img.png" mode="aspectFill" />
<image class="publisher-avatar" src="/static/img/person.png" mode="aspectFill" />
<text class="publisher-name">{{ noticeInfo.publisher }}</text>
</view>
<text class="publish-date">{{ noticeInfo.publishDate }}</text>
<text class="publish-date">{{ noticeInfo.publishDate ? sheep.$helper.timeFormat(noticeInfo.publishDate, 'yyyy/mm/dd hh:MM:ss') : '' }}</text>
</view>
<!-- 分割线 -->
@ -34,15 +34,15 @@
</view>
</view>
<!-- 占位防止内容被底部附件遮挡 -->
<view class="bottom-placeholder" v-if="noticeInfo.attachments && noticeInfo.attachments.length > 0"></view>
<view class="bottom-placeholder" v-if="noticeInfo.attachmentList && noticeInfo.attachmentList.length > 0"></view>
</scroll-view>
<!-- 附件列表 - 固定在底部 -->
<view class="attachment-section" v-if="noticeInfo.attachments && noticeInfo.attachments.length > 0">
<!-- 附件列表 - 动态高度随内容撑开 -->
<view class="attachment-section" v-if="noticeInfo.attachmentList && noticeInfo.attachmentList.length > 0">
<view class="attachment-inner">
<view
class="attachment-item"
v-for="(item, index) in noticeInfo.attachments"
v-for="(item, index) in noticeInfo.attachmentList"
:key="index"
@tap="downloadAttachment(item)"
>
@ -58,8 +58,9 @@
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
import { ref, onMounted, nextTick, getCurrentInstance, watch } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import NoticeApi from '@/sheep/api/community/notice';
import sheep from '@/sheep';
// px
@ -73,7 +74,7 @@ const noticeInfo = ref({
publisher: '',
publishDate: '',
content: '',
attachments: []
attachmentList: []
});
//
@ -83,8 +84,8 @@ onLoad((options) => {
}
});
// scroll-view
onMounted(() => {
// scroll-view
const calcScrollHeight = () => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
@ -107,55 +108,60 @@ onMounted(() => {
if (headerRect) {
// scroll-view = +
contentScrollTop.value = headerRect.height + headerRect.top;
// scroll-view = - scroll-view -
contentScrollHeight.value = sysInfo.windowHeight - contentScrollTop.value - attachmentHeight;
// scroll-view = - scroll-view - -
const safeBottom = sysInfo.safeAreaInsets?.bottom || 0;
contentScrollHeight.value = sysInfo.windowHeight - contentScrollTop.value - attachmentHeight - safeBottom;
}
});
});
};
//
onMounted(() => {
calcScrollHeight();
});
//
watch(() => noticeInfo.value.attachmentList, () => {
setTimeout(calcScrollHeight, 100);
}, { deep: true });
//
async function loadNoticeDetail(id) {
// TODO: API
// const { code, data } = await NoticeApi.getNoticeDetail(id);
// if (code === 0) {
// noticeInfo.value = data;
// }
//
const { code, data } = await NoticeApi.getDetail(id);
if (code === 0 && data) {
noticeInfo.value = {
title: '关于召开业主大会讨论物业费价格调整的通知',
publisher: 'x小区物业',
publishDate: '2026-01-20',
content: `<p>各位业主:</p>
<p>为进一步提升本小区物业服务质量保障小区公共设施设备的正常运维环境卫生整治安保服务升级等工作有序开展切实维护全体业主的共同利益根据物业管理条例业主大会和业主委员会指导规则及本小区管理规约相关规定经业主委员会研究决定召开业主大会专门讨论本小区物业费价格调整相关事宜现将具体事项通知如下</p>
<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: '' },
{ name: 'EXCEL-2309.XLXS', type: 'excel', url: '' }
]
title: data.title || '',
publisher: data.publisher || '',
publishDate: data.publishTime || '',
content: data.content || '',
attachmentList: (data.attachmentList || []).map((item) => ({
name: item.name,
url: item.url,
type: getFileType(item.name),
})),
};
// scroll-view
setTimeout(calcScrollHeight, 100);
}
}
//
function getFileType(fileName) {
if (!fileName) return 'file';
const ext = fileName.split('.').pop().toLowerCase();
const typeMap = {
png: 'image',
jpg: 'image',
jpeg: 'image',
gif: 'image',
xls: 'excel',
xlsx: 'excel',
pdf: 'pdf',
doc: 'word',
docx: 'word',
};
return typeMap[ext] || 'file';
}
//
@ -271,9 +277,9 @@ function downloadAttachment(item) {
padding: 0 32rpx;
}
/* 底部占位 */
/* 底部占位 - 根据附件数量动态调整 */
.bottom-placeholder {
height: 300rpx;
height: 40rpx;
}
/* 文章标题 */
@ -302,7 +308,7 @@ function downloadAttachment(item) {
.publisher-avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
// border-radius: 50%;
margin-right: 16rpx;
}
@ -358,16 +364,16 @@ function downloadAttachment(item) {
}
}
/* 附件区域 - 固定在底部 */
/* 附件区域 - 动态高度最多显示3个附件 */
.attachment-section {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 260rpx;
max-height: 360rpx;
background: #FFFFFF;
box-shadow: 0rpx -8rpx 64rpx 0rpx rgba(0, 0, 0, 0.16);
border-radius: 0;
border-radius: 24rpx 24rpx 0 0;
overflow-y: auto;
z-index: 9999;
}

View File

@ -0,0 +1,197 @@
<!-- 通知公告列表 -->
<template>
<s-layout title="通知公告">
<view class="notice-list-page">
<scroll-view
class="notice-scroll"
scroll-y
:style="{ height: scrollHeight + 'px' }"
@scrolltolower="loadMore"
>
<view
v-for="(item, index) in state.list"
:key="index"
class="notice-card"
@tap="goDetail(item)"
>
<text class="notice-title">{{ item.title }}</text>
<view class="notice-content">
<mp-html :content="item.content" />
</view>
<view class="notice-footer">
<text class="notice-time">{{ sheep.$helper.timeFormat(item.publishTime, 'yyyy/mm/dd hh:MM:ss') }}</text>
<image class="arrow-icon" src="/static/img/right-icon.png" mode="aspectFit" />
</view>
</view>
<!-- 加载状态 -->
<view class="load-more">
<text v-if="state.loading">...</text>
<text v-else-if="state.noMore">没有更多了</text>
</view>
</scroll-view>
</view>
</s-layout>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app';
import NoticeApi from '@/sheep/api/community/notice';
import sheep from '@/sheep';
// scroll-view
const scrollHeight = ref(0);
//
const state = reactive({
list: [],
pageNo: 1,
pageSize: 10,
loading: false,
noMore: true,
});
//
const fetchList = async (isRefresh = false) => {
if (state.loading) return;
if (isRefresh) {
state.pageNo = 1;
state.noMore = false;
}
if (state.noMore && !isRefresh) return;
state.loading = true;
const { code, data } = await NoticeApi.getPage({
pageNo: state.pageNo,
pageSize: state.pageSize,
});
state.loading = false;
if (code === 0 && data) {
const list = data.list || [];
if (isRefresh) {
state.list = list;
} else {
state.list = state.list.concat(list);
}
//
if (list.length < state.pageSize || state.list.length >= data.total) {
state.noMore = true;
}
state.pageNo++;
}
if (isRefresh) {
uni.stopPullDownRefresh();
}
};
//
const loadMore = () => {
fetchList(false);
};
//
const goDetail = (item) => {
uni.navigateTo({ url: `/pages/sub/notice/detail?id=${item.id}` });
};
onLoad(() => {
// scroll-view
const sys = uni.getSystemInfoSync();
const safeBottom = sys.safeAreaInsets?.bottom || 0;
scrollHeight.value = sys.windowHeight - 88 - safeBottom;
fetchList(true);
});
//
onPullDownRefresh(() => {
fetchList(true);
});
</script>
<style lang="scss" scoped>
/* 页面容器 */
.notice-list-page {
height: 100%;
background-color: #F5F5F5;
padding: 24rpx 0 calc(24rpx + env(safe-area-inset-bottom));
}
/* 滚动区域 */
.notice-scroll {
overflow: hidden;
}
/* 通知卡片 */
.notice-card {
background-color: #FFFFFF;
border-radius: 24rpx;
margin: 0 24rpx 24rpx;
padding: 28rpx 32rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
&:active {
opacity: 0.7;
}
.notice-title {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-bottom: 16rpx;
line-height: 1.4;
}
.notice-content {
font-size: 28rpx;
color: #666666;
line-height: 1.6;
margin-bottom: 20rpx;
height: 90rpx;
overflow: hidden;
:deep(p) {
font-size: 28rpx;
color: #666666;
line-height: 1.6;
margin: 0;
}
:deep(img) {
display: none;
}
}
.notice-footer {
display: flex;
align-items: center;
justify-content: space-between;
.notice-time {
font-size: 24rpx;
color: #999999;
}
.arrow-icon {
width: 28rpx;
height: 28rpx;
flex-shrink: 0;
}
}
}
/* 加载状态 */
.load-more {
display: flex;
justify-content: center;
padding: 24rpx 0 40rpx;
text {
font-size: 24rpx;
color: #999999;
}
}
</style>

View File

@ -0,0 +1,158 @@
<!-- 更多服务 - 模仿首页功能入口网格 -->
<template>
<view class="more-service-page">
<!-- 标题区 -->
<view class="page-header">
<text class="page-title">更多服务</text>
<text class="page-subtitle">可在此处查看所有服务哦~</text>
</view>
<!-- 功能入口网格 4列自适应 -->
<view class="service-grid" v-if="serviceList.length > 0">
<view
class="grid-item"
v-for="(item, index) in serviceList"
:key="index"
@tap="handleServiceTap(item)"
>
<view class="icon-wrapper" :style="{ background: item.bgGradient }">
<image class="icon-img" :src="item.icon" mode="aspectFit"></image>
</view>
<text class="icon-label">{{ item.label }}</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<text class="empty-text">暂无数据</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import NoticeApi from '@/sheep/api/community/notice';
//
const serviceList = ref([]);
// 使
const bgGradientPresets = [
'linear-gradient(35deg, #FF7F69 0%, #FC5A5D 100%)',
'linear-gradient(35deg, #52C41A 0%, #36AD1A 100%)',
'linear-gradient(35deg, #FFA940 0%, #FA8C16 100%)',
'linear-gradient(35deg, #4096FF 0%, #1890FF 100%)',
'linear-gradient(35deg, #69B1FF 0%, #4096FF 100%)',
'linear-gradient(35deg, #9254DE 0%, #722ED1 100%)',
'linear-gradient(35deg, #7B61FF 0%, #597EF7 100%)',
'linear-gradient(35deg, #36CFC9 0%, #13C2C2 100%)',
];
// position=2
const fetchServiceList = async () => {
const { code, data } = await NoticeApi.getMiniAppConfigList(2);
if (code === 0 && data && data.length > 0) {
serviceList.value = data.map((item, index) => ({
label: item.name,
icon: item.icon,
path: item.url,
bgGradient: bgGradientPresets[index % bgGradientPresets.length],
}));
}
};
//
const handleServiceTap = (item) => {
const path = item.path ? item.path.trim() : '';
if (path && path !== '/') {
uni.navigateTo({ url: path });
} else {
uni.showToast({ title: `${item.label}功能开发中`, icon: 'none' });
}
};
onLoad(() => {
fetchServiceList();
});
</script>
<style lang="scss" scoped>
.more-service-page {
min-height: 100vh;
background-color: #F5F5F5;
padding: 24rpx;
.page-header {
margin-bottom: 32rpx;
.page-title {
display: block;
font-size: 40rpx;
font-weight: 600;
color: #333333;
font-family: 'PingFang SC', sans-serif;
margin-bottom: 12rpx;
}
.page-subtitle {
display: block;
font-size: 28rpx;
color: #999999;
font-family: 'PingFang SC', sans-serif;
}
}
.service-grid {
background-color: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx 16rpx;
display: flex;
flex-wrap: wrap;
.grid-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
.icon-wrapper {
width: 88rpx;
height: 88rpx;
border-radius: 34rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
.icon-img {
width: 100%;
height: 100%;
}
}
.icon-label {
font-size: 24rpx;
color: #333333;
font-family: 'PingFang SC', sans-serif;
}
}
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 120rpx 0;
background-color: #FFFFFF;
border-radius: 24rpx;
.empty-text {
font-size: 28rpx;
color: #999999;
font-family: 'PingFang SC', sans-serif;
}
}
}
</style>

View File

@ -0,0 +1,30 @@
import request from '@/sheep/request';
const DynamicsApi = {
// 查询社区动态列表
getPage: (params) => {
return request({
url: '/community/post/page',
method: 'GET',
data: params,
custom: {
showLoading: false,
auth: true,
},
});
},
// 查询社区动态详情
getDetail: (id) => {
return request({
url: '/community/post/get',
method: 'GET',
data: { id },
custom: {
showLoading: true,
auth: true,
},
});
},
};
export default DynamicsApi;

View File

@ -0,0 +1,30 @@
import request from '@/sheep/request';
const KnowledgeApi = {
// 查询知识课堂列表
getPage: (params) => {
return request({
url: '/community/knowledge-class/page',
method: 'GET',
data: params,
custom: {
showLoading: false,
auth: true,
},
});
},
// 查询知识课堂详情
getDetail: (id) => {
return request({
url: '/community/knowledge-class/get',
method: 'GET',
data: { id },
custom: {
showLoading: true,
auth: true,
},
});
},
};
export default KnowledgeApi;

View File

@ -0,0 +1,54 @@
import request from '@/sheep/request';
const NoticeApi = {
// 查询通知列表
getPage: (params) => {
return request({
url: '/community/notice/page',
method: 'GET',
data: params,
custom: {
showLoading: false,
auth: true,
},
});
},
// 查询通知详情
getDetail: (id) => {
return request({
url: '/community/notice/get',
method: 'GET',
data: { id },
custom: {
showLoading: true,
auth: true,
},
});
},
// 查询Banner列表
getBannerList: (position) => {
return request({
url: '/community/banner/list',
method: 'GET',
data: { position },
custom: {
showLoading: false,
auth: false,
},
});
},
// 查询小程序配置列表(功能入口)
getMiniAppConfigList: (position) => {
return request({
url: '/community/mini-app-config/list',
method: 'GET',
data: { position },
custom: {
showLoading: false,
auth: false,
},
});
},
};
export default NoticeApi;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB