完成大体功能和样式

This commit is contained in:
2026-01-15 17:18:24 +08:00
parent 036eb3a206
commit 44a4b33502
211 changed files with 5480 additions and 7826 deletions

313
pages-biz/bill/bill.vue Normal file
View File

@@ -0,0 +1,313 @@
<template>
<view class="bill-list-page">
<!-- 自定义导航栏 -->
<customNavbar title="我的账单"></customNavbar>
<!-- 筛选栏 -->
<view class="filter-bar">
<!-- 年份筛选点击区域 -->
<view class="year-filter" @click="toggleYearPicker">
<text class="year-text">{{ currentYear }}</text>
<u-icon name="arrow-down" size="24" color="#666"></u-icon>
</view>
<!-- 自定义年份选择器弹窗 -->
<u-popup v-model="showYearPicker" mode="bottom" border-radius="20rpx" :closeable="true">
<view class="year-picker-popup">
<!-- 弹窗标题 -->
<view class="popup-header">
<text class="popup-title">选择年份</text>
</view>
<!-- 年份列表 -->
<view class="year-list">
<view class="year-item" v-for="year in years" :key="year"
:class="{ active: currentYear === year }" @click="selectYear(year)">
{{ year }}
</view>
</view>
</view>
</u-popup>
</view>
<!-- 账单列表 -->
<scroll-view scroll-y class="scroll-content" :scroll-top="0" :refresher-enabled="true"
:refresher-triggered="isRefreshing" @refresherrefresh="refresh" @scrolltolower="loadMore">
<view v-if="flowList.length > 0">
<view class="bill-item" v-for="item in flowList" :key="item.id" @click="goDetail(item)">
<!-- 左侧信息 -->
<view class="bill-left">
<text class="bill-name">{{ item.billName }}</text>
<text class="bill-date" v-if="item.payTime">支付时间{{ item.payTime }}</text>
<text class="bill-date" v-if="!item.payTime">账单截止日期: {{ item.billEndDate }}</text>
<text :class="['bill-status', (item.billStatus === '待缴费' ? 'unpay' : paid)]" >{{ item.billStatus }}</text>
</view>
<!-- 右侧金额 + 箭头 -->
<view class="bill-right">
<text class="amount">{{ formatMoney(item.billAmount) }}</text>
<u-icon name="arrow-right" size="28" color="#ccc" />
</view>
</view>
</view>
<view v-else class="empty">
<u-empty mode="list" text="暂无账单" />
</view>
<u-loadmore :status="loadStatus" />
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
bills: [],
flowList: [],
isRefreshing: false,
loadStatus: 'more', // u-loadmore 状态
// 年份筛选相关
currentYear: null,
years: [],
pageNo: 1,
pageSize: 10,
showYearPicker: false
};
},
onLoad(options) {
// 可以从 options 获取用户ID或合同ID
let year = new Date().getFullYear();
this.currentYear = year;
this.years = [];
for (let i = 5; i >= 1; i--) {
this.years.push(year - i);
}
this.loadBills();
},
onReachBottom() {
this.loadStatus = 'loading';
// 获取数据
this.loadBills();
},
onShow(){
this.$checkToken(this.$getToken())
},
methods: {
// 切换年份选择器显示
toggleYearPicker() {
this.showYearPicker = !this.showYearPicker;
},
// 选择年份
selectYear(year) {
this.currentYear = year;
this.showYearPicker = false;
// 重新加载账单数据
this.loadBills();
},
loadBills() {
let url = '/bill/pageQueryContractBill'
this.$u.post(url, {
pageNo: this.pageNo,
pageSize: this.pageSize,
year: this.currentYear,
billStatus: '已缴费'
},{
WT: this.$getToken()
}).then(result => {
const data = result.data.result;
this.bills = data;
for (let i = 0; i < this.bills.length; i++) {
let item = this.bills[i]
this.flowList.push(item);
}
++this.pageNo
}).catch(err => {
console.log("获取合同账单信息失败:", err)
})
},
refresh() {
this.isRefreshing = true;
setTimeout(() => {
this.loadBills();
this.isRefreshing = false;
}, 1000);
},
loadMore() {
// 可以调用接口分页加载更多数据
this.loadStatus = 'noMore';
},
formatMoney(val) {
if (!val && val !== 0) return '—';
return '¥' + Number(val).toFixed(2);
},
goDetail(item) {
uni.navigateTo({
url: `/pages-biz/bill/billDetail?id=${item.id}`,
});
},
},
};
</script>
<style lang="scss" scoped>
.bill-list-page {
background: #f7f8fa;
min-height: 100vh;
padding-top: 160rpx;
/* 给导航栏留空间 */
}
.filter-bar {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
padding: 24rpx 30rpx;
margin: 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
/* 年份筛选 */
.year-filter {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 32rpx;
color: #333;
cursor: pointer;
}
/* 支出/收入切换 */
.tab-bar {
display: flex;
gap: 20rpx;
}
.tab-item {
padding: 12rpx 32rpx;
font-size: 28rpx;
border-radius: 24rpx;
color: #333;
background-color: #f5f5f5;
transition: all 0.2s ease;
&.active {
color: #fff;
background: #ff3b30;
}
}
/* 自定义年份选择器样式 */
.year-picker-popup {
background: #fff;
padding: 30rpx 0;
}
.popup-header {
text-align: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.popup-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.year-list {
padding: 20rpx;
}
.year-item {
font-size: 32rpx;
color: #666;
text-align: center;
padding: 24rpx;
border-radius: 12rpx;
margin-bottom: 16rpx;
background: #f5f5f5;
transition: all 0.2s ease;
&:last-child {
margin-bottom: 0;
}
&.active {
background: #ff3b30;
color: #fff;
}
&:hover {
background: #ff5252;
color: #fff;
}
}
.scroll-content {
margin-top: 20rpx;
padding-bottom: 20rpx;
}
.bill-item {
display: flex;
justify-content: space-between;
background: #fff;
margin: 0 20rpx 20rpx 20rpx;
border-radius: 12rpx;
padding: 20rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.bill-left {
display: flex;
flex-direction: column;
.bill-name {
font-size: 28rpx;
color: #2D2B2C;
font-weight: 500;
}
.bill-status{
font-size: 30rpx;
font-weight: 800;
&.paid {
color: red;
}
&.unpay {
color: green;
}
}
.bill-date,
.bill-status {
margin-top: 18rpx;
}
}
.bill-right {
display: flex;
align-items: center;
.amount {
font-size: 28rpx;
color: #F34038;
margin-right: 10rpx;
}
}
.empty {
margin-top: 200rpx;
text-align: center;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<view class="bill-detail-page">
<!-- 顶部自定义导航栏 -->
<custom-navbar title="账单详情" />
<scroll-view scroll-y class="scroll-content">
<!-- 基本信息 -->
<view class="section">
<view class="section-title">账单信息</view>
<view class="info-item">
<text class="label">费用名称</text>
<text class="value">{{ bill.feeName }}</text>
</view>
<view class="info-item">
<text class="label">支付时间</text>
<text class="value">{{ bill.payTime || '未支付' }}</text>
</view>
<view class="info-item">
<text class="label">支付状态</text>
<text class="value">{{ bill.status }}</text>
</view>
<view class="info-item">
<text class="label">金额</text>
<text class="value">{{ formatMoney(bill.amount) }}</text>
</view>
<view class="info-item" v-if="bill.contractId">
<text class="label">关联合同</text>
<text class="value" @click="goContractDetail(bill.contractId)" style="color:#007aff;">点击查看</text>
</view>
<view class="info-item" v-if="bill.payMethod">
<text class="label">支付方式</text>
<text class="value">{{ bill.payMethod }}</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
bill: {},
};
},
onLoad(options) {
const { id } = options; // 列表页传过来的账单ID
this.loadBillDetail(id);
},
onShow() {
this.$checkToken(this.$getToken())
},
methods: {
loadBillDetail(id) {
// 模拟请求接口
this.bill = {
id,
feeName: '第一期租金2024.12.01-2024.12.31',
payTime: '2024-12-05',
status: '已支付',
amount: 3000,
contractId: 'c123',
payMethod: '线上支付',
};
},
formatMoney(val) {
if (!val && val !== 0) return '—';
return '¥' + Number(val).toFixed(2);
},
goDetail(contractId) {
uni.navigateTo({
url: `/pages-biz/contract/contractDetail?id=${contractId}`,
});
},
},
};
</script>
<style lang="scss" scoped>
.bill-detail-page {
background: #f7f8fa;
min-height: 100vh;
padding-top: 175rpx; // 自定义导航栏高度
}
.scroll-content {
padding: 20rpx;
}
.section {
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
.section-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
.info-item {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.label {
font-size: 26rpx;
color: #666;
}
.value {
font-size: 26rpx;
color: #333;
margin-right: 16rpx; /* 增加右侧安全间距 */
max-width: 70%; /* 防止过长文字挤到箭头或屏幕边缘 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
</style>

View File

@@ -0,0 +1,328 @@
<template>
<view class="pay-history-page">
<customNavbar title="账单" />
<!-- 筛选栏 -->
<view class="filter-bar">
<!-- 年份筛选点击区域 -->
<view class="year-filter" @click="toggleYearPicker">
<text class="year-text">{{ currentYear }}</text>
<u-icon name="arrow-down" size="24" color="#666"></u-icon>
</view>
<!-- 自定义年份选择器弹窗 -->
<u-popup v-model="showYearPicker" mode="bottom" border-radius="20rpx" :closeable="true">
<view class="year-picker-popup">
<!-- 弹窗标题 -->
<view class="popup-header">
<text class="popup-title">选择年份</text>
</view>
<!-- 年份列表 -->
<view class="year-list">
<view class="year-item" v-for="year in years" :key="year"
:class="{ active: currentYear === year }" @click="selectYear(year)">
{{ year }}
</view>
</view>
</view>
</u-popup>
<!-- 顶部tab切换 -->
<view class="tab-bar">
<view v-for="(tab, index) in tabs" :key="index"
:class="['tab-item', activeTab === tab.value ? 'active' : '']" @click="activeTab = tab.value">
{{ tab.label }}
</view>
</view>
</view>
<!-- 收支记录列表 -->
<scroll-view scroll-y class="scroll-content" @scrolltolower="loadMore">
<view v-if="flowList.length > 0" class="record-list">
<view v-for="(item, index) in flowList" :key="index" class="record-item" @click="goDetail(item)">
<!-- 左侧信息 -->
<view class="left">
<view class="title">{{ item.itemName }}</view>
<view class="sub">{{ item.payDate || item.inDate || '未知'}}</view>
</view>
<!-- 右侧金额 + 图标 -->
<view class="right">
<view class="amount" :class="item.inOutType === '收' ? 'income' : 'expense'">
{{ item.inOutType === '收' ? '+' : '-' }}{{ item.itemAmount.toFixed(2) }}
</view>
<u-icon name="arrow-right" size="32" color="#ccc"></u-icon>
</view>
</view>
</view>
<view v-else class="empty">
<u-empty mode="list" text="暂无记录" />
</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
pageNo: 1,
pageSize:10,
flowList: [],
loadStatus: 'loadmore',
activeTab: 'in', // 默认显示“收”
tabs: [{
label: '收入',
value: 'in'
},
{
label: '支出',
value: 'out'
},
],
// 年份筛选相关
currentYear: null,
years: [],
showYearPicker: false
};
},
onLoad(options) {
let year = new Date().getFullYear();
this.currentYear = year;
this.years = [];
for (let i = 5; i >= 1; i--) {
this.years.push(year - i);
}
this.fetchPayRecord();
},
onShow() {
this.$checkToken(this.$getToken())
},
watch: {
activeTab(newVal, oldVal) {
if (newVal !== oldVal) {
this.resetAndLoad()
}
}
},
computed: {
},
methods: {
fetchPayRecord() {
if (this.loadStatus !== 'loadmore') return;
this.loadStatus = 'loading';
let url = '/bill/pageQueryPayRecord'
this.$u.post(url, {
pageNo: this.pageNo,
pageSize: this.pageSize,
year: this.currentYear,
leType: this.activeTab
},{
WT:this.$getToken()
}).then(res => {
const rows = res.data.result || [];
console.log(rows)
if (this.pageNo === 1) this.flowList = [];
this.flowList = this.flowList.concat(rows);
if (rows.length < this.pageSize) {
this.loadStatus = 'nomore';
} else {
this.pageNo++;
this.loadStatus = 'loadmore';
}
}).catch(err => {
console.log("获取账单记录失败:", err)
this.loadStatus = 'loadmore';
})
},
// 切换年份选择器显示
toggleYearPicker() {
this.showYearPicker = !this.showYearPicker;
},
// 选择年份
selectYear(year) {
this.currentYear = year;
this.showYearPicker = false;
this.resetAndLoad();
},
resetAndLoad(){
// 重新加载数据
this.pageNo = 1;
this.flowList = [];
this.fetchPayRecord();
},
loadMore() {
// 只有在 loadStatus 为 'loadmore' 时才触发
if (this.loadStatus !== 'loadmore') return;
// 调用接口获取下一页
this.fetchPayRecord();
},
goDetail(item) {
},
},
};
</script>
<style lang="scss" scoped>
.pay-history-page {
background: #f8f8f8;
min-height: 100vh;
padding-top: 175rpx;
.filter-bar {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
padding: 24rpx 30rpx;
margin: 20rpx;
margin-bottom: 0;
border-radius: 12rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
/* 年份筛选 */
.year-filter {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 32rpx;
color: #333;
cursor: pointer;
}
/* 支出/收入切换 */
.tab-bar {
display: flex;
gap: 20rpx;
}
.tab-item {
padding: 12rpx 32rpx;
font-size: 28rpx;
border-radius: 24rpx;
color: #333;
background-color: #f5f5f5;
transition: all 0.2s ease;
&.active {
color: #fff;
background: #ff3b30;
}
}
/* 自定义年份选择器样式 */
.year-picker-popup {
background: #fff;
padding: 30rpx 0;
}
.popup-header {
text-align: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.popup-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.year-list {
padding: 20rpx;
}
.year-item {
font-size: 32rpx;
color: #666;
text-align: center;
padding: 24rpx;
border-radius: 12rpx;
margin-bottom: 16rpx;
background: #f5f5f5;
transition: all 0.2s ease;
&:last-child {
margin-bottom: 0;
}
&.active {
background: #ff3b30;
color: #fff;
}
&:hover {
background: #ff5252;
color: #fff;
}
}
.record-list {
padding: 20rpx;
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 20rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.03);
.left {
flex: 1;
.title {
font-size: 28rpx;
font-weight:5600;
color: #2D2B2C;
}
.sub {
font-size: 24rpx;
color: #86868C;
margin-top: 18rpx;
}
.status {
font-size: 28rpx;
&.paid {
color: #73B936;
}
&.unpaid {
color: #F34038;
}
}
}
.right {
display: flex;
align-items: center;
.amount {
font-size: 28rpx;
font-weight: 500;
margin-right: 10rpx;
&.income {
color: #73B936;
}
&.expense {
color: #F34038;
}
}
}
}
}
.empty {
margin-top: 100rpx;
}
}
</style>

View File

@@ -0,0 +1,248 @@
<template>
<view>
<view class="list">
<view class="keyList" v-for="(keyItem, keyIndex) in renderList" :key="keyIndex">
<view :id="'keyword' + keyItem.key" class="keyword">
<text>{{ keyItem.key === 'AAA' ? '热门地区' : keyItem.key }}</text>
</view>
<view class="item b-b" v-for="(item, index) in keyItem.list" :key="index" @click="chooseCity(item)">
<text>{{ item.label }}</text>
</view>
</view>
</view>
<view class="key-list column" @click.stop.prevent="stopPrevent">
<view
class="key-item"
:class="{active: index === keywordCurrent}"
v-for="(item, index) in renderList"
:key="index"
@click="onKeywordClick(index)"
>
<text>{{ item.key === 'AAA' ? '#' : item.key }}</text>
</view>
</view>
<view class="key-show center" :class="{show: keyChanged}">
<text>{{ keywordCurrentName }}</text>
</view>
</view>
</template>
<script>
import getFirstLetter from './js/getFirstLetter.js'
import cityData from './js/city'
const _anchorList = [];
let _onKeywordClicking = false;
let _timer = 0;
export default {
data() {
return {
keyChanged: false,
keywordCurrentName: '',
keywordCurrent: 0, //当前索引
renderList: [],
list: [],
}
},
onPageScroll(e) {
if (_onKeywordClicking) {
return;
}
const top = e.scrollTop;
for (let i = 0; i < _anchorList.length; i++) {
if (top > _anchorList[i]) {
this.keywordCurrent = i;
}
}
},
watch: {
keywordCurrent(val) {
this.keywordCurrentName = this.renderList[val].key;
if (_timer) {
clearTimeout(_timer);
}
this.keyChanged = true;
_timer = setTimeout(() => {
this.keyChanged = false;
}, 500)
}
},
onLoad(options) {
this.initList();
},
methods: {
chooseCity(item){
const pages = getCurrentPages();
const prePage = (pages[pages.length - 2]).$vm;
prePage.city = item.label;
prePage.searchList();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url:'/pages/index/index'
})
}
},
//初始化列表
initList() {
const list = cityData;
const tempData = {};
list.forEach(item => {
let key = getFirstLetter.getLetter(item.label).firstletter;
if (!tempData[key]) {
tempData[key] = [];
}
tempData[key].push(item);
})
//排序用
const tempKeyList = [];
for (let key in tempData) {
tempKeyList.push(key);
}
tempKeyList.sort();
const renderList = [];
tempKeyList.forEach(keyword => {
for (let key in tempData) {
if (key == keyword) {
renderList.push({
key,
list: tempData[key]
})
}
}
})
this.renderList = renderList;
//生成右侧索引
setTimeout(() => {
this.calcAnchor();
}, 500)
},
//计算锚点高度
calcAnchor() {
const list = this.renderList;
let size = {};
const placeFillHeight = this.systemInfo.navigationBarHeight + this.systemInfo.statusBarHeight;
list.forEach(async item => {
let size = await this.getElSize('keyword' + item.key);
item.top = size.top;
_anchorList.push(size.top);
})
},
//右侧索引点击
onKeywordClick(index) {
_onKeywordClicking = true;
setTimeout(() => {
_onKeywordClicking = false;
}, 500)
this.keywordCurrent = index;
uni.pageScrollTo({
scrollTop: this.renderList[index].top
})
},
//获得元素的size
getElSize(id) {
return new Promise(res => {
let el = uni.createSelectorQuery().select('#' + id);
el.boundingClientRect(data => {
res(data);
}).exec();
});
}
}
}
</script>
<style>
page {
background-color: #f7f7f7;
}
</style>
<style scoped lang="scss">
.key-show {
position: fixed;
left: 50%;
top: 50%;
z-index: 95;
transform: translate(-50%, -50%);
width: 80rpx;
height: 80rpx;
background-color: #2979ff;
border-radius: 100rpx;
font-size: 40rpx;
font-weight: 600;
color: #fff;
opacity: 0;
&.show {
opacity: 1;
}
}
.keyList {
position: relative;
.keyword {
position: sticky;
left: 0;
top: var(--window-top);
z-index: 95;
padding-top: 6rpx;
padding-left: 30rpx;
width: 100%;
height: 66rpx;
line-height: 60rpx;
font-size: 28rpx;
color: #333;
font-weight: 700;
background-color: #f7f7f7;
}
.item {
padding-left: 30rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #333;
background-color: #fff;
&:after {
left: 30rpx;
}
.cn-name {
margin-left: 12rpx;
color: #666;
}
}
}
/* 右侧索引 */
.key-list {
position: fixed;
right: 0;
top: 50%;
z-index: 96;
padding-right: 20rpx;
padding-left: 40rpx;
transform: translateY(-50%);
.key-item {
width: 40rpx;
height: 40rpx;
margin-bottom: 2rpx;
text-align: center;
line-height: 40rpx;
font-size: 26rpx;
color: #333;
border-radius: 100rpx;
}
.active {
background-color: #2979ff;
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,407 @@
<template>
<view class='app'>
<!-- 当前选择地址 -->
<view class="current-address row b-b">
<view class="left">
<view class="row">
<text class="red">[当前]</text>
<text class="title">{{currentAddress.title}}</text>
</view>
<text class="addr clamp">{{currentAddress.address}}</text>
</view>
<!-- 搜索图标 -->
<image
class="ser-icon"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAENElEQVRoQ+2a/1XcMAzHlQkKExQmaJmAMgHtBJQJqkzQMsGpExQm6HWCHhMUJihMUJjAfd97znuucc6Sklxf4PwXjzixPtYPS/I19MJG88J4aQf83DW+0/BOw89sByY3aWbea5rmDREdhBAO0v1rmmYVQrgXkbtt7eskwMz8lojOiOgdEeHv2nggolXTNMvFYnFVmzzk+ajAbduehhA4gnrlArwQ0VcRwd+jjlGAo0YXA0FzsDW4iFyMSTwYmJk/E9GXDULdEtGSiOCnqa/uRXOHX78nolc937ghog9j+bkbGMGIiKDVjwVB7+MmLLVmGa0E7gDfL2n7REQAP2i4gCPsz0JAWoOKyKVXKmaGxmExJfDzId+GTF5gwCICp+MHtK3VaG1DmBlmjo1LTR1+PUjTZmBmRgT9lAl8ISKb/LjGV3wezRz+/zqZAOhD78aagOOuf8+kaxFKXUSKl6L7INilml6JyIni9SdT1MBx4V/ImJKvXIlIKWh5ZOl9J2p6lUG7/NkCDJPFEdSNWxHRZFGjwDMzIjhOhW64TFsFHLX7m4hwFHUDwQO7vrXBzFjvOFnQrGUtMMz2W7LQtYjkUXpycGbGmjghunEnIoeWhbXAiJSnyYePxkgCLIJ2c5kZyQeqr26YZKkCR3P+kyyAcu6fMs8juPedgi+bjkQNcG5GW4nMfRsSMzHEk26Y3EsDnEdnJPIw8f82MrN+EJF9rTAaYKR3aV679eicw+TRWkSqHN03qhMLR8G+N63TaqE2j5ndSjADW3azJrj3OTPnbqa2urkC5wUMiglVI1ADnJ/B6t30arD23tQ+7DafmuDe51MD52nlpOVgbRMKidDo5zCyqvSgvxGRo5pgUz0v1ORo56KSUo2qD+MrzIyAkHYd1EFCJYVh0pAjCctogXM/NuWvBp6NU2NaiSZEV6Y+ikhaslaX0gLnZo3iG1WK6iioSqGcUNCuOa9XAUezzrMb82JKruK0QtHwiHaTNeuzAOdahmDmjoMHuqcP7nIrNXDUcu7L+LepAHcCo1OKPnU3XNpVB61UyELHYXBzfNMmMDNaS3ln1L3JJg1HLcO00WbJbwSQkLivWHLoDdc5gxIfM3CERns27xPjEYAh0KB73bZtz0IIKBDyI8fctMs30gVcgXZfaMeuJHrfmzqilyJy7okFLh/O/BnmjWoq7SKmU5bxdxzId59cdcbffxyHEBCQAKltDrqh3RrOwEvRu08JAAeYJkO6jgGrdH3qgh4FOAlmffe6Vgt8cs9cyLLWMcNq3qMBd0QxI0L1AjNNCw4NNO6YcRlZvMIZA3p04IKPd75ZCkTd7z5w/am6pxoKPSmwRqWeOUOgZwkcY0ZezODf1WbAbIH7oGtt5FkDF6Cr/a3ZA0fodWDU5PLPAtgS+HbAlt2a49ydhueoNYvMOw1bdmuOc/8C4xLETHORTG0AAAAASUVORK5CYII="
@click="navTo('search?city='+curCity)"
></image>
</view>
<!-- 补充详细地址 -->
<view class="confirm-wrap row">
<!-- <input class="input" placeholder="补充详细地址:楼号、门牌等(选填)" placeholder-class="placeholder" v-model="room" @confirm='submit'></input> -->
<view class="btn" @click="submit">确定</view>
</view>
<map
id="map"
class="map"
:scale="15"
:show-location="true"
:longitude="map.longitude"
:latitude="map.latitude"
@regionchange="onRegionchange"
>
<cover-image class="map-center-icon" src="https://7478-tx-cloud-mix-mall-d6944c-1302673523.tcb.qcloud.la/5bfbd58b18200d8e16f6b17a02e6ed9.png"></cover-image>
</map>
<!-- 结果集 -->
<scroll-view scroll-y class="addr-list-scroll">
<view class="addr-list">
<view class="addr-item b-b row" v-for="(item, index) in list" :key="index" @click="chooseAddress(index)">
<view class="left">
<text class="title">{{item.title}}</text>
<text class="addr">{{item.address}}</text>
</view>
<icon type="success" color="#2979ff" size="18" class="icon_circle" v-if='checked === index' />
</view>
</view>
</scroll-view>
</view>
</template>
<script>
const QQMapWX = require('./js/qqmap-wx-jssdk.min.js')
const qqmapsdk = new QQMapWX({
key: 'FALBZ-J2G3I-ZY5GX-5ATUZ-GHOOZ-YVFAR'
})
let _mapCtx = null;
export default {
data() {
return {
curCity: '',
mapStatus: 1,
map: {
longitude: 116.39742,
latitude: 39.909,
},
room: '', //补充地址 门牌号、房间号
list: [], //地址列表
checked: 0, //当前选择选择地址下标
tempAddress: null, //编辑或者搜索到地址时需要手动将结果添加到poi集
}
},
computed: {
currentAddress(){
if(this.list.length === 0){
return {};
}
return this.list[this.checked];
}
},
created() {
_mapCtx = uni.createMapContext('map');
},
onNavigationBarButtonTap() {
this.submit();
},
async onLoad(options) {
//编辑地址时参数
let optionData = options.data;
let lng, lat;
if(optionData){
//编辑地址时
this.tempAddress = JSON.parse(optionData);
this.room = this.tempAddress.room;
lng = this.tempAddress.location.lng;
lat = this.tempAddress.location.lat;
}else{
//没有传坐标时获取用户当前定位
const userLocation = await this.getLocation();
lng = +userLocation.longitude;
lat = +userLocation.latitude;
}
this.map = {
longitude: lng,
latitude: lat
}
this.position = {
longitude: lng,
latitude: lat
}
this.getAddressList(1)
},
methods: {
//确定选择
submit() {
const {currentAddress, room} = this;
const {ad_info, address, location, title} = currentAddress;
// this.$util.prePage().setAddress(Object.assign({}, {
// ad_info, address, location, title,room
// }));
uni.$emit('changeAddressConfig', address,location.lng,location.lat);
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url:'/pages/index/index'
})
}
},
//获取poi列表
getAddressList(s = 0) {
//在ios下防止搜索返回时多次加载地址列表的问题
if(this.isSetTempAddress === 1){
return;
}
qqmapsdk.reverseGeocoder({
location: {
latitude: this.position.latitude,
longitude: this.position.longitude
},
get_poi: 1,
poi_options: "page_size=30;page_index=1",
success: res=> {
res.result.pois.forEach(poi=>{
if(!poi.ad_info){
poi.ad_info = {
adcode: poi.adcode,
city: poi.city,
district: poi.district,
province: poi.province
}
}
})
//有搜索结果时,手动追加到列表顶部
if(this.tempAddress){
if(this.tempAddress.title != res.result.pois[0].title){
if(!this.tempAddress.ad_info){
this.tempAddress.ad_info = {
adcode: this.tempAddress.adcode,
city: this.tempAddress.city,
district: this.tempAddress.district,
province: this.tempAddress.province
}
}
res.result.pois.unshift(this.tempAddress);
}
this.tempAddress = null;
this.isSetTempAddress = 1;
setTimeout(()=>{
this.isSetTempAddress = 0;
}, 500)
}
if (s) {
const ad_info = res.result.pois[0].ad_info;
this.curCity = ad_info.city || '';
res.result.pois[0].select = 1
this.list = res.result.pois;
this.checked = 0;
} else {
this.list = res.result.pois;
}
},
fail: err=> {
console.log(err)
}
})
},
//地图区域改变
onRegionchange(e) {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
//h5 end 安卓 regionchange
if (e.type === 'end' || e.type === 'regionchange') {
_mapCtx.getCenterLocation({
success: res => {
this.position = {
latitude: res.latitude,
longitude: res.longitude
}
if (this.mapStatus == 1) { // 防止地图点击时 进行多次加载
this.getAddressList(1)
}
},
fail: err=>{
console.log(err);
}
})
}
}, 200)
},
//地址列表点击
chooseAddress(index) {
let list = this.list
this.map = {
longitude: list[index].location.lng,
latitude: list[index].location.lat
}
this.checked = index;
this.mapStatus = 0;
//防止ios下触发多次加载列表的bug
clearTimeout(this.mapStatusTimer);
this.mapStatusTimer = setTimeout(()=>{
this.mapStatus = 1;
}, 1000)
},
//获取用户定位
getLocation (){
return new Promise(resolve=> {
// #ifndef H5
uni.getLocation({
type: 'gcj02',
success: res=> {
resolve({
longitude: res.longitude,
latitude: res.latitude
})
},
err: err=> {
console.log(err)
resolve({
longitude: 116.39742,
latitude: 39.909,
})
},
complete(res) {
console.log(res);
}
})
// #endif
// #ifdef H5
//h5的定位没有写这里直接默认天安门可以根据项目需求使用对应jssdk获取定位
resolve({
longitude: 116.39742,
latitude: 39.909,
})
// #endif
})
},
navTo(url){
uni.navigateTo({
url
})
}
}
}
</script>
<style>
page {
height: 100%;
background: #F6F6F6;
}
</style>
<style scoped lang="scss">
.app{
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.row{
display: flex;
align-items: center;
}
.b-b{
position: relative;
&:after{
position: absolute;
z-index: 3;
left: 0;
top: auto;
bottom: 0;
right: 0;
height: 0;
content: '';
transform: scaleY(.5);
border-bottom: 1px solid #e5e5e5;
}
}
.clamp {
/* #ifdef APP-PLUS-NVUE */
lines: 1;
/* #endif */
/* #ifndef APP-PLUS-NVUE */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
/* #endif */
}
.current-address{
height: 120rpx;
padding: 0 24rpx;
background-color: #fff;
.left{
width: 634rpx;
}
.red{
flex-shrink: 0;
margin-right: 6rpx;
font-size: 28rpx;
color: #2979ff;
line-height: 36rpx;
}
.title{
font-size: 28rpx;
color: #333;
font-weight: bold;
line-height: 36rpx;
}
.addr{
width: 700rpx;
margin-top: 12rpx;
font-size: 24rpx;
color: #909399;
line-height: 1.4;
}
.ser-icon{
flex-shrink: 0;
width: 66rpx;
height: 66rpx;
padding: 12rpx 4rpx 12rpx 20rpx;
}
}
.confirm-wrap{
height: 88rpx;
padding: 0 24rpx;
background-color: #fff;
.input{
flex: 1;
font-size: 28rpx;
color: #303133;
}
.btn{
width: 700rpx;
padding: 0 25rpx;
font-size: 26rpx;
color: #fff;
height: 60rpx;
line-height: 60rpx;
background-color: #2979ff;
border-radius: 100rpx;
text-align: center;
}
}
.map{
width: 750rpx;
height: 700rpx;
.map-center-icon{
position: absolute;
left: 339rpx;
top: 264rpx;
width: 72rpx;
height: 72rpx;
}
}
.addr-list-scroll{
flex: 1;
overflow: hidden;
.addr-list{
background-color: #fff;
}
.addr-item{
padding: 24rpx;
}
.left{
flex: 1;
display: flex;
flex-direction: column;
padding-right: 50rpx;
}
.title{
font-size: 28rpx;
color: #303133;
}
.addr{
margin-top: 10rpx;
font-size: 24rpx;
color: #909399;
line-height: 1.4;
}
}
</style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,179 @@
<template>
<view class='app'>
<view class="search-wrap">
<view class="city" @click="navTo('cityList?city='+city)">
<image class="icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAC70lEQVRIS8VW7VXcMBDctQsAKshRQYRcAJcKIBUAFQAVhFSQowKggpAKuBTgRVcBRwWQAiTljZ/Ek235436hf3e2NTuzO7vL9EmHPwmXZgMrpRZlWZ4Q0dJ7v2TmfQTtvX9n5jURra21D8aY9zlkJoGVUvtFUVwS0VUEG7oYQRDRyjl3OxXAKLBSSpVleUdEKrDbENG9c84Q0TYEsCiKAs/Pmflr+M9Yay+MMXgvewaBAVoUxVNg+c9ae2qMgaSDp6qqUwRGRHtg75w7MsbEAFvfZYEhb1mWT2Dqvd8455ZT0sVbQ2rWgT2Yf8t9mwXWWt8w8w8iAtNF90NcHmVFYLnnZVmCKZj/FJGbrkw94BDxCyT23l+ICKRrTnj2i5nP04u898j7dRqA1ho5vwuSH3aD6wEnH2xEpCmqCBrlhxKwT3i0BDMi6smqtTZQpksA3/WAq6p6JKIT7/2tiFxFYK31ipkvvfd/nXMotMavQYVHZj4e+oaI/tR1jcL7ODnGKIzjUBQfVVxVFYD2rLUHAzl9g6wicpCotIRKCFZEoMwo8JaZv6TAoWu95C5IFIkBI5+NhZRSEbiVtqzUWuse42CvN+SxruujnJFjPlNFEuBZjBvgrg201j0lMpK+isgiUaGx5Vypo4cf6rr+sE3HHt9jFwOroih+5+xXVRWseJbzcs7HMS+tQgGLeFFOaiJqBRrejwWJ7tVqt0Odq5E15z8wD5OqGQjoXJhIaaPB/4lCLflj0KMt03u/FZHDAYajf8eamN0yky4Ve+21iKx2AZ/q9Vk7JRXZ9Fr8ttZivA3O1jSoMMOfQxpavX60gaQPk/a5DbN1dK0J7fOZmWGpXpucDTx3tibpmT3D5+xcWH9ghTiBeitNZ0XCDFdDm8doVXcLKVzcgIf5OtRAXsPkmqyHScZJW8S2GVca+LepdGZuRueuK9Js4CSPADxLVcEcds7dzN3LRu005ltskynjuq6xPOx0dmK8080TL38a8H+fVUE94KTW6AAAAABJRU5ErkJggg=="></image>
<text>{{ city }}</text>
<image class="c-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAvElEQVQ4T+3TwQ3CMAwF0G9lAQbgwgRlhEgegBEQm3UEFrCUEWACLgzAAlaQpQa1EJxK9MChvaVKXp3vmrDwQwt7WMHfE31lGGPchBBuAM4icppDM3MP4KCqu5TSw85MmsLMFyLqcs59CzWMiI4556uI7EsBE3CoMrXQMaaqsVT3UaG9aKEeVgU9tIV9BWuo5V0ye7/muIHupJTrA+iICNYAD3MrLF8d0LutVXU7bkDt11pnec7A+Xv+P8Mn98h9FeV7S88AAAAASUVORK5CYII="></image>
</view>
<input class="input" maxlength="30" @input="bindConfirm" focus auto-focus placeholder="请输入地址关键字搜索" @confirm='bindConfirm'></input>
</view>
<view class="addr-list">
<view class="addr-item b-b" v-for="(item, index) in list" :key="index" @click="confirm(item)">
<text class="title">{{item.title}}</text>
<text class="addr">{{item.address}}</text>
</view>
</view>
</view>
</template>
<script>
const QQMapWX = require('./js/qqmap-wx-jssdk.min.js')
var qqmapsdk = new QQMapWX({
key: 'FALBZ-J2G3I-ZY5GX-5ATUZ-GHOOZ-YVFAR'
})
export default {
data() {
return {
city: '',
keyword: '',
list: []
}
},
onLoad(options) {
uni.setNavigationBarTitle({
title: '搜索地址'
})
this.city = options.city || '';
},
methods: {
//选择地址
confirm(item) {
const pages = getCurrentPages()
const prePages = pages[pages.length - 2].$vm
prePages.tempAddress = item;
prePages.position = {
longitude: item.location.lng,
latitude: item.location.lat,
}
prePages.map = {
longitude: item.location.lng,
latitude: item.location.lat,
}
// #ifdef H5 || MP-WEIXIN
prePages.getAddressList(1); //h5没触发地图regionchange事件需要手动调用获取新地址列表。微信小程序开发工具自动触发真机不触发同样需要调用一下。
// #endif
uni.navigateBack({
delta: 1
})
},
//搜索地址
searchList() {
qqmapsdk.getSuggestion({
keyword: this.keyword,
policy: 1, //默认0常规策略 policy=1本策略主要用于收货地址、上门服务地址的填写
page_size: 20, //每页条目数最大限制为20条默认值10
page_index: 1,
region: this.city || '全国',
success: res=> {
this.list = res.data;
},
fail: err => {
this.list = [];
}
})
},
bindConfirm(e) {
this.keyword = e.detail.value;
this.searchList()
},
navTo(url){
uni.navigateTo({
url
})
}
}
}
</script>
<style scoped lang="scss">
view{
box-sizing: border-box;
}
.app{
padding-top: 100rpx;
}
.search-wrap{
position: fixed;
left: 0;
top: var(--window-top);
z-index: 90;
display: flex;
background-color: #fff;
align-items: center;
width: 100%;
height: 100rpx;
padding: 0 30rpx;
background-color: #fff;
.city{
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #333;
}
.icon{
width: 34rpx;
height: 34rpx;
margin-right: 4rpx;
}
.c-icon{
width: 22rpx;
height: 22rpx;
margin-left: 4rpx;
}
.input{
flex: 1;
margin-left: 16rpx;
padding: 0 28rpx;
height: 70rpx;
line-height: 70rpx;
font-size: 30rpx;
color: #333;
background-color: #f5f5f5;
border-radius: 100rpx;
}
}
.addr-list{
background-color: #fff;
.addr-item{
display: flex;
flex-direction: column;
padding: 24rpx 30rpx;
position: relative;
&:after{
position: absolute;
z-index: 3;
left: 0;
top: auto;
bottom: 0;
right: 0;
height: 0;
content: '';
transform: scaleY(.5);
border-bottom: 1px solid #e5e5e5;
}
}
.left{
flex: 1;
display: flex;
flex-direction: column;
padding-right: 50rpx;
}
.title{
font-size: 28rpx;
color: #303133;
}
.addr{
margin-top: 10rpx;
font-size: 24rpx;
color: #909399;
line-height: 1.4;
}
}
</style>

View File

@@ -0,0 +1,305 @@
<template>
<view class="contract-page">
<!-- 顶部导航栏 -->
<customNavbar title="我的合同" :showHome = "true"/>
<!-- Tabs -->
<view class="tab-wrapper">
<u-tabs :list="tabList" :current="currentTab" @change="onTabChange" lineColor="#EA4D3E" activeColor="#EA4D3E"
itemStyle="padding: 0 30rpx;"></u-tabs>
</view>
<DateFilter :start="startDate" :end="endDate" @update:start="startDate = $event" @update:end="endDate = $event"
@change="onDateFilterChange" />
<!-- 合同列表 -->
<scroll-view scroll-y class="scroll-content" @scrolltolower="loadMore" :refresher-enabled="true"
:refresher-triggered="isRefreshing" @refresherrefresh="refresh">
<view v-if="contracts.length > 0" class="contract-list">
<view class="contract-card" v-for="item in contracts" :key="item.contractNo" @click="goDetail(item)">
<image class="contract-cover" :src="staticHost + (item.coverImgUrl || defaultCover)" mode="aspectFill" />
<view class="contract-info">
<view class="top-row">
<text class="asset-name">{{ $ellipsis(item.assetName,12) }}</text>
<u-icon name="arrow-right" size="28" color="#ccc" />
</view>
<!-- <view class="asset-room">{{ item.assetRoom || "—" }}</view> -->
<view class="date-range">{{ item.startDate }} ~ {{ item.endDate }}</view>
<view :class="['status-tag', item.signStatus]">
{{ getStatusText(item.signStatus) }}
</view>
</view>
</view>
<u-loadmore :status="loadStatus" />
</view>
<view v-else class="empty">
<u-empty mode="list" text="暂无合同记录" />
</view>
</scroll-view>
</view>
</template>
<script>
import DateFilter from '../../components/DatePicker/DateFilter.vue';
export default {
components: { DateFilter },
data() {
return {
tabList: [{
name: "全部",
value: "all"
},
{
name: "待签署",
value: "pending"
},
{
name: "已签署",
value: "signed"
},
{
name: "已过期",
value: "expired"
},
],
currentTab: 0,
statusFilter: "all",
// --- 时间筛选 ---
startDate: "",
endDate: "",
// 可选:限制日期范围
minDate: "2000-01-01",
maxDate: "2099-12-31",
contracts: [],
pageNo: 1,
pageSize: 10,
total: 0,
loadStatus: "loadmore",
isRefreshing: false,
defaultCover: "/public/static/assets.jpg",
};
},
onLoad(options) {
this.loadContracts();
},
onShow() {
this.$checkToken(this.$getToken())
},
computed: {
staticHost() {
return this.$config.staticUrl
}
},
methods: {
onTabChange(index) {
this.currentTab = index;
this.statusFilter = this.tabList[index].value;
this.resetAndLoad();
},
onDateFilterChange({ start, end }) {
this.resetAndLoad();
},
formatDate(time) {
const d = new Date(time);
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${d.getFullYear()}-${m}-${day}`;
},
resetAndLoad() {
this.pageNo = 1;
this.contracts = [];
this.loadStatus = "loadmore";
this.loadContracts();
},
/** 你的 loadContracts() 保持不变,只需加:按时间筛选 */
loadContracts() {
if (this.loadStatus !== 'loadmore') return;
this.loadStatus = 'loading';
this.$u.post('/contract/queryPage', {
pageNo: this.pageNo,
pageSize: this.pageSize,
startTime: this.startDate,
endTime: this.endDate,
signStatus: this.statusFilter
}, {
WT: this.$getToken()
}).then(result => {
const rows = result.data.result || [];
// 第一页 & 无数据
if (this.pageNo === 1 && rows.length === 0) {
this.contracts = [];
this.loadStatus = 'nomore';
return;
}
// 追加数据
this.contracts = this.contracts.concat(rows);
// 是否还有下一页(核心判断)
if (rows.length < this.pageSize) {
this.loadStatus = 'nomore';
} else {
this.pageNo++; // ✅ 只有这里才能 +1
this.loadStatus = 'loadmore';
}
}).catch(err => {
console.log("获取合同信息失败:", err);
this.loadStatus = 'loadmore';
}).finally(() => {
this.isRefreshing = false;
});
},
refresh() {
this.isRefreshing = true;
this.resetAndLoad();
},
loadMore() {
if (this.loadStatus !== "loadmore") return;
this.loadContracts();
},
goDetail(item) {
uni.navigateTo({
url: `/pages-biz/contract/contractDetail?contractNo=${item.contractNo}`,
});
},
getStatusText(status) {
switch (status) {
case "pending":
return "待签署";
case "signed":
return "已签署";
case "expired":
return "已过期";
}
},
},
};
</script>
<style lang="scss" scoped>
.contract-page {
background-color: #f8f8f8;
padding-top: 170rpx;
/* 给导航栏留空间 */
min-height: 100vh;
}
.scroll-content {
height: calc(100vh - 100rpx);
}
.contract-list {
padding: 20rpx;
}
.contract-card {
display: flex;
background: #fff;
border-radius: 16rpx;
margin-bottom: 24rpx;
padding: 20rpx;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.05);
position: relative;
}
.contract-cover {
width: 193rpx;
height: 145rpx;
border-radius: 10rpx;
background-color: #FCE5E0;
flex-shrink: 0;
}
.contract-info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
position: relative;
}
.top-row {
display: flex;
justify-content: space-between;
align-items: center;
.asset-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
flex: 1;
}
}
.asset-room {
font-size: 26rpx;
color: #555;
margin-top: 18rpx;
}
.date-range {
font-size: 24rpx;
color: #888;
margin-top: 20rpx;
}
.status-tag {
position: absolute;
bottom: 20rpx;
right: 0rpx;
padding: 12rpx 15rpx;
border-radius: 6rpx;
font-size: 26rpx;
&.pending {
background: #FEEDDD;
color: #EFA049;
}
&.signed {
background: #FCE5E0;
color: #ED7748;
}
&.expired {
background: #CDCDCD;
color: #969696;
}
}
.empty {
margin-top: 200rpx;
}
.tab-wrapper {
background-color: #ffffff;
padding: 10rpx 0;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.06);
position: sticky;
top: 0;
z-index: 10;
}
</style>

View File

@@ -0,0 +1,341 @@
<template>
<view class="contract-detail-page">
<customNavbar title="我的合同"></customNavbar>
<!-- 合同基本信息 -->
<view class="section">
<view class="section-title">合同基本信息</view>
<view
class="info-item"
v-for="(item, index) in baseInfo"
:key="index"
@click="item.hasArrow && handleClick(item)"
>
<text class="label">{{ item.label }}</text>
<view class="value-box">
<text class="value">{{ item.value }}</text>
<u-icon v-if="item.hasArrow" name="arrow-right" size="30" color="#ccc" />
</view>
</view>
</view>
<!-- 该部分仍保留 -->
<!-- <view
v-for="(asset, index) in assetList"
:key="index"
class="asset-block"
>
<view class="asset-header" @click="toggleAsset(index)">
<view class="left">
<image
class="cover"
:src="asset.coverUrl || '/static/default-cover.png'"
mode="aspectFill"
/>
<view class="info">
<text class="name">{{ asset.assetName }}</text>
<text class="room">{{ asset.assetRoom || '—' }}</text>
</view>
</view>
<u-icon
:name="asset.expanded ? 'arrow-up' : 'arrow-down'"
size="28"
color="#999"
/>
</view>
<view v-show="asset.expanded">
<view class="section">
<view class="section-title">费用信息</view>
<view
class="info-item"
v-for="(item, i) in feeItems(asset)"
:key="i"
>
<text class="label">{{ item.label }}</text>
<text class="value">{{ item.value }}</text>
</view>
</view>
</view>
</view> -->
<!-- <view
v-if="hasMoreAssets"
class="load-more"
@click="loadMoreAssets"
> -->
<!-- <text v-if="!loadingAsset">加载更多资产</text>
<text v-else>加载中...</text>
</view> -->
<!-- 底部操作栏完全保留原逻辑和样式 -->
<view v-if="contract.signStatus === '待签署'" class="bottom-bar">
<u-button class="sign-btn" @click="goSign">
去签署
</u-button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
contractNo: null,
contract: {},
assetList: [],
assetPageNo: 1,
assetPageSize: 1,
hasMoreAssets: true,
loadingAsset: false
}
},
onShow() {
this.$checkToken(this.$getToken())
this.getContractDetail()
},
onLoad(options) {
this.contractNo = options.contractNo
},
onReachBottom() {
this.loadMoreAssets()
},
computed: {
baseInfo() {
return [
{ label: '合同名称', value: this.contract.contractName || '—' },
{ label: '签约状态', value: this.contract.signStatus || '—' },
{
label: '合同生效期',
value: this.contract.startDate
? `${this.contract.startDate}${this.contract.endDate}`
: '—'
},
{
label: '电子合同',
value: '查看',
hasArrow: true
}
]
}
},
methods: {
/** 合同详情 + 第一条资产 */
getContractDetail() {
this.$u.get(`/contract/detail?contractNo=${this.contractNo}`, {},{
WT: this.$getToken()
}).then(res => {
console.log(res)
this.contract = res.data || {}
// this.assetList = (res.assetsInfos.assets || []).map((item, index) => ({
// ...item,
// expanded: index === 0
// }))
// this.hasMoreAssets =
// this.assetList.length < (res.totalAssets || 0)
})
},
/** 加载更多资产 */
loadMoreAssets() {
if (this.loadingAsset || !this.hasMoreAssets) return
this.loadingAsset = true
this.assetPageNo += 1
this.$u.get('/contract/assets', {
contractNo: this.contractNo,
pageNo: this.assetPageNo,
pageSize: this.assetPageSize,
WT: this.$getToken()
}).then(res => {
const list = (res.assets || []).map(item => ({
...item,
expanded: false
}))
this.assetList = this.assetList.concat(list)
this.hasMoreAssets = list.length === this.assetPageSize
}).finally(() => {
this.loadingAsset = false
})
},
/** 折叠 / 展开(手风琴效果) */
toggleAsset(index) {
this.assetList.forEach((item, i) => {
item.expanded = i === index ? !item.expanded : false
})
},
feeItems(asset) {
return [
{ label: '押金/保证金', value: asset.fee?.deposit || '—' },
{ label: '租金', value: asset.fee?.rent || '—' },
{ label: '电费', value: asset.fee?.electricity || '—' },
{ label: '水费', value: asset.fee?.water || '—' },
{ label: '物业/增值服务', value: asset.fee?.property || '—' }
]
},
handleClick(item) {
if (item.label === '电子合同') {
uni.downloadFile({
url: this.$config.staticUrl + this.contract.eContractUrl,
header: {
'X-Token': this.$getToken(),
'USERTYPE': this.$getUserType()
},
success: res => {
uni.openDocument({
filePath: res.tempFilePath,
fileType: 'pdf'
})
}
})
}
},
goSign() {
let url = `/contract/getSignLink?eFlowId=${this.contract.eContractFlowId}`
let signLink
this.$u.get(url,{},{
WT: this.$getToken()
}).then(res=>{
if(res.flag) {
signLink = res.data
// 开发者微信小程序内通过WebView打开e签宝的认证授权/签署页面
// 场景二当前小程序内打开H5页面操作认证授权/签署
wx.navigateTo({
url: '/pages-biz/webview/webview?url=' + encodeURIComponent(signLink),
})
}
})
}
}
}
</script>
<style lang="scss" scoped>
.contract-detail-page {
background: #f7f8fa;
min-height: 100vh;
padding-top: 175rpx;
padding-bottom: 150rpx;
}
.asset-block {
background: #fff;
border-radius: 12rpx;
margin: 20rpx;
overflow: hidden;
}
.asset-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
.left {
display: flex;
align-items: center;
}
.cover {
width: 120rpx;
height: 96rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
.name {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.room {
font-size: 26rpx;
color: #999;
}
}
.section {
background: #fff;
border-radius: 12rpx;
margin: 20rpx;
padding: 20rpx 24rpx;
.section-title {
font-weight: 500;
font-size: 40rpx;
margin-bottom: 12rpx;
color: #212121;
}
.info-item {
display: flex;
justify-content: space-between;
padding: 40rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.label {
font-size: 30rpx;
color: #86868c;
}
.value {
font-size: 30rpx;
color: #222;
}
}
}
.load-more {
text-align: center;
padding: 30rpx;
font-size: 28rpx;
color: #999;
}
/* 底部按钮样式保持不变 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 35rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.06);
z-index: 99;
.sign-btn {
width: 100%;
height: 88rpx;
}
&::v-deep .u-btn {
font-size: 30rpx;
color: #fff;
background: linear-gradient(90deg, #ff6f63 0%, #fb392a 100%);
border-radius: 10rpx;
}
}
</style>

134
pages-biz/face/faceAuth.vue Normal file
View File

@@ -0,0 +1,134 @@
<template>
<view class="container">
<view class="loading-content">
<view class="image-content">
<image :src="staticHost + '/public' + '/static/loading.svg'" class="image"></image>
</view>
<view class="loading-tect-content">
<text class="loading-text">加载中</text>
</view>
</view>
<view class="btn-content">
<text>如未成功跳转</text>
<text class="btn-click" bindtap="goFaceAuth">点击此处</text>
<text>手动跳转</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
bizToken: '',
redirectUrl: '',
goFaceDone: false // 是否已跳转至公证签做人脸
}
},
onLoad(e) {
console.log('---middle onLoad', e)
this.bizToken = e.bizToken || ''
this.redirectUrl = e.redirectUrl
? decodeURIComponent(e.redirectUrl)
: ''
this.goFaceAuth()
},
computed: {
staticHost() {
return this.$config.staticUrl
}
},
methods: {
goFaceAuth() {
const { bizToken } = this
// ⚠️ 只有微信小程序支持
// #ifdef MP-WEIXIN
wx.navigateToMiniProgram({
appId: 'wx1c9e1d0b916674dc', // 公证签小程序 APPID
path: `/pages-biz/face/faceAuth?bizToken=${bizToken}`,
success: () => {
this.goFaceDone = true
}
})
// #endif
}
},
onShow() {
console.log('---middle onShow')
const { goFaceDone, redirectUrl } = this
// 防止首次进入直接触发
if (!goFaceDone) return
// 重置状态
this.goFaceDone = false
// ⚠️ 微信小程序专有
// #ifdef MP-WEIXIN
if (!wx.getEnterOptionsSync) return
const options = wx.getEnterOptionsSync()
console.log('---options', options)
// scene === 1038从其他小程序返回
if (
options.scene === 1038 &&
options.referrerInfo &&
options.referrerInfo.extraData &&
options.referrerInfo.extraData.faceResult
) {
const pages = getCurrentPages()
const pre = pages[pages.length - 2]
// 调用上一个页面的 reloadPage 方法
if (pre && typeof pre.reloadPage === 'function') {
pre.reloadPage(
`${redirectUrl}&timeStamp=${Date.now()}`
)
wx.navigateBack({
delta: 1
})
}
}
// #endif
}
}
</script>
<style>
.container {
width: 100%;
height: 100%;
}
.loading-content {
text-align: center;
width: 100%;
}
.image {
width: 172rpx;
height: 186rpx;
}
.loading-tect-content {
font-size: 28rpx;
margin-top: 48rpx;
color: #333;
}
.btn-content {
font-size: 28rpx;
color: #333;
margin-top: 24rpx;
}
.btn-click {
color: #F34038;
}
</style>

View File

@@ -0,0 +1,203 @@
<template>
<view class="location-page">
<custom-navbar title="选择城市" />
<view class="selected" >
<view class="title">
已选 :
<text style="color: #FF2F31;margin-left: 12rpx;">
{{vuex_city==''?'请选择城市':vuex_city}}
</text>
</view>
</view>
<view class="location" @click="setLocation">
<view class="title">当前定位</view>
<view class="body">
<view class="left">
<image src="../../static/navigate.png" mode="widthFix" class="img"></image>
{{locationCity}}
</view>
<view class="right">切换城市</view>
</view>
</view>
<view class="hot" >
<view class="title">热门城市</view>
<view class="body">
<view class="tag" v-for="(item,index) in hotList" :key="index" @click="clickCity(item)">{{item}}</view>
</view>
</view>
</view>
</template>
<script>
// import wxGetAddress from '@/common/utils/wxGetAddress.js'
export default {
data() {
return {
locationCity:'',
hotList:['杭州市','郑州市','北京市','上海市','广州市','深圳市','武汉市']
}
},
onLoad() {
this.findLocation()
},
methods: {
clickCity(item){
this.$u.vuex('vuex_city', item);
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url:'/pages/index/index'
})
}
},
setLocation(){
this.$u.vuex('vuex_city', this.locationCity);
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url:'/pages/index/index'
})
}
},
findLocation(){
let that = this
uni.showLoading({title:"定位中....",mask:true})
this.$u.get("/location/getRealCity").then(obj => {
let cityName = obj.msg
if(cityName){
this.locationCity = cityName
let lifeData = uni.getStorageSync('lifeData');
let vuex_city = lifeData.vuex_city
if(!vuex_city){
this.$u.vuex('vuex_city', cityName);
}
uni.hideLoading();
}else{
uni.request({
//获取真实IP
url:'https://tianqiapi.com/ip?version=v9&appid=23035354&appsecret=8YvlPNrz',
success(resp) {
let ip = resp.data.ip
that.$u.get("/api/profile/getRealCityByIP?ip="+ip).then(obj => {
let cityName = obj.msg
if(cityName){
that.locationCity = cityName
let lifeData = uni.getStorageSync('lifeData');
let vuex_city = lifeData.vuex_city
if(!vuex_city){
that.$u.vuex('vuex_city', cityName);
}
uni.hideLoading();
}else{
that.$mytip.toast('定位失败')
}
uni.hideLoading();
});
}
})
}
});
}
// 微信小程序定位审核太严厉不使用了。扩展后端接口使用ip定位
// findLocation(){
// uni.showLoading({title:"定位中....",mask:true})
// uni.getLocation({
// type: 'gcj02',
// success: async res => {
// let { longitude, latitude } = res;
// let result = await wxGetAddress({ longitude, latitude });
// let province = result.regeocodeData.addressComponent.province
// let cityName = result.regeocodeData.addressComponent.city
// this.locationCity = cityName
// let lifeData = uni.getStorageSync('lifeData');
// let vuex_city = lifeData.vuex_city
// if(!vuex_city){
// this.$u.vuex('vuex_city', cityName);
// }
// uni.hideLoading();
// }
// });
// }
}
}
</script>
<style lang="scss" scoped>
.location-page{
padding-top: 175rpx;
}
.selected{
.title {
font-size: 30rpx;
color: $u-main-color;
margin: 30rpx 20rpx;
font-weight: 600;
margin-left: 30rpx;
}
}
.location{
background: #FFFFFF;
border-radius: 18rpx;
padding: 3rpx 0;
margin: 20rpx;
.title {
font-size: 22rpx;
color: $u-tips-color;
margin: 30rpx 20rpx;
}
.body{
margin: 30rpx 20rpx;
display: flex;
justify-content: space-between;
.left{
font-size: 32rpx;
font-weight: 800;
display: flex;
justify-content: center;
align-items: center;
.img{
width: 35rpx;
margin-right: 12rpx;
}
}
.right{
color: #FF2F31;
font-weight: 600;
margin-right: 10rpx;
};
}
}
.hot{
.title {
font-size: 30rpx;
color: $u-main-color;
margin: 30rpx 20rpx;
font-weight: 600;
margin-left: 30rpx;
}
.body{
background: #FFFFFF;
padding-bottom: 15rpx;
border-radius: 18rpx;
margin: 20rpx;
.tag {
display: inline-block;
width: 137rpx;
height: 75rpx;
line-height: 75rpx;
font-size: 26rpx;
color: $u-content-color;
margin: 20rpx 20rpx 5rpx 20rpx;
padding: 5rpx 10rpx;
text-align: center;
background-color: $u-bg-color;
border-radius: 12rpx;
}
}
}
</style>

308
pages-biz/login/login.vue Normal file
View File

@@ -0,0 +1,308 @@
<template>
<view class="login-container">
<!-- 顶部状态栏 -->
<view class="status-bar"> <u-icon :name="btn.icon" :color="btn.color || '#333'" :size="btn.size || 40"></u-icon>
</view>
<!-- 主体内容区 -->
<view class="content">
<!-- 标题区域 -->
<view class="title">登录</view>
<!-- 登录插图区域 -->
<view class="illustration">
<image :src="staticHost + '/public/static/login/bg.png'" mode="aspectFit" />
</view>
<!-- 登录按钮 -->
<button class="login-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhone">
使用微信授权登录
</button>
<!-- 用户类型选择 -->
<view class="user-type" shape="square">
<u-radio-group v-model="loginType" active-color="#EA414A" shape="square">
<u-radio name="0">个人</u-radio>
<u-radio name="1">企业</u-radio>
</u-radio-group>
</view>
<!-- 协议勾选 -->
<view class="agreement">
<u-checkbox v-model="agreeProtocol" shape="square" active-color="#EA414A">
<text class="agreement-text">
我已阅读并同意
<text class="link" @tap.stop="goPrivacy('user')">用户协议</text>
<text class="link" @tap.stop="goPrivacy('privacy')">隐私政策</text>
</text>
</u-checkbox>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
designWidth: 750,
screenWidth: 0,
scale: 1,
loginType: "0",
agreeProtocol: false
}
},
onLoad() {
wx.getWindowInfo({
success: (res) => {
this.screenWidth = res.windowWidth
this.scale = this.screenWidth / this.designWidth
}
})
},
onUnload() {
},
computed: {
// title 样式
titleStyle() {
const s = this.scale;
return {
width: `${93 * s}rpx`,
height: `${45 * s}rpx`,
fontSize: `${48 * s}rpx`,
lineHeight: `${40 * s}rpx`,
fontWeight: 500,
color: '#222222',
fontFamily: 'Noto Sans S Chinese',
textAlign: 'center'
}
},
staticHost() {
return this.$config.staticUrl
},
headerStyle() {
const s = this.scale;
return {
marginTop: `${214 * s}rpx`,
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center', // 水平居中
justifyContent: 'flex-start' // 从顶部开始
}
},
// cover 样式
coverStyle() {
const s = this.scale;
return {
width: `${428 * s}rpx`,
height: `${608 * s}rpx`
}
}
},
methods: {
/** 微信手机号授权 */
onGetPhone(e) {
const {
code
} = e.detail;
if (!code) {
this.$mytip.toast("授权失败");
return;
}
this.doLogin(code);
},
/** 执行登录逻辑 */
async doLogin(phoneGetCode) {
if (!this.agreeProtocol) {
this.$mytip.toast('请先阅读并同意用户协议和隐私政策')
return
}
uni.showLoading({
title: "登录中...",
mask: true
});
try {
// 微信登录
const loginRes = await new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: resolve,
fail: reject
});
});
// 调用后端登录接口
const authRes = await this.$u.post(`/login/weChatLogin`, {
phoneGetCode: phoneGetCode,
loginCode: loginRes.code,
loginType: this.loginType
});
// let tempToken = 'asd5646'
// 保存token
let token = authRes.data;
this.$u.vuex('vuex_token', token);
// this.$u.vuex('vuex_token', tempToken);
uni.hideLoading();
this.getUserOtherInfo(this.loginType,token)
// 跳转首页
uni.switchTab({
url: '/pages/index/index'
});
} catch (err) {
uni.hideLoading();
this.$mytip.toast("登录失败");
console.error('Login failed:', err);
}
},
getUserOtherInfo(loginType, token) {
let url = `/login/userInfo`;
this.$u.get(url, {}, {
'WT': token,
'USERTYPE': loginType
}).then(obj => {
if(obj.flag){
uni.setStorageSync('userInfo', {
userType: obj.data.userType,
oaAuth: obj.data.oaAuth,
cusNo: obj.data.cusNo,
userName: obj.data.userName,
openId: obj.data.openId
})
}
});
// let tempUser = {
// userType: '0',
// oaAuth: '1',
// cusNo: 'cus0001',
// openId: '65456asd'
// }
// uni.setStorageSync('userInfo', tempUser)
},
/** 触发手机号选择(子组件 → 父组件) */
showPhoneSelector(phoneList, openid) {
this.$emit("choosePhone", {
phoneList,
openid
});
},
/** 查看协议 */
goPrivacy(type) {
console.log(type)
const url = type === 'user' ?
'/pages-biz/privacy/userAgreement' :
'/pages-biz/privacy/privacyPolicy';
uni.navigateTo({
url
});
}
}
}
</script>
<style lang="scss" scoped>
.login-container {
height: 100vh;
background-color: #fff;
overflow: hidden;
.status-bar {
margin-top: 7.56%;
height: var(--status-bar-height);
}
.content {
display: flex;
flex-direction: column;
align-items: center;
height: calc(100vh - var(--status-bar-height));
margin-top: 12.8%;
}
.title {
height: 45rpx;
font-family: Noto Sans S Chinese;
font-weight: 500;
font-size: 48rpx;
color: #222222;
line-height: 40rpx;
}
.illustration {
width: 100%;
height: 44.6%;
padding: 0 21.47%;
margin-bottom: 6%;
}
.illustration image {
width: 100%;
height: 100%;
}
.login-btn {
width: 78.6%;
height: 7.12%;
background: linear-gradient(90deg, #FF2F31 0%, #FF9379 100%);
border-radius: 49rpx;
font-size: 36rpx;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
}
.user-type {
display: flex; justify-content: center;
gap: 40rpx;
margin-bottom: 20rpx;
}
.agreement {
margin-top: 6.24%;
padding: 0 40rpx;
}
.agreement-text {
font-size: 26rpx;
color: #cecece;
}
.link {
color: #334254;
}
}
/* 响应式适配 */
@media (max-width: 320rpx) {
.title {
font-size: 42rpx;
margin-bottom: 100rpx;
}
.illustration {
height: 360rpx;
}
.login-btn {
height: 80rpx;
font-size: 30rpx;
}
}
</style>

View File

@@ -0,0 +1,168 @@
<template>
<view class="message-page">
<!-- 顶部导航栏 -->
<u-navbar title="消息中心" bg-color="#fff" title-color="#111" :border-bottom="true" @leftClick="goBack">
<view slot="left">
<u-icon name="arrow-left" size="44" color="#111"></u-icon>
</view>
</u-navbar>
<!-- 消息列表 -->
<scroll-view scroll-y class="message-list" @scrolltolower="loadMore" @refresherrefresh="refresh"
:refresher-enabled="true" :refresher-triggered="isRefreshing">
<view v-for="(msg,index) in flowList" :key="index" class="msg-item u-flex u-row-between" @click="viewMessage(msg)">
<view class="u-flex">
<image :src="staticHost + '/public' + msg.icon"></image>
<view class="msg-content u-m-l-20">
<view class="msg-title u-font-16">{{ msg.title }}</view>
<view class="msg-desc u-tips-color u-font-12 u-line-2">{{ msg.desc }}</view>
</view>
</view>
<view class="msg-right u-text-right">
<view class="msg-time u-font-12 u-tips-color">{{ msg.time }}</view>
<view v-if="!msg.read" class="unread-dot"></view>
</view>
</view>
<u-loadmore :status="loadStatus" :loading-text="'加载中...'" :nomore-text="'没有更多消息了'"></u-loadmore>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
flowList: [],
pageNo:1,
pageSize:10,
isRefreshing: false,
loadStatus: 'loadmore'
}
},
onShow() {
this.$checkToken(this.$getToken())
},
onload(){
this.loadMore();
},
methods: {
goBack() {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url:'/pages/index/index'
})
}
},
getIconUrl(type) {
if(type === 'BILL') {
return this.$config.staticUrl + '/static/icon/msg-1.png'
}
if(type === 'SIGN') {
return this.$config.baseUrl + '/static/icon/msg-2.png'
}
},
fetchMessageList(){
if (this.loadStatus !== 'loadmore') return;
this.loadStatus = 'loading';
let url = '/message/queryPage'
this.$u.post(url, {
pageNo: this.pageNo,
pageSize: this.pageSize
}, {
WT: this.$getToken()
}).then(res => {
const rows = res.data.result || [];
console.log(rows)
if (this.pageNo === 1) this.flowList = [];
this.flowList = this.flowList.concat(rows);
if (rows.length < this.pageSize) {
this.loadStatus = 'nomore';
} else {
this.pageNo++;
this.loadStatus = 'loadmore';
}
}).catch(err => {
console.log("获取消息列表失败:", err)
this.loadStatus = 'loadmore';
})
},
loadMore() {
// 只有在 loadStatus 为 'loadmore' 时才触发
if (this.loadStatus !== 'loadmore') return;
this.fetchMessageList()
},
viewMessage(msg) {
this.$u.route({url:'/pages-biz/message/messageDetail',params:{
id: msg.id,
read: msg.read
}});
},
refresh() {
this.isRefreshing = true;
setTimeout(() => {
this.isRefreshing = false;
}, 1000);
}
}
}
</script>
<style lang="scss" scoped>
.message-page {
background-color: #ffffff;
min-height: 100vh;
}
.message-list {
padding: 20rpx;
box-sizing: border-box;
}
.msg-item {
padding: 20rpx;
margin-bottom: 20rpx;
align-items: flex-start;
position: relative;
border-bottom: 1px solid #E6E6E6;
image{
width: 88rpx;
height: 88rpx;
}
}
.msg-content {
width: 500rpx;
}
.msg-title {
font-weight: bold;
color: #111;
margin-bottom: 10rpx;
}
.msg-right {
align-items: flex-end;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.msg-time{
position: absolute;
}
.unread-dot {
width: 14rpx;
height: 14rpx;
background-color: #ff4d4f;
border-radius: 50%;
margin-top: 50rpx;
}
</style>

View File

@@ -0,0 +1,73 @@
<template>
<view class="msg-detail-page">
<u-navbar title="消息详情" bg-color="#fff" title-color="#111" :border-bottom="true" @leftClick="goBack">
<view slot="left">
<u-icon name="arrow-left" size="44" color="#111"></u-icon>
</view>
</u-navbar>
<view class="msg-detail u-p-40">
<view class="msg-title u-font-20 u-m-b-20">{{ title }}</view>
<view class="msg-desc u-font-14 u-tips-color">{{ desc }}</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
id:null,
title: '',
desc: ''
}
},
onShow() {
this.$checkToken(this.$getToken())
},
onLoad(e) {
this.id = e.id
this.getDetail()
},
methods: {
getDetail(){
this.$u.get(`/message/detail?id=${this.id}`,{},{
WT: this.$getToken()
}).then(res=>{
if(res.flag) {
this.title = res.data.title;
this.desc = res.data.messageContent;
}
})
},
read(){
this.$u.get(`/message/read?id=${this.id}`,{},{
WT: this.$getToken()
})
},
goBack() {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url:'/pages/index/index'
})
}
}
}
}
</script>
<style lang="scss" scoped>
.msg-detail-page {
background: #fff;
min-height: 100vh;
}
.msg-detail {
line-height: 1.8;
}
.msg-title {
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<view class="page">
<!-- 顶部自定义导航栏 -->
<customNavbar title="我的租赁资产" />
<!-- 筛选栏 -->
<!-- 租赁资产列表组件 -->
<scroll-view
scroll-y
class="scroll-content"
@scrolltolower="loadMore"
>
<asset-list
:list="assetList"
@click="goDetail"
/>
</scroll-view>
</view>
</template>
<script>
import AssetList from '@/components/asset/assetList.vue'
export default {
name: 'MyLease',
components: { AssetList},
data() {
return {
assetList: [],
pageNo: 1,
pageSize: 10,
loadStatus: 'loadmore',
};
},
onLoad(options) {
this.cusNo = options.cusNo;
this.loadAssets();
},
onShow() {
this.$checkToken(this.$getToken())
},
staticHost() {
return this.$config.staticUrl
},
methods: {
changeFilter(value) {
if (this.currentFilter === value) return;
this.currentFilter = value;
this.pageNo = 1;
this.assetList = [];
this.loadAssets();
},
loadAssets() {
if (this.loadStatus !== 'loadmore') return;
this.loadStatus = 'loading';
this.$u.post('/assets/getMyAssetsList', {
pageNo: this.pageNo,
pageSize: this.pageSize
},{
WT: this.$getToken()
}).then(res => {
const rows = res.data.result || [];
if (this.pageNo === 1) this.assetList = [];
this.assetList = this.assetList.concat(rows);
if (rows.length < this.pageSize) {
this.loadStatus = 'nomore';
} else {
this.pageNo++;
this.loadStatus = 'loadmore';
}
}).catch(err => {
console.error(err);
this.loadStatus = 'loadmore';
});
},
loadMore() {
this.loadAssets();
},
goDetail(item) {
uni.navigateTo({
url: `/pages-assets/assets/assetsDetail?assetsNo=${item.assetsNo}`,
});
},
},
};
</script>
<style lang="scss" scoped>
.page {
display: flex;
flex-direction: column;
height: 100vh;
background: #f7f8fa;
padding-top: 175rpx;
}
.filter-bar {
display: flex;
justify-content: space-around;
align-items: center;
background-color: #fff;
padding: 20rpx 0;
margin-top: var(--custom-navbar-height);
border-bottom: 1rpx solid #f0f0f0;
.filter-btn {
min-width: 150rpx;
text-align: center;
font-size: 26rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 40rpx;
margin: 0 8rpx;
}
}
.scroll-content {
flex: 1;
height: calc(100vh - 200rpx);
}
</style>

161
pages-biz/notice/notice.vue Normal file
View File

@@ -0,0 +1,161 @@
<template>
<view>
<u-navbar :is-back="true" title="招商公告" :border-bottom="false"></u-navbar>
<view class="wrap">
<scroll-view scroll-y style="height: 100%;width: 100%;">
<view class="page-box">
<view class="tabSwiper" v-for="(item, index) in flowList" :key="item.noticeId" @click="clickContent(item)">
<u-icon name="bell" :size="35" color="#FF2F31" class="bell-icon"></u-icon>
<view class="content-wrapper">
<view class="top">
<view class="title">{{ item.noticeTitle }}</view>
<view class="date">{{ item.createTime }}</view>
</view>
<view class="item">
<view class="content u-line-2">{{ item.noticeContent }}</view>
</view>
</view>
<u-icon name="arrow-right" color="rgb(203,203,203)" :size="26" class="arrow-icon"></u-icon>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
data() {
// 静态公告数据
const staticNoticeList = [];
return {
pageNo: 1,
pageSize: 20,
flowList: [],
loadStatus: 'loadmore',
isRefreshing: false
};
},
onLoad() {
// 静态数据已在data中初始化无需调用接口
this.getNoticecList();
},
methods: {
clickContent(item) {
if (item.noticeId) {
this.$u.route({
url: '/pages-biz/notice/noticeDetail',
params: {
id: item.noticeId
}
});
}
},
getNoticecList() {
if (this.loadStatus !== 'loadmore') return;
this.loadStatus = 'loading';
let url = "/notice/pageQuery";
this.$u.post(url, {
pageNo: this.pageNo,
pageSize: this.pageSize
}).then(res => {
const rows = res.data.result || [];
console.log(rows)
if (this.pageNo === 1) this.flowList = [];
this.flowList = this.flowList.concat(rows)
if (rows.length < this.pageSize) {
this.loadStatus = 'nomore';
} else {
this.pageNo++;
this.loadStatus = 'loadmore';
}
}).catch(err => {
console.log("获取招商公告失败:", err)
});
}
}
};
</script>
<style lang="scss" scoped>
.tabSwiper {
width: 95%;
background-color: #ffffff;
margin: 16rpx auto;
border-radius: 12rpx;
box-sizing: border-box;
padding: 32rpx;
box-shadow: 0 1rpx 8rpx rgba(0, 0, 0, 0.04);
transition: all 0.2s ease;
border: 1rpx solid #f0f0f0;
display: flex;
align-items: center;
position: relative;
&:hover {
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.bell-icon {
margin-right: 32rpx;
flex-shrink: 0;
margin-top: 4rpx;
}
.content-wrapper {
flex: 1;
min-width: 0;
}
.arrow-icon {
margin-left: 24rpx;
flex-shrink: 0;
margin-top: 16rpx;
}
.top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-top: 20rpx;
.title {
font-size: 28rpx;
font-weight: 600;
color: #1a1a1a;
line-height: 38rpx;
flex: 1;
}
.date {
color: #999999;
font-size: 26rpx;
flex-shrink: 0;
position: absolute;
right: 30rpx;
top: 15rpx;
}
}
.item {
.content {
font-size: 26rpx;
line-height: 36rpx;
color: #666666;
letter-spacing: 0.5rpx;
margin-top: 10rpx;
}
}
}
.wrap {
display: flex;
flex-direction: column;
height: calc(100vh - var(--window-top));
width: 100%;
background-color: #fafafa;
}
.page-box {
padding: 16rpx 0 40rpx 0;
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<view>
<u-navbar :is-back="true" :title="title" :border-bottom="false"></u-navbar>
<view class="u-content">
<u-parse :html="content"
:autosetTitle="true"
:show-with-animation="true"
:selectable="true"></u-parse>
</view>
</view>
</template>
<script>
export default {
data() {
return {
id:null,
title:'资讯',
content: ``
}
},
onLoad(option) {
this.id = option.id
this.getDetail()
},
methods:{
getDetail(){
this.$u.get(`/notice/detail?id=${this.id}`,{},{
WT: this.$getToken()
}).then(res=>{
if(res.flag) {
this.title = res.data.noticeTitle;
this.content = res.data.noticeContent;
}
})
}
}
}
</script>
<style>
page{
background-color: #FFFFFF;
}
</style>
<style lang="scss" scoped>
.u-content{
margin:0 10rpx;
padding: 24rpx;
font-size: 34rpx;
color: $u-main-color;
line-height: 1.8;
white-space: pre-wrap !important;
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<view class="container">
<view class="header">
<view class="back" @click="goBack">
<u-icon name="arrow-left" size="36" color="#333" />
</view>
<text class="title">隐私协议</text>
</view>
<scroll-view class="content" scroll-y>
<view class="text">
<text class="section-title">我们如何收集信息</text>
<text class="paragraph">
在您使用本应用过程中我们可能会收集与您相关的必要信息
包括但不限于设备信息日志信息以及您主动提供的信息
</text>
<text class="section-title">信息的使用方式</text>
<text class="paragraph">
我们收集的信息仅用于提供和优化服务保障账号与系统安全
以及履行法律法规规定的义务
</text>
<text class="section-title">信息的存储与保护</text>
<text class="paragraph">
我们将采取合理的技术与管理措施保护您的个人信息不被泄露
篡改或非法访问
</text>
<text class="section-title">信息的共享与披露</text>
<text class="paragraph">
未经您的明确同意我们不会向第三方共享或披露您的个人信息
法律法规另有规定的除外
</text>
<text class="section-title">您的权利</text>
<text class="paragraph">
您有权依法查询更正或删除您的个人信息并可随时联系我们行使相关权利
</text>
<text class="section-title">政策变更</text>
<text class="paragraph">
本隐私政策如有更新我们将以合理方式向您告知
</text>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
methods: {
goBack() {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.navigateTo({
url:'/pages-biz/login/login'
})
}
}
}
}
</script>
<style scoped>
.container {
height: 100vh;
padding-top:120rpx;
display: flex;
flex-direction: column;
background-color: #fff;
}
.header {
position: relative;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
}
.back {
position: absolute;
left: 30rpx;
}
.title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.content {
flex: 1;
padding: 30rpx;
}
.text {
font-size: 28rpx;
color: #555;
line-height: 1.8;
}
.section-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-top: 30rpx;
margin-bottom: 10rpx;
}
.paragraph {
display: block;
margin-bottom: 16rpx;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<view class="container">
<view class="header">
<view class="back" @click="goBack">
<u-icon name="arrow-left" size="36" color="#333" />
</view>
<text class="title">用户协议</text>
</view>
<scroll-view class="content" scroll-y>
<view class="text">
<text class="section-title">协议说明</text>
<text class="paragraph">
欢迎您使用本应用请您在使用本应用前认真阅读并充分理解本用户协议
一旦您开始使用本应用即视为您已阅读理解并同意本协议的全部内容
</text>
<text class="section-title">用户行为规范</text>
<text class="paragraph">
1. 用户应遵守中华人民共和国相关法律法规不得利用本应用从事任何违法活动
</text>
<text class="paragraph">
2. 不得发布传播违法侵权虚假骚扰性或其他不当信息
</text>
<text class="paragraph">
3. 不得以任何形式干扰本应用的正常运行
</text>
<text class="section-title">服务变更与中断</text>
<text class="paragraph">
本应用有权根据业务发展需要对服务内容进行调整中断或终止
并尽可能提前向用户告知
</text>
<text class="section-title">免责声明</text>
<text class="paragraph">
因不可抗力或非本应用原因造成的服务中断或数据损失本应用不承担责任
</text>
<text class="section-title">其他</text>
<text class="paragraph">
本协议的解释权及修改权归本应用所有
</text>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
methods: {
goBack() {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.navigateTo({
url:'/pages-biz/login/login'
})
}
}
}
}
</script>
<style scoped>
.container {
height: 100vh;
padding-top:120rpx;
display: flex;
flex-direction: column;
background-color: #fff;
}
.header {
position: relative;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
}
.back {
position: absolute;
left: 30rpx;
}
.title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.content {
flex: 1;
padding: 30rpx;
}
.text {
font-size: 28rpx;
color: #555;
line-height: 1.8;
}
.section-title {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-top: 30rpx;
margin-bottom: 10rpx;
}
.paragraph {
display: block;
margin-bottom: 16rpx;
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<view class="u-m-20">
<u-navbar :is-back="true" title="账号实名信息" :border-bottom="false"></u-navbar>
<view>
<!-- <u-cell-group>
<u-cell-item title="头像" :arrow="false" hover-class="none" @click="updateAvatar">
<u-avatar :src="pic" size="100"></u-avatar>
</u-cell-item>
</u-cell-group> -->
<!-- <u-cell-group>
<u-cell-item title="昵称" :arrow="false" hover-class="none" @click="updateName">
{{vuex_user.user.nickName}}
</u-cell-item>
</u-cell-group> -->
<u-cell-group>
<u-cell-item :title="getTitle" :arrow="false" hover-class="none" @click="updateName">
{{(cardNo == null ? orgNo : cardNo) || ''}}
</u-cell-item>
</u-cell-group>
</view>
<u-modal v-model="showModel" @confirm="confirmAuthCode" ref="uModal" :async-close="true"
:title="'设置' + getTitle">
<view class="slot-content">
<u-input v-model="authCode" type="text" :border="false" :placeholder="'请输入' + getTitle" />
</view>
</u-modal>
<!-- <view class="u-m-t-20">
<u-button type="primary" @click="subProfile">提交</u-button>
</view> -->
<!-- 如果是微信登录小程序则获取用户的昵称与头像 -->
<!-- #ifdef MP-WEIXIN -->
<!-- <u-button type="default">使用微信头像与昵称</u-button> -->
<!-- #endif -->
</view>
</template>
<script>
import config from "@/common/config.js" // 全局配置文件
import {
rsaEncrypt
} from "@/common/utils/ras.js"
export default {
data() {
return {
user: {
userType: null,
oaAuth: null,
cusNo: null,
userName: null,
openId: null
},
pic: null,
authCode: '',
showModel: false,
cardNo: null,
orgNo: null
}
},
onLoad(options) {
let userVo = uni.getStorageSync('userInfo');
this.user = userVo;
},
methods: {
updateName() {
this.showModel = true;
},
// 简单身份证校验18 位)
checkIdCard(code) {
return /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(
code
);
},
// 统一社会信用代码校验18 位)
checkCreditCode(code) {
return /^[0-9A-Z]{18}$/.test(code);
},
confirmAuthCode() {
let loginType = this.user.userType
if (!this.authCode) {
this.showModel = false;
return this.$mytip.toast(loginType === '0' ? '请输入身份证号' : '请输入组织社会信用代码')
}
// 身份证校验
if (this.loginType === '0' && !this.checkIdCard(this.authCode)) {
uni.showToast({
title: '身份证号格式不正确',
icon: 'none'
});
return;
}
// 企业社会信用代码校验
if (this.loginType === '1' && !this.checkCreditCode(this.authCode)) {
uni.showToast({
title: '社会信用代码格式不正确',
icon: 'none'
});
return;
}
let life = uni.getStorageSync('lifeData') || {}
let token = life.vuex_token
let url = "/login/updateVerifyCode";
let encryptCode = rsaEncrypt(this.authCode);
this.$u.post(url, {
userType: loginType,
code: loginType === '0' ? encryptCode : this.authCode
}, {
'WT': token
}).then(obj => {
if (obj.flag) {
if (loginType === '0') {
this.cardNo = encryptCode
this.cardNo = this.applyMask(this.authCode)
} else {
this.orgNo = this.authCode
}
this.showModel = false;
this.$mytip.toast('修改成功');
this.getUserProfile();
uni.switchTab({
url: '/pages/center/center'
})
} else {
this.$mytip.toast('修改失败');
}
});
},
applyMask(idCard) {
if (idCard.length !== 18) return idCard; // 非18位直接返回
return idCard.replace(/(\d{6})(\d{8})(\d{4})/, '$1********$2****');
},
updateAvatar() {
this.$u.route('/pages-biz/profile/avatar')
},
subProfile() {
// 1.更新vuex中的用户信息
this.$mytip.toast('修改成功')
// 2.页面跳转
},
getUserProfile() {
// 如果是微信登录小程序,则获取用户的昵称与头像
// #ifdef MP-WEIXIN
// 此处执行微信才执行
// #endif
let token = this.$getToken();
let loginType = this.user.userType
let url = `/login/userInfo?loginType=${loginType}`;
this.$u.get(url, {}, {
'WT': token
}).then(obj => {
uni.setStorageSync('userInfo', {
userType: obj.data.userType,
oaAuth: obj.data.oaAuth,
cusNo: obj.data.cusNo,
userName: obj.data.userName,
openId: obj.data.openId
})
});
}
},
computed: {
getTitle() {
return this.user.userType === '0' ?
'身份证号' :
'企业社会信用代码'
}
}
}
</script>
<style lang="scss" scoped>
.slot-content {
padding: 40rpx;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<view>
<u-navbar :is-back="true" title="设置" :border-bottom="false">
</u-navbar>
<view class="setting-content">
<u-cell-group>
<u-cell-item title="账号实名信息" @click="profile"></u-cell-item>
<!-- <u-cell-item title="订阅系统消息" :arrow="false">
<u-switch v-model="longSubscribe" @change="toggleLongSubscribe" />
</u-cell-item> -->
</u-cell-group>
</view>
<view class="btn">
<u-button @click="logout" plain>退出登录</u-button>
</view>
<view class="version">Version {{vuex_version}}</view>
</view>
</template>
<script>
export default {
data() {
return {
longSubscribe: false // 开关状态
}
},
onLoad() {
},
methods: {
profile() {
this.$u.route('/pages-biz/profile/profile')
},
logout() {
// 登录成功修改token与用户信息
this.$u.vuex('vuex_token', '');
this.$u.vuex('vuex_user', {});
uni.removeStorageSync('userInfo');
return uni.reLaunch({
url: '/pages/index/index'
})
},
toggleLongSubscribe(e) {
this.longSubscribe = e
// 微信小程序专用
// #ifdef MP-WEIXIN
if (this.longSubscribe) {
const tmplIds = ['TEMPLATE_ID_1', 'TEMPLATE_ID_2'] // 在小程序后台配置的长期订阅模板
wx.requestSubscribeMessage({
tmplIds,
success: (res) => {
// 用户同意才生效
console.log('长期订阅授权结果', res)
if (Object.values(res).some((v) => v === 'accept')) {
uni.showToast({
title: '长期订阅开启成功',
icon: 'success'
})
// 可以把状态保存到后端
this.saveLongSubscribeStatus(true)
} else {
uni.showToast({
title: '未授权订阅',
icon: 'none'
})
this.longSubscribe = false
}
},
fail: (err) => {
console.error(err)
this.longSubscribe = false
},
})
} else {
// 取消订阅
uni.showToast({
title: '已关闭系统消息订阅',
icon: 'none'
})
this.saveLongSubscribeStatus(false)
}
// #endif
},
saveLongSubscribeStatus(enable) {
// TODO: 后端保存用户长期订阅状态
console.log('保存长期订阅状态到后端', enable)
}
}
}
</script>
<style lang="scss">
.setting-content {
padding: 0rpx 20rpx;
border-radius: 10rpx;
background: #FFFFFF;
width: 94%;
margin: 20rpx auto;
&::v-deep .u-cell {
color: #222222 !important;
}
&::v-deep .u-cell_title {
font-size: 30rpx !important;
}
}
.btn {
width: 94%;
margin: 30rpx auto;
&::v-deep .u-btn {
border-radius: 10rpx;
font-size: 36rpx !important;
color: #EA4047 !important;
border: 0 !important;
}
&::v-deep .u-hairline-border:after {
border: 0 !important;
}
}
.version {
position: absolute;
bottom: 20rpx;
width: 100%;
text-align: center;
}
</style>

View File

@@ -0,0 +1,291 @@
<template>
<view class="reserve-records">
<!-- 顶部导航栏 -->
<customNavbar title="我的预约" :is-transparent="navbarStyle.isTransparent" :bg-color="navbarStyle.bgColor"
:text-color="navbarStyle.textColor" :opacity="navbarStyle.opacity"
:show-home="true" />
<!-- 内容部分 -->
<scroll-view
scroll-y
class="scroll-content"
@scrolltolower="loadMore"
@refresherrefresh="refresh"
:refresher-enabled="true"
:refresher-triggered="isRefreshing"
>
<view v-if="flowList.length > 0" class="record-list">
<view
v-for="item in flowList"
:key="item.id"
class="record-card"
>
<!-- 资产名称和状态 -->
<view class="card-header">
<text class="asset-name">{{ item.assetName }}</text>
<text
:class="['record-status', item.status]"
>
{{ statusText(item.status) }}
</text>
</view>
<!-- 内容部分 -->
<view class="card-content">
<!-- 预约时间 -->
<view class="info-item">
<text class="info-label">预约时间</text>
<text class="info-value">{{ formatDate(item.date) }}</text>
</view>
<!-- 地址 -->
<view class="info-item">
<text class="info-label">地址</text>
<text class="info-value">{{ item.assetAddress }}</text>
</view>
<!-- 管家 -->
<view class="info-item">
<text class="info-label">管家</text>
<text class="info-value">{{ item.managerName || '未知'}}{{ item.managerPhone || '未知'}}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button
class="location-btn"
@click="viewLocation(item)"
>
查看位置
</button>
<button
class="call-btn"
@click="callManager(item.managerPhone)"
>
拨打电话
</button>
</view>
</view>
<u-loadmore :status="loadStatus" />
</view>
<view v-else class="empty">
<u-empty mode="list" text="暂无预约记录" />
</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
pageNo: 1,
pageSize:10,
flowList: [],
loadStatus: 'loadmore',
isRefreshing: false
}
},
onLoad(options){
this.fetchReserve()
},
onShow() {
this.$checkToken(this.$getToken())
},
methods: {
statusText(status) {
switch (status) {
case 'pending':
return '待确认';
case 'done':
return '已完成';
default:
return '未知';
}
},
formatDate(dateStr) {
// 格式化日期例如2025-11-10 周五 09:00
if(!dateStr){return '-'}
const date = new Date(dateStr);
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const weekday = weekdays[date.getDay()];
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${weekday} ${hours}:${minutes}`;
},
fetchReserve(){
if (this.loadStatus !== 'loadmore') return;
this.loadStatus = 'loading';
let url = '/reservate/queryPage'
this.$u.post(url, {
pageNo: this.pageNo,
pageSize: this.pageSize
},{
WT: this.$getToken()
}).then(res => {
const rows = res.data.result || [];
console.log(rows)
if (this.pageNo === 1) this.flowList = [];
this.flowList = this.flowList.concat(rows);
if (rows.length < this.pageSize) {
this.loadStatus = 'nomore';
} else {
this.pageNo++;
this.loadStatus = 'loadmore';
}
}).catch(err => {
console.log("获取预约记录失败:", err)
this.loadStatus = 'loadmore';
})
},
viewLocation(item) {
uni.openLocation({
latitude: item.lat,
longitude: item.lng,
name: item.assetName,
address: item.assetAddress
});
},
callManager(phone) {
if (!phone) return;
uni.makePhoneCall({ phoneNumber: phone });
},
loadMore() {
this.fetchReserve()
},
refresh() {
this.isRefreshing = true;
setTimeout(() => {
this.isRefreshing = false;
}, 1000);
}
}
};
</script>
<style lang="scss" scoped>
.reserve-records {
min-height: 100vh;
padding-top: 175rpx; /* 给导航栏留空间 */
background: linear-gradient(0deg, #F3F1ED 43%, #F5E9DB 100%);
}
.scroll-content {
height: calc(100vh - -120rpx - 120rpx);
}
.record-list {
padding: 30rpx 40rpx;
}
.record-card {
background: #fff;
border-radius: 12rpx;
padding: 32rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.asset-name {
font-size: 32rpx;
font-weight: 500;
color: #2D2B2C;
}
.record-status {
font-size: 26rpx;
padding: 10rpx 15rpx;
border-radius: 4rpx;
&.done {
background: #FCE5E0;
color: #ED7748;
}
&.pending {
background: #F2F3F7;
color: #86868C;
}
}
.card-content {
margin-bottom: 32rpx;
}
.info-item {
display: flex;
align-items: flex-start;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 24rpx;
color: #666;
width: 120rpx;
flex-shrink: 0;
line-height: 44rpx;
}
.info-value {
font-size: 24rpx;
color: #333;
flex: 1;
line-height: 44rpx;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 20rpx;
margin-top: 32rpx;
padding-top: 32rpx;
border-top: 1rpx solid #f0f0f0;
}
.location-btn {
width: 220rpx;
height: 72rpx;
background: #fff;
color: #2D2B2C;
font-size: 28rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 1px solid #222222;
}
.call-btn {
width: 220rpx;
height: 72rpx;
background: #ff3b30;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: none;
}
.empty {
margin-top: 200rpx;
}
</style>

142
pages-biz/search/search.vue Normal file
View File

@@ -0,0 +1,142 @@
<template>
<view class="u-margin-left-20 u-margin-right-20">
<u-navbar :is-back="true" title="搜索" :border-bottom="false"></u-navbar>
<u-search placeholder="请输入资产名称/街道/位置等信息" v-model="keyword" @search="clickSearch(value)"
:focus="true" action-text="取消" @custom="cancelSearch"></u-search>
<!-- 搜索记录 -->
<template v-if="historyList.length > 0">
<view style="position: relative;">
<u-card title="搜索记录" :border="false" :head-border-bottom="false" padding="0" title-size="28">
<view slot="body">
<u-cell-group :border="false">
<u-cell-item v-for="(item,index) in historyList" :key="index" :title="item" @click="clickSearchTag(item)" :border-bottom="false" :arrow="false">
<!-- <u-icon slot="icon" size="32" name="search"></u-icon> -->
</u-cell-item>
</u-cell-group>
</view>
</u-card>
<view class="arrow-right" @click="clearHistory">
<u-icon name="trash"></u-icon>
清除
</view>
</view>
</template>
</view>
</template>
<script>
export default {
data() {
return {
historyList:[],
keyword:"",
}
},
onLoad() {
// 加载历史记录
let history = uni.getStorageSync('searchHistory')
this.historyList = history ? JSON.parse(history) : []
},
onReady() {
},
methods: {
clickSearch(){
if(this.keyword === ''){
return uni.showToast({
title: '关键词不能为空',
icon: 'none'
});
}else{
uni.hideKeyboard()
this.addHistory()
this.$u.route({
url: '/pages-biz/search/searchList',
params: {
keyword: this.keyword
}
})
}
},
// 自定义取消搜索事件
cancelSearch(){
uni.navigateBack({
delta: 1
})
},
clickSearchTag(item){
this.keyword = item
this.clickSearch()
},
// 加入搜索记录
addHistory(){
let index = this.historyList.indexOf(this.keyword)
let history = this.historyList;
if(index === -1){
history.unshift(this.keyword)
}else{
history.unshift(history.splice(index,1)[0])
}
uni.setStorageSync('searchHistory',JSON.stringify(history))
},
clearHistory(){
// 清除搜索记录
uni.showModal({
title: '提示',
content: '是否清除搜索历史?',
cancelText: '取消',
confirmText: '确认',
success: res => {
if(res.confirm){
uni.removeStorageSync('searchHistory');
this.historyList = []
this.keyword = ''
this.$mytip.toast('清除成功')
}
}
});
}
}
}
</script>
<style>
page {
background: #FFFFFF;
}
</style>
<style lang="scss" scoped>
.item-title {
font-size: 28rpx;
color: $u-main-color;
font-weight: bold;
}
.item-price {
font-weight: normal;
font-size: 35rpx;
color: $u-type-warning;
margin-top: 3px;
}
.item-desc {
font-weight: normal;
font-size: 26rpx;
color: $u-tips-color;
}
.item-tag {
font-size: 24rpx;
color: $u-tips-color;
margin-top: 3px;
}
.arrow-right{
position: absolute;
top: 0rpx;
right: 28rpx;
font-weight: normal;
font-size: 28rpx;
color: $u-tips-color;
}
</style>

View File

@@ -0,0 +1,509 @@
<template>
<view class="search-list-page">
<!-- 顶部导航栏 -->
<!-- <customNavbar title="搜索资产" /> -->
<u-navbar title="搜索资产" :autoBack="true" :background="background" title-color="#2D2B2C"
back-icon-color="#2D2B2C">
</u-navbar>
<view class="search-list-content">
<!-- 筛选标签栏 -->
<view class="filter-dropdown-wrapper">
<u-sticky offset-top="0">
<view class="sticky" style="width: 100vw;">
<filterDropdown :menuTop="0" :filterData="filterData" :defaultSelected="defaultSelected"
:updateMenuName="true" @confirm="confirm" dataFormat="Object"></filterDropdown>
</view>
</u-sticky>
</view>
<!-- 搜索栏 -->
<view class="search-bar-wrapper">
<view class="search-bar">
<view class="city-select" @click="location()">{{ city ||'宜昌'}} <text class="icon-down"></text>
</view>
<view class="search-input" @click="search">
<text class="icon-search"></text>
<input type="text" placeholder="搜索商铺/住房/土地" />
</view>
</view>
</view>
<!-- 标签组 -->
<view class="tag-group">
<view class="tag-item" :class="{ active: selectedTags.includes(tag) }"
v-for="(tag, index) in tagGroupList" :key="index" @click="onTagClick(tag)">
{{ tag }}
</view>
</view>
</view>
<view class="u-p-l-10 u-p-r-10 waterfall">
<u-waterfall v-model="flowList" ref="uWaterfall">
<template v-slot:left="{ leftList }">
<view class="demo-warter" v-for="(item, index) in leftList" :key="index">
<u-lazy-load threshold="750" border-radius="8" :image="item.coverImgUrl" :index="index"
@click="clickImage(item.assetsNo)" mode="aspectFill"></u-lazy-load>
<view class="item-title">{{ item.assetsName }}</view>
<view class="item-desc">{{ item.assetsType }} {{ item.footPrint }} {{ item.orientation }}
</view>
<view class="item-tags">
<view class="tag" v-for="(tag, tagIndex) in item.tags" :key="tagIndex">{{ tag }}</view>
</view>
<view class="item-price">¥{{ item.rentFee }}/<text></text></view>
</view>
</template>
<template v-slot:right="{ rightList }">
<view class="demo-warter" v-for="(item, index) in rightList" :key="index">
<u-lazy-load threshold="750" border-radius="8" :image="item.coverImgUrl" :index="index"
@click="clickImage(item.assetsNo)" mode="aspectFill"></u-lazy-load>
<view class="item-title">{{ item.assetsName }}</view>
<view class="item-desc">{{ item.assetsType}} {{ item.footPrint }} {{ item.orientation }}</view>
<view class="item-tags">
<view class="tag" v-for="(tag, tagIndex) in item.tags" :key="tagIndex">{{ tag }}</view>
</view>
<view class="item-price">¥{{ item.rentFee }}/<text></text></view>
</view>
</template>
</u-waterfall>
<u-loadmore bg-color="rgb(240, 240, 240)" :status="loadStatus" @loadmore="loadMore"
style="height: 80rpx;line-height: 80rpx;"></u-loadmore>
<u-back-top :scroll-top="scrollTop" top="1000"></u-back-top>
<u-no-network></u-no-network>
</view>
</view>
</template>
<script>
import config from "@/common/config.js" // 全局配置文件
import searchData from '@/common/utils/searchData.js'; //筛选菜单数据
import filterDropdown from '@/components/zy/filterDropdown.vue';
export default {
components: {
filterDropdown
},
data() {
return {
keyWord: null,
city: null,
streetList: [],
indexArr: [],
valueArr: [],
defaultSelected: [],
filterData: [],
searchData: {},
pageNum: 1,
pageSize: 20,
scrollTop: 0,
houseList: [],
loadStatus: 'loadmore',
flowList: [],
// 标签组数据和选中状态
tagGroupList: ['全套家具', '配套齐全', '优选好房', '生活便利'],
selectedTags: [],
background: {
// 渐变色
backgroundImage: 'linear-gradient(-90deg, #F9DED9 0%, #F8DFC0 99%);'
},
}
},
onLoad(option) {
let type = option.type
let villageName = option.villageName
let lifeData = uni.getStorageSync('lifeData');
let vuex_city = lifeData.vuex_city
if (option.keyword) {
this.keyWord = option.keyword
}
this.city = vuex_city
this.searchData = {}
if (type) {
this.searchData.type = type
}
if (villageName) {
this.searchData.villageName = villageName
}
this.resetAndLoad()
// 获取小区数据
this.findVillageList()
},
onPageScroll(e) {
this.scrollTop = e.scrollTop;
},
// 下拉刷新
onPullDownRefresh() {
this.resetAndLoad();
// 关闭刷新
uni.stopPullDownRefresh();
},
computed: {
staticHost() {
return this.$config.staticUrl
}
},
methods: {
// 标签点击事件
onTagClick(tag) {
// 检查标签是否已选中
const isSelected = this.selectedTags.includes(tag);
if (isSelected) {
// 如果已选中,则移除
this.selectedTags = this.selectedTags.filter(item => item !== tag);
} else {
// 如果未选中,则添加
this.selectedTags.push(tag);
}
// 这里可以添加根据标签筛选数据的逻辑
console.log('标签点击:', tag);
console.log('当前选中的标签:', this.selectedTags);
this.resetAndLoad()
},
location() {
this.$u.route({
url: '/pages-biz/location/location',
})
},
search() {
this.$u.route('/pages-biz/search/search');
},
resetAndLoad() {
this.pageNo = 1;
this.flowList = [];
this.loadStatus = 'loadmore';
this.$nextTick(() => {
this.$refs.uWaterfall && this.$refs.uWaterfall.clear();
this.findHouseList();
});
},
findHouseList(type = null) {
if (this.loadStatus !== 'loadmore') return;
this.loadStatus = 'loading';
this.$u.post('/assets/queryPage', {
assetsStatus:'闲置中',
assetsType: type,
pageNo: this.pageNo,
pageSize: this.pageSize,
keyWord: this.keyWord
}).then(result => {
const rows = result.data.result || [];
// 第一页无数据
if (this.pageNo === 1 && rows.length === 0) {
this.flowList = [];
this.loadStatus = 'nomore';
return;
}
rows.forEach(row => {
if (row.coverImgUrl) {
row.coverImgUrl = this.$config.staticUrl + row.coverImgUrl
} else {
row.coverImgUrl = this.$config.staticUrl + this.defaultImgUrl
}
})
// 追加数据
this.flowList = this.flowList.concat(rows);
// 判断是否还有下一页(核心)
if (rows.length < this.pageSize) {
this.loadStatus = 'nomore';
} else {
this.pageNo++; // ✅ 只有这里能 +1
this.loadStatus = 'loadmore';
}
}).catch(err => {
console.log("获取资产信息失败:", err);
this.loadStatus = 'loadmore';
}).finally(() => {
uni.stopPullDownRefresh();
});
},
loadMore() {
// 只有在 loadStatus 为 'loadmore' 时才触发
if (this.loadStatus !== 'loadmore') return;
// 调用接口获取下一页
this.findHouseList();
},
findStreet() {
},
findVillageList() {
// 前端写死的测试小区数据
// const villageData = [{
// name: '景秀天成'
// },
// {
// name: '锦绣花园'
// },
// {
// name: '阳光小区'
// },
// {
// name: '幸福家园'
// },
// {
// name: '碧水湾'
// }
// ];
// 添加测试小区数据到筛选菜单
// for (let i = 0; i < villageData.length; i++) {
// searchData[0].submenu.push({
// name: villageData[i].name,
// value: villageData[i].name
// });
// }
// this.filterData = searchData;
},
clickImage(houseId) {
this.$u.route({
url: '/pages-assets/assets/detail',
params: {
assetsNo: houseId
}
})
},
//接收菜单结果
confirm(e) {
let type = e.value[1][0]
let villageName = e.value[0][0]
let price = e.value[2][0]
let combo = e.value[3]
let houseNum = combo[0]
let decoration = combo[1]
let feature = combo[2]
this.searchData = {}
if (type) {
this.searchData.type = type
}
if (villageName) {
this.searchData.villageName = villageName
}
if (price) {
this.searchData.price = price
}
if (houseNum && houseNum.length > 0) {
this.searchData.houseNum = houseNum.toString()
}
if (decoration && decoration.length > 0) {
this.searchData.decoration = decoration.toString()
}
if (feature && feature.length > 0) {
this.searchData.feature = feature.toString()
}
this.resetAndLoad()
},
code() {
this.$mytip.toast('请咨询技术支持')
}
}
}
</script>
<style lang="scss" scoped>
.search-list-page {
background-color: #f5f5f5;
}
.search-list-content {
// padding-top: 20rpx;
background: linear-gradient(-90deg, #F9DED9 0%, #F8DFC0 99%);
border-radius: 0rpx;
padding-bottom: 20rpx;
}
// 搜索栏样式
.search-bar-wrapper {
padding: 10rpx 20rpx;
}
.search-bar {
display: flex;
background-color: #fff;
align-items: center;
border-radius: 10rpx;
padding: 20rpx 30rpx;
}
.city-select {
margin-right: 20rpx;
font-size: 28rpx;
color: #333;
}
.icon-down {
display: inline-block;
width: 0;
height: 0;
border-left: 8rpx solid transparent;
border-right: 8rpx solid transparent;
border-top: 8rpx solid #666;
margin-left: 5rpx;
vertical-align: middle;
}
.search-input {
flex: 1;
display: flex;
align-items: center;
}
.icon-search {
margin-right: 10rpx;
color: #999;
font-size: 28rpx;
}
.search-input input {
flex: 1;
font-size: 28rpx;
border: none;
background: transparent;
outline: none;
}
.filter-dropdown-wrapper {
// padding: 0 20rpx;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
// 标签组样式
.tag-group {
padding: 15rpx 20rpx;
display: flex;
flex-wrap: wrap;
gap: 15rpx;
border-bottom: 1rpx solid #eee;
margin-top: 20rpx;
}
.tag-item {
padding: 5rpx 10rpx;
border-radius: 6rpx;
font-size: 24rpx;
color: #723324;
transition: all 0.3s ease;
border: 1px solid #DBB39D;
}
.tag-item.active {
background-color: #FCF2F3;
border: 1px solid #ffffff;
}
.waterfall {
padding-top: 20rpx;
background-color: #ffffff;
}
.nomore {
background-color: $u-bg-color;
}
.search {
width: 54px;
height: 44px;
&:active {
background-color: $u-bg-color;
}
}
.rowClass {
border-radius: 8px;
background-color: rgb(255, 255, 255);
margin-top: 10rpx;
}
.hoverClass {
background-color: #E4E7ED;
}
.tabName {
font-size: 28rpx;
color: $u-main-color;
}
.demo-warter {
border-radius: 8px;
margin-top: 15rpx;
margin-right: 10rpx;
margin-left: 10rpx;
background-color: #ffffff;
padding: 0rpx;
position: relative;
overflow: hidden;
}
.u-close {
position: absolute;
top: 20rpx;
right: 20rpx;
}
// 图片样式
.u-lazy-load {
width: 100%;
height: 300rpx;
}
.item-cover {
font-size: 55rpx;
color: $u-type-warning;
}
.item-title {
font-size: 28rpx;
color: #333;
font-weight: 600;
padding: 12rpx 15rpx 8rpx 15rpx;
line-height: 40rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-price {
font-size: 32rpx;
color: #FF2F31;
padding: 0 15rpx 15rpx 15rpx;
}
.item-price text {
font-size: 24rpx;
}
.item-desc {
font-weight: normal;
font-size: 24rpx;
color: #999;
padding: 0 15rpx 8rpx 15rpx;
}
// 标签样式
.item-tags {
display: flex;
flex-wrap: wrap;
padding: 0 15rpx 12rpx 15rpx;
gap: 10rpx;
}
.tag {
padding: 4rpx 12rpx;
background-color: #f5f5f5;
border-radius: 12rpx;
font-size: 22rpx;
color: #666;
}
.item-tag {
font-size: 24rpx;
color: $u-tips-color;
margin-top: 3px;
}
</style>

View File

@@ -0,0 +1,240 @@
<template>
<view class="calculator">
<customNavbar title="计算器" :showHome="true" />
<input class="display" v-model="expression" disabled />
<view class="keys">
<view
v-for="item in keys"
:key="item"
class="key"
:class="{
operator: operators.includes(item) || item === '%',
equal: item === '=',
control: ['C','⌫'].includes(item)
}"
@click="press(item)"
@touchstart="onTouchStart(item)"
@touchend="onTouchEnd"
>
{{ item }}
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
expression: '',
timer: null,
operators: ['+', '-', '*', '/'],
keys: [
'7','8','9','/',
'4','5','6','*',
'1','2','3','-',
'0','.','%','+',
'C','⌫',
'='
]
}
},
methods: {
press(key) {
const ops = this.operators
if (key === 'C') {
this.expression = ''
return
}
if (key === '⌫') {
return
}
if (key === '%') {
this.applyPercent()
return
}
if (key === '=') {
if (!this.expression) return
while (/[+\-*/.]$/.test(this.expression)) {
this.expression = this.expression.slice(0, -1)
}
if (!/^\d+(\.\d+)?([+\-*/]\d+(\.\d+)?)*$/.test(this.expression)) {
uni.showToast({ title: '格式错误', icon: 'none' })
return
}
this.expression = this.calculate(this.expression)
return
}
if (ops.includes(key)) {
if (!this.expression) return
if (ops.includes(this.expression.slice(-1))) {
this.expression =
this.expression.slice(0, -1) + key
return
}
}
if (key === '.') {
const last = this.expression.split(/[+\-*/]/).pop()
if (last.includes('.')) return
}
this.expression += key
},
/* 长按删除 */
onTouchStart(key) {
if (key !== '⌫') return
this.expression = this.expression.slice(0, -1)
this.timer = setInterval(() => {
if (!this.expression) {
clearInterval(this.timer)
return
}
this.expression = this.expression.slice(0, -1)
}, 120)
},
onTouchEnd() {
clearInterval(this.timer)
this.timer = null
},
/* 百分号处理(核心) */
applyPercent() {
if (!this.expression) return
const match = this.expression.match(/(\d+(\.\d+)?)([+\-*/])?(\d+(\.\d+)?)?$/)
if (!match) return
const base = parseFloat(match[1])
const op = match[3]
const value = match[4] ? parseFloat(match[4]) : null
let percentValue
if (!op) {
percentValue = base / 100
this.expression = percentValue.toString()
return
}
if (op === '+' || op === '-') {
percentValue = base * (value / 100)
} else {
percentValue = value / 100
}
this.expression =
this.expression.slice(0, match.index) +
base +
op +
percentValue
},
/* 计算(无 eval */
calculate(exp) {
const nums = exp.split(/[\+\-\*\/]/).map(Number)
const ops = exp.match(/[\+\-\*\/]/g) || []
for (let i = 0; i < ops.length; ) {
if (ops[i] === '*' || ops[i] === '/') {
const r =
ops[i] === '*'
? nums[i] * nums[i + 1]
: nums[i] / nums[i + 1]
nums.splice(i, 2, r)
ops.splice(i, 1)
} else {
i++
}
}
let result = nums[0]
for (let i = 0; i < ops.length; i++) {
result =
ops[i] === '+'
? result + nums[i + 1]
: result - nums[i + 1]
}
return Number(result.toFixed(10)).toString()
}
}
}
</script>
<style scoped>
.calculator {
padding: 30rpx;
padding-top: 175rpx;
background: #f7f8fa;
min-height: 100vh;
}
.display {
height: 120rpx;
font-size: 44rpx;
font-weight: 600;
border-radius: 12rpx;
background: #fff;
padding: 0 24rpx;
margin-bottom: 30rpx;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.08);
}
.keys {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx;
}
.key {
background: #ffffff;
text-align: center;
padding: 36rpx 0;
font-size: 36rpx;
font-weight: 500;
border-radius: 16rpx;
box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.08);
}
.key:active {
transform: scale(0.95);
}
.operator {
background: #ffe8d9;
color: #ff7a18;
box-shadow: 0 10rpx 20rpx rgba(255,122,24,0.35);
font-size: 40rpx;
}
.control {
background: #ff4d4f;
color: #fff;
box-shadow: 0 10rpx 20rpx rgba(255,77,79,0.4);
}
.equal {
grid-column: span 4;
background: linear-gradient(135deg, #4facfe, #00f2fe);
color: #fff;
font-size: 44rpx;
font-weight: 600;
box-shadow: 0 12rpx 28rpx rgba(79,172,254,0.5);
}
</style>

367
pages-biz/unpaid/unpaid.vue Normal file
View File

@@ -0,0 +1,367 @@
<template>
<view class="pending-bill-page">
<!-- 自定义导航栏 -->
<custom-navbar title="租金待付" />
<scroll-view scroll-y class="scroll-content" :refresher-enabled="true" :refresher-triggered="isRefreshing"
@refresherrefresh="refresh" @scrolltolower="loadMore">
<view v-if="flowList.length > 0">
<view class="bill-item" v-for="item in flowList" :key="item.billNo">
<!-- 左侧勾选 + 信息 -->
<view class="bill-left">
<u-checkbox v-model="item.checked" @change="toggleCheck(item)" size="32"
active-color="#EA414A" />
<view class="bill-info">
<text class="bill-name">{{ $ellipsis(item.billName,27) }}</text>
<text class="bill-date">缴费截止<text class="date">{{ item.billPayEndDate }}</text></text>
</view>
</view>
<!-- 右侧金额 + 支付按钮 -->
<view class="bill-right">
<text class="amount">{{ formatMoney(item.billAmount) }}</text>
<button class="pay-btn" @click="goPayTest([item])">去支付
</button>
</view>
</view>
</view>
<view v-else class="empty">
<u-empty mode="list" text="暂无待支付账单" />
</view>
<u-loadmore :status="loadStatus" />
</scroll-view>
<!-- 底部批量支付栏 -->
<view v-if="selectedBills.length > 0" class="batch-pay-bar">
<view class="batch-pay-bar-left"><text>已选 {{ selectedBills.length }} </text></view>
<view class="batch-pay-bar-center">
合计<text class="sumAmount">{{ formatMoney(sumAmount) }}</text>
</view>
<view class="batch-pay-bar-right">
<button class="bottomPayBtn" color="linear-gradient(90deg, #007aff, #00aaff)" type="primary"
size="small" @click="goPayTest(selectedBills)">
批量支付
</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
pageNo: 1,
pageSize: 10,
flowList: [],
isRefreshing: false,
loadStatus: 'loadmore',
sumAmount: 0,
selectedBills: [], // 勾选的账单集合
};
},
onLoad(options) {
this.loadBills();
},
onShow() {
this.$checkToken(this.$getToken())
},
methods: {
loadBills() {
if (this.loadStatus !== 'loadmore') return;
this.loadStatus = 'loading';
const token = this.$getToken()
let url = '/bill/pageQueryContractBill'
this.$u.post(url, {
pageNo: this.pageNo,
pageSize: this.pageSize,
billStatus: '待缴费'
}, {
WT: token
}).then(res => {
const rows = res.data.result || [];
console.log(rows)
if (this.pageNo === 1) this.flowList = [];
this.flowList = this.flowList.concat(rows);
if (rows.length < this.pageSize) {
this.loadStatus = 'nomore';
} else {
this.pageNo++;
this.loadStatus = 'loadmore';
}
}).catch(err => {
console.log("获取待付合同账单记录失败:", err)
this.loadStatus = 'loadmore';
})
this.updateSelected();
},
refresh() {
this.isRefreshing = true;
setTimeout(() => {
this.loadBills();
this.isRefreshing = false;
}, 1000);
},
loadMore() {
this.loadBills();
},
formatMoney(val) {
if (val === null || val === undefined || isNaN(val)) return '—';
val = Number(val);
if (val >= 10000) {
return '¥' + (val / 10000).toFixed(2) + '万';
}
return '¥' + val.toFixed(2);
},
toggleCheck(item) {
item.checked = !item.checked;
if (item.checked && !this.selectedBills.some(b => b.id === item.id)) {
this.selectedBills.push(item);
} else {
// 取消勾选时从已选数组移除
this.selectedBills = this.selectedBills.filter(b => b.id !== item.id);
}
let sumFee = 0;
this.selectedBills.forEach(b => sumFee += b.amount);
this.sumAmount = sumFee;
},
updateSelected() {
if (!Array.isArray(this.bills)) return;
this.selectedBills = this.bills.filter(b => b.checked);
},
goPayTest(billList) {
if (!billList || billList.length === 0) return;
// 模拟后台生成订单返回
const order = {
timeStamp: String(Date.now()), // 时间戳
nonceStr: Math.random().toString(36).substr(2, 15), // 随机字符串
package: 'prepay_id=TEST1234567890', // 模拟预支付ID
signType: 'MD5',
paySign: 'TEST_SIGN', // 模拟签名
};
// 调用微信支付接口(测试)
uni.requestPayment({
...order,
success: () => {
uni.showToast({
title: '支付成功(测试)',
icon: 'success'
});
// 清空勾选状态
billList.forEach(b => b.checked = false);
this.updateSelected();
// 刷新列表(可选)
this.loadBills();
},
fail: () => {
uni.showToast({
title: '支付失败(测试)',
icon: 'none'
});
}
});
},
async goPay(billList) {
if (!billList || billList.length === 0) return;
try {
// 1. 请求后台生成订单
const res = await uni.request({
url: 'https://api.example.com/payments/create',
method: 'POST',
data: {
userId: this.userId,
bills: billList.map(b => b.id),
}
});
if (res[1].data.code !== 0) {
uni.showToast({
title: res[1].data.msg,
icon: 'none'
});
return;
}
const order = res[1].data.data; // 后台返回的支付参数
// 2. 调用微信支付
uni.requestPayment({
timeStamp: order.timeStamp,
nonceStr: order.nonceStr,
package: order.package,
signType: order.signType,
paySign: order.paySign,
success: () => {
uni.showToast({
title: '支付成功',
icon: 'success'
});
// 更新勾选状态
billList.forEach(b => b.checked = false);
this.updateSelected();
// 刷新列表
this.loadBills();
},
fail: () => {
uni.showToast({
title: '支付失败',
icon: 'none'
});
}
});
} catch (error) {
console.error('支付请求失败', error);
uni.showToast({
title: '支付请求失败',
icon: 'none'
});
}
},
}
};
</script>
<style lang="scss" scoped>
.pending-bill-page {
background: #f7f8fa;
min-height: 100vh;
padding-top: 175rpx;
/* 自定义导航栏高度 */
}
.scroll-content {
padding: 20rpx;
box-sizing: border-box;
}
.bill-item {
display: flex;
justify-content: space-between;
background: #fff;
margin-bottom: 20rpx;
border-radius: 12rpx;
padding: 20rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.bill-left {
display: flex;
align-items: center;
.bill-info {
display: flex;
flex-direction: column;
.bill-name {
font-size: 30rpx;
color: #2D2B2C;
font-weight: 500;
}
.bill-date {
font-size: 26rpx;
color: #86868C;
margin-top: 6rpx;
.date {
font-size: 24rpx;
color: #2D2B2C;
}
}
}
}
.bill-right {
flex-direction: column;
align-items: flex-end;
width: 30%;
.amount {
text-align: center;
font-size: 30rpx;
height: 28rpx;
line-height: 28rpx;
color: #EF8849;
}
.pay-btn {
margin-top: 12rpx;
padding: 0 40rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 6rpx; // 圆角
font-size: 30rpx;
color: #fff;
background-color: #F34038;
text-align: center;
}
.btn-text {
padding-top: 13rpx;
}
}
.batch-pay-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 170rpx;
padding: 0 30rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 -4rpx 10rpx rgba(0, 0, 0, 0.08);
z-index: 999;
.batch-pay-bar-left {
font-size: 28rpx;
color: #86868C;
}
.batch-pay-bar-center {
font-size: 28rpx;
color: #222222;
.sumAmount {
font-size: 28rpx;
color: #ff7f00;
}
}
.batch-pay-bar-right {
flex-direction: column;
align-items: flex-end;
.bottomPayBtn {
height: 60rpx;
line-height: 60rpx;
padding: 0 40rpx;
border-radius: 6rpx;
font-size: 30rpx;
font-weight: bold;
text-align: center;
background: #F34038;
}
}
}
.empty {
margin-top: 200rpx;
text-align: center;
}
</style>

438
pages-biz/vr/vr.vue Normal file
View File

@@ -0,0 +1,438 @@
<template>
<view class="vr-page">
<!-- 返回按钮 -->
<view class="back-btn" @click="back"> 返回</view>
<!-- Canvas -->
<canvas canvas-id="vrCanvas" class="vr-canvas" @touchstart="onTouchStart" @touchmove="onTouchMove"
@touchend="onTouchEnd"></canvas>
<!-- 左上角 3D mini preview -->
<canvas v-if="currentSpace.type==='model'" canvas-id="miniCanvas" class="mini-canvas"></canvas>
<!-- Hotspot 点击由 Three.js raycaster 处理 -->
<!-- 底栏 -->
<view class="bottom-bar">
<!-- 左侧空间切换 -->
<view class="left">
<button @click="showSpaceList = !showSpaceList">
<image src="/static/icons/space.png" />
</button>
</view>
<!-- 右侧工具 -->
<view class="right">
<button @click="showTools = !showTools">
<image src="/static/icons/tools.png" />
</button>
</view>
</view>
<!-- 空间缩略图弹窗 -->
<view v-if="showSpaceList" class="space-list">
<view v-for="space in spaces" :key="space.id" class="space-item" @click="switchSpace(space.id)">
<image :src="space.thumbnail" />
<text>{{ space.name }}</text>
</view>
</view>
<!-- 工具栏弹窗 -->
<view v-if="showTools" class="tools-panel">
<button @click="resetView">重置视角</button>
<button @click="toggleFullScreen">全屏</button>
<button @click="takePhoto">拍照</button>
</view>
<!-- 上一/下一空间按钮 -->
<view class="switch-btn left" @click="prevSpace"></view>
<view class="switch-btn right" @click="nextSpace"></view>
</view>
</template>
<script>
import * as THREE from 'three'
import {
GLTFLoader
} from 'three/examples/jsm/loaders/GLTFLoader.js'
export default {
name: 'VRView',
data() {
return {
spaces: [], // VR 空间数组
currentIndex: 0,
currentSpace: null,
// Three.js 主场景
renderer: null,
scene: null,
camera: null,
sphere: null,
videoTexture: null,
// Mini 3D模型
miniRenderer: null,
miniScene: null,
miniCamera: null,
miniModel: null,
// Hotspot
raycaster: null,
mouse: new THREE.Vector2(),
// 触控
isDragging: false,
lastX: 0,
lastY: 0,
lon: 0,
lat: 0,
// UI
showSpaceList: false,
showTools: false
}
},
onLoad(options) {
if (options.vrList) {
try {
// JSON.parse(decodeURIComponent(options.spaces))
this.spaces = options.vrList
} catch (e) {
console.error('解析 VR 空间失败', e)
}
}
this.currentSpace = this.spaces[this.currentIndex]
this.initThree()
},
methods: {
back() {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url:'/pages/index/index'
})
}
},
// ==================== Three.js 主场景 ====================
initThree() {
const canvasQuery = uni.createSelectorQuery()
canvasQuery.select('#vrCanvas').node().exec(res => {
const canvasNode = res[0].node
const {
windowWidth: width,
windowHeight: height
} = uni.getSystemInfoSync()
// 主 renderer
this.renderer = new THREE.WebGLRenderer({
canvas: canvasNode,
antialias: true
})
this.renderer.setSize(width, height)
// scene & camera
this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 2000)
this.camera.position.set(0, 0, 0.01)
// raycaster
this.raycaster = new THREE.Raycaster()
this.loadSpace(this.currentSpace)
this.animate()
})
},
// 加载空间
loadSpace(space) {
// 清空场景
while (this.scene.children.length) this.scene.remove(this.scene.children[0])
if (this.videoTexture) {
this.videoTexture = null
}
// 图片
if (space.type === 'image') {
const geometry = new THREE.SphereGeometry(500, 60, 40)
geometry.scale(-1, 1, 1)
const texture = new THREE.TextureLoader().load(space.src)
const material = new THREE.MeshBasicMaterial({
map: texture
})
this.sphere = new THREE.Mesh(geometry, material)
this.scene.add(this.sphere)
}
// CubeMap
else if (space.type === 'cubemap') {
const urls = [space.cubemap.px, space.cubemap.nx, space.cubemap.py, space.cubemap.ny, space.cubemap.pz,
space.cubemap.nz
]
const cubeTexture = new THREE.CubeTextureLoader().load(urls)
this.scene.background = cubeTexture
}
// 视频
else if (space.type === 'video') {
const video = document.createElement('video')
video.src = space.src
video.crossOrigin = 'anonymous'
video.loop = true
video.muted = false
video.autoplay = true
video.play()
this.videoTexture = new THREE.VideoTexture(video)
const geometry = new THREE.SphereGeometry(500, 60, 40)
geometry.scale(-1, 1, 1)
const material = new THREE.MeshBasicMaterial({
map: this.videoTexture
})
this.sphere = new THREE.Mesh(geometry, material)
this.scene.add(this.sphere)
}
// 模型
else if (space.type === 'model') {
const loader = new GLTFLoader()
loader.load(space.src, gltf => {
this.sphere = gltf.scene
this.sphere.scale.set(space.modelScale, space.modelScale, space.modelScale)
this.scene.add(this.sphere)
this.initMiniModel(gltf.scene.clone(), space.modelScale)
})
}
// Hotspots
if (space.hotspots && space.hotspots.length) {
space.hotspots.forEach(h => {
const spriteMap = new THREE.TextureLoader().load('/static/icons/hotspot.png')
const spriteMaterial = new THREE.SpriteMaterial({
map: spriteMap
})
const sprite = new THREE.Sprite(spriteMaterial)
const phi = THREE.MathUtils.degToRad(90 - h.position.lat)
const theta = THREE.MathUtils.degToRad(h.position.lon)
const radius = 500
sprite.position.set(
radius * Math.sin(phi) * Math.cos(theta),
radius * Math.cos(phi),
radius * Math.sin(phi) * Math.sin(theta)
)
sprite.userData.targetId = h.targetId
this.scene.add(sprite)
})
}
},
animate() {
requestAnimationFrame(this.animate)
// 相机旋转
const phi = THREE.MathUtils.degToRad(90 - this.lat)
const theta = THREE.MathUtils.degToRad(this.lon)
this.camera.target = new THREE.Vector3(
500 * Math.sin(phi) * Math.cos(theta),
500 * Math.cos(phi),
500 * Math.sin(phi) * Math.sin(theta)
)
this.camera.lookAt(this.camera.target)
this.renderer.render(this.scene, this.camera)
// mini 模型同步旋转
if (this.currentSpace.type === 'model' && this.miniModel) {
this.miniModel.rotation.y = this.camera.rotation.y
this.miniRenderer.render(this.miniScene, this.miniCamera)
}
},
// ==================== Mini 3D 模型 ====================
initMiniModel(model, scale) {
const canvasQuery = uni.createSelectorQuery()
canvasQuery.select('#miniCanvas').node().exec(res => {
const canvasNode = res[0].node
this.miniRenderer = new THREE.WebGLRenderer({
canvas: canvasNode,
antialias: true,
alpha: true
})
this.miniRenderer.setSize(150, 150)
this.miniScene = new THREE.Scene()
this.miniCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 2000)
this.miniCamera.position.set(0, 50, 100)
this.miniModel = model
this.miniScene.add(this.miniModel)
})
},
// ==================== 空间切换 ====================
switchSpace(id) {
const index = this.spaces.findIndex(s => s.id === id)
if (index >= 0) {
this.currentIndex = index
this.currentSpace = this.spaces[index]
this.loadSpace(this.currentSpace)
this.showSpaceList = false
}
},
prevSpace() {
this.currentIndex = (this.currentIndex - 1 + this.spaces.length) % this.spaces.length;
this.switchSpace(this.spaces[this.currentIndex].id)
},
nextSpace() {
this.currentIndex = (this.currentIndex + 1) % this.spaces.length;
this.switchSpace(this.spaces[this.currentIndex].id)
},
// ==================== 手势 ====================
onTouchStart(e) {
this.isDragging = true;
const t = e.touches[0];
this.lastX = t.clientX;
this.lastY = t.clientY
},
onTouchMove(e) {
if (!this.isDragging) return;
const t = e.touches[0];
const dx = t.clientX - this.lastX;
const dy = t.clientY - this.lastY;
this.lastX = t.clientX;
this.lastY = t.clientY;
this.lon -= dx * 0.1;
this.lat += dy * 0.1;
this.lat = Math.max(-85, Math.min(85, this.lat))
},
onTouchEnd() {
this.isDragging = false
},
// ==================== 工具栏功能示例 ====================
resetView() {
this.lon = 0;
this.lat = 0
},
toggleFullScreen() {
uni.setScreenBrightness({
value: 1
})
}, // 示例
takePhoto() {
console.log('拍照功能可扩展')
}
}
}
</script>
<style scoped>
.vr-page {
width: 100%;
height: 100%;
background: #000;
position: relative
}
.vr-canvas {
width: 100%;
height: 100%;
display: block
}
.mini-canvas {
position: absolute;
top: 20rpx;
left: 20rpx;
width: 150rpx;
height: 150rpx;
z-index: 10;
border: 1px solid #ccc;
border-radius: 6rpx
}
.back-btn {
position: absolute;
top: 40rpx;
left: 20rpx;
color: #fff;
font-size: 28rpx;
z-index: 10
}
.bottom-bar {
position: absolute;
bottom: 0;
width: 100%;
height: 80rpx;
display: flex;
justify-content: space-between;
padding: 0 20rpx;
z-index: 10
}
.bottom-bar .left,
.bottom-bar .right {
display: flex;
align-items: center
}
.space-list {
position: absolute;
bottom: 80rpx;
left: 0;
width: 100%;
height: 150rpx;
background: rgba(0, 0, 0, 0.7);
display: flex;
overflow-x: scroll;
padding: 10rpx
}
.space-list .space-item {
margin-right: 10rpx;
display: flex;
flex-direction: column;
align-items: center
}
.space-list image {
width: 120rpx;
height: 80rpx;
border-radius: 6rpx
}
.tools-panel {
position: absolute;
bottom: 80rpx;
right: 0;
width: 200rpx;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
padding: 10rpx
}
.tools-panel button {
margin-bottom: 10rpx;
color: #fff
}
.switch-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
color: #fff;
font-size: 60rpx;
background: rgba(0, 0, 0, 0.3);
width: 80rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border-radius: 50%
}
.switch-btn.left {
left: 20rpx
}
.switch-btn.right {
right: 20rpx
}
</style>

366
pages-biz/wae/wae.vue Normal file
View File

@@ -0,0 +1,366 @@
<template>
<view class="pending-bill-page">
<!-- 自定义导航栏 -->
<custom-navbar title="水电费待付" />
<scroll-view scroll-y class="scroll-content" :refresher-enabled="true" :refresher-triggered="isRefreshing"
@refresherrefresh="refresh" @scrolltolower="loadMore">
<view v-if="bills.length > 0">
<view class="bill-item" v-for="item in bills" :key="item.billNo">
<!-- 左侧勾选 + 信息 -->
<view class="bill-left">
<u-checkbox v-model="item.checked" @change="toggleCheck(item)" size="32"
active-color="#EA414A" />
<view class="bill-info">
<text class="bill-name">{{ item.billName }}</text>
<text class="bill-date">缴费截止<text class="date">{{ item.billEndDate }}</text></text>
</view>
</view>
<!-- 右侧金额 + 支付按钮 -->
<view class="bill-right">
<text class="amount">{{ formatMoney(item.billAmount) }}</text>
<button class="pay-btn" @click="goPayTest([item])">去支付
</button>
</view>
</view>
</view>
<view v-else class="empty">
<u-empty mode="list" text="暂无水电费账单" />
</view>
<u-loadmore :status="loadStatus" />
</scroll-view>
<!-- 底部批量支付栏 -->
<view v-if="selectedBills.length > 0" class="batch-pay-bar">
<view class="batch-pay-bar-left"><text>已选 {{ selectedBills.length }} </text></view>
<view class="batch-pay-bar-center">
合计<text class="sumAmount">{{ formatMoney(sumAmount) }}</text>
</view>
<view class="batch-pay-bar-right">
<button class="bottomPayBtn" color="linear-gradient(90deg, #007aff, #00aaff)" type="primary"
size="small" @click="goPayTest(selectedBills)">
批量缴费
</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
pageNo: 1,
pageSize: 10,
bills: [],
isRefreshing: false,
loadStatus: 'loadmore',
sumAmount: 0,
selectedBills: [], // 勾选的账单集合
};
},
onLoad(options) {
this.loadBills();
},
onShow() {
this.$checkToken(this.$getToken())
},
methods: {
loadBills() {
if (this.loadStatus !== 'loadmore') return;
this.loadStatus = 'loading';
const token = this.$getToken();
let url = '/bill/pageQueryWaeBill'
this.$u.post(url, {
pageNo: this.pageNo,
pageSize: this.pageSize
}, {
WT: token
}).then(
result => {
const rows = result.data.result || [];
if (this.pageNo === 1) this.bills = [];
this.bills = this.bills.concat(rows);
if (rows.length < this.pageSize) {
this.loadStatus = 'nomore';
} else {
this.pageNo++;
this.loadStatus = 'loadmore';
}
})
this.updateSelected();
},
refresh() {
this.isRefreshing = true;
setTimeout(() => {
this.loadBills();
this.isRefreshing = false;
}, 1000);
},
loadMore() {
this.loadBills()
},
formatMoney(val) {
if (val === null || val === undefined || isNaN(val)) return '—';
val = Number(val);
if (val >= 10000) {
return '¥' + (val / 10000).toFixed(2) + '万';
}
return '¥' + val.toFixed(2);
},
toggleCheck(item) {
item.checked = !item.checked;
if (item.checked && !this.selectedBills.some(b => b.id === item.id)) {
this.selectedBills.push(item);
} else {
// 取消勾选时从已选数组移除
this.selectedBills = this.selectedBills.filter(b => b.id !== item.id);
}
let sumFee = 0;
this.selectedBills.forEach(b => sumFee += b.amount);
this.sumAmount = sumFee;
},
updateSelected() {
if (!Array.isArray(this.bills)) return;
this.selectedBills = this.bills.filter(b => b.checked);
},
goPayTest(billList) {
if (!billList || billList.length === 0) return;
// 模拟后台生成订单返回
const order = {
timeStamp: String(Date.now()), // 时间戳
nonceStr: Math.random().toString(36).substr(2, 15), // 随机字符串
package: 'prepay_id=TEST1234567890', // 模拟预支付ID
signType: 'MD5',
paySign: 'TEST_SIGN', // 模拟签名
};
// 调用微信支付接口(测试)
uni.requestPayment({
...order,
success: () => {
uni.showToast({
title: '支付成功(测试)',
icon: 'success'
});
// 清空勾选状态
billList.forEach(b => b.checked = false);
this.updateSelected();
// 刷新列表(可选)
this.loadBills();
},
fail: () => {
uni.showToast({
title: '支付失败(测试)',
icon: 'none'
});
}
});
},
async goPay(billList) {
if (!billList || billList.length === 0) return;
try {
// 1. 请求后台生成订单
const res = await uni.request({
url: 'https://api.example.com/payments/create',
method: 'POST',
data: {
userId: this.userId,
bills: billList.map(b => b.id),
}
});
if (res[1].data.code !== 0) {
uni.showToast({
title: res[1].data.msg,
icon: 'none'
});
return;
}
const order = res[1].data.data; // 后台返回的支付参数
// 2. 调用微信支付
uni.requestPayment({
timeStamp: order.timeStamp,
nonceStr: order.nonceStr,
package: order.package,
signType: order.signType,
paySign: order.paySign,
success: () => {
uni.showToast({
title: '支付成功',
icon: 'success'
});
// 更新勾选状态
billList.forEach(b => b.checked = false);
this.updateSelected();
// 刷新列表
this.loadBills();
},
fail: () => {
uni.showToast({
title: '支付失败',
icon: 'none'
});
}
});
} catch (error) {
console.error('支付请求失败', error);
uni.showToast({
title: '支付请求失败',
icon: 'none'
});
}
},
}
};
</script>
<style lang="scss" scoped>
.pending-bill-page {
background: #f7f8fa;
min-height: 100vh;
padding-top: 175rpx;
/* 自定义导航栏高度 */
}
.scroll-content {
padding: 20rpx;
box-sizing: border-box;
}
.bill-item {
display: flex;
justify-content: space-between;
background: #fff;
margin-bottom: 20rpx;
border-radius: 12rpx;
padding: 20rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.bill-left {
display: flex;
align-items: center;
.bill-info {
display: flex;
flex-direction: column;
.bill-name {
font-size: 30rpx;
color: #2D2B2C;
font-weight: 500;
}
.bill-date {
font-size: 24rpx;
color: #86868C;
margin-top: 6rpx;
.date {
font-size: 24rpx;
color: #2D2B2C;
}
}
}
}
.bill-right {
flex-direction: column;
align-items: flex-end;
width: 30%;
.amount {
text-align: center;
height: 28rpx;
line-height: 28rpx;
font-size: 30rpx;
color: #EF8849;
}
.pay-btn {
margin-top: 12rpx;
padding: 0 40rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 6rpx; // 圆角
font-size: 30rpx;
color: #fff;
background-color: #F34038;
text-align: center;
}
.btn-text {
padding-top: 13rpx;
}
}
.batch-pay-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 170rpx;
padding: 0 30rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 -4rpx 10rpx rgba(0, 0, 0, 0.08);
z-index: 999;
.batch-pay-bar-left {
font-size: 28rpx;
color: #86868C;
}
.batch-pay-bar-center {
font-size: 28rpx;
color: #222222;
.sumAmount {
font-size: 28rpx;
color: #ff7f00;
}
}
.batch-pay-bar-right {
flex-direction: column;
align-items: flex-end;
.bottomPayBtn {
height: 60rpx;
line-height: 60rpx;
padding: 0 40rpx;
border-radius: 6rpx;
font-size: 30rpx;
font-weight: bold;
text-align: center;
background: #F34038;
}
}
}
.empty {
margin-top: 200rpx;
text-align: center;
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<view class="water-electric-page">
<!-- 页面标题栏 -->
<customNavbar title="水电费明细" />
<!-- 空状态 -->
<!-- 列表部分 -->
<!-- 收支记录列表 -->
<scroll-view scroll-y class="scroll-content" @scrolltolower="loadMore">
<view v-if="flowList.length > 0" class="record-list">
<view
class="record-item"
v-for="(item, index) in flowList"
:key="index"
@click="toDetail(item)"
>
<!-- <view class="record-icon">
<image src="/static/icon/水费.png" class="icon-1" v-if="item.feeType=='水费'"></image>
<image src="/static/icon/电费.png" class="icon-2" v-else></image>
</view> -->
<!-- 左侧信息 -->
<view class="record-left">
<text class="record-title">{{ item.itemName }}</text>
<text class="record-time">支付时间{{ item.payDate }}</text>
</view>
<!-- 右侧金额 + 箭头 -->
<view class="record-right">
<view class="amount">
{{ item.itemAmount.toFixed(2) }}
<u-icon name="arrow-right" size="20" color="#ccc"></u-icon>
</view>
</view>
</view>
</view>
<view v-if="records.length === 0" class="empty">
<image src="/static/empty.png" mode="aspectFit" />
<text>暂明细记录</text>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
pageNo: 1,
pageSize:10,
flowList: [],
loadStatus: 'loadmore'
};
},
onShow() {
this.$checkToken(this.$getToken())
},
onLoad() {
this.fetchDataList();
},
methods: {
fetchDataList(){
if (this.loadStatus !== 'loadmore') return;
this.loadStatus = 'loading';
let url = '/bill/pageQueryPayRecord'
this.$u.post(url, {
pageNo: this.pageNo,
pageSize: this.pageSize,
recordType:'wae'
},{
WT: this.$getToken()
}).then(res => {
const rows = res.data.result || [];
console.log(rows)
if (this.pageNo === 1) this.flowList = [];
this.flowList = this.flowList.concat(rows);
if (rows.length < this.pageSize) {
this.loadStatus = 'nomore';
} else {
this.pageNo++;
this.loadStatus = 'loadmore';
}
}).catch(err => {
console.log("获取水电费账单缴费记录失败:", err)
this.loadStatus = 'loadmore';
})
},
loadMore() {
// 只有在 loadStatus 为 'loadmore' 时才触发
if (this.loadStatus !== 'loadmore') return;
// 调用接口获取下一页
this.fetchDataList();
},
toDetail(item) {
}
}
};
</script>
<style lang="scss" scoped>
.water-electric-page {
padding: 175rpx 20rpx 20rpx 20rpx;
.header {
background-color: #fff;
padding: 30rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
text-align: center;
.title {
font-size: 32rpx;
font-weight: 600;
}
}
.record-list {
margin-top: 20rpx;
}
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 24rpx 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
.record-icon{
display: flex;
align-items: center;
justify-content: center;
.icon-1{
width: 33rpx;
height: 47rpx;
}
.icon-2{
width: 35rpx;
height: 57rpx;
}
}
.record-left {
display: flex;
flex-direction: column;
gap: 10rpx;
.record-title {
font-size: 30rpx;
font-weight: 500;
color: #2D2B2C;
}
.record-time {
font-size: 24rpx;
color: #86868C;
}
}
.record-right {
display: flex;
align-items: center;
font-size: 30rpx;
font-weight: 400;
color: #222222;
.amount {
display: flex;
align-items: center;
gap: 8rpx;
}
}
}
.empty {
margin-top: 200rpx;
display: flex;
flex-direction: column;
align-items: center;
color: #aaa;
font-size: 28rpx;
image {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
}
}
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<view class="container">
<web-view v-if="src" :src="src" bindmessage="handleGetMessage"></web-view>
</view>
</template>
<script>
export default {
data() {
return {
src: '' // WebView URL
}
},
// 页面加载
onLoad(e) {
let url = e.url || ''
try {
url = decodeURIComponent(url)
} catch (err) {
console.warn('decode error', url)
}
console.log("签署链接:" + url)
// 强制刷新 WebView
this.src = ''
this.$nextTick(() => {
this.src = url
console.log('webview src set:', this.src)
})
},
methods: {
/**
* 刷脸完成后重新加载页面
*/
reloadPage(redirectUrl) {
console.log('---webview reloadPage', redirectUrl)
this.src = redirectUrl || this.src
},
/**
* 消息通知回调处理
* H5 / App / 小程序都可以调用
*/
handleGetMessage(e) {
console.log('handleGetMessage', e)
const data = e.detail?.data?.[0]
if (data && data.result === 'success') {
// 跳转中转页
// #ifdef MP-WEIXIN
wx.navigateBack()
// #endif
// #ifdef H5
if (window.history.length > 1) {
window.history.go(-delta)
} else {
// 没有上一页就跳首页
this.$router.replace('/')
}
return
// #endif
// #ifdef APP-PLUS
// 这里根据你 APP 路由跳转方法
// plus.webview.open('/redirect/bizpage')
// #endif
}
}
}
}
</script>