feat: 新增社区动态和消息页面及相关功能

main
cr 2026-04-23 23:51:52 +08:00
parent 055dfcf3fd
commit 3f64725886
28 changed files with 2382 additions and 188 deletions

5
.env
View File

@ -5,7 +5,8 @@ SHOPRO_VERSION=v2.4.1
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://127.0.0.1:48080
SHOPRO_DEV_BASE_URL=http://1.12.53.43:9999
### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
@ -32,4 +33,4 @@ SHOPRO_H5_URL=http://127.0.0.1:3000
SHOPRO_MPLIVE_ON=0
# 租户ID 默认 1
SHOPRO_TENANT_ID=1
SHOPRO_TENANT_ID=162

View File

@ -116,6 +116,18 @@
"group": "物业管理"
}
},
{
"path": "pages/index/switch-house",
"style": {
"navigationBarTitleText": ""
},
"meta": {
"auth": false,
"sync": true,
"title": "切换房号",
"group": "物业管理"
}
},
{
"path": "pages/index/auth-form",
"style": {
@ -636,7 +648,18 @@
"group": "物业管理"
}
},
{
{
"path": "income/index",
"style": {
"navigationBarTitleText": "收益公示"
},
"meta": {
"sync": true,
"title": "收益公示",
"group": "物业管理"
}
},
{
"path": "knowledge/classroom",
"style": {
"navigationBarTitleText": "知识课堂"

View File

@ -0,0 +1,160 @@
<!-- 消息中心 -->
<template>
<s-layout title="消息" navbar="inner">
<view class="msg-page">
<!-- 消息列表 -->
<scroll-view scroll-y class="msg-scroll">
<view class="msg-list">
<view
v-for="(item, index) in msgList"
:key="item.id"
class="msg-item"
@tap="handleMsgTap(item)"
>
<view class="msg-avatar">
<image :src="item.avatar" mode="aspectFill" />
<view v-if="item.unread" class="unread-dot"></view>
</view>
<view class="msg-content">
<view class="msg-header">
<text class="msg-name">{{ item.name }}</text>
<text class="msg-time">{{ item.time }}</text>
</view>
<view class="msg-body">
<text class="msg-text">{{ item.content }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 空状态无数据时显示 -->
<view v-if="msgList.length === 0" class="empty-state">
<text class="empty-text">暂无消息</text>
</view>
</view>
</s-layout>
</template>
<script setup>
import { ref } from 'vue';
//
const msgList = ref([
{ id: 1, name: '系统通知', avatar: '/static/img/login_img.png', content: '您的佣金已到账', time: '10:30', unread: true },
{ id: 2, name: '推广助手', avatar: '/static/img/login_img.png', content: '您有新的团队成员加入', time: '昨天', unread: false },
{ id: 3, name: '订单通知', avatar: '/static/img/login_img.png', content: '您推广的商品已成功下单', time: '01-08', unread: false }
]);
//
function handleMsgTap(item) {
console.log('查看消息:', item);
}
</script>
<style lang="scss" scoped>
.msg-page {
min-height: 100vh;
background-color: #F5F5F5;
}
/* 滚动区域 */
.msg-scroll {
height: calc(100vh - 88rpx);
}
/* 消息列表 */
.msg-list {
padding: 16rpx 24rpx;
}
/* 单条消息 */
.msg-item {
display: flex;
align-items: flex-start;
padding: 24rpx;
margin-bottom: 20rpx;
background-color: #FFFFFF;
border-radius: 16rpx;
/* 头像 */
.msg-avatar {
position: relative;
width: 88rpx;
height: 88rpx;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
image {
width: 100%;
height: 100%;
}
/* 未读红点 */
.unread-dot {
position: absolute;
top: 0;
right: 0;
width: 18rpx;
height: 18rpx;
background-color: #FA7E49;
border-radius: 50%;
border: 4rpx solid #FFFFFF;
}
}
/* 内容 */
.msg-content {
flex: 1;
margin-left: 20rpx;
overflow: hidden;
}
}
/* 头部:名称 + 时间 */
.msg-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
.msg-name {
font-size: 30rpx;
font-weight: 500;
color: #333333;
}
.msg-time {
font-size: 24rpx;
color: #999999;
flex-shrink: 0;
margin-left: 16rpx;
}
}
/* 消息内容 */
.msg-body {
.msg-text {
font-size: 26rpx;
color: #888888;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
}
/* 空状态 */
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding-top: 200rpx;
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
</style>

View File

@ -0,0 +1,290 @@
<!-- 社区动态页面 -->
<template>
<s-layout title="社区动态" :bgStyle="{ backgroundColor:'#F8EDE8' }" color="#333333">
<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>
<!-- 动态列表 - 独立滚动区域 -->
<scroll-view class="dynamics-list" scroll-y :style="{ height: scrollViewHeight + 'px' }">
<view class="list-inner">
<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 class="empty-state" v-if="dynamicsList.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 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'
}
]);
//
onLoad((options) => {
if (options.tab) {
currentTab.value = parseInt(options.tab);
}
loadDynamicsList();
});
// 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;
loadDynamicsList();
}
//
async function loadDynamicsList() {
// TODO: API
}
//
function goDetail(item) {
uni.navigateTo({
url: `/pages/community/dynamics-detail?id=${item.id}`
});
}
</script>
<style lang="scss" scoped>
/* 页面容器 - 伪元素渐变向上覆盖到导航栏 */
.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;
}
}
/* Tab 切换栏 */
.tab-bar {
display: flex;
align-items: center;
justify-content: space-around;
padding: 36rpx 50rpx;
// background-color: #FFFFFF;
.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动态绑定 */
.dynamics-list {
/* 高度由 :style="{ height: scrollViewHeight + 'px' }" 控制 */
}
/* 列表内容包裹层 - 负责padding间距 */
.list-inner {
padding: 24rpx 32rpx;
}
/* 动态项 */
.dynamics-item {
display: flex;
background-color: #FFFFFF;
border-radius: 31rpx;
padding: 24rpx;
margin-bottom: 19rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.item-cover {
width: 305rpx;
height: 168rpx;
border-radius: 19rpx;
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

@ -6,26 +6,26 @@
<!-- 小区名称 -->
<view class="form-item">
<text class="form-label">小区名称</text>
<view class="form-value">
<text class="value-text">融侨馨苑</text>
<view class="form-value" @tap="showCommunityPicker">
<text class="value-text">{{ state.form.communityName || '请选择小区' }}</text>
<image class="arrow-icon" src="/static/img/right-icon-black.png" mode="aspectFit" />
</view>
</view>
<!-- 序号楼号/单元/ -->
<view class="form-item form-row">
<view class="form-item form-row" @tap="showHousePicker">
<text class="form-label">序号</text>
<view class="row-inputs">
<view class="col-item">
<input class="col-input" v-model="state.form.building" />
<input class="col-input" v-model="state.form.buildingNo" placeholder="请选择" disabled />
<text class="col-label">号楼</text>
</view>
<view class="col-item">
<input class="col-input" v-model="state.form.unit" />
<input class="col-input" v-model="state.form.unitNo" placeholder="请选择" disabled />
<text class="col-label">单元</text>
</view>
<view class="col-item">
<input class="col-input" v-model="state.form.room" />
<input class="col-input" v-model="state.form.roomNo" placeholder="请选择" disabled />
<text class="col-label"></text>
</view>
</view>
@ -54,7 +54,7 @@
<view class="form-item">
<text class="form-label">与产权人关系</text>
<view class="form-value" @tap="showRelationPicker">
<text class="value-text placeholder-color">{{ state.form.relation || '请输入' }}</text>
<text class="value-text placeholder-color">{{ state.form.relationType || '请输入' }}</text>
<image class="arrow-icon" src="/static/img/right-icon-black.png" mode="aspectFit" />
</view>
</view>
@ -79,7 +79,7 @@
<text class="form-label">手机号</text>
<view class="form-value">
<input
v-model="state.form.phone"
v-model="state.form.mobile"
class="form-input"
type="number"
maxlength="11"
@ -119,7 +119,7 @@
<view class="form-item">
<text class="form-label">性别</text>
<view class="form-value" @tap="showGenderPicker">
<text class="value-text placeholder-color">{{ state.form.gender || '请选择' }}</text>
<text class="value-text placeholder-color">{{ state.form.sex === 1 ? '男' : state.form.sex === 2 ? '女' : '请选择' }}</text>
<image class="arrow-icon" src="/static/img/right-icon-black.png" mode="aspectFit" />
</view>
</view>
@ -145,13 +145,32 @@
<text>上传附件</text>
</view>
<view class="upload-area">
<view class="upload-btn" @tap="handleUpload">
<view v-if="state.attachmentUrl" class="upload-preview" @tap="handleUploadAttachment">
<image class="preview-img" :src="state.attachmentUrl" mode="aspectFill" />
</view>
<view v-else class="upload-btn" @tap="handleUploadAttachment">
<view class="upload-icon-wrapper">
<text class="upload-folder-icon">📁</text>
</view>
<text class="upload-text">选择文件</text>
</view>
</view>
<!-- 上传人脸照片 -->
<view class="form-section-title">
<text>上传人脸照片</text>
</view>
<view class="upload-area">
<view v-if="state.facePhotoUrl" class="upload-preview" @tap="handleUploadFace">
<image class="preview-img" :src="state.facePhotoUrl" mode="aspectFill" />
</view>
<view v-else class="upload-btn" @tap="handleUploadFace">
<view class="upload-icon-wrapper">
<text class="upload-folder-icon">📁</text>
</view>
<text class="upload-text">选择照片</text>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
@ -163,33 +182,152 @@
</template>
<script setup>
import { reactive, ref } from 'vue';
import { reactive, ref, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import MemberHouseApi from '@/sheep/api/community/memberHouse';
import CommunityApi from '@/sheep/api/community/community';
import FileApi from '@/sheep/api/infra/file';
import { tenantId } from '@/sheep/config';
import sheep from '@/sheep';
//
const userStore = sheep.$store('user');
const userInfo = computed(() => userStore.userInfo || {});
//
const state = reactive({
form: {
community: '融侨馨苑',
building: '',
unit: '',
room: '',
communityId: null, // ID
communityName: '',
houseId: null, // ID
buildingNo: '',
unitNo: '',
roomNo: '',
isOwner: true,
relation: '',
relationType: '',
name: '',
phone: '',
mobile: '',
idType: '',
idNumber: '',
gender: '',
sex: '',
birthday: ''
},
communityList: [], //
houseTree: [], // ->->
attachmentUrl: '', // URL
facePhotoUrl: '', // URL
});
onLoad(async () => {
//
const { code, data } = await CommunityApi.getSimpleList();
if (code === 0 && data && data.length > 0) {
state.communityList = data;
// SHOPRO_TENANT_ID
const target = data.find((item) => String(item.id) === String(tenantId)) || data[0];
state.form.communityId = target.id;
state.form.communityName = target.communityName;
//
await loadHouseTree(target.id);
} else {
// 使
state.form.communityName = userInfo.value.currentCommunityName || '请选择小区';
}
});
//
const loadHouseTree = async (communityId) => {
if (!communityId) return;
const { code, data } = await CommunityApi.getHouseTree(communityId);
if (code === 0) {
state.houseTree = data || [];
}
};
//
const showCommunityPicker = () => {
if (!state.communityList.length) return;
const itemList = state.communityList.map((item) => item.communityName);
uni.showActionSheet({
itemList,
success: async (res) => {
const selected = state.communityList[res.tapIndex];
state.form.communityId = selected.id;
state.form.communityName = selected.communityName;
//
await loadHouseTree(selected.id);
//
state.form.houseId = null;
state.form.buildingNo = '';
state.form.unitNo = '';
state.form.roomNo = '';
}
});
};
// -> ->
const showHousePicker = () => {
if (!state.houseTree.length) {
uni.showToast({ title: '该小区暂无房屋数据', icon: 'none' });
return;
}
//
const buildings = state.houseTree;
const buildingLabels = buildings.map((item) => item.label);
uni.showActionSheet({
itemList: buildingLabels,
success: (buildingRes) => {
const building = buildings[buildingRes.tapIndex];
//
const units = building.children || [];
if (!units.length) {
state.form.houseId = building.value;
state.form.buildingNo = building.label;
state.form.unitNo = '';
state.form.roomNo = '';
return;
}
const unitLabels = units.map((item) => item.label);
uni.showActionSheet({
itemList: unitLabels,
success: (unitRes) => {
const unit = units[unitRes.tapIndex];
//
const rooms = unit.children || [];
if (!rooms.length) {
state.form.houseId = unit.value;
state.form.buildingNo = building.label;
state.form.unitNo = unit.label;
state.form.roomNo = '';
return;
}
const roomLabels = rooms.map((item) => item.label);
uni.showActionSheet({
itemList: roomLabels,
success: (roomRes) => {
const room = rooms[roomRes.tapIndex];
state.form.houseId = room.value;
state.form.buildingNo = building.label;
state.form.unitNo = unit.label;
state.form.roomNo = room.label;
}
});
}
});
}
});
};
//
const showRelationPicker = () => {
uni.showActionSheet({
itemList: ['本人', '配偶', '父母', '子女', '其他'],
success: (res) => {
const relations = ['本人', '配偶', '父母', '子女', '其他'];
state.form.relation = relations[res.tapIndex];
state.form.relationType = relations[res.tapIndex];
}
});
};
@ -210,7 +348,7 @@ const showGenderPicker = () => {
uni.showActionSheet({
itemList: ['男', '女'],
success: (res) => {
state.form.gender = res.tapIndex === 0 ? '男' : '女';
state.form.sex = res.tapIndex === 0 ? 1 : 2;
}
});
};
@ -224,7 +362,6 @@ const onDateChange = (e) => {
const datePickerRef = ref(null);
const triggerDatePicker = () => {
if (datePickerRef.value) {
// uni-datetime-picker show
const picker = datePickerRef.value.$children?.[0] || datePickerRef.value;
if (picker.show) {
picker.show();
@ -232,25 +369,79 @@ const triggerDatePicker = () => {
}
};
//
const handleUpload = () => {
uni.chooseMessageFile({
//
const uploadImage = (field) => {
uni.chooseImage({
count: 1,
type: 'file',
success: (res) => {
console.log('选择文件:', res.tempFiles[0]);
sourceType: ['album', 'camera'],
success: async (res) => {
const filePath = res.tempFilePaths[0];
const result = await FileApi.uploadFile(filePath, 'member-house');
if (result && result.data) {
state[field] = result.data;
uni.showToast({ title: '上传成功', icon: 'success' });
} else {
uni.showToast({ title: '上传失败', icon: 'none' });
}
}
});
};
//
const handleUploadAttachment = () => uploadImage('attachmentUrl');
//
const handleUploadFace = () => uploadImage('facePhotoUrl');
//
const validateForm = () => {
const f = state.form;
if (!f.communityName) return '请选择小区';
if (!f.buildingNo) return '请输入楼号';
if (!f.unitNo) return '请输入单元号';
if (!f.roomNo) return '请输入门牌号';
if (f.isOwner === null) return '请选择是否产权人';
if (!f.isOwner && !f.relationType) return '请选择与产权人关系';
if (!f.name) return '请输入姓名';
if (!f.mobile) return '请输入手机号';
if (!/^1[3-9]\d{9}$/.test(f.mobile)) return '手机号格式不正确';
if (!f.idType) return '请选择证件类型';
if (!f.idNumber) return '请输入证件号';
if (!f.sex) return '请选择性别';
if (!f.birthday) return '请选择出生日期';
return '';
};
//
const handleSubmit = () => {
console.log('提交表单:', state.form);
//
uni.redirectTo({
url: '/pages/index/auth-success'
});
const handleSubmit = async () => {
const errMsg = validateForm();
if (errMsg) {
uni.showToast({ title: errMsg, icon: 'none' });
return;
}
const params = {
communityId: state.form.communityId || userInfo.value.currentCommunityId || '',
communityName: state.form.communityName,
houseId: state.form.houseId || null,
buildingNo: state.form.buildingNo,
unitNo: state.form.unitNo,
roomNo: state.form.roomNo,
isOwner: state.form.isOwner,
relationType: state.form.relationType,
name: state.form.name,
mobile: state.form.mobile,
idType: state.form.idType,
idNumber: state.form.idNumber,
sex: state.form.sex,
birthday: state.form.birthday,
attachmentUrl: state.attachmentUrl,
facePhotoUrl: state.facePhotoUrl,
};
const { code } = await MemberHouseApi.create(params);
if (code === 0) {
uni.redirectTo({ url: '/pages/index/auth-success' });
}
};
</script>
@ -513,6 +704,18 @@ const handleSubmit = () => {
color: #999999;
}
}
.upload-preview {
width: 180rpx;
height: 180rpx;
border-radius: 16rpx;
overflow: hidden;
.preview-img {
width: 100%;
height: 100%;
}
}
}
/* 底部按钮 */

View File

@ -246,13 +246,6 @@
cart.delete(state.selectedIds);
}
function getCartList() {
cart.getList();
}
onShow(() => {
getCartList();
});
</script>
<style lang="scss" scoped>

View File

@ -22,8 +22,8 @@
</view>
<view class="house-info">
<view class="house-name-row">
<text class="house-name">{{ item.name }}</text>
<text :class="['status-tag', `status-${item.status}`]">{{ getStatusText(item.status) }}</text>
<text class="house-name">{{ item.communityName }}</text>
<text :class="['status-tag', getStatusClass(item.status)]">{{ getStatusText(item.status) }}</text>
</view>
<text class="house-address">{{ item.address }}</text>
</view>
@ -31,17 +31,17 @@
<!-- 操作区已认证/已驳回显示 -->
<view
v-if="item.status !== 'pending'"
v-if="item.status !== 0"
class="house-action"
@tap.stop="handleItemTap(item)"
>
<view class="action-left">
<image
class="action-icon"
:src="item.status === 'approved' ? '/static/img/yz-icon1.png' : '/static/img/yz-icon2.png'"
:src="item.status === 1 ? '/static/img/yz-icon1.png' : '/static/img/yz-icon2.png'"
mode="aspectFit"
/>
<text class="action-text">{{ item.status === 'approved' ? '查看关联人员' : '查看原因' }}</text>
<text class="action-text">{{ item.status === 1 ? '查看关联人员' : '查看原因' }}</text>
</view>
<image class="action-arrow-icon" src="/static/img/right-icon.png" mode="aspectFit" />
</view>
@ -82,52 +82,57 @@
<script setup>
import { reactive } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import MemberHouseApi from '@/sheep/api/community/memberHouse';
//
const state = reactive({
houseList: [
{
id: 1,
name: '融侨馨苑',
address: '12号楼1单元701',
status: 'pending', // pending, approved, rejected
},
{
id: 2,
name: '融侨馨苑',
address: '12号楼1单元701',
status: 'approved',
},
{
id: 3,
name: '融侨馨苑',
address: '12号楼1单元701',
status: 'rejected',
rejectReason: '因房产合同照片模糊,该认证信息不予通过',
},
],
houseList: [],
showRejectModal: false,
rejectReason: '',
});
// 0-, 1-, 2-
const STATUS_MAP = {
0: { text: '待审核', class: 'status-pending' },
1: { text: '已认证', class: 'status-approved' },
2: { text: '已驳回', class: 'status-rejected' },
};
//
const getStatusText = (status) => {
const map = {
pending: '待审核',
approved: '已认证',
rejected: '已驳回'
};
return map[status] || '未知';
return STATUS_MAP[status]?.text || '未知';
};
//
const getStatusClass = (status) => {
return STATUS_MAP[status]?.class || '';
};
//
const fetchHouseList = async () => {
const { code, data } = await MemberHouseApi.getMyList();
if (code === 0) {
state.houseList = (data || []).map((item) => ({
...item,
//
address: `${item.buildingNo}${item.unitNo}${item.roomNo}`,
}));
}
};
onLoad(() => {
fetchHouseList();
});
//
const handleItemTap = (item) => {
if (item.status === 'approved') {
if (item.status === 1) {
// -
uni.navigateTo({
url: '/pages/index/related-members'
});
} else if (item.status === 'rejected') {
} else if (item.status === 2) {
// -
state.rejectReason = item.rejectReason || '认证信息不完整,请重新提交';
state.showRejectModal = true;

View File

@ -1,12 +1,6 @@
<template>
<s-layout title="访客管理" navbar="custom">
<s-layout title="访客管理" >
<view class="guest-page">
<!-- 自定义导航栏 -->
<view class="navbar">
<view class="navbar-content">
<text class="navbar-title">访客管理</text>
</view>
</view>
<!-- 空状态内容区 -->
<view class="empty-state">
@ -38,6 +32,7 @@
min-height: 100vh;
background: linear-gradient(180deg, #FFF5F0 0%, #FFFFFF 30%);
padding-bottom: 120rpx;
overflow:hidden;
}
/* 导航栏 */
@ -72,7 +67,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 200rpx;
padding-top: 200rpx;
/* 空状态图标 */
.empty-icon {

View File

@ -6,10 +6,15 @@
<!-- 顶部小区选择器 -->
<view class="community-selector">
<text class="community-name" @tap="goLogin"></text>
<text class="community-name" @tap="showCommunityPicker">{{ communityName }}</text>
<text class="arrow-down"></text>
<text class="not-auth" @tap="goCheck">!!</text>
<!-- 未认证 -->
<text v-if="!isHouseAuth" class="not-auth" @tap="goCheck">!!</text>
<!-- 已认证但未选择房屋 -->
<text v-else-if="!hasSelectedHouse" class="not-auth" @tap="goCheck"></text>
<!-- 已认证且已选择房屋 -->
<text v-else class="house-address" @tap="goCheck">{{ houseAddress }}</text>
</view>
<!-- Banner卡片 -->
@ -103,12 +108,43 @@
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { ref, onMounted, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import MemberHouseApi from '@/sheep/api/community/memberHouse';
import sheep from '@/sheep';
//
const statusBarHeight = ref(0);
//
const userStore = sheep.$store('user');
const userInfo = computed(() => userStore.userInfo || {});
//
const communityTree = ref([]);
//
const selectedCommunityName = ref('');
//
const communityName = computed(() => {
return selectedCommunityName.value || userInfo.value.currentCommunityName || '请选择小区';
});
// currentHouseId
const isHouseAuth = computed(() => {
return !!userInfo.value.currentHouseId;
});
// currentHouseId
const hasSelectedHouse = computed(() => {
return !!userInfo.value.currentHouseId;
});
//
const houseAddress = computed(() => {
return userInfo.value.currentHouseAddress || '';
});
// Banner
const bannerList = ref([
{
@ -127,7 +163,7 @@ const bannerList = ref([
const functionList = ref([
{ 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_3.png', bgGradient: 'linear-gradient(35deg, #FFA940 0%, #FA8C16 100%)', path: '/pages/sub/income/index' },
{ 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: '/pages/sub/staff/index' },
@ -139,8 +175,68 @@ onLoad(() => {
//
const systemInfo = uni.getSystemInfoSync();
statusBarHeight.value = systemInfo.statusBarHeight || 0;
//
fetchCommunityTree();
});
onShow(() => {
//
if (userStore.isLogin) {
userStore.getInfo();
}
});
//
const fetchCommunityTree = async () => {
const { code, data } = await MemberHouseApi.getCommunityTree();
if (code === 0 && data && data.length > 0) {
communityTree.value = data;
//
selectedCommunityName.value = data[0].communityName;
}
};
//
const showCommunityPicker = () => {
if (!communityTree.value.length) {
uni.showToast({ title: '暂无小区数据', icon: 'none' });
return;
}
//
const communityNames = communityTree.value.map((item) => item.communityName);
uni.showActionSheet({
itemList: communityNames,
success: (communityRes) => {
const selectedCommunity = communityTree.value[communityRes.tapIndex];
selectedCommunityName.value = selectedCommunity.communityName;
//
const houses = selectedCommunity.houses || [];
if (houses.length > 0) {
const houseNames = houses.map((item) => item.fullAddress || `${item.buildingNo}${item.unitNo}${item.roomNo}`);
uni.showActionSheet({
itemList: houseNames,
success: async (houseRes) => {
const selectedHouse = houses[houseRes.tapIndex];
//
const switchRes = await MemberHouseApi.switchHouse({
memberHouseId: selectedHouse.memberHouseId,
houseId: selectedHouse.houseId,
communityId: selectedCommunity.communityId,
});
if (switchRes.code === 0) {
//
await userStore.getInfo();
uni.showToast({ title: '切换成功', icon: 'success' });
}
}
});
}
}
});
};
//
const handleFunctionTap = (item) => {
if (item.path) {
@ -161,7 +257,13 @@ const goLogin = () => {
};
const goCheck = () => {
uni.navigateTo({ url: '/pages/index/check-page' });
if (!isHouseAuth.value) {
// /
uni.navigateTo({ url: '/pages/index/check-page' });
} else {
//
uni.navigateTo({ url: '/pages/index/switch-house' });
}
};
//
@ -223,6 +325,18 @@ const goActivityDetail = () => {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 24rpx;
font-family: 'PingFang SC', sans-serif;
}
.house-address {
margin-left: 30rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 24rpx;
color: #666666;
font-family: 'PingFang SC', sans-serif;
}
}

View File

@ -57,9 +57,15 @@
</view>
</view>
<!-- 底部微信登录 -->
<view class="wechat-login" @tap="wechatLogin">
<image class="wechat-icon" src="/static/img/wx-icon.png" mode="aspectFit"></image>
<!-- 底部微信一键登录 -->
<view class="wechat-login">
<button
class="phone-btn"
open-type="getPhoneNumber"
@getphonenumber="onMobileLogin"
>
<image class="wechat-icon" src="/static/img/wx-icon.png" mode="aspectFit"></image>
</button>
</view>
</view>
</template>
@ -185,25 +191,31 @@ const handleLogin = async () => {
}
};
//
const wechatLogin = async () => {
//
const onMobileLogin = async (e) => {
if (!agreed.value) {
uni.showToast({ title: '请先同意隐私政策和用户协议', icon: 'none' });
return;
}
// 使
if (sheep.$platform.name === 'WechatMiniProgram') {
const result = await sheep.$platform.useProvider('wechat').login();
if (result) {
await sheep.$store('user').getInfo();
uni.navigateBack();
} else {
uni.showToast({ title: '微信登录失败', icon: 'none' });
}
} else {
uni.showToast({ title: '当前环境不支持微信登录', icon: 'none' });
// #ifdef MP-WEIXIN
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
uni.showToast({ title: '手机号授权失败', icon: 'none' });
return;
}
const result = await sheep.$platform.useProvider('wechat').mobileLogin(e.detail);
if (result) {
await sheep.$store('user').getInfo();
uni.navigateBack();
} else {
uni.showToast({ title: '登录失败', icon: 'none' });
}
// #endif
// #ifndef MP-WEIXIN
uni.showToast({ title: '当前环境不支持微信登录', icon: 'none' });
// #endif
};
</script>
@ -356,10 +368,28 @@ const wechatLogin = async () => {
align-items: flex-end;
justify-content: center;
padding-bottom: 80rpx;
.wechat-icon {
.phone-btn {
width: 88rpx;
height: 88rpx;
padding: 0;
margin: 0;
background: transparent;
border: none;
border-radius: 0;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
&::after {
display: none;
}
.wechat-icon {
width: 88rpx;
height: 88rpx;
}
}
}
</style>

View File

@ -0,0 +1,260 @@
<!-- 切换房号页面 -->
<template>
<s-layout title="切换房号">
<view class="switch-house-page">
<!-- 小区名称 -->
<view class="section-label">小区名称</view>
<picker
mode="selector"
:range="state.communityList"
range-key="name"
:value="state.communityIndex"
@change="onCommunityChange"
>
<view class="card community-card">
<text class="community-name">{{ state.currentCommunity }}</text>
<!-- 下拉箭头 SVG -->
<svg class="arrow-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 9L12 15L18 9" stroke="#999999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</view>
</picker>
<!-- 房号 -->
<view class="section-label">房号</view>
<scroll-view class="house-scroll" scroll-y :style="{ height: scrollHeight + 'px' }">
<view class="card house-card">
<view
v-for="(item, index) in state.houseList"
:key="index"
class="house-item"
:class="{ active: state.selectedHouseId === item.id }"
@tap="selectHouse(item)"
>
<!-- 单选按钮 -->
<radio
class="house-radio"
:checked="state.selectedHouseId === item.id"
color="#FF7B54"
/>
<text class="house-name">{{ item.name }}</text>
</view>
</view>
</scroll-view>
<!-- 底部去认证 -->
<view class="bottom-link-wrapper">
<text class="auth-link" @tap="goAddHouse"></text>
</view>
</view>
</s-layout>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import MemberHouseApi from '@/sheep/api/community/memberHouse';
import sheep from '@/sheep';
// scroll-view
const scrollHeight = ref(0);
//
const state = reactive({
//
communityTree: [],
// picker
communityList: [],
//
communityIndex: 0,
//
currentCommunity: '',
//
houseList: [],
// ID memberHouseId
selectedHouseId: '',
});
//
const fetchCommunityTree = async () => {
const { code, data } = await MemberHouseApi.getCommunityTree();
if (code === 0 && data && data.length > 0) {
state.communityTree = data;
state.communityList = data.map((item) => ({
id: item.communityId,
name: item.communityName,
}));
//
state.communityIndex = 0;
state.currentCommunity = data[0].communityName;
updateHouseListByIndex(0);
}
};
//
const updateHouseListByIndex = (index) => {
const community = state.communityTree[index];
if (!community) return;
const houses = community.houses || [];
state.houseList = houses.map((item) => ({
id: String(item.memberHouseId),
name: item.fullAddress || `${item.buildingNo}${item.unitNo}${item.roomNo}`,
//
memberHouseId: item.memberHouseId,
houseId: item.houseId,
communityId: community.communityId,
}));
//
state.selectedHouseId = state.houseList[0]?.id || '';
};
onLoad(() => {
// scroll-view - - - - -
const sys = uni.getSystemInfoSync();
const safeBottom = sys.safeAreaInsets?.bottom || 0;
// + 88px 140px 60px
scrollHeight.value = sys.windowHeight - 88 - 140 - 60 - safeBottom;
fetchCommunityTree();
});
//
const selectHouse = async (item) => {
state.selectedHouseId = item.id;
const switchRes = await MemberHouseApi.switchHouse({
memberHouseId: item.memberHouseId,
houseId: item.houseId,
communityId: item.communityId,
});
if (switchRes.code === 0) {
//
await sheep.$store('user').getInfo();
uni.showToast({ title: '已切换至 ' + item.name, icon: 'none' });
}
};
//
const onCommunityChange = (e) => {
const index = e.detail.value;
state.communityIndex = index;
state.currentCommunity = state.communityList[index].name;
updateHouseListByIndex(index);
};
//
const goAddHouse = () => {
uni.navigateTo({
url: '/pages/index/auth-form',
});
};
</script>
<style lang="scss" scoped>
/* 页面容器 */
.switch-house-page {
height: 100%;
background-color: #F5F5F5;
padding: 24rpx 0 calc(40rpx + env(safe-area-inset-bottom));
display: flex;
flex-direction: column;
}
/* 房号滚动区域 */
.house-scroll {
overflow: hidden;
}
/* 区块标签 */
.section-label {
font-size: 28rpx;
font-weight: 400;
color: #666666;
margin: 24rpx 38rpx 16rpx;
line-height: 1.4;
}
/* 白色卡片通用样式 */
.card {
background-color: #FFFFFF;
border-radius: 24rpx;
margin: 0 38rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
/* 小区选择卡片 */
.community-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 32rpx;
.community-name {
font-size: 30rpx;
font-weight: 500;
color: #333333;
}
.arrow-icon {
width: 40rpx;
height: 40rpx;
flex-shrink: 0;
}
}
/* 房号卡片 */
.house-card {
padding: 8rpx 32rpx;
.house-item {
display: flex;
align-items: center;
padding: 28rpx 0;
border-bottom: 1rpx solid #F0F0F0;
transition: background-color 0.2s;
&:last-child {
border-bottom: none;
}
&:active {
opacity: 0.7;
}
.house-radio {
margin-right: 20rpx;
flex-shrink: 0;
transform: scale(0.85);
}
.house-name {
font-size: 30rpx;
font-weight: 400;
color: #333333;
}
&.active .house-name {
color: #FF7B54;
font-weight: 500;
}
}
}
/* 底部认证链接 */
.bottom-link-wrapper {
display: flex;
justify-content: center;
margin-top: auto;
padding: 40rpx 0 calc(40rpx + env(safe-area-inset-bottom));
.auth-link {
font-size: 28rpx;
font-weight: 400;
color: #FF7B54;
text-decoration: underline;
line-height: 1.4;
&:active {
opacity: 0.7;
}
}
}
</style>

View File

@ -95,6 +95,7 @@
import { ref, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import AuthUtil from '@/sheep/api/member/auth';
//
const userInfo = ref({
@ -131,10 +132,15 @@ const handleLogout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
sheep.$store('user').logout();
sheep.$router.go('/pages/index/login-page');
const { code } = await AuthUtil.logout();
if (code === 0) {
sheep.$store('user').logout();
sheep.$router.go('/pages/index/login-page');
} else {
uni.showToast({ title: '退出登录失败', icon: 'none' });
}
}
}
});

View File

@ -0,0 +1,411 @@
<!-- 通知公告详情页 -->
<template>
<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>
</view>
<!-- 发布信息 -->
<view class="publish-info">
<view class="publisher">
<image class="publisher-avatar" src="/static/img/login_img.png" mode="aspectFill" />
<text class="publisher-name">{{ noticeInfo.publisher }}</text>
</view>
<text class="publish-date">{{ noticeInfo.publishDate }}</text>
</view>
<!-- 分割线 -->
<view class="divider"></view>
</view>
</view>paddi n g n
<!-- 富文本内容 - 独立滚动区域 -->
<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 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"
:key="index"
@tap="downloadAttachment(item)"
>
<view class="attachment-left">
<image class="attachment-icon" :src="getAttachmentIcon(item.type)" mode="aspectFit" />
<text class="attachment-name">{{ item.name }}</text>
</view>
<image class="download-icon" src="/static/img/right-icon.png" mode="aspectFit" />
</view>
</view>
</view>
</s-layout>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import sheep from '@/sheep';
// px
const contentScrollHeight = ref(0);
// px
const contentScrollTop = ref(0);
//
const noticeInfo = ref({
title: '',
publisher: '',
publishDate: '',
content: '',
attachments: []
});
//
onLoad((options) => {
if (options.id) {
loadNoticeDetail(options.id);
}
});
// scroll-view
onMounted(() => {
const instance = getCurrentInstance();
nextTick(() => {
const sysInfo = uni.getSystemInfoSync();
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;
}
if (headerRect) {
// scroll-view = +
contentScrollTop.value = headerRect.height + headerRect.top;
// scroll-view = - scroll-view -
contentScrollHeight.value = sysInfo.windowHeight - contentScrollTop.value - attachmentHeight;
}
});
});
});
//
async function loadNoticeDetail(id) {
// TODO: API
// const { code, data } = await NoticeApi.getNoticeDetail(id);
// if (code === 0) {
// noticeInfo.value = 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: '' }
]
};
}
//
function getAttachmentIcon(type) {
const iconMap = {
image: '/static/img/eli-icon1.png',
excel: '/static/img/eli-icon2.png',
pdf: '/static/img/eli-icon2.png',
word: '/static/img/eli-icon2.png'
};
return iconMap[type] || '/static/img/eli-icon2.png';
}
//
function downloadAttachment(item) {
if (!item.url) {
uni.showToast({
title: '附件链接不存在',
icon: 'none'
});
return;
}
uni.showLoading({
title: '下载中...'
});
uni.downloadFile({
url: item.url,
success: (res) => {
if (res.statusCode === 200) {
//
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
uni.hideLoading();
uni.showToast({
title: '下载成功',
icon: 'success'
});
//
uni.openDocument({
filePath: saveRes.savedFilePath,
showMenu: true,
success: () => {
console.log('打开文档成功');
},
fail: () => {
uni.showToast({
title: '无法打开该文件',
icon: 'none'
});
}
});
},
fail: () => {
uni.hideLoading();
uni.showToast({
title: '保存失败',
icon: 'none'
});
}
});
} else {
uni.hideLoading();
uni.showToast({
title: '下载失败',
icon: 'none'
});
}
},
fail: () => {
uni.hideLoading();
uni.showToast({
title: '下载失败',
icon: 'none'
});
}
});
}
</script>
<style lang="scss" scoped>
/* 渐变背景 */
.gradient-bg {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 660rpx;
background: linear-gradient(180deg, #F8EDE8 0%, #F5F5F5 50%);
z-index: 0;
pointer-events: none;
}
/* 页面容器 */
.notice-detail-page {
position: relative;
z-index: 1;
}
/* 固定头部区域(标题+发布信息) */
.detail-header {
position: fixed;
left: 0;
right: 0;
z-index: 10;
background: transparent;
}
.header-inner {
padding: 0 32rpx;
}
/* 底部占位 */
.bottom-placeholder {
height: 300rpx;
}
/* 文章标题 */
.article-title {
padding: 32rpx 0 24rpx;
.title-text {
font-size: 36rpx;
font-weight: 600;
color: #333333;
line-height: 1.5;
}
}
/* 发布信息 */
.publish-info {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 24rpx;
.publisher {
display: flex;
align-items: center;
.publisher-avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
margin-right: 16rpx;
}
.publisher-name {
font-size: 28rpx;
color: #666666;
}
}
.publish-date {
font-size: 26rpx;
color: #999999;
}
}
/* 分割线 */
.divider {
height: 1rpx;
background-color: #E5E5E5;
margin-bottom: 32rpx;
}
/* 富文本滚动区域 - 纯定位不加padding */
.article-scroll {
position: fixed;
left: 0;
right: 0;
z-index: 5;
}
/* 富文本内容 - 圆角背景卡片 */
.article-content {
margin: 0 32rpx; /* 左右间距 */
border-radius: 24rpx;
background-color: #FFFFFF;
}
/* 富文本内容内层 - 控制padding */
.article-inner {
padding: 32rpx;
:deep(p) {
font-size: 28rpx;
color: #333333;
line-height: 1.8;
margin-bottom: 16rpx;
}
:deep(img) {
width: 100%;
border-radius: 16rpx;
margin: 24rpx 0;
}
}
/* 附件区域 - 固定在底部 */
.attachment-section {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 260rpx;
background: #FFFFFF;
box-shadow: 0rpx -8rpx 64rpx 0rpx rgba(0, 0, 0, 0.16);
border-radius: 0;
overflow-y: auto;
z-index: 9999;
}
.attachment-inner {
padding: 24rpx 32rpx;
}
.attachment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
&:not(:last-child) {
border-bottom: 1rpx solid #F0F0F0;
}
.attachment-left {
display: flex;
align-items: center;
flex: 1;
.attachment-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.attachment-name {
font-size: 28rpx;
color: #333333;
}
}
.download-icon {
width: 32rpx;
height: 32rpx;
}
}
</style>

View File

@ -0,0 +1,549 @@
<!-- 收益公示页面 -->
<template>
<s-layout title="收益公示" :bgStyle="{ backgroundColor: '#EDDCD2' }" navbar="inner" color="#333333">
<view class="income-page">
<!-- 渐变背景装饰 -->
<view class="gradient-bg"></view>
<!-- 固定头部财务卡片 + Tab -->
<view class="page-header">
<!-- 财务总览卡片 -->
<view class="finance-card">
<!-- 背景图 -->
<image class="finance-bg" src="/static/img/jr-icon3.png" mode="aspectFill" />
<!-- 右上角标签 -->
<view class="corner-badge">公共财务</view>
<!-- 左侧内容区 -->
<view class="card-left">
<!-- 账单标题 -->
<view class="card-top">
<image class="bill-icon" src="/static/img/jr-icon2.png" mode="aspectFit" />
<text class="bill-title">2025年1月财务账单</text>
</view>
<!-- 余额 -->
<view class="balance-row">
<text class="balance-label">余额</text>
<text class="balance-value">{{ financeInfo.balance }}</text>
</view>
<!-- 收支统计 -->
<view class="stats-row">
<view class="stats-item">
<text class="stats-label">收入</text>
<text class="stats-value">{{ financeInfo.income }}</text>
</view>
<view class="stats-item">
<text class="stats-label">支出</text>
<text class="stats-value">{{ financeInfo.expense }}</text>
</view>
<view class="stats-item">
<text class="stats-label">未收</text>
<text class="stats-value">{{ financeInfo.unreceived }}</text>
</view>
</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>
<!-- 明细列表 - 独立滚动区域 -->
<scroll-view class="income-scroll" scroll-y :style="{ height: scrollHeight + 'px' }">
<view class="income-list">
<!-- 分类项 -->
<view
class="category-block"
v-for="(item, index) in filteredIncomeList"
:key="item.id"
>
<!-- 分类标题行 -->
<view class="category-header" @tap="toggleExpand(item)">
<view class="header-left">
<image class="category-icon" src="/static/img/jr-icon3.png" mode="aspectFit" />
<text class="category-name">{{ item.name }}</text>
</view>
<view class="header-right">
<text class="category-total">{{ item.total }}</text>
<image
class="arrow-icon"
:class="{ 'arrow-up': item.expanded }"
src="/static/img/jr-icon4.png"
mode="aspectFit"
/>
</view>
</view>
<!-- 子项列表展开时显示 -->
<view class="sub-list" v-if="item.expanded">
<view
class="sub-item"
v-for="(sub, subIndex) in item.children"
:key="subIndex"
>
<text class="sub-name">{{ sub.name }}</text>
<text class="sub-value">{{ sub.value }}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="filteredIncomeList.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';
// scroll-view
const scrollHeight = ref(0);
// Tab
const currentTab = ref('income');
// Tab
const tabList = ref([
{ key: 'income', label: '收入' },
{ key: 'expense', label: '支出' },
{ key: 'unreceived', label: '未收' }
]);
//
const financeInfo = ref({
balance: '16,520.00',
income: '19,700.00',
expense: '3,180.00',
unreceived: '0.00'
});
//
const incomeData = ref([
{
id: 1,
category: 'income',
name: '物业费',
total: '¥20000.50',
expanded: true,
children: [
{ name: '物业费', value: '¥10000.50' },
{ name: '物业费', value: '¥10000.00' }
]
},
{
id: 2,
category: 'income',
name: '水电公摊费',
total: '¥200.50',
expanded: false,
children: [
{ name: '水费', value: '¥100.50' },
{ name: '电费', value: '¥100.00' }
]
},
{
id: 3,
category: 'income',
name: '广告创收',
total: '¥200.50',
expanded: true,
children: [
{ name: '电梯广告', value: '¥200.50' },
{ name: '门闸广告', value: '¥200.50' },
{ name: '公区场地广告', value: '¥200.50' }
]
},
{
id: 4,
category: 'income',
name: '公共场地创收',
total: '¥200.50',
expanded: false,
children: [
{ name: '场地租赁', value: '¥200.50' }
]
},
{
id: 5,
category: 'income',
name: '停车收费',
total: '¥200.50',
expanded: false,
children: [
{ name: '月租停车', value: '¥200.50' }
]
}
]);
//
const expenseData = ref([
{
id: 6,
category: 'expense',
name: '保洁费用',
total: '¥1,000.00',
expanded: false,
children: [
{ name: '日常保洁', value: '¥500.00' },
{ name: '垃圾清运', value: '¥500.00' }
]
},
{
id: 7,
category: 'expense',
name: '维修费用',
total: '¥1,500.00',
expanded: false,
children: [
{ name: '电梯维修', value: '¥1,000.00' },
{ name: '水管维修', value: '¥500.00' }
]
},
{
id: 8,
category: 'expense',
name: '绿化养护',
total: '¥680.00',
expanded: false,
children: [
{ name: '绿植采购', value: '¥380.00' },
{ name: '养护人工', value: '¥300.00' }
]
}
]);
//
const unreceivedData = ref([]);
// Tab
const filteredIncomeList = computed(() => {
const allData = [...incomeData.value, ...expenseData.value, ...unreceivedData.value];
return allData.filter(item => item.category === currentTab.value);
});
// /
function toggleExpand(item) {
item.expanded = !item.expanded;
}
// scroll-view
onMounted(() => {
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();
});
});
</script>
<style lang="scss" scoped>
/* ==================== 页面容器 ==================== */
.income-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;
}
/* ==================== 财务总览卡片 ==================== */
.finance-card {
margin: 24rpx 32rpx 24rpx;
padding: 28rpx 0 24rpx 32rpx;
border-radius: 24rpx;
display: flex;
align-items: flex-start;
justify-content: space-between;
position: relative;
overflow: hidden;
min-height: 300rpx;
/* 背景图 */
.finance-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* 右上角标签 */
.corner-badge {
position: absolute;
top: 0;
right: 30rpx;
background: linear-gradient(135deg, #FA7E49 0%, #E86935 100%);
color: #FFFFFF;
font-size: 28rpx;
font-weight: 500;
padding: 12rpx 28rpx;
border-radius: 0 24rpx 0 24rpx;
z-index: 2;
}
/* 左侧内容区 */
.card-left {
flex: 1;
min-width: 0;
position: relative;
z-index: 1;
}
/* 卡片顶部:账单标题 */
.card-top {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.bill-icon {
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.bill-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
}
/* 余额行 - 纵向排列 */
.balance-row {
display: flex;
flex-direction: column;
margin-bottom: 20rpx;
.balance-label {
font-size: 26rpx;
color: #666666;
margin-bottom: 8rpx;
}
.balance-value {
font-size: 52rpx;
font-weight: 700;
color: #FA7E49;
}
}
/* 收支统计行 */
.stats-row {
display: flex;
justify-content: flex-start;
gap: 48rpx;
.stats-item {
display: flex;
flex-direction: column;
.stats-label {
font-size: 24rpx;
color: #666666;
margin-bottom: 8rpx;
}
.stats-value {
font-size: 28rpx;
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;
flex-shrink: 0;
&: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;
}
}
}
}
/* ==================== 明细列表滚动区域 ==================== */
.income-scroll {
width: 100%;
}
/* ==================== 明细列表内容 ==================== */
.income-list {
padding: 0 32rpx 160rpx;
}
/* ==================== 分类块 ==================== */
.category-block {
background-color: #FFFFFF;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
}
/* ==================== 分类标题行 ==================== */
.category-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 24rpx;
.header-left {
display: flex;
align-items: center;
.category-icon {
width: 40rpx;
height: 40rpx;
margin-right: 16rpx;
flex-shrink: 0;
}
.category-name {
font-size: 30rpx;
font-weight: 500;
color: #333333;
}
}
.header-right {
display: flex;
align-items: center;
.category-total {
font-size: 30rpx;
font-weight: 600;
color: #FA7E49;
margin-right: 16rpx;
}
.arrow-icon {
width: 32rpx;
height: 32rpx;
transition: transform 0.3s;
&.arrow-up {
transform: rotate(180deg);
}
}
}
}
/* ==================== 子项列表 ==================== */
.sub-list {
padding: 0 24rpx 16rpx;
.sub-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
border-top: 1rpx solid #F5F5F5;
.sub-name {
font-size: 26rpx;
color: #999999;
}
.sub-value {
font-size: 26rpx;
font-weight: 500;
color: #FA7E49;
}
}
}
/* ==================== 空状态 ==================== */
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
</style>

View File

@ -9,34 +9,49 @@
<view class="page-header">
<!-- 统计卡片 -->
<view class="stats-card">
<!-- 小区名称 -->
<view class="card-top">
<view class="building-info">
<text class="sicon-building building-icon"></text>
<!-- 背景图 -->
<image class="stats-bg" src="/static/img/wy-icon3.png" mode="aspectFill" />
<!-- 右上角标签 -->
<view class="corner-badge">物业人员</view>
<!-- 左侧内容区 -->
<view class="card-left">
<!-- 小区名称 -->
<view class="card-top">
<image class="building-icon" src="/static/img/wy-icon2.png" mode="aspectFit" />
<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="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 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>
<!-- 右侧叠加背景图 -->
<image class="right-bg" src="/static/img/wy-icon1.png" mode="aspectFit" />
<!-- 右侧区域 -->
<view class="card-right"></view>
</view>
<!-- 人员信息标题 -->
<view class="section-title">人员信息</view>
<!-- 分类Tab栏 -->
<scroll-view class="tab-scroll" scroll-x :show-scrollbar="false">
<view class="tab-bar">
@ -51,9 +66,6 @@
</view>
</view>
</scroll-view>
<!-- 人员信息标题 -->
<view class="section-title">人员信息</view>
</view>
<!-- 人员列表 - 独立滚动区域 -->
@ -81,10 +93,7 @@
<!-- 电话 -->
<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>
<text class="phone-text" @tap="callPhone(item.phone)">{{ item.phone }}</text>
</view>
<!-- 职责描述 -->
@ -236,27 +245,68 @@ function callPhone(phone) {
/* ==================== 统计卡片 ==================== */
.stats-card {
margin: 24rpx 32rpx 24rpx;
padding: 28rpx 32rpx 24rpx;
background: linear-gradient(135deg, #EBF4FF 0%, #D6E7FF 100%);
padding: 28rpx 0 24rpx 32rpx;
border-radius: 24rpx;
display: flex;
align-items: flex-start;
justify-content: space-between;
position: relative;
overflow: hidden;
min-height: 320rpx;
/* 卡片顶部:小区名+标签 */
/* 背景图 */
.stats-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* 右侧叠加背景图 */
.right-bg {
position: absolute;
top: 50%;
right: 24rpx;
transform: translateY(-50%);
width: 160rpx;
height: 180rpx;
z-index: 1;
}
/* 右上角标签 */
.corner-badge {
position: absolute;
top: 0;
right: 30rpx;
color: #FFFFFF;
font-size: 28rpx;
font-weight: 500;
padding: 12rpx 28rpx;
border-radius: 0 24rpx 0 24rpx;
z-index: 2;
}
/* 左侧内容区 */
.card-left {
flex: 1;
min-width: 0;
position: relative;
z-index: 1;
}
/* 卡片顶部:小区名 */
.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;
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.building-name {
@ -266,35 +316,25 @@ function callPhone(phone) {
}
}
/* 物业人员标签 */
.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;
flex-direction: column;
margin-bottom: 20rpx;
.total-label {
font-size: 26rpx;
color: #666666;
margin-right: 16rpx;
margin-bottom: 8rpx;
}
.total-count {
font-size: 48rpx;
font-size: 52rpx;
font-weight: 700;
color: #FA7E49; /* 橙色主题 */
.total-unit {
font-size: 28rpx;
font-size: 30rpx;
font-weight: 400;
color: #FA7E49;
margin-left: 4rpx;
@ -305,7 +345,8 @@ function callPhone(phone) {
/* 分类统计行 */
.category-row {
display: flex;
justify-content: space-between;
justify-content: space-around;
gap: 32rpx;
.category-item {
display: flex;
@ -313,9 +354,9 @@ function callPhone(phone) {
align-items: center;
.cat-name {
font-size: 26rpx;
font-size: 24rpx;
color: #666666;
margin-bottom: 8rpx;
margin-bottom: 10rpx;
}
.cat-count {
@ -325,6 +366,18 @@ function callPhone(phone) {
}
}
}
/* 右侧区域 */
.card-right {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-right: 24rpx;
padding-left: 16rpx;
position: relative;
z-index: 1;
}
}
/* ==================== 分类Tab栏 ==================== */
@ -341,6 +394,7 @@ function callPhone(phone) {
padding: 20rpx 0;
margin-right: 40rpx;
position: relative;
flex-shrink: 0; /* 防止Tab项被压缩 */
&:last-child {
margin-right: 0;
@ -397,13 +451,14 @@ function callPhone(phone) {
.staff-item {
display: flex;
padding: 28rpx 0;
position: relative;
/* 分割线(非第一项) */
&:not(:last-child)::after {
/* 分割线 */
&::after {
content: '';
position: absolute;
bottom: 0;
left: 112rpx;
left: 0;
right: 0;
height: 1rpx;
background-color: #F0F0F0;
@ -442,6 +497,9 @@ function callPhone(phone) {
font-weight: 500;
margin-right: 24rpx;
}
.name-text{
margin-right:100rpx;
}
}
/* 电话行 */
@ -478,17 +536,23 @@ function callPhone(phone) {
}
}
/* 职责描述行 */
/* 职责描述行 - 标签与内容同行换行 */
.duty-row {
display: flex;
align-items: flex-start;
.duty-label {
font-size: 26rpx;
color: #999999;
flex-shrink: 0;
}
.duty-text {
flex: 1;
font-size: 26rpx;
color: #666666;
line-height: 1.6;
word-break: break-all;
}
}
}

View File

@ -0,0 +1,29 @@
import request from '@/sheep/request';
const CommunityApi = {
// 获取小区简单列表
getSimpleList: () => {
return request({
url: '/community/community/simple-list',
method: 'GET',
custom: {
showLoading: true,
auth: true,
},
});
},
// 获取小区房屋树
getHouseTree: (communityId) => {
return request({
url: '/community/house/tree',
method: 'GET',
params: { communityId },
custom: {
showLoading: true,
auth: true,
},
});
},
};
export default CommunityApi;

View File

@ -0,0 +1,56 @@
import request from '@/sheep/request';
const MemberHouseApi = {
// 获取我的房屋认证列表
getMyList: () => {
return request({
url: '/community/member-house/my-list',
method: 'GET',
custom: {
showLoading: true,
auth: true,
},
});
},
// 获取小区房屋树
getCommunityTree: () => {
return request({
url: '/community/member-house/community-tree',
method: 'GET',
custom: {
showLoading: true,
auth: true,
},
});
},
// 切换当前房屋
switchHouse: (data) => {
return request({
url: '/community/member-house/switch-house',
method: 'POST',
data,
custom: {
showLoading: true,
auth: true,
showSuccess: true,
successMsg: '切换成功',
},
});
},
// 新增业主认证信息
create: (data) => {
return request({
url: '/community/member-house/create',
method: 'POST',
data,
custom: {
showLoading: true,
auth: true,
showSuccess: true,
successMsg: '提交成功',
},
});
},
};
export default MemberHouseApi;

View File

@ -8,7 +8,7 @@ let subscribeEventList = []
function load() {
checkUpdate()
getSubscribeTemplate()
// getSubscribeTemplate() // 已移除:不需要获取订阅消息模板列表
}
// ================= 登录相关逻辑===================

View File

@ -10,7 +10,7 @@ let subscribeEventList = [];
// 加载微信小程序
function load() {
checkUpdate();
getSubscribeTemplate();
// getSubscribeTemplate(); // 已移除:不需要获取订阅消息模板列表
}
// 微信小程序静默授权登陆

View File

@ -67,12 +67,12 @@ const app = defineStore({
await adaptTenant();
// 加载装修配置
await adaptTemplate(this.template, templateId);
// await adaptTemplate(this.template, templateId);
// TODO 芋艿:【初始化优化】未来支持管理后台可配;对应 https://api.shopro.sheepjs.com/shop/api/init
if (true) {
this.info = {
name: '芋道商城',
name: '物业管理',
logo: 'https://static.iocoder.cn/ruoyi-vue-pro-logo.png',
version: '2026.01',
copyright: '全部开源,个人与企业可 100% 免费使用',
@ -172,7 +172,7 @@ const adaptTenant = async () => {
userStore.setToken();
// 设置新的 tenantId 到本地存储
uni.setStorageSync('tenant-id', newTenantId);
uni.setStorageSync('tenant-id', '162'||newTenantId);
console.log('租户 ID 已更新:', `${oldTenantId} -> ${newTenantId}`);
}
} catch (error) {

View File

@ -16,6 +16,11 @@ const defaultUserInfo = {
gender: 0, // 性别
mobile: '', // 手机号
point: 0, // 积分
currentCommunityId: null, // 当前小区ID
currentCommunityName: '', // 当前小区名称
currentHouseId: null, // 当前房屋ID
currentHouseAddress: '', // 当前房屋完整地址
isHouseAuth: false, // 是否已完成房屋认证(有认证记录)
};
// 默认钱包信息
@ -110,8 +115,8 @@ const user = defineStore({
// 获取最新信息
await this.getInfo();
this.getWallet();
this.getNumData();
// this.getWallet();
// this.getNumData();
return this.userInfo;
},
@ -132,17 +137,17 @@ const user = defineStore({
await this.updateUserData();
// 加载购物车
cart().getList();
// cart().getList();
// 登录后设置全局分享参数
$share.getShareInfo();
// 提醒绑定手机号
if (app().platform.bind_mobile && !this.userInfo.mobile) {
showAuthModal('changeMobile');
}
// if (app().platform.bind_mobile && !this.userInfo.mobile) {
// showAuthModal('changeMobile');
// }
// 绑定推广员
$share.bindBrokerageUser();
// $share.bindBrokerageUser();
},
// 登出系统

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB