Compare commits

...

65 Commits

Author SHA1 Message Date
zhouyanli
eaa61c52f6 修改bug 2026-08-29 17:43:48 +08:00
zhouyanli
0661687bc5 修复bug 2026-08-28 17:25:55 +08:00
zhouyanli
ea1acf1bbc 修复bug 2026-08-27 18:01:35 +08:00
zhouyanli
56641cbc56 修改bug 2026-08-26 18:00:43 +08:00
zhouyanli
c862c6ae4b 修改bug 2026-08-26 14:10:23 +08:00
zhouyanli
e1522e7792 修改bug 2026-08-25 15:00:50 +08:00
zhouyanli
d7436f5c54 修复bug,修改图片路径地址 2026-08-24 17:04:18 +08:00
zhouyanli
453f563a6e 修复bug 2026-08-22 08:08:01 +08:00
zhouyanli
a078513221 问卷调查修改 2026-08-20 17:43:35 +08:00
zhouyanli
28ca715710 选择小区加下拉刷新 2026-08-20 15:48:31 +08:00
zhouyanli
ec66ed3076 优化全局下拉刷新 2026-08-20 15:41:27 +08:00
zhouyanli
325813c67e 优化内容+修复bug 2026-08-19 10:21:58 +08:00
zhouyanli
1710cba509 修复bug 2026-08-17 18:02:46 +08:00
zhouyanli
a4c7318ff6 修复bug 2026-08-17 16:32:12 +08:00
zhouyanli
365391b490 修复bug 2026-08-15 16:43:34 +08:00
zhouyanli
53ea3996a0 修复bug 2026-08-14 16:22:44 +08:00
zhouyanli
9e8f5d6acc 修复bug 2026-08-13 17:42:59 +08:00
zhouyanli
32765cc004 修复bug 2026-08-13 14:12:11 +08:00
zhouyanli
94146ce27a 优化内容加修复bug 2026-08-13 08:03:26 +08:00
zhouyanli
f793e16263 修改生活缴费功能 2026-08-11 11:44:30 +08:00
zhouyanli
5f8f5fc73d 修复bug 2026-08-10 14:15:59 +08:00
zhouyanli
92dd39cf4b 修复bug 2026-08-07 18:05:16 +08:00
zhouyanli
3851aebe66 修改车位车牌号输入限制bug 2026-08-07 09:24:15 +08:00
zhouyanli
d44ebd075e 修复bug 2026-08-06 10:36:06 +08:00
zhouyanli
81e0ccf574 修复bug,添加我的积分页面 2026-08-05 10:55:30 +08:00
zhouyanli
466f710b33 修复bug 2026-08-04 15:43:20 +08:00
zhouyanli
57756f016e 修改bug 2026-08-04 11:56:12 +08:00
zhouyanli
5c8671a91e 修复注册登录输入框限制等bug 2026-08-03 17:39:27 +08:00
zhouyanli
015ed25400 修复报修bug 2026-08-03 14:20:17 +08:00
zhouyanli
27b547a82f 修复bug 2026-08-03 10:42:17 +08:00
zhouyanli
2010c63d70 修复bug 2026-08-01 18:10:56 +08:00
zhouyanli
f74fb33e11 修复bug 2026-08-01 11:56:28 +08:00
zhouyanli
a34b625092 修复bug 2026-08-01 08:01:17 +08:00
zhouyanli
3b66b76e40 修复bug 2026-07-31 14:06:11 +08:00
zhouyanli
8c0a713512 修改bug 2026-07-30 18:01:39 +08:00
zhouyanli
80ff7588b1 修改bug,优化导航 2026-07-30 15:35:02 +08:00
zhouyanli
a6243cf466 优化登录,修改bug 2026-07-29 17:47:10 +08:00
zhouyanli
9be76c6386 支付返回,修改禅道bug 2026-07-28 17:48:06 +08:00
zhouyanli
a9a5afaf26 修改bug 2026-07-28 11:11:39 +08:00
zhouyanli
ebc8cfa3cb 修改支付页跳转 2026-07-25 16:30:50 +08:00
zhouyanli
6cb10fee08 对接公区收益 2026-07-22 17:38:59 +08:00
zhouyanli
e68579a5d3 修复已知bug 2026-07-21 09:41:54 +08:00
zhouyanli
7b1864f439 修改房屋下拉弹窗显示bug 2026-07-16 17:59:41 +08:00
zhouyanli
289825363b 修改bug\优化内容、加上公区收益和巡检记录页面 2026-07-15 17:42:39 +08:00
zhouyanli
173f1c4585 修改cid无效、登录缓存问题,正在对接支付功能 2026-07-14 14:35:54 +08:00
zhouyanli
3124f95cb3 修改活动报名显示问题,升级版本bug,支付正在开发 2026-07-11 11:11:43 +08:00
zhouyanli
be4bae64d1 修改立即跳转重复跳转问题 2026-07-08 16:11:29 +08:00
zhouyanli
cb3166302d ios配置描述 2026-07-02 15:43:12 +08:00
zhouyanli
eea062ee2d 设置添加注销账号功能 2026-07-02 10:51:34 +08:00
zhouyanli
78fc468bc4 投诉类型接口添加参数 2026-06-30 14:24:37 +08:00
zhouyanli
1a8abc6d47 更换天地图key,修改push云函数配置 2026-06-29 15:39:26 +08:00
zhouyanli
d0ed33ccc3 添加logo图片 2026-06-29 08:13:59 +08:00
zhouyanli
5acbc0b154 添加协议链接 2026-06-27 07:57:30 +08:00
zhouyanli
47079da1a4 推送关联云服务空间、添加用户协议和隐私政策外部链接、修改活动状态和图片定位显示问题 2026-06-25 17:03:08 +08:00
zhouyanli
8939fec34b 问卷调查详情显示问题、已结束不能点击提交; 2026-06-24 17:12:43 +08:00
zhouyanli
a501524646 修改活动报名按钮状态,报修添加小区参数;访客返回数据 2026-06-24 09:06:10 +08:00
zhouyanli
d5cacf4087 修改活动接口参数,报修接口参数,首页活动参数 2026-06-22 17:21:41 +08:00
zhouyanli
36ee7c3562 修改我的车辆审核状态、问卷调查、登录添加用户隐私协议弹窗 2026-06-18 16:38:56 +08:00
zhouyanli
1efa212d26 缴费记录对接数据、我的车位新增选择业主 2026-06-17 17:38:03 +08:00
zhouyanli
c363c90f95 修改生活缴费页面、扫码功能、车位参数 2026-06-16 17:18:12 +08:00
zhouyanli
bea0077272 添加物业费支付页面、活动状态报名显示、车位参数 2026-06-16 08:49:30 +08:00
zhouyanli
5040564655 修改首页轮播报错、活动显示、公共页样式显示、我的车位等bug 2026-06-13 17:49:19 +08:00
zhouyanli
b395631400 修改活动详情和实况样式 2026-06-09 18:06:38 +08:00
zhouyanli
aaf828c6e3 修改切换账号绑定小区 2026-06-09 17:11:25 +08:00
zhouyanli
ebdfa8d95e 丢弃报错的银联插件 2026-06-08 17:16:46 +08:00
117 changed files with 10788 additions and 5036 deletions

View File

@@ -11,7 +11,36 @@
"Bash(node *)",
"Bash(git checkout *)",
"Bash(sed -i '207,213d' D:/wuye-jumin-front/property-uniapp-project/pageSubPack/loginSub/login.vue)",
"Bash(sed -n '203,213p' D:/wuye-jumin-front/property-uniapp-project/pageSubPack/loginSub/login.vue)"
"Bash(sed -n '203,213p' D:/wuye-jumin-front/property-uniapp-project/pageSubPack/loginSub/login.vue)",
"Bash(xargs -I{} sh -c 'echo \"== {} ==\" && cat {}')",
"WebSearch",
"Bash(sed -i '115s/\\(result\\)/\\(type, result\\)/' pageSubPack/payment-list/addHydropower.vue)",
"Bash(sed -i \"117s/'扫码成功', result/'扫码成功 type=' + type + ' result=' + result/\" pageSubPack/payment-list/addHydropower.vue)",
"Bash(xxd)",
"Bash(sed -i '311s/^\\\\t//' pageSubPack/payment-list/paymentIndex.vue)",
"Bash(sed -i '312s/^\\\\t//' pageSubPack/payment-list/paymentIndex.vue)",
"Bash(sed -i '225,242d' pages/index/index.vue)",
"Bash(sed -i '224a\\\\\\\\n\\\\t// banner点击跳转\\\\n\\\\tconst bannerClick = \\(index\\) => {\\\\n\\\\t const item = bannerRawData.value[index]\\\\n\\\\t if \\(item \\\\&\\\\& item.link\\) {\\\\n\\\\t uni.navigateTo\\({ url: item.link }\\)\\\\n\\\\t }\\\\n\\\\t}' pages/index/index.vue)",
"Bash(sed -i '229s/^\\\\t\\\\/\\\\//\\\\t\\\\/\\\\//' pages/index/index.vue)",
"Bash(sed -i '230s/^\\\\t\\\\tconst/\\\\tconst/' pages/index/index.vue)",
"Bash(sed -i '231s/^\t\t / /' pages/index/index.vue)",
"Bash(sed -i '232s/^\t\t /\\\\t\\\\t/' pages/index/index.vue)",
"Bash(sed -i '233s/^\t\t /\\\\t\\\\t\\\\t/' pages/index/index.vue)",
"Bash(awk 'NR==235 { print substr\\($0, 2\\); next } 1' \"D:/wuye-jumin-front/property-uniapp-project/pages.json\")",
"Bash(mv /tmp/pages_temp.json \"D:/wuye-jumin-front/property-uniapp-project/pages.json\")",
"Bash(sed -i '32,36s/.*//' App.vue)",
"Bash(sed -i '32d;33d;34d;35d;36d' App.vue)",
"Bash(perl -i -pe 's/\\\\\\\\t\\\\t\\\\t\\\\t\\\\t\"disableScroll\": true/\\\\t\\\\t\\\\t\\\\t\\\\t\\\\t\"disableScroll\": true/' pages.json)",
"Bash(perl -i -pe 's/^t\\\\t\\\\t\\\\t\\\\t\"disableScroll\"/\\\\t\\\\t\\\\t\\\\t\\\\t\\\\t\"disableScroll\"/' pages.json)",
"Bash(sed -i '113s|// loadRevenue\\(true\\)|loadRevenue\\(true\\)|' pageSubPack/inspection-revenue/public-revenue.vue)",
"Bash(sed -i '120s|// loadRevenue\\(\\)|loadRevenue\\(\\)|' pageSubPack/inspection-revenue/public-revenue.vue)",
"Bash(xxd pageSubPack/visitor/visitor-add.vue)",
"Bash(sed -i '24s/^\\\\t\\\\t\\\\t\\\\t/\\\\t\\\\t /' pageSubPack/online-repair/publicRepair.vue)",
"Bash(sed -i '25s/^ \\\\t\\\\t\\\\t\\\\t/\\\\t\\\\t \\\\t/' pageSubPack/online-repair/publicRepair.vue)",
"Bash(sed -i '26s/^\\\\t\\\\t\\\\t\\\\t/\\\\t\\\\t \\\\t/' pageSubPack/online-repair/publicRepair.vue)",
"Bash(sed -i '27s/^\\\\t\\\\t\\\\t\\\\t/\\\\t\\\\t /' pageSubPack/online-repair/publicRepair.vue)",
"Bash(sed -i '416s/.*/ } else {\\\\n submitting.value = false\\\\n uni.showToast\\({ title: res.msg || '\\\\''提交失败'\\\\'', icon: '\\\\''none'\\\\'' }\\)\\\\n }/' pageSubPack/online-repair/personalRepair.vue)",
"Bash(sed -i '420s/.*/ } catch \\(e\\) {\\\\n submitting.value = false\\\\n uni.showToast\\({ title: '\\\\''提交失败,请检查网络或重试'\\\\'', icon: '\\\\''none'\\\\'' }\\)\\\\n } finally {\\\\n uni.hideLoading\\(\\)\\\\n }/' pageSubPack/online-repair/personalRepair.vue)"
]
}
}

View File

@@ -2,3 +2,4 @@ node_modules/
unpackage/
.hbuilderx/
.DS_Store
*.so

View File

@@ -1,30 +1,128 @@
<script setup>
import { onLaunch } from '@dcloudio/uni-app'
import { onLaunch,onShow,onHide } from '@dcloudio/uni-app'
import { isPublicPath, requireLogin } from '@/common/checkLogin.js'
let cidRetryTimer = null
let cidRetryCount = 0
const MAX_RETRY = 5 // 最多重试 5 次
// 应用启动时执行
onLaunch(() => {
console.log('App 启动')
uni.onPushMessage((res) => {
console.log("收到推送消息:",res) //监听推送消息
})
// #ifdef APP-PLUS
// 只有 App 平台才获取 CID
getPushCID()
// #endif
// 路由分发:全局拦截器注册(首页 onLoad 中处理首次引导页跳转)
const token = uni.getStorageSync('token') || ''
console.log('App 启动hasToken=' + !!token + '')
// 全局 navigateTo 拦截器:未登录用户访问需登录页面时自动跳转登录页
uni.addInterceptor('navigateTo', {
invoke(args) {
const t = uni.getStorageSync('token')
if (!t && !isPublicPath(args.url)) {
// 使用 requireLogin 统一处理,自带防重复锁
requireLogin({ message: '请先登录', redirect: true })
return false
}
}
})
})
onShow((options) => {
console.log('App Show', options)
// 处理从小程序返回的支付结果
if (options && options.referrerInfo && options.referrerInfo.extraData) {
const extraData = options.referrerInfo.extraData
console.log('小程序返回数据:', extraData)
if (extraData.retCode === 'SUCCESS') {
// 支付成功:返回缴费列表页并刷新数据
uni.showToast({ title: '支付成功', icon: 'success' })
setTimeout(() => {
uni.navigateTo({
url: '/pageSubPack/payment-list/paymentIndex'
})
}, 1500)
return
} else if (extraData.retCode === 'FAIL') {
uni.showToast({ title: extraData.retMsg || '支付失败', icon: 'none' })
return
}
}
// #ifdef APP-PLUS
// 每次回到前台时检查 CID 是否有效,没有则重新获取
const savedCid = uni.getStorageSync('push_cid')
if (!savedCid) {
console.log('App Show 检测到 CID 为空,重新获取')
cidRetryCount = 0
getPushCID()
}
// #endif
})
// 获取 uniPush 2.0 CID
onHide(() =>{
console.log('App Hide')
})
// 获取 uniPush 2.0 CID带重试机制解决离线安装首次启动 CID 为空的问题)
function getPushCID() {
// #ifdef APP-PLUS
// 先尝试 plus.push.getClientInfo 获取(兼容老版本)
try {
const clientInfo = plus.push.getClientInfo()
if (clientInfo && clientInfo.clientid) {
console.log('推送 CIDplus.push=', clientInfo.clientid)
uni.setStorageSync('push_cid', clientInfo.clientid)
return
}
} catch (e) {
console.log('plus.push.getClientInfo 失败,改用 uni.getPushClientId')
}
uni.getPushClientId({
success: (res) => {
console.log('推送初始化成功')
console.log('CID =', res.cid)
// 存起来备用
uni.setStorageSync('push_cid', res.cid)
if (res.cid) {
console.log('推送初始化成功, CID =', res.cid)
uni.setStorageSync('push_cid', res.cid)
cidRetryCount = 0
if (cidRetryTimer) {
clearTimeout(cidRetryTimer)
cidRetryTimer = null
}
} else {
// CID 返回为空(常见于离线安装首次启动)
console.warn('获取到 CID 为空,将重试')
scheduleRetry()
}
},
fail: (err) => {
console.error('获取推送 CID 失败', err)
scheduleRetry()
}
})
// #endif
}
// 定时重试获取 CID间隔递增2s → 4s → 8s → 16s → 32s
function scheduleRetry() {
if (cidRetryCount >= MAX_RETRY) {
console.error('CID 获取重试已达上限(' + MAX_RETRY + '次),放弃重试')
return
}
cidRetryCount++
const delay = Math.pow(2, cidRetryCount) * 1000
console.log('将在 ' + delay / 1000 + 's 后第 ' + cidRetryCount + ' 次重试获取 CID')
if (cidRetryTimer) clearTimeout(cidRetryTimer)
cidRetryTimer = setTimeout(() => {
getPushCID()
}, delay)
}
</script>

View File

@@ -0,0 +1,133 @@
/**
* 登录检查工具 — 统一拦截未登录用户的功能操作
*
* 使用方式:
* import { requireLogin } from '@/common/checkLogin.js'
* if (!requireLogin()) return
*
* 页面级调用即可,全局 navigateTo 拦截器App.vue onLaunch 中注册)作为兜底。
*
* 防重复机制:
* _loginPending — requireLogin 立即设置,阻止在 setTimeout 等待期内重复调度
* _loginNavigating — navigateToLogin 设置,阻止实际跳转重复
*/
/** 无需登录即可访问的页面路径白名单(全局拦截器使用) */
export const PUBLIC_PATHS = [
'/pageSubPack/loginSub/login',
'/pageSubPack/loginSub/register-new-user',
'/pageSubPack/loginSub/forget-pwd',
'/pageSubPack/loginSub/change-pwd',
'/pageSubPack/loginSub/privacy',
'/pageSubPack/loginSub/user-agreement',
'/pages/navigation/navigation'
]
// ==================== 登录页跳转防重复锁 ====================
// 使用 getApp().globalData 存储,避免模块被分包/异步 chunk 多次实例化导致锁失效
function getLock() {
const app = getApp()
if (!app.globalData) app.globalData = {}
return app.globalData
}
/**
* 判断当前是否已在登录页
*/
function isOnLoginPage() {
const pages = getCurrentPages()
if (!pages || pages.length === 0) return false
const route = pages[pages.length - 1].route || ''
return route.includes('loginSub/login')
}
/**
* 跳转到登录页(统一入口,带防重复锁)
* 所有需要跳转登录页的地方都应调用此函数,不要直接调用 uni.navigateTo / uni.reLaunch
*
* @param {object} [options]
* @param {string} [options.method='navigateTo'] - 跳转方式:'navigateTo' | 'reLaunch' | 'redirectTo'
* @param {string} [options.url='/pageSubPack/loginSub/login?mode=pwd'] - 目标 url
*/
export function navigateToLogin(options = {}) {
const {
method = 'navigateTo',
url = '/pageSubPack/loginSub/login?mode=pwd'
} = options
// 已经在登录页,跳过
if (isOnLoginPage()) return
const lock = getLock()
// reLaunch 优先级最高,强制放行(用于 token 过期、退出登录等场景)
if (method !== 'reLaunch' && lock._loginNavigating) return
lock._loginNavigating = true
const navFn = uni[method]
if (!navFn) {
lock._loginNavigating = false
return
}
navFn({
url,
fail() {
// 跳转失败时重置锁,允许后续重试
lock._loginNavigating = false
}
})
}
/**
* 重置登录跳转锁(登录页 onLoad 时调用)
*/
export function resetLoginLock() {
const lock = getLock()
lock._loginPending = false
lock._loginNavigating = false
}
/**
* 判断指定路径是否为公开页面(无需登录)
* @param {string} url - navigateTo 的目标 url
* @returns {boolean}
*/
export function isPublicPath(url) {
if (!url) return true
// 去掉查询参数
const path = url.split('?')[0]
return PUBLIC_PATHS.some(p => path === p || path.startsWith(p + '?'))
}
/**
* 检查是否已登录;未登录则弹出提示并跳转登录页
* @param {object} [options]
* @param {string} [options.message='请先登录'] - 提示文案
* @param {boolean} [options.redirect=true] - 是否自动跳转登录页
* @returns {boolean} 已登录返回 true否则返回 false
*/
export function requireLogin(options = {}) {
const {
message = '请先登录',
redirect = true
} = options
const token = uni.getStorageSync('token')
if (token) return true
uni.showToast({ title: message, icon: 'none' })
if (redirect) {
// 立即加锁,防止在 setTimeout 等待期内重复调度
const lock = getLock()
if (lock._loginPending) return false
lock._loginPending = true
setTimeout(() => {
lock._loginPending = false
navigateToLogin({ method: 'navigateTo' })
}, 1000)
}
return false
}

View File

@@ -0,0 +1,174 @@
<template>
<view class="privacy-mask" v-if="show" @click="handleMaskClick">
<view class="privacy-popup" @click.stop>
<view class="popup-header">
<text class="popup-title">请先阅读并同意协议</text>
<view class="popup-close" @click="handleClose"></view>
</view>
<scroll-view class="popup-body" scroll-y>
<view class="popup-content">
<view class="section-text">
我已仔细阅读并同意
<text class="privacy-link" @click="goToUserAgreement">用户协议</text>
<text class="privacy-link" @click="goToPrivacy">隐私政策</text>
全部内容
</view>
</view>
</scroll-view>
<view class="popup-footer">
<button class="popup-btn disagree-btn" @click="handleDisagree">不同意</button>
<button class="popup-btn agree-btn" @click="handleAgree">同意</button>
</view>
</view>
</view>
</template>
<script setup>
defineProps({
show: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['agree', 'disagree', 'close', 'update:show'])
const handleAgree = () => {
emit('agree')
}
const handleDisagree = () => {
emit('disagree')
}
const handleClose = () => {
emit('close')
emit('update:show', false)
}
const handleMaskClick = () => {
emit('close')
emit('update:show', false)
}
// 打开用户协议
const goToUserAgreement = () => {
// #ifdef APP-PLUS
plus.runtime.openURL('https://wuye.ckdzkj.com/parivw/ownler_xieyi.html')
// #endif
// #ifdef H5
window.open('https://wuye.ckdzkj.com/parivw/ownler_xieyi.html')
// #endif
}
// 打开隐私政策
const goToPrivacy = () => {
// #ifdef APP-PLUS
plus.runtime.openURL('https://wuye.ckdzkj.com/parivw/owner.html')
// #endif
// #ifdef H5
window.open('https://wuye.ckdzkj.com/parivw/owner.html')
// #endif
}
</script>
<style scoped>
.privacy-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.privacy-popup {
width: 640rpx;
max-height: 80vh;
background: #fff;
border-radius: 16rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 30rpx 16rpx;
border-bottom: 1rpx solid #eee;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.popup-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #999;
border-radius: 50%;
background: #f5f5f5;
}
.popup-body {
flex: 1;
padding: 24rpx 30rpx;
max-height: 50vh;
box-sizing: border-box;
}
.popup-content .section-text {
font-size: 26rpx;
line-height: 1.8;
color: #666;
text-align: justify;
}
.privacy-link {
color: #007aff;
}
.popup-footer {
display: flex;
padding: 20rpx 30rpx 30rpx;
gap: 20rpx;
border-top: 1rpx solid #eee;
}
.popup-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 8rpx;
font-size: 28rpx;
border: none;
padding: 0;
margin: 0;
}
.popup-btn::after {
border: none;
}
.disagree-btn {
background: #f5f5f5;
color: #666;
}
.agree-btn {
background: #007aff;
color: #fff;
}
</style>

View File

@@ -0,0 +1,158 @@
<template>
<scroll-view
scroll-y
class="prs-scroll"
:class="customClass"
:style="scrollStyle"
:refresher-enabled="refresherEnabled"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onScrollToLower"
>
<!-- 下拉刷新指示器 -->
<view class="prs-refresh" v-if="refreshing">
<uni-icons type="spinner-cycle" class="prs-spin" size="16" color="#999" />
<text class="prs-refresh-text">刷新中...</text>
</view>
<!-- 主体内容 -->
<slot></slot>
<!-- 空状态 -->
<view v-if="empty && showEmpty" class="prs-empty">
<slot name="empty">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="prs-empty-text">{{ emptyText }}</text>
</slot>
</view>
<!-- 底部加载/无更多状态 -->
<view class="prs-bottom">
<view v-if="loadingMore" class="prs-load-more">加载中...</view>
<view v-else-if="noMoreData && !empty" class="prs-no-more">{{ noMoreText }}</view>
</view>
</scroll-view>
</template>
<script setup>
defineProps({
/** 是否正在刷新 */
refreshing: {
type: Boolean,
default: false
},
/** 下拉刷新是否启用 */
refresherEnabled: {
type: Boolean,
default: true
},
/** 加载更多状态 */
loadingMore: {
type: Boolean,
default: false
},
/** 是否无更多数据 */
noMoreData: {
type: Boolean,
default: false
},
/** 列表是否为空 */
empty: {
type: Boolean,
default: false
},
/** 是否显示空状态 */
showEmpty: {
type: Boolean,
default: true
},
/** 空状态文案 */
emptyText: {
type: String,
default: '暂无数据'
},
/** 没有更多文案 */
noMoreText: {
type: String,
default: '没有更多了'
},
/** 自定义类名,允许外部设置 scroll-view 样式 */
customClass: {
type: String,
default: ''
},
/** 自定义 scroll-view 内联样式 */
scrollStyle: {
type: String,
default: ''
}
})
const emit = defineEmits(['refresh', 'loadMore'])
const onRefresh = () => {
emit('refresh')
}
const onScrollToLower = () => {
emit('loadMore')
}
</script>
<style lang="scss" scoped>
.prs-scroll {
flex: 1;
min-height: 0;
overflow: hidden;
}
// 刷新指示器
.prs-refresh {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
}
.prs-spin {
animation: prs-rotate 1s linear infinite;
}
.prs-refresh-text {
margin-left: 10rpx;
font-size: 24rpx;
color: #999;
}
// 空状态
.prs-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
}
.prs-empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
// 底部状态
.prs-bottom {
.prs-load-more,
.prs-no-more {
text-align: center;
padding: 20rpx;
color: #999;
font-size: 24rpx;
}
}
@keyframes prs-rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@@ -1,10 +1,14 @@
{
"name" : "花开物业",
"appid" : "__UNI__29C3D60",
"description" : "物业用户端app",
"versionName" : "1.0.1",
"versionCode" : 105,
"description" : "智慧社区居民端面向小区业主使用,支持注册、验证码、账号密码多种登录方式,提供密码找回功能,账号使用安全便捷。支持多小区切换,首页设有轮播 banner、社区公告集成远程开门、生活缴费、在线报修、访客邀请、社区活动常用服务。全服务板块统一涵盖门禁开门、线上缴费、报修申报、访客预约、投诉反馈、问卷调研、活动报名、社区公示、一键联系物业功能。个人中心可管理房屋、住户、车位、车辆信息支持更换手机号、修改密码、消息通知设置、版本更新及账号退出一站式满足业主居家生活、社区服务、房产车辆管理需求实现社区生活数字化、便捷化。",
"versionName" : "1.2.1",
"versionCode" : 135,
"transformPx" : false,
"uniCloud" : {
"provider" : "aliyun",
"spaceId" : "mp-2d1912d1-5d25-47a9-a4c5-2669573cd0c2"
},
/* 5+App */
"app-plus" : {
"usingComponents" : true,
@@ -13,19 +17,21 @@
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"autoclose" : false,
"delay" : 0
},
/* */
"modules" : {
"Geolocation" : {},
"Maps" : {},
"Camera" : {},
"Contacts" : {},
"Gallery" : {},
"Push" : {},
"Barcode" : {},
"Payment" : {}
"Payment" : {},
"Bluetooth" : {},
"Maps" : {},
"Share" : {}
},
/* */
"distribute" : {
@@ -51,17 +57,31 @@
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>"
],
"schemes" : ""
"schemes" : "",
"targetSdkVersion" : 30
},
/* ios */
"ios" : {
"dSYMs" : false,
"privacyDescription" : {
"NSPhotoLibraryUsageDescription" : "需要访问您的相册,用于选择图片上传",
"NSPhotoLibraryAddUsageDescription" : "保存二维码图片到相册"
"NSPhotoLibraryUsageDescription" : "需要访问手机相册,选取本地水电表二维码、故障照片、活动照片用于抄表扫码、提交报修工单、活动实况,仅读取本次业务所需图片,不会获取相册内其他无关照片。",
"NSPhotoLibraryAddUsageDescription" : "需要相册写入权限,将访客通行凭证二维码图片保存至手机相册,方便线下小区出示通行,图片仅本地存储,不会上传至无关服务器",
"NSCameraUsageDescription" : "本应用需要使用相机,用于扫描水电表二维码读取表计账单、拍摄故障现场提交报修工单、拍摄访客登记凭证,拍摄内容仅用于小区物业业务处理,不会私自留存或对外分享。",
"NSBluetoothAlwaysUsageDescription" : "本应用需要蓝牙权限,用于扫描、连接小区蓝牙门禁设备实现一键开门,仅和本小区门禁硬件建立通信,不会扫描、连接无关蓝牙设备。",
"NSBluetoothPeripheralUsageDescription" : "需要蓝牙权限连接小区智能门禁,实现蓝牙一键开门通行,仅用于小区门禁解锁业务,不采集蓝牙设备信息。",
"NSMicrophoneUsageDescription" : "需要麦克风权限,用于拨打物业客服电话、与维修人员通话沟通报修问题,仅通话时采集语音,不会录制、留存通话音频。"
},
"urltypes" : "uppaylyhuts",
"urlschemewhitelist" : "uppaywallet,uppaysdk,uppayx1,uppayx2,uppayx3,paesuperbank,ccbmbsylunionpay,bocom,bankabc,cmbmobilebank,cmblife,com.icbc.iphoneclient,spdbbank,spdbcccUnionPay,citicbankpay,credit,uppayxingyecredit,uppayhuaxiacredit,uppaybeijingcredit,uppayzhonghangcredit,uppayguangdacredit,uppaygonghangcredit,uppayjianhangcredit,uppayjiaohangcredit,uppayguangfa,psbccashier,uppayyouchucredit,com.cebbank.ebank,BankOfShangHai,boscgdlf,citicbankdkkj,cmbc,cibmb,bocmbankpsn,bankofbjpay,uppaynonghangcredit,uppayhuaxia,zytuppay,LZYHSJYHAUTHSSOAPP,bnbpay,dlrcb,czbank,uppayCbhb"
"urlschemewhitelist" : "bocom,bankabc,cmbmobilebank,cmblife,com.icbc.iphoneclient,spdbbank,citicbankpay,credit,com.cebbank.ebank,BankOfShangHai,boscgdlf,cmbc,cibmb,bocmbankpsn,bankofbjpay",
"idfa" : false,
"capabilities" : {
"entitlements" : {
"com.apple.developer.associated-domains" : [
"applinks:static-mp-2d1912d1-5d25-47a9-a4c5-2669573cd0c2.next.bspapp.com",
"applinks:verification.ckdzkj.com"
]
}
}
},
/* SDK */
"sdkConfigs" : {
@@ -83,15 +103,15 @@
"offline" : true,
"icons" : {
"small" : {
"mdpi" : "D:/图片文档/logo-app.png"
"mdpi" : "unpackage/res/icons/logo-app.png"
}
}
}
},
"share" : {
"weixin" : {
"appid" : "",
"UniversalLinks" : ""
"appid" : "wx84b02b2ec80715f4",
"UniversalLinks" : "https://verification.ckdzkj.com/uni-universallinks/__UNI__29C3D60/"
}
},
"payment" : {
@@ -99,13 +119,46 @@
"alipay" : {
"__platform__" : [ "ios", "android" ]
},
"unionpay" : {
"__platform__" : [ "ios", "android" ]
},
"weixin" : {
"__platform__" : [ "ios", "android" ],
"__platform__" : [ "ios", "android" ]
}
},
"oauth" : {
"weixin" : {
"appid" : "",
"UniversalLinks" : ""
"UniversalLinks" : "https://verification.ckdzkj.com/uni-universallinks/__UNI__29C3D60/"
}
}
},
"icons" : {
"android" : {
"hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
}
@@ -119,6 +172,7 @@
"setting" : {
"urlCheck" : false
},
"navigateToMiniProgramAppIdList" : [ "wxc0189436c58b59cd" ],
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ],
"permission" : {
"scope.camera" : {
@@ -145,7 +199,26 @@
"vueVersion" : "3",
"app-harmony" : {
"distribute" : {
"bundleName" : "com.chengkong.property.owner"
"bundleName" : "com.chengkong.property.owner",
"modules" : {
"uni-push" : {}
},
"icons" : {
"foreground" : "unpackage/res/icons/logo-app.png",
"background" : "unpackage/res/icons/background.png"
},
"signingConfigs" : {
"release" : {
"certpath" : "D:/wuye-app-ios-harmony-zhengshu/new_harmony/新花开物业正式.cer",
"keyAlias" : "wuye_release",
"keyPassword" : "0000001AF15B7DC5508CF003B98714F03EE31957599E5E8049D0FC065421D5AEE8BB82BCEDBEC274DA79",
"profile" : "D:/wuye-app-ios-harmony-zhengshu/new_harmony/yonghu_profileRelease.p7b",
"signAlg" : "SHA256withECDSA",
"storeFile" : "D:/wuye-app-ios-harmony-zhengshu/new_harmony/huakaiwuye.p12",
"storePassword" : "0000001AEACD44D53617A151C164F02D7E97C92016ABBD7117C982B31D8744F8EFC60B3D51E521212495"
}
}
}
}
},
"_spaceID" : "mp-2d1912d1-5d25-47a9-a4c5-2669573cd0c2"
}

View File

@@ -1,18 +1,11 @@
<template>
<view class="container">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 顶部标题栏 固定 -->
<view class="top-bar">
<uni-nav-bar left-icon="left" title="活动列表" backgroundColor="none" :border="false" :statusBar="false"
@clickLeft="narBack" />
</view>
<!-- 滚动区域从banner开始滚动 -->
<scroll-view class="scroll-container" scroll-y="true" @scrolltolower="loadMoreData"
:lower-threshold="100">
<view class="scroll-container">
<!-- 顶部图片 -->
<view class="top-img">
<image class="banner-img" src="http://47.104.199.163:9090/images/home/banner.png" mode="aspectFill"></image>
<image class="banner-img" src="http://wuye.ckdzkj.com/images/home/banner.png" mode="aspectFill"></image>
</view>
<!-- 选项卡 -->
@@ -52,9 +45,6 @@
<!-- 活动卡片 -->
<view v-for="activity in displayedActivities" :key="activity.id" class="activity-card"
@tap="viewActivityDetail(activity)">
<view class="status-badge">
{{activity.statusText}}
</view>
<view v-if="activity.applied && activeTab === 'list'" class="signed-badge">
<text>已报名</text>
@@ -65,7 +55,10 @@
<up-row justify="space-between" customStyle="margin-bottom: 10px">
<up-col span="4">
<view class="info-image-left">
<image :src="activity.coverImage" mode="aspectFit" style="width: 100%;height: 100%;border-radius: 16rpx;"></image>
<view class="status-badge">
{{activity.statusText}}
</view>
<image :src="activity.coverImage" mode="aspectFill" style="width: 100%;height: 100%;border-radius: 16rpx;"></image>
</view>
</up-col>
<up-col span="8">
@@ -83,7 +76,10 @@
</view>
<view class="activity-detail">
<text class="detail-text-num">活动名额{{ activity.quota }}已报名{{ activity.appliedCount }}</text>
<text class="detail-text-num">
<text v-if="activity.quota !== 0">活动名额{{ activity.quota }},</text>
已报名{{ activity.appliedCount }}
</text>
</view>
</view>
</up-col>
@@ -91,11 +87,14 @@
</view>
<view class="action-buttons">
<button v-if="activeTab === 'list' && activity.applied === false" class="action-btn signup-btn"
<button v-if="activeTab === 'list' && activity.applied === false && activity.statusText !== '已结束' && !isDeadlinePassed(activity) && !isQuotaFull(activity)" class="action-btn signup-btn"
@tap.stop="signupActivity(activity)">
立即报名
</button>
<button v-if="activeTab === 'list' && activity.applied === true" class="action-btn signup-btn"
<view v-if="activeTab === 'list' && activity.applied === false && activity.statusText !== '已结束' && getDisabledText(activity)" class="action-btn disabled-btn">
{{ getDisabledText(activity) }}
</view>
<button v-if="activeTab === 'list' && activity.applied === true && activity.statusText !== '已结束'" class="action-btn signup-btn"
@tap.stop="cancelActivity(activity)">
取消报名
</button>
@@ -115,30 +114,29 @@
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { ref, watch } from 'vue'
import { onLoad, onUnload, onShow } from '@dcloudio/uni-app'
import { onLoad, onUnload, onShow, onPullDownRefresh, onReachBottom, onBackPress } from '@dcloudio/uni-app'
import moment from 'moment';
import { getActivityList,getMyActivityList} from '../api/api.js'
// ===================== 响应式数据 =====================
const activeTab = ref('list') // 'list' 或 'my'
const myFilter = ref('') // 我的活动筛选条件:''审核中, '0'未开始, '1'进行中, '2'已结束
const myFilter = ref('') // 我的活动筛选条件:''全部, '0'未开始, '1'进行中, '2'已结束
const isLoading = ref(false)
const hasMoreData = ref(true)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const displayedActivities = ref([]) // 当前显示的活动列表
const statusBarHeight = ref(uni.getSystemInfoSync().statusBarHeight || 0)
// 筛选条件status ''全部, '0'未开始, '1'进行中, '2'已结束
const filters = ref([
{ label: '审核中', value: '' },
{ label: '全部', value: '' },
{ label: '未开始', value: '0' },
{ label: '进行中', value: '1' },
{ label: '已结束', value: '2' }
@@ -155,19 +153,17 @@ const resetAndLoad = () => {
getActivityListData(1)
}
/**
* 返回上一级页面
*/
const narBack = () => {
// 页面返回拦截:根据来源页返回对应 tab否则走默认返回
onBackPress(() => {
const sourcePage = uni.getStorageSync('sourcePage')
if (sourcePage === "index") {
uni.switchTab({ url: "/pages/index/index" })
return true
} else if (sourcePage === "serviceIndex") {
uni.switchTab({ url: "/pages/serviceIndex/serviceIndex" })
} else {
uni.navigateBack()
return true
}
}
})
/**
* 切换选项卡(活动列表/我的活动)
@@ -215,6 +211,7 @@ const getActivityListData = async (pageNum = 1) => {
const params = {
pageNum: pageNum,
pageSize: pageSize.value,
villageId:uni.getStorageSync('currentVillageId')
}
const res = await getActivityList(params)
@@ -238,10 +235,11 @@ const getActivityListData = async (pageNum = 1) => {
if (pageNum === 1) displayedActivities.value = []
}
} catch (err) {
uni.showToast({ title: "网络异常", icon: 'none' })
uni.showToast({ title: err.msg || "网络异常", icon: 'none' })
if (pageNum === 1) displayedActivities.value = []
} finally {
isLoading.value = false
uni.stopPullDownRefresh();
}
}
@@ -257,6 +255,7 @@ const getMyActivityListData = async (pageNum = 1) => {
const params = {
pageNum: pageNum,
pageSize: pageSize.value,
villageId:uni.getStorageSync('currentVillageId')
}
// status 为空时不传,返回全部
if (myFilter.value !== '') {
@@ -282,10 +281,11 @@ const getMyActivityListData = async (pageNum = 1) => {
if (pageNum === 1) displayedActivities.value = []
}
} catch (err) {
uni.showToast({ title: '网络异常', icon: 'none' })
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
if (pageNum === 1) displayedActivities.value = []
} finally {
isLoading.value = false
uni.stopPullDownRefresh();
}
}
@@ -311,13 +311,44 @@ const formatDate = (date) => {
return moment(date).format('YYYY-MM-DD HH:mm')
}
/**
* 报名是否已截止applyDeadline 已过)
* @param {object} activity - 活动对象
* @returns {boolean}
*/
const isDeadlinePassed = (activity) => {
if (!activity.applyDeadline) return false
return moment().isAfter(moment(activity.applyDeadline))
}
/**
* 报名名额是否已满quota 为 0 表示不限名额)
* @param {object} activity - 活动对象
* @returns {boolean}
*/
const isQuotaFull = (activity) => {
if (!activity.quota) return false
return (Number(activity.appliedCount) || 0) >= Number(activity.quota)
}
/**
* 获取报名不可用时的置灰文案
* @param {object} activity - 活动对象
* @returns {string} 截止返回“报名已结束”,已满返回“名额已满”,否则空字符串
*/
const getDisabledText = (activity) => {
if (isDeadlinePassed(activity)) return '报名已结束'
if (isQuotaFull(activity)) return '名额已满'
return ''
}
/**
* 报名活动(跳转详情页)
* @param {object} activity - 活动对象
*/
const signupActivity = (activity) => {
uni.navigateTo({
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}`
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}&statusText=${activity.statusText || ''}`
})
}
@@ -327,7 +358,7 @@ const signupActivity = (activity) => {
*/
const cancelActivity = (activity) => {
uni.navigateTo({
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}`
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}&statusText=${activity.statusText || ''}`
})
}
@@ -337,7 +368,7 @@ const cancelActivity = (activity) => {
*/
const viewActivityDetail = (activity) => {
uni.navigateTo({
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}`
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}&statusText=${activity.statusText || ''}`
})
}
@@ -368,7 +399,21 @@ onLoad(() => {
*/
onUnload(() => {
uni.$off('refreshActivityList')
})
});
// 下拉刷新
onPullDownRefresh(() => {
if (activeTab.value === 'list') {
resetAndLoad();
} else {
resetAndLoadMy();
}
});
// 上拉加载更多
onReachBottom(() => {
loadMoreData();
});
/**
* 监听我的活动筛选条件变化
@@ -384,33 +429,14 @@ watch(() => myFilter.value, () => {
/* 页面容器 */
.container {
width: 100%;
height: 100vh;
min-height: 100vh;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 0;
}
/* 状态栏 - 自适应设备状态栏高度 */
.status-bar {
/* height: var(--status-bar-height); */
/* height: 46px; */
width: 100%;
flex-shrink: 0;
}
/* 顶部标题 固定 */
.top-bar {
width: 100%;
flex-shrink: 0;
background: #fff;
}
/* 滚动区域 */
.scroll-container {
flex: 1;
width: 100%;
overflow-y: auto;
padding: 0 30rpx;
box-sizing: border-box;
}
@@ -517,8 +543,8 @@ watch(() => myFilter.value, () => {
/* 活动状态标签 */
.status-badge {
position: absolute;
top: 33rpx;
left: 11rpx;
top: 0;
left: 0;
padding: 6rpx 20rpx;
font-size: 22rpx;
border-radius: 15rpx 0rpx 15rpx 0rpx;
@@ -555,6 +581,7 @@ watch(() => myFilter.value, () => {
.info-image-left {
width: 170rpx;
height: 220rpx;
position: relative;
}
.activity-title {
@@ -568,6 +595,7 @@ watch(() => myFilter.value, () => {
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
padding-right: 100rpx;
}
.activity-detail {
@@ -598,6 +626,9 @@ watch(() => myFilter.value, () => {
.detail-text-address {
flex: 1;
font-size: 22rpx;
word-break: break-all;
overflow-wrap: break-word;
white-space: pre-wrap;
}
/* 操作按钮 */
@@ -626,6 +657,12 @@ watch(() => myFilter.value, () => {
background-color: #0056cc;
}
.disabled-btn {
background-color: #f2f3f5;
color: #999;
text-align: center;
}
/* 加载更多 */
.load-more-container {
display: flex;

View File

@@ -43,14 +43,16 @@
<view class="info-item">
<view class="info-label">活动时间</view>
<view class="info-value">
<text>{{formatTime(activityConent.startTime)}} {{formatTime(activityConent.endTime)}}</text>
<view>{{formatTime(activityConent.startTime)}}</view>
<view> {{formatTime(activityConent.endTime)}}</view>
</view>
</view>
<view class="info-item">
<view class="info-label">签到时间</view>
<view class="info-value">
<text>{{formatTime(activityConent.signinStartTime)}} {{formatTime(activityConent.signinEndTime)}}</text>
<view>{{formatTime(activityConent.signinStartTime)}}</view>
<view v-if="activityConent.signinEndTime"> {{formatTime(activityConent.signinEndTime)}}</view>
</view>
</view>
@@ -61,7 +63,7 @@
<view class="info-item">
<view class="info-label">活动名额</view>
<view class="info-value">{{activityConent.quota}}</view>
<view class="info-value">{{ activityConent.quota == 0 ? '无限名额' : activityConent.quota + '人' }}</view>
</view>
<view class="info-item">
@@ -79,26 +81,23 @@
<view class="info-value">{{activityConent.contactPhone}}</view>
</view>
<view class="info-item">
<!-- <view class="info-item">
<view class="info-label">积分奖励</view>
<view class="info-value">{{activityConent.pointsReward}}</view>
</view>
</view> -->
</view>
<!-- 招募要求 -->
<view class="section">
<view class="section-title">招募要求</view>
<view class="section-content">
{{activityConent.requirements}}
</view>
<view class="section-content" v-html="formatRichText(activityConent.requirements)"></view>
</view>
<!-- 活动详情 -->
<view class="section" style="margin-bottom: 40rpx;">
<view class="section-title">活动详情</view>
<view class="section-content">
<view v-html="activityConent.content"></view>
<view></view>
<view class="section-content rich-content">
<view v-html="formatRichText(activityConent.content)"></view>
</view>
</view>
</view>
@@ -107,7 +106,7 @@
<view v-else class="status-content">
<view class="status-header">
<view class="status-title">
<image src="http://47.104.199.163:9090/images/activity/liveScene.png" style="width: 38rpx;height: 23rpx;"></image>
<image src="http://wuye.ckdzkj.com/images/activity/liveScene.png" style="width: 38rpx;height: 23rpx;"></image>
现场实况
</view>
<view class="status-count">{{ liveRecords.length }}</view>
@@ -119,10 +118,11 @@
<view class="record-user">
<view class="user-avatar">
<uni-icons type="person" size="20" color="#999"></uni-icons>
<!-- <image :src="record.userAvatar" mode="aspectFill"></image> -->
</view>
<view class="user-info">
<view class="user-name">微信用户</view>
<view class="record-time">{{ record.time }}</view>
<view class="user-name">{{record.userNickname || '微信用户'}}</view>
<view class="record-time">{{ record.createTime }}</view>
<view class="record-content">{{ record.content }}</view>
<!-- 图片展示 -->
<view v-if="record.images && record.images.length > 0" class="record-images">
@@ -149,9 +149,18 @@
<!-- 底部操作栏 -->
<view class="bottom-bar">
<!-- 活动已结束显示不可点击的结束按钮 -->
<button
v-if="isActivityEnded && activeTab === 'detail'"
class="action-btn ended-btn"
disabled
>
报名已结束
</button>
<!-- 未报名状态显示报名按钮 -->
<button
v-if="!hasSigned && activeTab === 'detail'"
v-if="!isActivityEnded && !hasSigned && activeTab === 'detail'"
class="action-btn signup-btn"
@tap="handleSignup"
>
@@ -160,15 +169,18 @@
<!-- 已报名状态显示取消报名按钮 -->
<button
v-if="hasSigned && activeTab === 'detail'"
v-if="hasSigned && !isActivityEnded && activeTab === 'detail'"
class="action-btn cancel-btn"
@tap="handleCancelSignup"
>
取消报名
</button>
<!-- 活动实况页显示添加实况按钮 -->
<button class="action-btn add-status-btn" v-if="hasSigned &&activeTab === 'status'" @click="liveClick">添加实况</button>
<!-- 活动实况页显示添加实况按钮活动开始之后结束之前显示已报名显示 -->
<button class="action-btn add-status-btn"
v-if="isActivityStarted && !isActivityEndedByTime && activeTab === 'status'&& hasSigned" @click="liveClick">
添加实况
</button>
</view>
<!-- 报名确认模态框 -->
@@ -178,6 +190,7 @@
<view class="modal-body">
<view>{{navTitle}}</view>
<view class="modal-tip">报名后请按时参加活动</view>
<view class="modal-tip-second"> 报名成功后在活动时间内不能取消报名</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel" @tap="hideSignupModal">取消</view>
@@ -192,7 +205,7 @@
<view class="modal-header">取消报名</view>
<view class="modal-body">
<view>{{navTitle}}</view>
<view class="modal-tip">取消后如需参加重新报名</view>
<view class="modal-tip">取消后如需参加请在规定时间内重新报名</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel" @tap="hideCancelModal">再想想</view>
@@ -209,7 +222,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import {onLoad,onUnload} from '@dcloudio/uni-app'
import moment from 'moment'
import {getActivityDetails,confirmResident,cancelActivity,getActivityLive} from '../api/api.js'
@@ -225,25 +238,30 @@ const newStatusContent = ref('')
const navTitle = ref('')
const curActivityId = ref('') // 当前活动的id
const activityConent = ref({}) // 获取详情数据
// 实况记录数据
const liveRecords = ref([
// {
// id: 1,
// user: '微信用户',
// time: '2023-08-20 16:00:02',
// content: '参与活动打卡',
// images: []
// },
// {
// id: 2,
// user: '微信用户',
// time: '2023-08-20 16:20:02',
// content: '目前现场的互动氛围很好',
// images: ['http://47.104.199.163:9090/images/logo.png', 'http://47.104.199.163:9090/images/repair/house1.png']
// }
])
const liveRecords = ref([])
// 判断活动是否已结束
const isActivityEnded = computed(() => {
if (statusTextFromParam.value === '已结束') return true
// 报名截止时间已过
if (activityConent.value.applyDeadline && moment().isAfter(moment(activityConent.value.applyDeadline))) return true
// 活动结束时间已过
if (activityConent.value.endTime && moment().isAfter(moment(activityConent.value.endTime))) return true
return false
})
// 判断活动是否已开始(开始时间已到)
const isActivityStarted = computed(() => {
if (!activityConent.value.startTime) return false
return moment().isSameOrAfter(moment(activityConent.value.startTime))
})
// 判断活动是否已到结束时间
const isActivityEndedByTime = computed(() => {
if (!activityConent.value.endTime) return false
return moment().isAfter(moment(activityConent.value.endTime))
})
// ========方法=====
// 时间格式化函数
@@ -252,17 +270,33 @@ const formatTime = (timeStr) => {
return moment(timeStr).format('YYYY-MM-DD HH:mm')
}
// 富文本处理:给 img 注入内联样式
const formatRichText = (html) => {
if (!html) return ''
return html.replace(/<img\b([^>]*)>/gi, (match, attrs) => {
// 去掉尾部自闭合斜杠,避免 style 追加到斜杠之后
const inner = attrs.replace(/\/\s*$/, '')
const imgStyle = 'max-width:100%;height:auto;display:block;'
if (/style\s*=/i.test(inner)) {
// 已有 style在 style 值开头注入
return `<img${inner.replace(/(style\s*=\s*["'])/i, `$1${imgStyle}`)}>`
}
return `<img${inner} style="${imgStyle}">`
})
}
// ---接收参数---
const statusTextFromParam = ref('') // 从列表页传入的活动状态文本
onLoad((option) =>{
navTitle.value = option?.title
curActivityId.value = option?.id
// 处理applied参数可能是字符串类型需要转布尔
hasSigned.value = option?.applied === 'true' || option?.applied === true
statusTextFromParam.value = option?.statusText || ''
hasSigned.value = option?.applied === 'true'
// console.log('hasSigned.value',hasSigned.value)
// 2. 初始化数据
if (curActivityId.value) {
getActivityDetailsData(curActivityId.value) // 获取活动详情
getLiveActivityList() // 获取实况列表
// getLiveActivityList() // 获取实况列表
}
// 3. 监听实况列表刷新事件
@@ -274,7 +308,8 @@ onLoad((option) =>{
// 获取列表详情
const getActivityDetailsData = async (id) =>{
try{
const res = await getActivityDetails(id)
let params = {villageId:uni.getStorageSync('currentVillageId')}
const res = await getActivityDetails(id,params)
if(res.code === 200){
activityConent.value = res.data || {}
}else{
@@ -293,9 +328,13 @@ const getActivityDetailsData = async (id) =>{
}
}
// 切换选项卡
const switchTab = (tab) => {
activeTab.value = tab
if (tab === 'status') {
getLiveActivityList()
}
}
// 返回上一页
@@ -322,7 +361,8 @@ const confirmSignup = () => {
// 确认报名接口
const confirmRegist = async () =>{
try{
const res = await confirmResident(curActivityId.value)
let params = {villageId:uni.getStorageSync('currentVillageId')}
const res = await confirmResident(curActivityId.value,params)
if(res.code === 200){
hasSigned.value = true // 报名成功后更新状态
uni.showToast({
@@ -344,7 +384,7 @@ const confirmRegist = async () =>{
}
}catch(err){
uni.showToast({
title: "网络异常",
title: err.msg ||"网络异常",
icon:"none"
})
// console.error('报名失败:', err)
@@ -363,15 +403,14 @@ const hideCancelModal = () => {
// 确认取消报名
const confirmCancel = () => {
hasSigned.value = false // 取消报名后更新状态
cancelActivityData()
hideCancelModal()
// showToastMessage('已取消报名')
}
// 确认取消报名接口
const cancelActivityData = async () =>{
try{
const res = await cancelActivity(curActivityId.value)
let params = {villageId:uni.getStorageSync('currentVillageId')}
const res = await cancelActivity(curActivityId.value,params)
if(res.code === 200){
hasSigned.value = false // 报名成功后更新状态
uni.showToast({
@@ -393,7 +432,7 @@ const cancelActivityData = async () =>{
}
}catch(err){
uni.showToast({
title: "网络异常",
title: err.msg ||"网络异常",
icon:"none"
})
}
@@ -403,7 +442,8 @@ const cancelActivityData = async () =>{
// -----获取活动实况列表----
const getLiveActivityList = async () =>{
try{
const res = await getActivityLive(curActivityId.value)
let params = {villageId:uni.getStorageSync('currentVillageId')}
const res = await getActivityLive(curActivityId.value,params)
if(res.code === 200){
liveRecords.value = res.rows || []
}else{
@@ -415,7 +455,7 @@ const getLiveActivityList = async () =>{
}
}catch(err){
uni.showToast({
title: "网络异常",
title: err.msg ||"网络异常",
icon:"none"
})
}
@@ -567,6 +607,8 @@ const showToastMessage = (message) => {
font-weight: 500;
line-height: 1.4;
text-align: right;
white-space: pre-wrap;
word-break: break-all;
}
/* 区块样式 */
@@ -587,8 +629,11 @@ const showToastMessage = (message) => {
.section-content {
font-size: 28rpx;
white-space: pre-wrap;
word-break: break-all;
color: #666;
line-height: 1.6;
overflow-x: hidden;
}
.section-content view {
@@ -663,6 +708,8 @@ const showToastMessage = (message) => {
display: flex;
flex-direction: column;
justify-content: flex-start;
flex: 1;
min-width: 0;
}
.user-name {
@@ -682,6 +729,9 @@ const showToastMessage = (message) => {
color: #333;
line-height: 1.5;
margin: 25rpx 0rpx;
word-break: break-all;
overflow-wrap: break-word;
white-space: pre-wrap;
}
/* 实况图片 */
@@ -757,6 +807,12 @@ const showToastMessage = (message) => {
box-shadow: 0 4rpx 20rpx rgba(0, 122, 255, 0.2);
}
.ended-btn {
background: linear-gradient(135deg, #ccc 0%, #999 100%);
color: #fff;
opacity: 0.7;
}
/* 模态框 */
.modal {
display: none;
@@ -818,6 +874,11 @@ const showToastMessage = (message) => {
color: #999;
margin-top: 20rpx;
}
.modal-tip-second{
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.modal-footer {
display: flex;
@@ -886,4 +947,26 @@ const showToastMessage = (message) => {
margin: 0 auto;
}
}
</style>
<style lang="scss">
/* 富文本v-html内图片自适应 — 非 scoped否则穿透不到 v-html 内容 */
.section-content {
img {
max-width: 100% !important;
height: auto !important;
display: block;
}
}
.rich-content {
white-space: normal !important;
word-break: break-all !important;
overflow-wrap: break-word !important;
overflow: hidden;
/* 富文本内嵌的所有元素不超出容器 */
* {
max-width: 100% !important;
box-sizing: border-box !important;
}
}
</style>

View File

@@ -22,7 +22,7 @@
<!-- 已上传图片 -->
<view class="uploaded-img" v-for="(img, index) in imgList" :key="index">
<image :src="img" mode="aspectFill" class="upload-img"></image>
<image src="http://47.104.199.163:9090/images/imgs/delete.png" class="upload-delete" @click="deleteImage(index)"></image>
<image src="https://wuye.ckdzkj.com/images/imgs/delete.png" class="upload-delete" @click="deleteImage(index)"></image>
<!-- <text class="del-img" @click="deleteImg(index)">×</text> -->
</view>
<!-- 上传按钮最多3张 -->
@@ -48,7 +48,7 @@
<!-- 底部提交按钮 -->
<view class="submit-btn-area">
<button class="submit-btn" @click="submitStatus">提交</button>
<button class="submit-btn" :disabled="submitting" @click="submitStatus">{{ submitting ? '提交中...' : '提交' }}</button>
</view>
</view>
</template>
@@ -58,8 +58,8 @@ import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { addActivityLive, signinActivityLive, uploadFile } from '../api/api.js';
// 高德地图 Web API Key用于逆地址解析,需要去 https://lbs.amap.com 申请免费 Key
const AMAP_KEY = 'f362f595be12ab7d84bd502b48b30d2a';
// 地图 Web API Key用于逆地址解析
const TIANDITU_KEY = 'b0201708e26cb56e514d74d1686f6f91';
// 活动ID
const activityId = ref('');
@@ -67,6 +67,8 @@ const activityId = ref('');
const statusDesc = ref('');
// 输入字符长度
const currentLength = ref(0);
// 提交状态锁(防重复点击)
const submitting = ref(false);
// 图片列表(本地临时路径)
const imgList = ref([]);
// 定位信息
@@ -78,6 +80,8 @@ const latitude = ref('');
// 接收活动ID
onLoad((option) => {
activityId.value = option?.id || '';
// 进入页面自动定位,获取不到时可通过“重新定位”重试
reLocate();
});
// 监听输入框字数变化
@@ -85,20 +89,20 @@ const handleTextInput = (e) => {
currentLength.value = e.detail.value.length;
};
// 逆地址解析(坐标转地址)
// 逆地址解析(坐标转地址)- 天地图 API
const reverseGeocode = async (lng, lat) => {
if (!AMAP_KEY) return null;
if (!TIANDITU_KEY) return null;
try {
const res = await uni.request({
url: 'https://restapi.amap.com/v3/geocode/regeo',
url: 'http://api.tianditu.gov.cn/geocoder',
data: {
key: AMAP_KEY,
location: `${lng},${lat}`,
extensions: 'base'
type: 'rgc',
postStr: JSON.stringify({ lon: lng, lat: lat, ver: 1 }),
tk: TIANDITU_KEY
}
});
if (res.data?.status === '1' && res.data.regeocode?.formatted_address) {
return res.data.regeocode.formatted_address;
if (res.data?.status === '0' && res.data.result?.formatted_address) {
return res.data.result.formatted_address;
}
return null;
} catch (e) {
@@ -117,7 +121,6 @@ const chooseImg = () => {
imgList.value = [...imgList.value, ...res.tempFilePaths];
},
fail: (err) => {
uni.showToast({ title: '选择图片失败', icon: 'none' });
console.error('选择图片失败:', err);
}
});
@@ -128,66 +131,41 @@ const chooseImg = () => {
imgList.value.splice(index, 1);
};
// 检查 App 端定位权限Android/iOS
const checkAppLocationPermission = () => {
// 跳转系统设置App 端Android/iOSH5/小程序另有处理
const openAppSettings = () => {
// #ifdef APP-PLUS
try {
const platform = uni.getSystemInfoSync().platform;
if (platform === 'android') {
const main = plus.android.runtimeMainActivity();
const PackageManager = plus.android.importClass('android.content.pm.PackageManager');
return main.checkSelfPermission('android.permission.ACCESS_FINE_LOCATION') === PackageManager.PERMISSION_GRANTED;
}
if (platform === 'ios') {
const CLAuthorizationStatus = plus.ios.importClass('CLAuthorizationStatus');
const CLLocationManager = plus.ios.importClass('CLLocationManager');
return CLLocationManager.authorizationStatus() !== 2; // 2 = denied
const Intent = plus.android.importClass('android.content.Intent');
const Settings = plus.android.importClass('android.provider.Settings');
const Uri = plus.android.importClass('android.net.Uri');
const mainActivity = plus.android.runtimeMainActivity();
const intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse('package:' + mainActivity.getPackageName()));
mainActivity.startActivity(intent);
} else if (platform === 'ios') {
const UIApplication = plus.ios.importClass('UIApplication');
const NSURL = plus.ios.importClass('NSURL');
UIApplication.sharedApplication().openURL(NSURL.URLWithString('app-settings:'));
}
} catch (e) {
console.error('检查权限失败', e);
console.error('跳转系统设置异常:', e);
uni.showToast({ title: '跳转失败,请手动前往系统设置开启定位权限', icon: 'none' });
}
// #endif
return true; // 非 App 端默认通过
};
// 跳转系统设置App 端)
const openAppSettings = () => {
// #ifdef APP-PLUS
const platform = uni.getSystemInfoSync().platform;
if (platform === 'android') {
const Intent = plus.android.importClass('android.content.Intent');
const Settings = plus.android.importClass('android.provider.Settings');
const Uri = plus.android.importClass('android.net.Uri');
const intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse('package:' + plus.runtime.packageName));
plus.android.runtimeMainActivity().startActivity(intent);
} else if (platform === 'ios') {
const UIApplication = plus.ios.importClass('UIApplication');
const app = UIApplication.sharedApplication();
const NSURL = plus.ios.importClass('NSURL');
const url = NSURL.URLWithString('app-settings:');
app.openURL(url);
}
// #ifdef H5
uni.showToast({ title: '请手动在浏览器设置中允许定位权限', icon: 'none' });
// #endif
// #ifdef MP-WEIXIN
uni.openSetting({});
// #endif
};
// 重新定位(获取当前位置 + 逆地址解析)
const reLocate = () => {
// App 端先检查权限,无权限直接引导设置
// #ifdef APP-PLUS
if (!checkAppLocationPermission()) {
uni.showModal({
title: '需要定位权限',
content: '请前往系统设置中允许本应用使用定位权限',
confirmText: '去设置',
success: (res) => {
if (res.confirm) openAppSettings();
}
});
return;
}
// #endif
uni.showLoading({ title: '正在定位...', mask: true });
uni.getLocation({
@@ -205,7 +183,7 @@ const reLocate = () => {
uni.hideLoading();
const errMsgStr = err?.errMsg || err?.message || JSON.stringify(err);
console.error('定位失败:', err);
if (err?.code === -22 || /权限|auth|deny|permission/i.test(errMsgStr)) {
if (err?.code === -22 || /权限|授权|拒绝|auth|deny|permission/i.test(errMsgStr)) {
uni.showModal({
title: '需要定位权限',
content: '请允许获取位置信息以显示具体地点',
@@ -229,37 +207,42 @@ const uploadAllImages = async () => {
// 提交实况
const submitStatus = async () => {
if (submitting.value) return;
if (!statusDesc.value.trim()) {
uni.showToast({ title: '请输入实况描述', icon: 'none' });
return;
}
// if (!activityId.value) {
// uni.showToast({ title: '活动ID缺失', icon: 'none' });
// if (!location.value) {
// uni.showToast({ title: '请先签到', icon: 'none' });
// return;
// }
submitting.value = true;
uni.showLoading({ title: '提交中...', mask: true });
try {
// 先上传图片
const imageUrls = await uploadAllImages();
// 同时签到
await signinActivityLive(activityId.value, {
location: location.value,
longitude: longitude.value,
latitude: latitude.value,
villageId:uni.getStorageSync('currentVillageId')
});
// 提交实况
const liveData = {
content: statusDesc.value.trim(),
images: imageUrls,
location: location.value,
longitude: longitude.value,
latitude: latitude.value
latitude: latitude.value,
villageId:uni.getStorageSync('currentVillageId')
};
console.log('ssasss',uni.getStorageSync('currentVillageId'))
console.log("xxx",activityId.value)
await addActivityLive(activityId.value, liveData);
// 同时签到
await signinActivityLive(activityId.value, {
location: location.value,
longitude: longitude.value,
latitude: latitude.value
});
uni.hideLoading();
uni.showToast({ title: '添加成功', icon: 'success' });
@@ -268,8 +251,9 @@ const submitStatus = async () => {
uni.navigateBack();
}, 1500);
} catch (err) {
submitting.value = false;
uni.hideLoading();
uni.showToast({ title: err || '提交失败', icon: 'none' });
uni.showToast({ title: err.msg || '提交失败', icon: 'none' });
}
};
</script>
@@ -391,12 +375,16 @@ const submitStatus = async () => {
.location-text {
font-size: 14px;
color: #333;
flex: 1;
word-break: break-all;
margin-right: 10px;
}
.re-locate {
font-size: 14px;
color: #007aff;
display: flex;
align-items: center;
flex-shrink: 0;
}
.re-icon{
color: #007aff;

View File

@@ -27,8 +27,8 @@ export const getBannarList = (data) =>{
return get('/resident/banner',data)
}
// 首页最新公告
export const getNoticeLatest = () =>{
return get('/api/resident/notice/latest')
export const getNoticeLatest = (data) =>{
return get('/api/resident/notice/latest', data)
}
// 查询已认证的小区
@@ -44,8 +44,12 @@ export const getVillageSwitch = (data)=>{
return post('/api/resident/village/switch',data)
}
// 首页最新活动列表
export const getActivitylatest = () =>{
return get('/api/resident/activity/latest')
export const getActivitylatest = (data) =>{
return get('/api/resident/activity/latest',data)
}
// 重新加载用户信息
export const getReloadUserInfo = () =>{
return get('/api/resident/auth/reloadUserInfo')
}
// ========公告列表======
@@ -72,16 +76,16 @@ export const getActivityList = (data) =>{
return get("/api/resident/activity/list",data)
}
// 获取活动列表详情
export const getActivityDetails = (id) =>{
return get(`/api/resident/activity/${id}`)
export const getActivityDetails = (id,data) =>{
return get(`/api/resident/activity/${id}`,data)
}
// 确认报名
export const confirmResident = (id) =>{
return post(`/api/resident/activity/${id}/apply`)
export const confirmResident = (id,data) =>{
return post(`/api/resident/activity/${id}/apply`,data)
}
// 取消报名
export const cancelActivity = (id) =>{
return post(`/api/resident/activity/${id}/cancel`)
export const cancelActivity = (id,data) =>{
return post(`/api/resident/activity/${id}/cancel`,data)
}
// =========我的活动========
export const getMyActivityList = (data) =>{
@@ -89,8 +93,8 @@ export const getMyActivityList = (data) =>{
}
// ===========活动实况============
// 活动实况列表
export const getActivityLive = (id) =>{
return get(`/api/resident/activity/${id}/live`)
export const getActivityLive = (id,data) =>{
return get(`/api/resident/activity/live/${id}`,data)
}
// 添加实况
export const addActivityLive = (id, data) =>{
@@ -115,7 +119,7 @@ export const getQueryRepair = (data) =>{
}
// 获取房屋列表
export const getHouseList = (data) =>{
return get('/api/resident/my/house/list',data)
return get('/repairOrder/certifiedHouseList',data)
}
// 报修详情
export const getRepairDetail = (data) =>{

View File

@@ -57,6 +57,12 @@ export const deleteMyHouse = (data) => {
export const setMyHouseDefault = (data) => {
return post(`/api/resident/my/house/default`, data)
}
// 添加房屋与业主关系-下拉数据
export const getHouseDataList = (data) => {
return get(`/api/resident/my/house/dataList`, data)
}
// 获取当前用户登录信息
export const getUserProfile = (data) => {
return get(`/api/resident/user/profile`, data)
@@ -83,6 +89,10 @@ export const authChangePassword = (data) => {
export const authLogout = (data) => {
return post(`/api/resident/auth/logout`, data)
}
// 注销账号
export const authDeleteAccount = (data) => {
return post('/api/resident/auth/cancelAccount', data)
}
// 编辑房屋
export const upDateMyHouse = (data) => {
return putMethod(`/api/resident/my/house/${data.id}`, data)
@@ -95,17 +105,17 @@ export const getQuestionnaireList = (data) => {
return get('/api/resident/questionnaire/list',data)
}
// 问卷详情
export const getQuestionnaireDetail = (data) => {
return get('/api/resident/questionnaire/myAnswers',data)
export const getQuestionnaireDetail = (id,data) => {
return get(`/api/resident/questionnaire/${id}`,data)
}
// 提交问卷
export const submitQuestionnair = (id,data) => {
return post(`/api/resident/questionnaire/${id}/submit`, data)
return post(`/api/resident/questionnaire/submit/${id}`, data)
}
// =====投诉意见=======
// 获取投诉类型
export const getComplainType = () => {
return get('/complain/getComplainType')
export const getComplainType = (data) => {
return post('/complain/getComplainType',data)
}
// 获取投诉列表
export const getComplainList = (data) => {
@@ -134,6 +144,22 @@ export const getTopUpSelectByResident = (data) => {
console.log(data);
return get(`/api/topUp/selectByResident/${data.residentUserId}`, data)
}
// 账单明细列表
export const getTopUpDetails = (data) => {
return post(`/api/topUp/billDetails/list`, data)
}
// 账单按月汇总
export const getTopUpDetailsByMonth = (data) => {
return post(`/api/topUp/billDetails/listByMonth`, data)
}
// 绑定设备
export const getTopUpBindDevice = (data) => {
return post(`/api/topUp/bindDevice/${data.deviceCode}/${data.residentUserId}/${data.deviceType}`, data)
@@ -151,6 +177,10 @@ export const getopenIdByCode = (data) => {
export const getUnionPayTn = (data) => {
return post('/api/topUp/unionPay/getTn', data)
}
// 获取日照银行小程序拉起参数
export const getBankMiniProgramParams = (data) => {
return post('/api/pay/yidang/create', data)
}
// 缴费记录查询
export const getTopupRecordWithStat = (data) => {
return get(
@@ -222,10 +252,14 @@ export const getParkingDetail = (data) => {
export const rebindCar = (data) => {
return post(`/ResidentCarAreaManage/rebind`, data)
}
// 获取业主列表
export const getOwnerList = (data) => {
return get('/api/resident/car/bind/ResidentList', data)
}
// 查询版本信息
export const getAppVersion = (platform, channel) => {
return get(`/api/app/version/${platform}/${channel}`)
export const getAppVersion = (platform, channel,appChannel) => {
return get(`/api/app/version/${platform}/${channel}/${appChannel}`)
}
// 检查是否需要更新
export const checkAppUpdate = (data) => {
@@ -235,3 +269,45 @@ export const checkAppUpdate = (data) => {
export const postUpdateLog = (data) => {
return post(`/api/app/updateLog`, data)
}
// ===========巡检记录========
// 列表
export const getInspectionList = (data) =>{
return get('/api/resident/inspection/list',data)
}
// 详情
export const inspectionDetails = (data) =>{
return get(`/api/resident/inspection/${data.id}`,data)
}
// =======公区收益=========
// 列表
export const getRevenueList = (data) =>{
return get('/api/resident/revenueNotice/list',data)
}
// 详情
export const revenueDetails = (data) =>{
return get(`/api/resident/revenueNotice/${data.id}`)
}
// =======积分相关=========
// 获取积分余额
export const getPointsBalance = (data) => {
return get('/api/resident/points/balance', data)
}
// 获取积分商城商品列表
export const getPointsMallList = (data) => {
return get('/api/resident/points/mall/list', data)
}
// 积分兑换
export const postPointsExchange = (data) => {
return post('/api/resident/points/mall/exchange', data)
}
// 获取积分明细列表
export const getPointsRecordList = (data) => {
return get('/api/resident/points/record/list', data)
}
// 获取积分兑换订单列表
export const getPointsOrderList = (data) => {
return get('/api/resident/points/order/list', data)
}

View File

@@ -1,28 +1,36 @@
import { navigateToLogin } from '@/common/checkLogin.js'
let baseUrl = '';
if (process.env.NODE_ENV === 'development') {
baseUrl = 'http://192.168.0.239:8080'; // 开发环境
baseUrl = 'http://192.168.1.6:8080'; // 开发环境
// baseUrl = 'http://192.168.1.222:8080';
} else {
baseUrl = 'http://192.168.0.224:8080'; // 生产环境
baseUrl = 'https://wuyeapi.ckdzkj.com'; // 生产环境
}
export default function request(url, data = {}, method = "GET") {
return new Promise((resolve, reject) => {
uni.showLoading({
title: '请求中...',
mask: true
});
// uni.showLoading({
// title: '请求中...',
// mask: true
// });
const token = uni.getStorageSync("token") || "";
const header = {
"clientid": "resident",
'content-type': 'application/json'
};
if (token) {
header["Authorization"] = "Bearer " + token;
}
uni.request({
url: baseUrl + url,
data: data,
method: method,
header: {
"Authorization": "Bearer " + (uni.getStorageSync("token") || ''),
"clientid": "resident",
'content-type': 'application/json'
},
header,
success: (res) => {
uni.hideLoading();
// uni.hideLoading();
// HTTP 失败
if (res.statusCode !== 200) {
@@ -32,13 +40,12 @@ export default function request(url, data = {}, method = "GET") {
const resData = res.data || {};
// token 过期
// token 过期:仅在有 token 且过期时跳转登录页(无 token 用户正常浏览首页,由点击拦截处理)
if (resData.code === 401) {
uni.showToast({ title: '登录过期,请重新登录', icon: 'none' });
uni.clearStorageSync('token');
setTimeout(() => {
uni.reLaunch({ url: '/pageSubPack/loginSub/pwd-login' });
}, 1500);
if (token) {
uni.clearStorageSync('token');
navigateToLogin({ method: 'reLaunch' });
}
reject({ msg: '登录过期', code: 401 });
return;
}
@@ -53,7 +60,7 @@ export default function request(url, data = {}, method = "GET") {
}
},
fail: (err) => {
uni.hideLoading();
// uni.hideLoading();
reject({ msg: '网络异常', code: -1, raw: err });
}
});
@@ -68,14 +75,19 @@ export const putMethod = (url, data) => request(url, data, 'PUT');
// 上传
export const upload = (filePath, url) => {
return new Promise((resolve, reject) => {
const t = uni.getStorageSync("token") || "";
const uploadHeader = {
"clientid": "resident"
};
if (t) {
uploadHeader["Authorization"] = "Bearer " + t;
}
uni.uploadFile({
url: baseUrl + url,
filePath: filePath,
name: 'file',
header: {
"Authorization": "Bearer " + (uni.getStorageSync("token") || ''),
"clientid": "resident"
},
header: uploadHeader,
success: (res) => {
try {
const data = JSON.parse(res.data);

View File

@@ -1,5 +1,6 @@
// 服务器地址(与 request.js 保持一致)
const baseUrl = 'http://192.168.0.239:8080';
const baseUrl = 'https://wuyeapi.ckdzkj.com';
// const baseUrl = "http://192.168.1.6:8080"
/**
* 图片上传 Hook

View File

@@ -0,0 +1,234 @@
<template>
<view class="page">
<scroll-view scroll-y class="scroll">
<!-- 加载中 -->
<view class="loading-wrap" v-if="loading">
<uni-load-more status="loading" :content-text="{ contentdown: '上拉加载更多', contentrefresh: '加载中...', contentnomore: '没有更多了' }" />
</view>
<template v-if="!loading && detail">
<!-- 第一块基本信息 -->
<view class="card">
<view class="row-title">
<text>{{ detail.locationName }}</text>
<text class="status">{{ detail.status }}</text>
</view>
<view class="text-item">巡检项目{{ detail.inspectionType }}</view>
<view class="text-item">巡检时间{{ detail.taskTime }}</view>
<view class="text-item">巡检小区{{ detail.villageName }}</view>
<view class="text-item">巡检路线{{ detail.routeName }}</view>
<view class="text-item">巡检点{{ detail.pointNames }}</view>
</view>
<!-- 第三块巡检明细列表 -->
<view class="card" v-for="(sub, idx) in detail.details" :key="sub.id">
<view class="card-subtitle">巡检结果</view>
<view class="text-item">巡检点{{ sub.pointName }}</view>
<view class="text-item">巡检人{{ detail.inspectorName }}</view>
<view class="text-item">手机号码{{ detail.phoneNumber }}</view>
<view class="text-item">巡检时间{{ sub.checkTime }}</view>
<view class="text-item">巡检状态{{ getXunJianStatus(sub.status) }}</view>
<view class="text-item">巡检结果{{ sub.checkResult }}</view>
<!-- <view class="text-item" v-if="sub.remark">备注{{ sub.remark }}</view> -->
<view class="text-item" v-if="sub.photos">巡检照片</view>
<view class="img-wrap" v-if="sub.photos">
<view class="img-box" v-for="(img, i) in sub.photos.split(',')" :key="i" @click="previewImage(img)">
<image class="img" :src="img" mode="aspectFill"></image>
</view>
</view>
</view>
</template>
<!-- 加载失败 -->
<view class="error-wrap" v-if="!loading && !detail">
<text class="empty-text">数据加载失败</text>
<view class="retry-btn" @click="fetchDetail">点击重试</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { inspectionDetails } from '../api/apiSub.js'
const statusBarHeight = ref(uni.getSystemInfoSync().statusBarHeight || 0)
const loading = ref(false)
const detail = ref(null)
let recordId = ''
let queryDate = ''
onLoad((options) => {
if (options.id) {
recordId = options.id
queryDate = options.date || ''
fetchDetail()
}
})
// 巡检状态字段映射 0正常1异常
function getXunJianStatus(status){
const staMap = {
"0":'正常',
"1":"异常"
}
return staMap[status]
}
const fetchDetail = async () => {
if (!recordId) return
loading.value = true
detail.value = null
try {
const res = await inspectionDetails({ id: recordId, date: queryDate })
if (res.code === 200) {
detail.value = res.data
} else {
uni.showToast({ title: res.msg || '获取详情失败', icon: 'none' })
}
} catch (err) {
console.error('获取巡检详情失败', err)
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
} finally {
loading.value = false
}
}
const goBack = () => {
uni.navigateBack()
}
const previewImage = (url) => {
if (!url) return
// 收集当前所有可预览图片
const allPhotos = []
if (detail.value.photos) allPhotos.push(...detail.value.photos)
if (detail.value.details) {
detail.value.details.forEach(d => {
if (d.photos) allPhotos.push(...d.photos.split(','))
})
}
uni.previewImage({
current: url,
urls: allPhotos
})
}
</script>
<style scoped>
page {
background-color: #f7f8fa;
}
.status-bar {
width: 100%;
flex-shrink: 0;
}
.page {
height: 100vh;
display: flex;
flex-direction: column;
}
.scroll {
flex: 1;
padding: 24rpx;
box-sizing: border-box;
}
.card {
background: #fff;
border-radius: 20rpx;
padding: 36rpx;
margin-bottom: 24rpx;
}
.row-title {
display: flex;
justify-content: space-between;
font-size: 32rpx;
color: #222;
margin-bottom: 24rpx;
}
.status {
font-size: 24rpx;
color: #555;
}
.card-subtitle {
font-size: 32rpx;
font-weight: 500;
color: #222;
margin-bottom: 24rpx;
}
.text-item {
font-size: 28rpx;
color: #333;
line-height: 2;
}
.result-desc {
font-size: 28rpx;
color: #333;
display: flex;
flex-direction: column;
line-height: 1.8;
margin: 12rpx 0;
}
/* 图片容器 flex-wrap换行 */
.img-wrap {
margin-top: 16rpx;
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.img-box {
width: 140rpx;
height: 140rpx;
background-color: #d8d9db;
border-radius: 8rpx;
overflow: hidden;
}
.img {
width: 100%;
height: 100%;
}
.empty-img-tip {
font-size: 26rpx;
color: #999;
}
.loading-wrap {
padding-top: 200rpx;
}
.error-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 200rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
.retry-btn {
margin-top: 24rpx;
padding: 12rpx 48rpx;
border: 2rpx solid #2F77FD;
border-radius: 8rpx;
color: #2F77FD;
font-size: 28rpx;
}
</style>

View File

@@ -0,0 +1,303 @@
<template>
<view class="contentStyle">
<view class="content-wrap">
<!-- 时间选择器 -->
<view class="timebox">
<view class="daybox flex ">
<uni-datetime-picker type="date" :clear-icon="false" @change="handleChangeTime" v-model="datetime">
<view class="inner-date-box">
<view class="day">{{ showDate }}</view>
<view class="searchiconbox">
<view class="arrow"> </view>
<view class="arrow1"> </view>
</view>
</view>
</uni-datetime-picker>
</view>
</view>
<!-- 巡检列表 -->
<view class="scroll-view">
<view class="list-container" v-if="listData.length > 0">
<view class="card-item" v-for="(item, index) in listData" :key="index" @click="goToInspectionDetail(item)">
<view class="card-head">
<text class="address">{{ item.locationName }}</text>
<text class="status">{{ item.status }}</text>
</view>
<view class="tag-label">{{ item.inspectionType }}</view>
<view class="text-row">巡检时间{{ item.taskTime }}</view>
<view class="text-row">巡检人{{ item.inspectorName }}</view>
</view>
</view>
<!-- 暂无数据 -->
<view class="empty-wrap" v-if="!loading && listData.length === 0">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {ref} from 'vue'
import {onLoad,onReachBottom,onPullDownRefresh,onBackPress} from '@dcloudio/uni-app'
import {getInspectionList} from '../api/apiSub.js'
import moment from 'moment' // 新增
// ---------- 基础状态 ----------
const datetime = ref('')
const showDate = ref('选择日期')
const listData = ref([])
const statusBarHeight = ref(uni.getSystemInfoSync().statusBarHeight || 0)
// ---------- 分页状态 ----------
const pageNum = ref(1)
const pageSize = ref(10)
const total = ref(0)
const loading = ref(false)
const noMore = ref(false)
// ---------- 初始化 ----------
onLoad(() => {
// 使用 moment 格式化今天
const today = moment().format('YYYY-MM-DD')
datetime.value = today
showDate.value = moment().format('MM-DD') // 显示 MM-DD
fetchList(true)
})
// ---------- 获取列表 ----------
const fetchList = async (reset = false) => {
if (loading.value) return
if (!reset && noMore.value) return
loading.value = true
try {
const params = {
date: datetime.value, // 已经是 YYYY-MM-DD
pageNum: reset ? 1 : pageNum.value,
pageSize: pageSize.value
}
const res = await getInspectionList(params)
if (res.code === 200) {
const rows = res.rows
total.value = res.total || 0
if (reset) {
listData.value = rows
pageNum.value = 1
noMore.value = rows.length < pageSize.value
} else {
listData.value = [...listData.value, ...rows]
noMore.value = rows.length < pageSize.value || listData.value.length >= total.value
}
if (!reset && rows.length > 0) {
pageNum.value += 1
}
} else {
uni.showToast({ title: res.msg || '获取列表失败', icon: 'none' })
}
} catch (err) {
console.error('获取巡检列表失败', err)
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
} finally {
loading.value = false
uni.stopPullDownRefresh()
}
}
// ---------- 日期变更(使用 moment ----------
const handleChangeTime = (e) => {
if (e) {
showDate.value = moment(e).format('MM-DD')
datetime.value = e
} else {
showDate.value = '选择日期'
datetime.value = ''
}
// 重置分页并刷新
pageNum.value = 1
noMore.value = false
fetchList(true)
}
// ---------- 上拉加载更多 ----------
onReachBottom(() => {
if (!noMore.value && !loading.value) {
fetchList(false)
}
})
// ---------- 下拉刷新 ----------
onPullDownRefresh(() => {
pageNum.value = 1
noMore.value = false
fetchList(true)
})
// ---------- 页面返回拦截 ----------
onBackPress(() => {
const pages = getCurrentPages()
if (pages.length > 1) {
return false
}
uni.switchTab({ url: '/pages/serviceIndex/serviceIndex' })
return true
})
// ---------- 跳转详情 ----------
const goToInspectionDetail = (item) => {
uni.navigateTo({
url: `/pageSubPack/inspection-revenue/inspection-details?id=${item.id}&date=${datetime.value}`
})
}
</script>
<style lang="scss" scoped>
.contentStyle {
// display: flex;
// flex-direction: column;
min-height: 100vh;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.content-wrap {
flex: 1;
padding: 20rpx 40rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
.timebox {
width: 188rpx;
height: 48rpx;
background: #ffffff;
border-radius: 24rpx;
text-align: center;
line-height: 48rpx;
font-weight: 400;
font-size: 28rpx;
color: #999;
margin-bottom: 20rpx;
.daybox {
height: 100%;
box-sizing: border-box;
position: relative;
.inner-date-box {
display: flex;
align-items: center;
width: 100%;
height: 100%;
}
.day {
flex: 1;
font-size: 28rpx;
text-align: left;
padding-left: 15rpx;
}
.searchiconbox {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 38rpx;
height: 48rpx;
padding-right: 10rpx;
flex-shrink: 0;
margin-left: auto;
.arrow,
.arrow1 {
border: 1rpx solid #ccc;
width: 8rpx;
height: 8rpx;
}
.arrow {
border-right-color: transparent;
border-top-color: transparent;
transform: rotateZ(135deg);
}
.arrow1 {
border-left-color: transparent;
border-bottom-color: transparent;
transform: rotateZ(135deg);
}
}
}
}
//滚动容器 App重点高度自适应 \*/
// .scroll-view {
// flex: 1;
// height: 0;
// }
.list-container {
display: flex;
flex-direction: column;
gap: 24rpx;
}
//卡片 \*/
.card-item {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
}
.card-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20rpx;
}
.address {
font-size: 32rpx;
color: #222;
font-weight: 500;
}
.status {
font-size: 24rpx;
color: #666;
}
// 标签 蓝色边框 \*/
.tag-label {
display: inline-block;
border: 2rpx solid #2F77FD;
color: #222;
padding: 6rpx 16rpx;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.text-row {
font-size: 28rpx;
color: #333;
line-height: 2;
}
// 暂无数据
.empty-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
}
.empty-img {
width: 240rpx;
height: 240rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
</style>

View File

@@ -0,0 +1,480 @@
<template>
<view class="container">
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<uni-nav-bar title="收益详情" backgroundColor="transparent" :border="false" left-icon="left"
@click-left="goBack" /> -->
<!-- 暂无数据 -->
<view v-if="isLoaded && !noticeDetail.noticeTitle" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<!-- 文章标题 -->
<view class="article-header" v-if="noticeDetail.noticeTitle">
<view class="article-title">
{{noticeDetail.noticeTitle}}
</view>
</view>
<!-- 作者和日期 -->
<view class="notice-meta" v-if="noticeDetail.noticeTitle">
<view class="notice-author">
<!-- <text class="author-name">{{noticeDetail.author}}</text> -->
<!-- <text class="separator">·</text> -->
<text class="publish-time">{{noticeDetail.publishTime}}</text>
</view>
</view>
<!-- 文章内容 -->
<view class="article-content" v-if="noticeDetail.noticeTitle">
<view class="paragraph" v-html="noticeDetail.noticeContent"></view>
<view class="articleimg">
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import {revenueDetails} from '../api/apiSub.js'
const isLoading = ref(false)
const isLoaded = ref(false)
const noticeDetail = ref({})
const currentNoticeId = ref('') // 接收公告跳转的id
const statusBarHeight = ref(uni.getSystemInfoSync().statusBarHeight || 0)
// =====方法====
onLoad((options) =>{
currentNoticeId.value = options?.id
if(currentNoticeId.value){
getDetailsData(currentNoticeId.value)
} else {
isLoaded.value = true
}
// console.log('currentNoticeId',currentNoticeId.value)
})
// 获取详情数据
const getDetailsData = async (noticeId) =>{
try{
const res = await revenueDetails({ id: noticeId })
if(res.code === 200){
noticeDetail.value = res.data
uni.$emit('noticeRead',{
noticeId:noticeId,
readStatus:noticeDetail.value.readStatus
})
}
}catch(err){
uni.showToast({
title: err.msg ||"网络异常",
icon:"none"
})
}finally{
isLoaded.value = true
}
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss" scoped>
.container {
min-height: 100vh;
background-color: #ffffff;
padding: 20px;
}
.status-bar {
width: 100%;
flex-shrink: 0;
}
/* 暂无数据 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
}
/* 内容滚动区域 */
.content-scroll {
padding-top: 15px;
box-sizing: border-box;
background-color: #ffffff;
}
/* 文章头部 */
.article-header {
// padding: 24px 20px 20px;
// border-bottom: 1px solid #f0f0f0;
.article-title {
word-break: break-all;
white-space: pre-wrap;
font-weight: bold;
color: #333333;
line-height: 1.4;
margin-bottom: 16px;
text-align: center;
}
.article-meta {
display: flex;
align-items: center;
justify-content: center;
color: #666666;
font-size: 14px;
.meta-item {
display: flex;
align-items: center;
.meta-icon {
font-size: 14px;
margin-right: 4px;
}
.meta-text {
font-size: 14px;
}
}
.meta-divider {
margin: 0 12px;
color: #cccccc;
}
}
}
/* 元信息 */
.notice-meta {
display: flex;
align-items: center;
justify-content: center;
// margin-bottom: 12px;
color: #666;
font-size: 24rpx;
margin: 0 30rpx;
.notice-author {
display: flex;
align-items: center;
.author-name {
color: #666;
}
.separator {
margin: 0 6px;
color: #ccc;
}
.publish-time {
color: #999;
}
}
.notice-time {
display: flex;
align-items: center;
.date-text {
margin-left: 4px;
color: #999;
}
}
}
/* 文章内容 */
.article-content {
padding: 20px;
color: #333333;
line-height: 1.8;
font-size: 28rpx;
.articleimg{
width: 100%;
height: 330rpx;
}
.paragraph {
white-space: pre-wrap;
word-break: break-word;
margin-bottom: 20px;
text-align: justify;
text-indent: 2em;
/* 富文本里的图片不超出屏幕 */
:deep(img),
:deep(image) {
max-width: 100% !important;
height: auto !important;
display: block;
margin: 10px 0;
}
}
.bullet-list {
margin: 20px 0 20px 2em;
.bullet-item {
display: flex;
margin-bottom: 12px;
.bullet-dot {
color: #666666;
margin-right: 8px;
flex-shrink: 0;
}
.bullet-text {
flex: 1;
color: #333333;
}
}
}
}
/* 附件区域 */
.attachments-section {
padding: 20px;
background-color: #f9f9f9;
border-top: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
margin-top: 20px;
.section-title {
display: flex;
align-items: center;
margin-bottom: 20px;
.title-text {
font-size: 18px;
font-weight: 600;
color: #333333;
white-space: nowrap;
margin-right: 12px;
}
.title-line {
flex: 1;
height: 1px;
background-color: #e0e0e0;
}
}
.attachments-list {
.attachment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background-color: #ffffff;
border-radius: 8px;
margin-bottom: 12px;
border: 1px solid #e8e8e8;
&:active {
background-color: #f5f5f5;
}
&:last-child {
margin-bottom: 0;
}
.attachment-left {
display: flex;
align-items: center;
flex: 1;
.file-icon {
font-size: 24px;
margin-right: 12px;
&.doc-icon {
color: #2b579a; /* Word文档蓝色 */
}
&.pdf-icon {
color: #e74c3c; /* PDF红色 */
}
}
.file-info {
.file-name {
font-size: 16px;
color: #333333;
margin-bottom: 4px;
font-weight: 500;
}
.file-size {
font-size: 13px;
color: #666666;
}
}
}
.download-btn {
width: 40px;
height: 40px;
border-radius: 20px;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
.download-icon {
font-size: 18px;
color: #333333;
}
}
}
}
}
/* 页面底部 */
.page-footer {
padding: 30px 20px 40px;
display: flex;
flex-direction: column;
align-items: center;
.footer-text {
font-size: 16px;
color: #999999;
margin-bottom: 20px;
}
.footer-info {
display: flex;
align-items: center;
font-size: 13px;
color: #999999;
.info-item {
padding: 0 8px;
}
.info-divider {
color: #dddddd;
}
}
}
/* 响应式调整 */
@media (min-width: 768px) {
.container {
max-width: 768px;
margin: 0 auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.article-header,
.article-content,
.attachments-section,
.page-footer {
padding-left: 40px;
padding-right: 40px;
}
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.container {
background-color: #1a1a1a;
}
.custom-navbar {
background-color: #1a1a1a;
border-bottom-color: #333333;
.nav-back,
.nav-title {
color: #ffffff;
}
.nav-actions .action-icon {
color: #ffffff;
}
}
.content-scroll {
background-color: #1a1a1a;
}
.article-header {
border-bottom-color: #333333;
.article-title {
word-break: break-all;
white-space: pre-wrap;
color: #ffffff;
}
.article-meta {
color: #999999;
}
}
.article-content {
color: #e0e0e0;
.bullet-item .bullet-text {
color: #e0e0e0;
}
}
.attachments-section {
background-color: #2a2a2a;
border-top-color: #333333;
border-bottom-color: #333333;
.section-title .title-text {
color: #ffffff;
}
.attachment-item {
background-color: #2a2a2a;
border-color: #333333;
&:active {
background-color: #333333;
}
.file-info .file-name {
color: #ffffff;
}
.file-info .file-size {
color: #999999;
}
}
}
.page-footer {
.footer-text,
.footer-info {
color: #666666;
}
}
}
</style>

View File

@@ -0,0 +1,206 @@
<template>
<view class="page">
<!-- 列表 -->
<view class="list-wrap">
<view
v-for="revenue in revenueList"
:key="revenue.noticeId"
class="item">
<view class="title-box">
<view class="title">{{ revenue.noticeTitle }}</view>
<view class="dot" v-if="revenue.readStatus === '0'"></view>
</view>
<view class="info">{{ revenue.publishTime }}</view>
<view class="content" v-html="revenue.noticeContent"></view>
<view class="detail" @click="goToDetail(revenue)">
<text style="color:#2F77FD;font-size: 24rpx;">详情</text>
<uni-icons type="right" size="13" color="#007AFF"/>
</view>
</view>
</view>
<!-- 加载中 / 无更多 / 空布局 -->
<view class="load-more" v-if="loadingMore">加载中...</view>
<view class="no-more" v-if="noMoreData && revenueList.length">没有更多了</view>
<view v-if="revenueList.length === 0 && !loading" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onShow, onUnload, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getRevenueList } from '../api/apiSub.js'
const revenueList = ref([])
const loadingMore = ref(false)
const noMoreData = ref(false)
const loading = ref(false)
const pageNum = ref(1)
const pageSize = 10
const isFirstShow = ref(true) // 详情返回列表刷新状态
const statusBarHeight = ref(uni.getSystemInfoSync().statusBarHeight || 0)
const goToDetail = (notice) => {
uni.navigateTo({
url: `/pageSubPack/inspection-revenue/public-revenue-details?id=${notice.noticeId}`
})
}
const loadRevenue = async (isRefresh = false) => {
if (loading.value) return
loading.value = true
if (isRefresh) {
pageNum.value = 1
noMoreData.value = false
}
try {
const res = await getRevenueList({ pageNum: pageNum.value, pageSize })
if (res.code === 200) {
const list = res.data.rows || []
revenueList.value = isRefresh ? list : [...revenueList.value, ...list]
if (list.length < pageSize) noMoreData.value = true
}
} catch (e) {}
finally {
loading.value = false
loadingMore.value = false
uni.stopPullDownRefresh()
}
}
// 更新单条已读状态
const updateReadStatus = ({ noticeId, readStatus }) => {
const item = revenueList.value.find(n => String(n.noticeId) === String(noticeId))
if (item) {
item.readStatus = readStatus
}
}
onShow(() =>{
if(isFirstShow.value) {
isFirstShow.value = false
return
}
loadRevenue(true)
})
onPullDownRefresh(() => {
loadRevenue(true)
})
onReachBottom(() => {
loadMore()
})
const loadMore = () => {
if (noMoreData.value || loadingMore.value) return
loadingMore.value = true
pageNum.value++
loadRevenue()
}
const narBack = () => {
uni.switchTab({ url: '/pages/serviceIndex/serviceIndex' })
}
onLoad(() => {
uni.$on('noticeRead',updateReadStatus)
loadRevenue(true)
})
onUnload(()=>{
uni.$off('noticeRead',updateReadStatus)
})
</script>
<style lang="scss">
page {
background: linear-gradient(to bottom left, #E2F0FF 0%, #f9f9f9 50%);
}
.page {
min-height: 100vh;
}
.list-wrap {
padding: 50rpx 40rpx;
}
.item {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.title-box {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 32rpx;
font-weight: bold;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dot {
width: 16rpx;
height: 16rpx;
background: red;
border-radius: 50%;
}
.info {
color: #999;
font-size: 24rpx;
margin: 10rpx 0;
}
.content {
color: #222;
font-size: 24rpx;
line-height: 1.5;
margin: 20rpx 0;
background-color: #f9f9f9;
padding: 24rpx;
word-break: break-all;
/* 按高度截断,避免 line-clamp 导致文字重叠 */
max-height: 160rpx;
overflow: hidden;
/* 富文本里的图片不超出屏幕 */
:deep(img),
:deep(image) {
max-width: 100% !important;
height: auto !important;
display: block;
}
}
.detail {
display: flex;
justify-content: space-between;
align-items: center;
}
.load-more, .no-more {
text-align: center;
padding: 20rpx;
color: #999;
font-size: 24rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
</style>

View File

@@ -2,10 +2,10 @@
<view class="reset-pwd-page">
<view class="status_bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<!-- <view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">修改密码</view>
</view>
</view> -->
<!-- 密码提示 -->
<view class="pwd-tips">密码为 6-16 需同时包含字母和数字</view>
@@ -13,14 +13,14 @@
<!-- 新密码输入框 -->
<view class="input-item">
<uni-easyinput class="input-content" type="password" v-model="newPassword"
placeholder="请输入密码" :inputBorder="false" :placeholderStyle="placeholderStyle">
placeholder="请输入密码" :inputBorder="false" maxlength="16" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 确认密码输入框 -->
<view class="input-item">
<uni-easyinput class="input-content" type="password" v-model="confirmPassword"
placeholder="请再次输入密码" :inputBorder="false" :placeholderStyle="placeholderStyle">
placeholder="请再次输入密码" :inputBorder="false" maxlength="16" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
@@ -95,16 +95,16 @@ const handleConfirm = () => {
//uni.setStorageSync('token', res.token); // 假设后端返回token字段
uni.showToast({ title: '修改成功', icon: 'success' });
setTimeout(() =>{
uni.navigateTo({ url: '/pageSubPack/loginSub/pwd-login' });
uni.navigateTo({ url: '/pageSubPack/loginSub/login?mode=pwd' });
},1500)
}else{
uni.showToast({ title: "修改失败", icon: 'error' });
uni.showToast({ title: "修改失败", icon: 'none' });
}
}).catch(err =>{
uni.showToast({
title: err.msg || '网络异常',
icon: 'error'
icon: 'none'
});
})
@@ -116,7 +116,9 @@ const handleConfirm = () => {
/* 页面容器 */
.reset-pwd-page {
padding: 20rpx 40rpx 40rpx;
min-height: 100vh;
height: 100vh;
overflow: hidden;
box-sizing: border-box;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar{

View File

@@ -1,29 +1,22 @@
<template>
<view class="forget-page">
<view class="status_bar"></view>
<view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">忘记密码</view>
</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
<image src="http://wuye.ckdzkj.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber" trim="all"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<input class="input-content native-input" :type="inputType" v-model="phoneNumber"
placeholder="请输入手机号" placeholder-style="font-size:24rpx" maxlength="11"/>
</view>
<!-- 验证码输入 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
<image src="http://wuye.ckdzkj.com/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode" trim="all"
placeholder="请输入验证码" :inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<button
<input class="input-content verify-input native-input" :type="inputType" v-model="verifyCode"
placeholder="请输入验证码" maxlength="6" placeholder-style="font-size:24rpx"/>
<button
class="get-verify-btn"
:disabled="!canGetVerify || countDown > 0"
@click="getVerifyCode"
@@ -43,9 +36,12 @@
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import { ref, computed, onUnmounted } from 'vue';
import {getSendCode} from '../api/api.js'
// 输入框类型:统一用 text避免 number 在真机上把 -/. 过滤掉导致手机号被误判合法;非法手机号由 canGetVerify 禁用获取验证码按钮
const inputType = 'text'
// ====响应式数据======
const phoneNumber = ref('')
const verifyCode = ref('')
@@ -87,13 +83,13 @@ const getVerifyCode = () => {
}else{
uni.showToast({
title: '验证码发送失败',
icon: 'error'
icon: 'none'
});
}
}).catch(err =>{
uni.showToast({
title: err.msg || '网络异常',
icon: 'error'
icon: 'none'
});
})
@@ -131,29 +127,11 @@ const goBack = () => {
<style scoped>
.forget-page{
padding: 20rpx 40rpx 40rpx;
min-height: 100vh;
height: 100vh;
overflow: hidden;
box-sizing: border-box;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar{
width: 100%;
height: 46px;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
/* height: 88rpx; */
margin-bottom: 70rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #333;
margin-right: 100rpx;
}
/* 输入框通用样式 */
.input-item {
display: flex;
@@ -188,6 +166,11 @@ const goBack = () => {
height: 100%;
}
/* 原生 input 垂直居中 */
.native-input {
line-height: 88rpx;
}
/* 深度覆盖 uni-easyinput 内部默认样式,解决高度偏移 */
.input-content :deep(.uni-easyinput__content) {
height: 100% !important;

View File

@@ -1,274 +1,304 @@
<template>
<view class="login-page">
<view class="status_bar"></view>
<view class="status_bar"></view>
<!-- 登录标题 -->
<view class="login-title">手机验证码登录</view>
<view class="login-title">{{ loginMode === 'sms' ? '手机验证码登录' : '账号密码登录' }}</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="phone" size="24" color="#999"></uni-icons> -->
<image src="https://wuyeadmin.bugtc.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- ====== 验证码登录模式 ====== -->
<template v-if="loginMode === 'sms'">
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<input class="input-content native-input" :type="inputType" v-model="phoneNumber"
placeholder="请输入手机号" placeholder-style="font-size:24rpx" maxlength="11" />
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<!-- <input
class="input-content"
type="number"
placeholder="请输入手机号"
v-model="phoneNumber"
maxlength="11"
@input="checkForm"
/> -->
</view>
<!-- 验证码输入框 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="https://wuyeadmin.bugtc.com/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- 验证码输入框 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<input class="input-content verify-input native-input" :type="inputType" v-model="verifyCode"
placeholder="请输入验证码" maxlength="6" placeholder-style="font-size:24rpx"/>
<button class="get-verify-btn" :disabled="!canGetVerify || countDown > 0" @click="getVerifyCode">
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode"
placeholder="请输入验证码" :inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<button
class="get-verify-btn"
:disabled="!canGetVerify || countDown > 0"
@click="getVerifyCode"
>
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
</template>
<!-- 协议勾选 -->
<view class="agreement-item" >
<view @click="toggleAgreement">
<uni-icons
type="checkbox-filled"
size="24"
color="#007aff"
v-if="agreeAgreement"
></uni-icons>
<uni-icons
type="checkbox"
size="24"
color="#999"
v-else
></uni-icons>
</view>
<text class="agreement-text" @click.stop="goToPrivacy">我已阅读并同意<text class="agreePrivacy">用户隐私政策</text></text>
<!-- ====== 账号密码登录模式 ====== -->
<template v-if="loginMode === 'pwd'">
<!-- 账号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="text" v-model="account"
placeholder="请输入帐号" :inputBorder="false" maxlength="20" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 密码输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/password.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="password" v-model="password"
placeholder="请输入密码" :inputBorder="false" maxlength="16" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
</template>
<!-- 协议勾选 + 忘记密码 -->
<view class="agreement-wrap">
<view class="agreement-item">
<view @click="toggleAgreement" class="checkbox-icon">
<uni-icons type="checkbox-filled" size="24" color="#007aff" v-if="agreeAgreement"></uni-icons>
<uni-icons type="checkbox" size="24" color="#999" v-else></uni-icons>
</view>
<text class="agreement-text">我已阅读并同意
<text class="agreePrivacy" @click.stop="goToUserAgreement">用户协议</text>
<text class="agreePrivacy" @click.stop="goToPrivacy">隐私政策</text>
</text>
</view>
<text class="forget-pwd" v-if="loginMode === 'pwd'" @click="goForgetPwd">忘记密码</text>
</view>
<!-- 登录按钮 -->
<button
class="login-btn"
@click="handleLogin"
>
登录
</button>
<button class="login-btn" @click="handleLogin">登录</button>
<!-- 底部链接 -->
<view class="bottom-link">
<navigator class="link-item" url="/pageSubPack/loginSub/pwd-login">账号密码登录</navigator>
<text class="link-item" @click="switchMode">{{ loginMode === 'sms' ? '账号密码登录' : '验证码登录' }}</text>
<navigator class="link-item" url="/pageSubPack/loginSub/register-new-user">注册新用户</navigator>
</view>
<!-- 隐私协议弹窗 -->
<privacy-popup v-model:show="showPrivacyPopup" @agree="agreePrivacy" @disagree="disagreePrivacy" @close="showPrivacyPopup = false" />
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import {getLoginBySms,getSendCode} from '../api/api.js'
import { onLoad } from '@dcloudio/uni-app'
import { getLoginBySms, getLoginPassword, getSendCode } from '../api/api.js'
import PrivacyPopup from '@/components/privacy-popup/privacy-popup.vue'
import { resetLoginLock } from '@/common/checkLogin.js'
// 输入框类型:统一用 text避免 number 在真机上把 -/. 过滤掉导致手机号被误判合法;非法手机号由 canGetVerify 禁用获取验证码按钮
const inputType = 'text'
// ========== 模式切换 ==========
// 'sms' = 手机验证码登录, 'pwd' = 账号密码登录
const loginMode = ref('pwd')
// ========== 响应式数据 ==========
// 手机号
const phoneNumber = ref('');
// 验证码
const verifyCode = ref('');
const phoneNumber = ref('') // 短信登录:手机号
const verifyCode = ref('') // 短信登录:验证码
const account = ref('') // 密码登录:账号
const password = ref('') // 密码登录:密码
const agreeAgreement = ref(false)
const placeholderStyle = ref('font-size:24rpx')
// 是否同意协议
const agreeAgreement = ref(false); // 默认不勾选
// 倒计时(秒)
const countDown = ref(0);
// 定时器
let timer = null;
// 验证码倒计时
const countDown = ref(0)
let timer = null
// 隐私弹窗
const showPrivacyPopup = ref(false)
const pendingAction = ref('')
// ========== onLoad: 读取 mode 参数 ==========
onLoad((options) => {
// 重置登录跳转锁,允许下次未登录时再次跳转
resetLoginLock()
if (options && options.mode === 'sms') {
loginMode.value = 'sms'
} else if (options && options.mode === 'pwd') {
loginMode.value = 'pwd'
}
})
// ========== 计算属性 ==========
// 是否可获取验证码(手机号格式正确)
// 短信模式:是否可获取验证码
const canGetVerify = computed(() => {
// 手机号正则11位数字以1开头
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
if (loginMode.value !== 'sms') return false
const phoneReg = /^1[3-9]\d{9}$/
return phoneReg.test(phoneNumber.value)
})
// 是否可以登录(手机号+验证码+协议都满足)
const canLogin = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
const verifyReg = /^\d{6}$/;
return phoneReg.test(phoneNumber.value) && verifyReg.test(verifyCode.value) && agreeAgreement.value;
});
// ========== 共享方法 ==========
const toggleAgreement = () => { agreeAgreement.value = !agreeAgreement.value }
// ========== 方法 ==========
// 检查表单状态
const checkForm = () => {
// 无需额外逻辑,计算属性已实时监听
};
const goToUserAgreement = () => {
uni.navigateTo({ url: '/pageSubPack/loginSub/user-agreement' })
}
// 切换协议勾选状态
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 跳转隐私协议
const goToPrivacy = () => {
uni.navigateTo({
url:'/pageSubPack/loginSub/privacy',
uni.navigateTo({ url: '/pageSubPack/loginSub/privacy' })
}
const openPrivacyPopup = (action) => {
pendingAction.value = action || ''
showPrivacyPopup.value = true
}
const agreePrivacy = () => {
showPrivacyPopup.value = false
agreeAgreement.value = true
if (pendingAction.value === 'login') {
doLogin()
}
pendingAction.value = ''
}
const disagreePrivacy = () => {
showPrivacyPopup.value = false
pendingAction.value = ''
}
// 切换登录模式
const switchMode = () => {
if (loginMode.value === 'sms') {
loginMode.value = 'pwd'
} else {
loginMode.value = 'sms'
}
// 重置相关字段
agreeAgreement.value = false
if (timer) { clearInterval(timer); timer = null }
countDown.value = 0
}
// 忘记密码跳转
const goForgetPwd = () => {
uni.navigateTo({ url: '/pageSubPack/loginSub/forget-pwd' })
}
// ========== 验证码登录:获取验证码 ==========
const getVerifyCode = () => {
if (!canGetVerify.value) return
uni.showLoading({ title: '发送中...', mask: true })
getSendCode({ phone: phoneNumber.value, scene: 'login' }).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '验证码已发送', icon: 'success' })
countDown.value = 60
timer = setInterval(() => {
countDown.value--
if (countDown.value <= 0) {
clearInterval(timer)
timer = null
}
}, 1000)
} else {
uni.showToast({ title: '验证码发送失败', icon: 'none' })
}
}).catch(err => {
uni.hideLoading()
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
})
}
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone:phoneNumber.value,
scene:'login'
}
uni.showLoading({ title: '发送中...', mask: true });
getSendCode(params).then(res =>{
if(res.code === 200){
uni.hideLoading();
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
}else{
uni.showToast({
title: '验证码发送失败',
icon: 'error'
});
}
}).catch(err => {
uni.showToast({
title: '网络异常',
icon: 'error'
});
})
};
// 登录处理
// ========== 登录处理(分发) ==========
const handleLogin = () => {
if (!phoneNumber.value) {
uni.showToast({ title: '请输入手机号', icon: 'none' });
return;
if (loginMode.value === 'sms') {
handleSmsLogin()
} else {
handlePwdLogin()
}
if (!/^1[3-9]\d{9}$/.test(phoneNumber.value)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
// ========== 验证码登录 ==========
const handleSmsLogin = () => {
if (!phoneNumber.value) { uni.showToast({ title: '请输入手机号', icon: 'none' }); return }
if (!/^1[3-9]\d{9}$/.test(phoneNumber.value)) { uni.showToast({ title: '请输入正确的手机号', icon: 'none' }); return }
if (!verifyCode.value) { uni.showToast({ title: '请输入验证码', icon: 'none' }); return }
if (!/^\d{6}$/.test(verifyCode.value)) { uni.showToast({ title: '验证码为 6 位数字', icon: 'none' }); return }
if (!agreeAgreement.value) { openPrivacyPopup('login'); return }
doLogin()
}
// ========== 密码登录 ==========
const handlePwdLogin = () => {
if (!account.value) { uni.showToast({ title: '请输入账号', icon: 'none' }); return }
if (account.value.length < 4 || account.value.length > 20) { uni.showToast({ title: '账号长度需为 4-20 位', icon: 'none' }); return }
if (!password.value) { uni.showToast({ title: '请输入密码', icon: 'none' }); return }
if (password.value.length < 6) { uni.showToast({ title: '密码至少输入 6 位', icon: 'none' }); return }
if (password.value.length > 16) { uni.showToast({ title: '密码不能超过 16 位', icon: 'none' }); return }
if (/\s/.test(password.value)) { uni.showToast({ title: '密码格式不对', icon: 'none' }); return }
if (!agreeAgreement.value) { openPrivacyPopup('login'); return }
doLogin()
}
// ========== 执行登录 ==========
const doLogin = () => {
uni.showLoading({ title: '登录中...', mask: true })
const push_cid = uni.getStorageSync('push_cid')
if (loginMode.value === 'sms') {
// 验证码登录
getLoginBySms({ phone: phoneNumber.value, smsCode: verifyCode.value, scene: 'login', cid: push_cid })
.then(onLoginSuccess).catch(onLoginError)
} else {
// 密码登录
getLoginPassword({ username: account.value, password: password.value, cid: push_cid })
.then(onLoginSuccess).catch(onLoginError)
}
if (!verifyCode.value) {
uni.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
const onLoginSuccess = (res) => {
if (res.code === 200 && res.data) {
// 换号登录时清除旧用户的所有缓存,防止新用户看到旧用户的小区数据和公告
const oldUserId = uni.getStorageSync('userId')
if (res.data.userId && String(oldUserId) !== String(res.data.userId)) {
uni.removeStorageSync('home_cache')
uni.removeStorageSync('home_cache_' + oldUserId)
uni.removeStorageSync('communityName')
uni.removeStorageSync('communityId')
uni.removeStorageSync('needReloadUserInfo')
}
uni.setStorageSync('token', res.data.token)
if (res.data.userId) { uni.setStorageSync('userId', res.data.userId) }
const currentVillageId = res.data.currentVillageId || 0
uni.setStorageSync('currentVillageId', currentVillageId)
uni.hideLoading()
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => { uni.switchTab({ url: '/pages/index/index' }) }, 1500)
} else {
uni.hideLoading()
uni.showToast({ title: res.msg || '登录失败', icon: 'none' })
}
if (!/^\d{6}$/.test(verifyCode.value)) {
uni.showToast({ title: '验证码为 6 位数字', icon: 'none' });
return;
}
if (!agreeAgreement.value) {
uni.showToast({ title: '请勾选协议', icon: 'none' });
return;
}
uni.showLoading({ title: '登录中...', mask: true });
const push_cid = uni.getStorageSync('push_cid')
let params = {
phone:phoneNumber.value,
smsCode:verifyCode.value,
scene:'login',
cid:push_cid
}
getLoginBySms(params).then(res =>{
if(res.code === 200){
if(res.data){
uni.setStorageSync('token', res.data.token);
// 存用户ID
if(res.data.userId) {
uni.setStorageSync('userId', res.data.userId);
}
// 存当前绑定的小区ID0 表示未绑定
const currentVillageId = res.data.currentVillageId || 0;
uni.setStorageSync('currentVillageId', currentVillageId);
uni.hideLoading();
uni.showToast({ title: '登录成功', icon: 'success' });
}
setTimeout(() =>{
uni.switchTab({ url: '/pages/index/index' });
},1500)
}else{
uni.showToast({ title: '登录失败', icon: 'error' });
}
}else{
uni.showToast({ title:'登录失败', icon: 'error' });
}
}).catch(err => {
uni.showToast({
title: err.msg || '网络异常,请重试',
icon: 'error'
});
})
// 模拟登录请求
// uni.showLoading({
// title: '登录中...'
// });
// uni.showToast({
// title:"登录成功",
// icon:'success'
// })
// 登录成功后上报推送 CID
// #ifdef APP-PLUS
const cid = uni.getStorageSync('push_cid')
if (cid) {
reportPushCID(cid)
}
// #endif
// setTimeout(() =>{
// uni.switchTab({ url: '/pages/index/index' });
// },1500)
const onLoginError = (err) => {
uni.hideLoading()
};
uni.showToast({ title: err.msg || '网络异常,请重试', icon: 'none' })
}
// ========== 生命周期 ==========
// 页面卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
if (timer) { clearInterval(timer) }
})
</script>
<style scoped>
/* 页面容器 */
.login-page {
padding: 80rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
height: 100vh;
overflow: hidden;
box-sizing: border-box;
background: repeating-linear-gradient(to bottom, #E2F0FF, #F6FAFF );
}
.status_bar {
height: 160px;
width: 100%;
}
.status_bar {
height: 160px;
width: 100%;
}
/* 登录标题 */
.login-title {
@@ -299,10 +329,9 @@ onUnmounted(() => {
/* 输入框图标 */
.input-icon {
width: 50rpx;
height: 50rpx;
width: 50rpx;
height: 50rpx;
margin-right: 20rpx;
color: #999;
}
/* 输入框内容 */
@@ -313,7 +342,13 @@ onUnmounted(() => {
height: 100%;
}
/* 深度覆盖 uni-easyinput 内部默认样式,解决高度偏移 */
/* 原生 input 垂直居中 */
.native-input {
line-height: 88rpx;
}
/* 深度覆盖 uni-easyinput 内部默认样式 */
.input-content :deep(.uni-easyinput__content) {
height: 100% !important;
min-height: unset !important;
@@ -350,36 +385,56 @@ onUnmounted(() => {
margin: 0;
}
/* 获取验证码按钮禁用状态 */
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
border-radius: 50px;
}
.get-verify-btn::after{
border-radius: 50px;
.get-verify-btn::after {
border-radius: 50px;
}
/* 协议和忘记密码行 */
.agreement-wrap {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin: 70rpx 0 28rpx 0;
padding: 0 10rpx;
}
/* 协议勾选项 */
.agreement-item {
display: flex;
align-items: center;
margin: 70rpx 0 28rpx 0;
padding-left: 10rpx;
align-items: flex-start;
flex: 1;
min-width: 0;
}
.checkbox-icon {
flex-shrink: 0;
margin-top: 2rpx;
}
/* 协议文本 */
.agreement-text {
font-size: 24rpx;
color: #666;
margin-left: 10rpx;
line-height: 1.5;
}
.agreePrivacy{
color: #2F77FD;
.agreePrivacy {
color: #2F77FD;
}
/* .agreePrivacy.active{
color: #2F77FD;
} */
.forget-pwd {
font-size: 24rpx;
color: #666;
flex-shrink: 0;
margin-left: 16rpx;
line-height: 1.5;
}
/* 登录按钮 */
.login-btn {
width: 100%;
@@ -392,15 +447,13 @@ onUnmounted(() => {
margin-bottom: 60rpx;
}
/* 登录按钮禁用状态 */
.login-btn:disabled {
background-color: #99c8ff;
color: #fff;
/* border-radius: 50px; */
}
/* 禁用按钮边框 */
button:after{
border-radius: 8px;
button:after {
border-radius: 8px;
}
/* 底部链接 */
@@ -410,7 +463,6 @@ button:after{
padding: 0 20rpx;
}
/* 链接项 */
.link-item {
font-size: 28rpx;
color: #007aff;

View File

@@ -1,169 +1,134 @@
<template>
<view class="privacy-page">
<!-- 协议内容 -->
<scroll-view class="privacy-content" scroll-y>
<view class="content-wrap">
<view class="title">隐私政策</view>
<view class="date-info">
<view class="date-item">更新日期2026年06月18日</view>
<view class="date-item">生效日期2026年06月18日</view>
</view>
<view class="section">
<!-- <view class="section-title">适用范围</view> -->
<view class="section-text">
欢迎使用我们的产品/服务以下统称服务我们高度重视用户隐私保护
致力于为您提供安全可靠的服务体验本隐私政策旨在向您说明我们如何收集使用存储保护您的个人信息
以及您享有的相关权利请您在使用我们的服务前仔细阅读并理解本政策全部内容
一旦您使用我们的服务即视为您同意本政策的所有条款
我们收集的个人信息 为了向您提供更优质个性化的服务我们会根据服务功能的需要收集您主动提供或在使用服务过程中产生的个人信息
具体如下 您主动提供的信息 账户注册信息当您注册我们的账户时您可能需要提供姓名手机号码电子邮箱密码等信息用于身份验证账户登录及找回
服务相关信息根据您使用的具体服务您可能需要提供相关补充信息如使用客服咨询服务时提供问题描述及相关凭证使用交易类服务时提供收货地址支付相关信息等
自愿提交的信息您主动向我们反馈的意见建议或参与我们的问卷调查活动时提交的相关信息
若您通过第三方平台如微信支付宝QQ等登录我们的服务我们会根据您的授权
从第三方获取您的相关账户信息如昵称头像用于完成登录及账户绑定
我们不会获取第三方平台的其他未授权信息
欢迎使用我们的产品/服务以下统称"服务"我们高度重视用户隐私保护
致力于为您提供安全可靠的服务体验本隐私政策旨在向您说明我们如何收集使用存储保护您的个人信息
以及您享有的相关权利请您在使用我们的服务前仔细阅读并理解本政策全部内容
一旦您使用我们的服务即视为您同意本政策的所有条款
</view>
</view>
<view class="section">
<view class="section-title">我们收集的个人信息</view>
<view class="section-text">
为了向您提供更优质个性化的服务我们会根据服务功能的需要收集您主动提供或在使用服务过程中产生的个人信息具体如下
</view>
<view class="section-text">
您主动提供的信息账户注册信息当您注册我们的账户时您可能需要提供姓名手机号码电子邮箱密码等信息用于身份验证账户登录及找回
服务相关信息根据您使用的具体服务您可能需要提供相关补充信息如使用客服咨询服务时提供问题描述及相关凭证使用交易类服务时提供收货地址支付相关信息等
自愿提交的信息您主动向我们反馈的意见建议或参与我们的问卷调查活动时提交的相关信息
若您通过第三方平台如微信支付宝QQ等登录我们的服务我们会根据您的授权从第三方获取您的相关账户信息如昵称头像用于完成登录及账户绑定我们不会获取第三方平台的其他未授权信息
</view>
</view>
<view class="section">
<view class="section-title">我们如何使用信息</view>
<view class="section-text">
我们使用收集的信息用于提供维护和改进我们的服务向您发送服务相关的通知响应您的请求和反馈保障服务的安全性和稳定性
</view>
</view>
<view class="section">
<view class="section-title">信息存储和安全</view>
<view class="section-text">
我们承诺保护您的个人信息安全采用业界通行的安全技术和措施来保护您的个人信息免受未经授权的访问公开披露使用修改损坏或丢失
我们将在实现本政策所述目的所必需的期限内保留您的个人信息除非法律法规另有规定
</view>
</view>
<view class="section">
<view class="section-title">您的权利</view>
<view class="section-text">
您有权查阅更正删除您的个人信息有权撤回同意有权注销账户您可以通过应用内的设置页面或联系我们的客服来行使这些权利
</view>
</view>
<view class="section">
<view class="section-title">未成年人保护</view>
<view class="section-text">
我们非常重视未成年人个人信息的保护若您是未成年人建议您在监护人的指导下阅读本政策并决定是否使用我们的服务
</view>
</view>
<view class="section">
<view class="section-title">联系我们</view>
<view class="section-text">
如果您对本隐私政策有任何疑问或建议请通过应用内的"联系物业"功能与我们取得联系
</view>
</view>
</view>
</scroll-view>
<!-- 底部同意按钮新增 -->
<!-- <view class="agree-btn-wrap" v-if="needAgree">
<button class="agree-btn" @click="agreePrivacy">同意隐私政策</button>
</view> -->
</view>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app';
// 返回上一页
// const navigateBack = () => {
// uni.navigateBack({
// delta: 1
// });
// };
// 页面加载
// onLoad((options) => {
// // 判断是否需要同意
// if (options.needAgree === '1') {
// needAgree.value = true;
// }
// });
// 返回上一页
// const goToBack = () => {
// uni.navigateBack({
// delta: 1
// });
// };
// 同意隐私政策(新增)
// const agreePrivacy = () => {
// // #ifdef MP-WEIXIN
// wx.requirePrivacyAuthorize({
// success: () => {
// uni.showToast({
// title: '已同意隐私政策',
// icon: 'success'
// });
// // 记录授权状态(可选,存入本地缓存)
// uni.setStorageSync('privacyAgreed', true);
// navigateBack();
// },
// fail: () => {
// uni.showToast({
// title: '授权失败',
// icon: 'none'
// });
// }
// });
// // #endif
// // 非微信小程序直接标记同意
// // #ifndef MP-WEIXIN
// uni.setStorageSync('privacyAgreed', true);
// uni.showToast({
// title: '已同意隐私政策',
// icon: 'success'
// });
// navigateBack();
// // #endif
// };
const goBack = () => {
uni.navigateBack({ delta: 1 })
}
</script>
<style scoped>
.privacy-page {
min-height: 100vh;
background-color: #f8f8f8;
}
/* 导航栏 */
.nav-bar {
height: 100vh;
display: flex;
align-items: center;
padding: 20rpx;
flex-direction: column;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.nav-back {
display: flex;
align-items: center;
}
.back-text {
font-size: 30rpx;
color: #333;
margin-left: 10rpx;
}
.nav-title {
font-size: 32rpx;
font-weight: 500;
margin-left: 60rpx;
}
/* 协议内容 */
.privacy-content {
height: calc(100vh - 88rpx);
flex: 1;
overflow-y: auto;
}
.content-wrap {
padding: 30rpx;
}
.title {
font-size: 28rpx;
font-size: 32rpx;
font-weight: 600;
text-align: center;
margin-bottom: 40rpx;
margin-bottom: 24rpx;
color: #333;
}
.date-info {
text-align: center;
margin-bottom: 40rpx;
}
.date-item {
font-size: 24rpx;
color: #999;
line-height: 1.8;
}
.section {
margin-bottom: 30rpx;
padding: 0 20rpx;
}
.section-title {
font-size: 30rpx;
font-size: 28rpx;
font-weight: 500;
color: #333;
margin-bottom: 10rpx;
margin-bottom: 16rpx;
}
.section-text {
font-size: 28rpx;
line-height: 1.6;
font-size: 26rpx;
line-height: 1.8;
color: #666;
}
/* 同意按钮样式(新增) */
.agree-btn-wrap {
padding: 20rpx;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
border-top: 1rpx solid #eee;
}
.agree-btn {
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
height: 88rpx;
font-size: 32rpx;
}
</style>
</style>

View File

@@ -1,297 +0,0 @@
<template>
<view class="login-page">
<view class="status_bar"></view>
<!-- 标题 -->
<view class="login-title">账号密码登录</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="phone" size="24" color="#999"></uni-icons> -->
<image src="https://wuyeadmin.bugtc.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="text" v-model="phoneNumber"
placeholder="请输入帐号" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 密码输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
<image src="https://wuyeadmin.bugtc.com/images/login/password.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="password" v-model="password"
placeholder="请输入密码" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 协议勾选 + 忘记密码 -->
<view class="agreement-wrap">
<view class="agreement-item">
<view @click="toggleAgreement">
<uni-icons
type="checkbox-filled"
size="24"
color="#007aff"
v-if="agreeAgreement"
></uni-icons>
<uni-icons
type="checkbox"
size="24"
color="#999"
v-else
></uni-icons>
</view>
<text class="agreement-text" @click.stop="goToPrivacy">我已阅读并同意
<text class="agreePrivacy" >用户隐私政策</text>
</text>
</view>
<navigator class="forget-pwd" url="/pageSubPack/loginSub/forget-pwd">忘记密码</navigator>
</view>
<!-- 登录按钮 -->
<button
class="login-btn"
@click="handleLogin"
>
登录
</button>
<!-- 底部链接 -->
<view class="bottom-link">
<navigator class="link-item" url="/pageSubPack/loginSub/login">验证码登录</navigator>
<navigator class="link-item" url="/pageSubPack/loginSub/register-new-user">注册新用户</navigator>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import {onLoad} from "@dcloudio/uni-app"
import {getLoginPassword} from '../api/api.js'
// 响应式数据
const phoneNumber = ref('');
const password = ref('');
const agreeAgreement = ref(false); // 默认勾选
const placeholderStyle = ref('font-size:24rpx')
// 计算属性:是否可登录(帐号+密码+协议都满足)
const canLogin = computed(() => {
const phoneReg = /^[a-zA-Z0-9_-]{4,16}$/; // 4-16位
const pwdReg = /^.{6,16}$/; // 密码6-16位
return phoneReg.test(phoneNumber.value) && pwdReg.test(password.value) && agreeAgreement.value;
});
// 切换协议勾选
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 跳转隐私协议
const goToPrivacy = () => {
uni.navigateTo({
url:'/pageSubPack/loginSub/privacy',
})
}
// 检查表单
const checkForm = () => {};
onLoad(() =>{
});
// 登录逻辑
const handleLogin = () => {
if (!phoneNumber.value) {
uni.showToast({ title: '请输入账号', icon: 'none' });
return;
}
if (phoneNumber.value.length < 4 || phoneNumber.value.length > 16) {
uni.showToast({ title: '账号长度需为 4-16 位', icon: 'none' });
return;
}
if (!password.value) {
uni.showToast({ title: '请输入密码', icon: 'none' });
return;
}
if (password.value.length < 6) {
uni.showToast({ title: '密码至少输入 6 位', icon: 'none' });
return;
}
if (password.value.length > 16) {
uni.showToast({ title: '密码不能超过 16 位', icon: 'none' });
return;
}
if(!agreeAgreement.value){
uni.showToast({ title: '请勾选协议', icon: 'none' });
return;
}
uni.showLoading({ title: '登录中...', mask: true });
const push_cid = uni.getStorageSync('push_cid')
let params = {
username:phoneNumber.value,
password:password.value,
cid:push_cid
}
getLoginPassword(params).then(res =>{
if(res.code === 200){
uni.setStorageSync('token', res.data.token);
// 存用户ID
if(res.data) {
// 存当前绑定的小区ID0 表示未绑定
const currentVillageId = res.data.currentVillageId || 0;
uni.setStorageSync('currentVillageId', currentVillageId);
uni.setStorageSync('userId',res.data.userId)
}
uni.hideLoading();
uni.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() =>{
uni.switchTab({
url: '/pages/index/index'
},1000);
})
}else{
uni.showToast({ title: res.msg, icon: 'error' });
}
}).catch(err => {
// console.log(err)
uni.showToast({
title: err.msg || '网络异常',
icon: 'error'
});
})
};
</script>
<style scoped>
.login-page {
padding: 80rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar {
height: 160px;
width: 100%;
}
.login-title {
font-size: 52rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 142rpx;
}
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.input-icon {
margin-right: 20rpx;
width: 50rpx;
height: 50rpx;
}
.input-content {
flex: 1;
font-size: 28rpx;
color: #333;
height: 100%;
}
/* 深度覆盖 uni-easyinput 内部默认样式,解决高度偏移 */
.input-content :deep(.uni-easyinput__content) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
background-color: transparent !important;
}
.input-content :deep(.uni-easyinput__content-input) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
line-height: 88rpx;
}
.agreement-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin: 70rpx 0 28rpx 0;
padding: 0 10rpx;
}
.agreement-item {
display: flex;
align-items: center;
}
.agreement-text {
font-size: 24rpx;
color: #666;
margin-left: 10rpx;
}
.agreePrivacy{
color: #2F77FD;
}
/* .agreePrivacy.active{
color: #2F77FD;
} */
.forget-pwd {
font-size: 24rpx;
color: #666;
text-decoration: none;
}
.login-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 60rpx;
}
.login-btn:disabled {
background-color: #99c8ff;
color: #fff;
/* border-radius: 50px; */
}
/* 禁用按钮边框 */
button:after{
border-radius: 8px;
}
.bottom-link {
display: flex;
justify-content: space-between;
padding: 0 20rpx;
}
.link-item {
font-size: 28rpx;
color: #007aff;
text-decoration: none;
}
</style>

View File

@@ -1,455 +1,547 @@
<template>
<view class="register-page">
<view class="status_bar"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">注册</view>
</view>
<view class="register-page">
<view class="status_bar"></view>
<!-- 注册标题 -->
<view class="register-title">注册用户</view>
<!-- 注册标题 -->
<view class="register-title">注册用户</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuyeadmin.bugtc.com/images/login/phone.png" mode=""
style="height: 100%;width: 100%;"></image>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/phone.png" mode=""
style="height: 100%;width: 100%;"></image>
</view>
<input class="input-content native-input" :type="inputType" v-model="phoneNumber" placeholder="请输入手机号"
placeholder-style="font-size:24rpx" maxlength="11"/>
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber" placeholder="请输入手机号"
:inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 验证码输入 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="https://wuyeadmin.bugtc.com/images/login/code.png" mode="" style="height: 100%;width: 100%;">
</image>
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
<!-- 验证码输入 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/code.png" mode="" style="height: 100%;width: 100%;">
</image>
</view>
<input class="input-content verify-input native-input" :type="inputType" v-model="verifyCode" placeholder="请输入验证码"
maxlength="6" placeholder-style="font-size:24rpx"/>
<button class="get-verify-btn" :disabled="!canGetVerify || countDown > 0" @click="getVerifyCode">
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode" placeholder="请输入验证码"
:inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<button class="get-verify-btn" :disabled="!canGetVerify || countDown > 0" @click="getVerifyCode">
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
<!-- 账号输入框 -->
<view class="input-item-wrap">
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/account.png" mode=""
style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="text" v-model="account" @blur="validateAccount" @focus="clearAccountError" placeholder="请输入账号4-20位" :inputBorder="false"
maxlength="20" :placeholderStyle="placeholderStyle" />
</view>
<text class="input-error" v-if="accountError">{{ accountError }}</text>
</view>
<!-- 密码输入框 -->
<view class="input-item-wrap">
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/password.png" mode=""
style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="password" v-model="password" @blur="validatePassword" @focus="clearPasswordError" placeholder="请输入密码6-16位"
:inputBorder="false" maxlength="16" :placeholderStyle="placeholderStyle" />
</view>
<text class="input-error" v-if="passwordError">{{ passwordError }}</text>
</view>
<!-- 协议勾选 -->
<view class="agreement-item">
<view @click="toggleAgreement">
<uni-icons type="checkbox-filled" size="24" color="#007aff" v-if="agreeAgreement"></uni-icons>
<uni-icons type="checkbox" size="24" color="#999" v-else></uni-icons>
</view>
<text class="agreement-text" >我已阅读并同意
<text class="agreePrivacy" @click.stop="goToUserAgreement">用户协议</text>
<text class="agreePrivacy" @click.stop="goToPrivacy">隐私政策</text>
</text>
</view>
<!-- 立即注册按钮 -->
<button class="register-btn" @click="handleRegister">
立即注册
</button>
</view>
<!-- 账号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuyeadmin.bugtc.com/images/login/account.png" mode=""
style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="person" size="24" color="#999"></uni-icons> -->
<!-- 跳转登录链接 -->
<view class="login-link">
<navigator class="link-item" url="/pageSubPack/loginSub/login">已有账号去登录</navigator>
</view>
<uni-easyinput class="input-content" type="text" v-model="account" placeholder="请输入账号" :inputBorder="false"
:placeholderStyle="placeholderStyle">
</uni-easyinput>
<!-- 隐私协议弹窗 -->
<privacy-popup v-model:show="showPrivacyPopup" @agree="agreePrivacy" @disagree="disagreePrivacy" @close="showPrivacyPopup = false" />
</view>
</template>
<!-- 密码输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuyeadmin.bugtc.com/images/login/password.png" mode=""
style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content" type="password" v-model="password" placeholder="请输入密码"
:inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<script setup>
import {
ref,
computed,
watch,
onUnmounted
} from 'vue';
import {
getRegisterUser,
getSendCode
} from '../api/api.js'
import PrivacyPopup from '@/components/privacy-popup/privacy-popup.vue'
</view>
<!-- 协议勾选 -->
<view class="agreement-item">
<view @click="toggleAgreement">
<uni-icons type="checkbox-filled" size="24" color="#007aff" v-if="agreeAgreement"></uni-icons>
<uni-icons type="checkbox" size="24" color="#999" v-else></uni-icons>
</view>
<text class="agreement-text" @click.stop="goToPrivacy">我已阅读并同意<text
class="agreePrivacy">用户隐私政策</text></text>
</view>
// 输入框类型:统一用 text避免 number 在真机上把 -/. 过滤掉导致手机号被误判合法;非法手机号由 canGetVerify 禁用获取验证码按钮
const inputType = 'text'
// ========== 响应式数据 ==========
const phoneNumber = ref('');
const verifyCode = ref('');
const account = ref('');
const accountError = ref('');
const password = ref('');
const passwordError = ref('');
const placeholderStyle = ref('font-size:24rpx')
const agreeAgreement = ref(false);
const showPrivacyPopup = ref(false)
const pendingAction = ref('')
const countDown = ref(0);
let timer = null;
<!-- 立即注册按钮 -->
<button class="register-btn" @click="handleRegister">
立即注册
</button>
<!-- 跳转登录链接 -->
<view class="login-link">
<navigator class="link-item" url="/pageSubPack/loginSub/login">已有账号去登录</navigator>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
onUnmounted
} from 'vue';
import {
getRegisterUser,
getSendCode
} from '../api/api.js'
// ========== 响应式数据 ==========
// 表单数据
const phoneNumber = ref('');
const verifyCode = ref('');
const account = ref('');
const password = ref('');
const placeholderStyle = ref('font-size:24rpx')
// 协议勾选
const agreeAgreement = ref(false);
// 验证码倒计时
const countDown = ref(0);
let timer = null;
// ========== 计算属性 ==========
// 是否可获取验证码(手机号格式正确)
const canGetVerify = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
// 是否可注册(所有表单项+协议都满足)
const canRegister = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/; // 手机号11位
const verifyReg = /^\d{6}$/; // 验证码6位
const accountReg = /^.{4,20}$/; // 账号4-20位
const pwdReg = /^.{6,16}$/; // 密码6-16位
return phoneReg.test(phoneNumber.value) &&
verifyReg.test(verifyCode.value) &&
accountReg.test(account.value) &&
pwdReg.test(password.value) &&
agreeAgreement.value;
});
// ========== 方法 ==========
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 切换协议勾选状态
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 跳转隐私协议
const goToPrivacy = () => {
uni.navigateTo({
url: '/pageSubPack/loginSub/privacy',
})
}
// 检查表单状态(实时更新按钮状态)
const checkForm = () => {};
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone: phoneNumber.value,
scene: 'register'
}
getSendCode(params).then(res => {
if (res.code === 200) {
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
} else {
uni.showToast({
title: '验证码发送失败',
icon: 'error'
});
}
}).catch(err => {
uni.showToast({
title: err.msg || '网络异常',
icon: 'error'
});
})
};
// 处理注册逻辑
const handleRegister = () => {
if (!phoneNumber.value) {
uni.showToast({ title: '请输入手机号', icon: 'none' });
return;
}
if (!/^1[3-9]\d{9}$/.test(phoneNumber.value)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
if (!verifyCode.value) {
uni.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
if (!/^\d{6}$/.test(verifyCode.value)) {
uni.showToast({ title: '验证码为 6 位数字', icon: 'none' });
return;
}
if (!account.value) {
uni.showToast({ title: '请输入账号', icon: 'none' });
return;
}
if (account.value.length < 4 || account.value.length > 20) {
uni.showToast({ title: '账号长度需为 4-20 位', icon: 'none' });
return;
}
if (!password.value) {
uni.showToast({ title: '请输入密码', icon: 'none' });
return;
}
if (password.value.length < 6) {
uni.showToast({ title: '密码至少输入 6 位', icon: 'none' });
return;
}
if (password.value.length > 16) {
uni.showToast({ title: '密码不能超过 16 位', icon: 'none' });
return;
}
if (!agreeAgreement.value) {
uni.showToast({ title: '请勾选协议', icon: 'none' });
return;
}
uni.showLoading({
title: '注册中...',
mask: true
// ========== 计算属性 ==========
const canGetVerify = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
let params = {
phone: phoneNumber.value,
smsCode: verifyCode.value,
username: account.value,
password: password.value
const canRegister = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
const verifyReg = /^\d{6}$/;
const accountReg = /^.{4,20}$/;
const pwdReg = /^.{6,16}$/;
return phoneReg.test(phoneNumber.value) &&
verifyReg.test(verifyCode.value) &&
accountReg.test(account.value) &&
pwdReg.test(password.value) &&
agreeAgreement.value;
});
// 实时校验密码格式(禁止颜文字/emoji/特殊符号,只允许中文、字母、数字)
watch(password, (val) => {
if (!val) { passwordError.value = ''; return }
if (val.length < 6 || val.length > 16) {
passwordError.value = '密码需 6-16 位'
} else if (/[^一-龥a-zA-Z0-9]/.test(val)) {
passwordError.value = '密码不能包含特殊符号或表情'
} else {
passwordError.value = ''
}
})
// ========== 方法 ==========
const goBack = () => {
uni.navigateBack();
};
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 账号失焦校验(禁止颜文字/emoji/特殊符号,只允许中文、字母、数字)
const validateAccount = () => {
const val = account.value
if (!val) { accountError.value = ''; return }
if (/\s/.test(val)) {
accountError.value = '账号不能包含空格'
} else if (val.length < 4 || val.length > 20) {
accountError.value = '账号需 4-20 位'
} else if (/[^一-龥a-zA-Z0-9]/.test(val)) {
accountError.value = '账号不能包含特殊符号或表情'
} else {
accountError.value = ''
}
};
// 账号聚焦清除错误提示
const clearAccountError = () => {
accountError.value = ''
};
// 密码失焦校验(禁止颜文字/emoji/特殊符号,只允许中文、字母、数字)
const validatePassword = () => {
const val = password.value
if (!val) { passwordError.value = ''; return }
if (val.length < 6 || val.length > 16) {
passwordError.value = '密码需 6-16 位'
} else if (/[^一-龥a-zA-Z0-9]/.test(val)) {
passwordError.value = '密码不能包含特殊符号或表情'
} else {
passwordError.value = ''
}
};
// 密码聚焦清除错误提示
const clearPasswordError = () => {
passwordError.value = ''
};
const goToUserAgreement = () => {
uni.navigateTo({ url: '/pageSubPack/loginSub/user-agreement' })
}
getRegisterUser(params).then(res => {
if (res.code === 200) {
const goToPrivacy = () => {
uni.navigateTo({ url: '/pageSubPack/loginSub/privacy' })
}
const checkForm = () => {};
const openPrivacyPopup = (action) => {
pendingAction.value = action || ''
showPrivacyPopup.value = true
}
const agreePrivacy = () => {
showPrivacyPopup.value = false
agreeAgreement.value = true
if (pendingAction.value === 'register') {
doRegister()
}
pendingAction.value = ''
}
const disagreePrivacy = () => {
showPrivacyPopup.value = false
pendingAction.value = ''
}
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone: phoneNumber.value,
scene: 'register'
}
getSendCode(params).then(res => {
if (res.code === 200) {
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
} else {
uni.showToast({
title: '验证码发送失败',
icon: 'none'
});
}
}).catch(err => {
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
});
})
};
// 处理注册逻辑
const handleRegister = () => {
if (!phoneNumber.value) {
uni.showToast({ title: '请输入手机号', icon: 'none' });
return;
}
if (!/^1[3-9]\d{9}$/.test(phoneNumber.value)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
if (!verifyCode.value) {
uni.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
if (!/^\d{6}$/.test(verifyCode.value)) {
uni.showToast({ title: '验证码为 6 位数字', icon: 'none' });
return;
}
if (!account.value) {
uni.showToast({ title: '请输入账号', icon: 'none' });
return;
}
if (account.value.length < 4 || account.value.length > 20) {
uni.showToast({ title: '账号长度需为 4-20 位', icon: 'none' });
return;
}
if (/\s/.test(account.value)) {
uni.showToast({ title: '账号不能包含空格', icon: 'none' });
return;
}
if (/[^一-龥a-zA-Z0-9]/.test(account.value)) {
uni.showToast({ title: '账号不能包含特殊符号或表情', icon: 'none' });
return;
}
if (!password.value) {
uni.showToast({ title: '请输入密码', icon: 'none' });
return;
}
if (password.value.length < 6) {
uni.showToast({ title: '密码至少输入 6 位', icon: 'none' });
return;
}
if (password.value.length > 16) {
uni.showToast({ title: '密码不能超过 16 位', icon: 'none' });
return;
}
if (/[^一-龥a-zA-Z0-9]/.test(password.value)) {
uni.showToast({ title: '密码不能包含特殊符号或表情', icon: 'none' });
return;
}
if (!agreeAgreement.value) {
openPrivacyPopup('register')
return;
}
doRegister()
};
const doRegister = () => {
uni.showLoading({
title: '注册中...',
mask: true
});
let params = {
phone: phoneNumber.value,
smsCode: verifyCode.value,
username: account.value,
password: password.value
}
getRegisterUser(params).then(res => {
if (res.code === 200) {
uni.hideLoading();
uni.showToast({
title: '注册成功',
icon: 'success'
});
setTimeout(() => {
uni.navigateTo({
url: '/pageSubPack/loginSub/login?mode=pwd'
});
}, 1500);
} else {
uni.showToast({
title: '注册失败',
icon: 'none'
});
}
}).catch((err) => {
uni.hideLoading();
uni.showToast({
title: '注册成功',
icon: 'success'
title: err.msg || '网络异常',
icon: 'none'
});
setTimeout(() => {
uni.redirectTo({
url: '/pageSubPack/loginSub/pwd-login'
});
}, 1500);
})
};
} else {
uni.showToast({
title: '注册失败',
icon: 'error'
});
// ========== 生命周期 ==========
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
}).catch((err) => {
uni.showToast({
title: err.msg || '网络异常',
icon: 'error'
});
})
// 注册成功后跳转到登录页
// uni.redirectTo({
// url: '/pages/login/pwd-login'
// });
});
</script>
// setTimeout(() => {
// }, 1500);
};
// ========== 生命周期 ==========
// 页面卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer);
<style scoped>
/* 页面容器 */
.register-page {
padding: 20rpx 40rpx 40rpx;
height: 100vh;
overflow: hidden;
box-sizing: border-box;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF);
}
});
</script>
<style scoped>
/* 页面容器 */
.register-page {
padding: 20rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF);
}
.status_bar {
height: 46px;
width: 100%;
}
.status_bar {
height: 46px;
width: 100%;
}
.nav-bar {
display: flex;
align-items: center;
margin-bottom: 70rpx;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
/* height: 88rpx; */
margin-bottom: 70rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #333;
margin-right: 100rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #333;
margin-right: 100rpx;
}
.register-title {
font-size: 40rpx;
font-weight: 600;
color: #333;
margin-bottom: 60rpx;
}
/* 注册标题 */
.register-title {
font-size: 40rpx;
font-weight: 600;
color: #333;
margin-bottom: 60rpx;
}
/* 输入框外层包装(用于承载错误提示) */
.input-item-wrap {
margin-bottom: 30rpx;
}
/* 输入框通用样式 */
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.input-item-wrap .input-item {
margin-bottom: 0;
}
/* 验证码输入项特殊布局 */
.verify-item {
position: relative;
padding-right: 220rpx;
}
/* 输入框通用样式 */
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
/* 输入框图标 */
.input-icon {
margin-right: 20rpx;
color: #999;
width: 50rpx;
height: 50rpx;
}
/* 验证码输入项特殊布局 */
.verify-item {
position: relative;
padding-right: 220rpx;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
/* 输入框图标 */
.input-icon {
margin-right: 20rpx;
color: #999;
width: 50rpx;
height: 50rpx;
}
/* 深度覆盖 uni-easyinput 内部默认样式,解决高度偏移 */
.input-content :deep(.uni-easyinput__content) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
background-color: transparent !important;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
.input-content :deep(.uni-easyinput__content-input) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
line-height: 88rpx;
}
/* 原生 input 垂直居中 */
.native-input {
line-height: 88rpx;
}
.verify-input {
padding-right: 20rpx;
}
/* 深度覆盖 uni-easyinput 内部默认样式 */
.input-content :deep(.uni-easyinput__content) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
background-color: transparent !important;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #007aff;
color: #fff;
border-radius: 35rpx;
font-size: 26rpx;
padding: 0;
margin: 0;
}
.input-content :deep(.uni-easyinput__content-input) {
height: 100% !important;
min-height: unset !important;
padding: 0 !important;
line-height: 88rpx;
}
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
}
.verify-input {
padding-right: 20rpx;
}
.get-verify-btn::after {
border-radius: 50px;
}
/* 输入框错误提示(在输入框下方) */
.input-error {
color: #e74c3c;
font-size: 22rpx;
line-height: 1.4;
padding: 10rpx 30rpx 0;
}
/* 协议勾选项 */
.agreement-item {
display: flex;
align-items: center;
margin: 70rpx 0 28rpx 0;
padding-left: 10rpx;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #007aff;
color: #fff;
border-radius: 35rpx;
font-size: 26rpx;
padding: 0;
margin: 0;
}
.agreement-text {
font-size: 24rpx;
color: #666;
margin-left: 10rpx;
}
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
}
.agreePrivacy {
color: #2F77FD;
}
.get-verify-btn::after {
border-radius: 50px;
}
/* 立即注册按钮 */
.register-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 40rpx;
}
/* 协议勾选项 */
.agreement-item {
display: flex;
align-items: center;
margin: 70rpx 0 28rpx 0;
padding-left: 10rpx;
}
button:after {
border-radius: 8px;
}
.agreement-text {
font-size: 24rpx;
color: #666;
margin-left: 10rpx;
}
.register-btn:disabled {
background-color: #99c8ff;
color: #fff;
}
.agreePrivacy {
color: #2F77FD;
}
/* 跳转登录链接 */
.login-link {
text-align: center;
}
/* 立即注册按钮 */
.register-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 40rpx;
}
.link-item {
font-size: 28rpx;
color: #007aff;
text-decoration: none;
}
</style>
button:after {
border-radius: 8px;
}
.register-btn:disabled {
background-color: #99c8ff;
color: #fff;
}
/* 跳转登录链接 */
.login-link {
text-align: center;
}
.link-item {
font-size: 28rpx;
color: #007aff;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,159 @@
<template>
<view class="agreement-page">
<!-- 协议内容 -->
<scroll-view class="agreement-content" scroll-y>
<view class="content-wrap">
<view class="title">用户协议</view>
<view class="date-info">
<view class="date-item">更新日期2026年06月18日</view>
<view class="date-item">生效日期2026年06月18日</view>
</view>
<view class="section">
<view class="section-text">
欢迎使用我们的产品/服务以下统称"服务"请您在使用我们的服务前仔细阅读并充分理解本协议的全部内容
一旦您使用我们的服务即视为您已阅读并同意接受本协议所有条款的约束
</view>
</view>
<view class="section">
<view class="section-title">账户注册与使用</view>
<view class="section-text">
1. 您在注册账户时应提供真实准确的个人信息并确保及时更新因信息不真实或不准确产生的一切后果由您自行承担
</view>
<view class="section-text">
2. 您应妥善保管您的账户及密码因账户或密码泄露导致的一切损失由您自行承担
</view>
<view class="section-text">
3. 您不得将账户借给他人使用每个账户仅供您个人使用
</view>
</view>
<view class="section">
<view class="section-title">服务内容</view>
<view class="section-text">
1. 我们为您提供物业服务相关的各项功能包括但不限于物业缴费在线报修访客邀请小区公告投诉建议等
</view>
<view class="section-text">
2. 我们有权根据业务需要对服务内容进行调整优化并在应用内公示
</view>
</view>
<view class="section">
<view class="section-title">用户行为规范</view>
<view class="section-text">
1. 您在使用服务过程中应遵守国家法律法规不得从事违法违规行为
</view>
<view class="section-text">
2. 您不得利用服务进行任何危害网络安全的行为包括但不限于传播病毒攻击服务器非法获取他人信息等
</view>
<view class="section-text">
3. 如发现任何违法或不当内容请及时通过应用内的"联系物业"功能向我们举报
</view>
</view>
<view class="section">
<view class="section-title">知识产权</view>
<view class="section-text">
本服务涉及的软件界面设计文字图片音频视频等内容的知识产权归我们所有未经书面许可任何人不得以任何方式进行使用复制修改或传播
</view>
</view>
<view class="section">
<view class="section-title">免责声明</view>
<view class="section-text">
1. 因不可抗力或不可预见的原因导致服务中断或数据丢失我们不承担责任但将尽力减少因此给您带来的损失
</view>
<view class="section-text">
2. 您理解并同意使用本服务可能存在风险我们不对因使用本服务导致的任何间接损失承担责任
</view>
</view>
<view class="section">
<view class="section-title">协议变更</view>
<view class="section-text">
我们有权根据需要修改本协议条款修改后的协议将在应用内公布如您继续使用服务即表示您同意接受修改后的协议
</view>
</view>
<view class="section">
<view class="section-title">联系我们</view>
<view class="section-text">
如果您对本协议有任何疑问或建议请通过应用内的"联系物业"功能与我们取得联系
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
const goBack = () => {
uni.navigateBack({ delta: 1 })
}
</script>
<style scoped>
.agreement-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #fff;
}
.agreement-content {
flex: 1;
overflow-y: auto;
}
.content-wrap {
padding: 30rpx;
}
.title {
font-size: 32rpx;
font-weight: 600;
text-align: center;
margin-bottom: 24rpx;
color: #333;
}
.date-info {
text-align: center;
margin-bottom: 40rpx;
}
.date-item {
font-size: 24rpx;
color: #999;
line-height: 1.8;
}
.title {
font-size: 32rpx;
font-weight: 600;
text-align: center;
margin-bottom: 40rpx;
color: #333;
}
.section {
margin-bottom: 30rpx;
}
.section-title {
font-size: 28rpx;
font-weight: 500;
color: #333;
margin-bottom: 16rpx;
}
.section-text {
font-size: 26rpx;
line-height: 1.8;
color: #666;
margin-bottom: 12rpx;
}
</style>

View File

@@ -10,13 +10,13 @@
<view class="form-item">
<view class="item-label">车牌号</view>
<input class="item-input" v-model="formData.carNum" placeholder="请输入"
placeholder-class="input-placeholder" />
placeholder-class="input-placeholder" maxlength="8" @input="onCarNumInput" />
</view>
<view class="form-item">
<text class="item-label">车辆品牌</text>
<input class="item-input" v-model="formData.carBrand" placeholder="请输入"
placeholder-class="input-placeholder" />
placeholder-class="input-placeholder" maxlength="32"/>
</view>
<!-- <view class="form-item" @click="selectItem('车辆品牌')">
<text class="item-label">车辆品牌</text>
@@ -28,13 +28,13 @@
<view class="form-item">
<view class="item-label">车辆型号</view>
<input class="item-input" v-model="formData.carModel" placeholder="请输入"
placeholder-class="input-placeholder" />
placeholder-class="input-placeholder" maxlength="32"/>
</view>
<view class="form-item">
<view class="item-label">车辆颜色</view>
<input class="item-input" v-model="formData.carColor" placeholder="请输入"
placeholder-class="input-placeholder" />
placeholder-class="input-placeholder" maxlength="5"/>
</view>
<view class="form-item upload-item">
@@ -50,7 +50,7 @@
</view>
</scroll-view>
<view class="bottom-btn-wrap">
<button class="next-btn" @click="saveCar">保存</button>
<button class="next-btn" :disabled="isSubmitting" @click="saveCar">保存</button>
</view>
</view>
</template>
@@ -76,15 +76,15 @@
} from '/pageSubPack/api/useImageUpload.js'
const statusBarHeight = ref(20)
const goBack = () => {
uni.navigateBack()
}
const id = ref(undefined)
const type = ref('')
const isSubmitting = ref(false)
const formData = ref({
// carNum: uni.getStorageSync('carNum'),
// carBrand: uni.getStorageSync('carBrand'),
// carModel: uni.getStorageSync('carModel'),
// carColor: uni.getStorageSync('carColor'),
// carImageUrl: uni.getStorageSync('carImageUrl'),
// carBrandId: uni.getStorageSync('carBrandId')
carNum: '',
carBrand: '',
carModel: '',
@@ -137,10 +137,9 @@
} catch (err) {
uni.showToast({
title: `${err}`,
title: err.msg || '网络异常',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
}
@@ -151,6 +150,33 @@
uploadUrl: '/api/resident/file/uploadImage'
})
// 车牌号输入限制:省份简称(中文)+ 城市代码(字母)+ 序号(字母数字)
const onCarNumInput = (e) => {
let value = e.detail.value || ''
// 过滤非法字符,只保留中文、大写字母、数字
value = value.replace(/[^一-龥A-Z0-9]/g, '')
// 首字符必须是中文省份简称
if (value.length > 0 && !/^[一-龥]/.test(value)) {
value = value.replace(/[^一-龥]/g, '')
}
// 第二位必须是字母
if (value.length > 1) {
const first = value.charAt(0)
const second = value.charAt(1)
if (!/^[A-Z]$/.test(second)) {
value = first + second.replace(/[^A-Za-z]/g, '').toUpperCase()
}
// 剩余位只允许字母和数字
if (value.length > 2) {
const rest = value.substring(2)
value = value.substring(0, 2) + rest.replace(/[^A-Za-z0-9]/g, '').toUpperCase()
}
}
// 统一转大写
value = value.toUpperCase()
formData.value.carNum = value
}
const inputChange = (typeStr) => {
if (typeStr == '车牌号') {
uni.setStorageSync('carNum', formData.value.carNum)
@@ -171,32 +197,26 @@
}
}
// 车牌号正则:省份简称 + 城市代码字母 + 序号
// 蓝牌5位字母数字新能源绿牌6位其中第3位或末位必须是 D/F
const CAR_NUM_REGEX = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤川青藏琼宁][A-Z](?:[A-Z0-9]{5}|[DF][A-Z0-9]{5}|[A-Z0-9]{5}[DF])$/
const saveCar = async () => {
if (isSubmitting.value) return
if (!formData.value.carNum) {
return uni.showToast({
title: "请输入车牌号",
icon: "none"
})
}
if (!formData.value.carBrand) {
if (!CAR_NUM_REGEX.test(formData.value.carNum)) {
return uni.showToast({
title: "请输入车辆品牌",
icon: "none"
})
}
if (!formData.value.carModel) {
return uni.showToast({
title: "请输入车辆型号",
icon: "none"
})
}
if (!formData.value.carColor) {
return uni.showToast({
title: "请输入车辆颜色",
title: "请输入正确的车牌号",
icon: "none"
})
}
isSubmitting.value = true
uni.showLoading({
title: '提交中...',
mask: true
@@ -219,14 +239,16 @@
submitCarData(carImageUrl)
} catch (err) {
isSubmitting.value = false
uni.hideLoading()
uni.showToast({
title: err.msg || '上传失败',
title: err.msg || '网络异常',
icon: 'none'
})
}
}
const submitCarData = (carImageUrl) => {
// 处理 carImageUrl 可能是对象 { url: string } 的情况
const imageUrl = typeof carImageUrl === 'object' ? carImageUrl.url : carImageUrl
@@ -237,7 +259,8 @@
carModel: formData.value.carModel,
carColor: formData.value.carColor,
carImageUrl: imageUrl,
residentId: uni.getStorageSync('userId')
villageId:uni.getStorageSync('currentVillageId'),
userId: uni.getStorageSync('userId')
}
@@ -253,16 +276,31 @@
icon: 'success'
})
setTimeout(() => {
goBack()
uni.removeStorageSync("carNum")
uni.removeStorageSync("carBrand")
uni.removeStorageSync("carModel")
uni.removeStorageSync("carColor")
uni.removeStorageSync("carImageUrl")
uni.removeStorageSync("carBrandId")
uni.removeStorageSync("checkType")
uni.navigateBack()
}, 1500)
} else {
isSubmitting.value = false
uni.showToast({
title: res.msg || '提交失败',
icon: 'none'
})
}
}).catch((err) => {
isSubmitting.value = false
uni.hideLoading()
uni.showToast({
title: err.msg || '提交失败',
icon: 'none'
})
})
}else{
} else {
postCar(params).then(res => {
uni.hideLoading()
if (res.code === 200) {
@@ -271,40 +309,43 @@
icon: 'success'
})
setTimeout(() => {
goBack()
uni.removeStorageSync("carNum")
uni.removeStorageSync("carBrand")
uni.removeStorageSync("carModel")
uni.removeStorageSync("carColor")
uni.removeStorageSync("carImageUrl")
uni.removeStorageSync("carBrandId")
uni.removeStorageSync("checkType")
uni.navigateBack()
}, 1500)
} else {
isSubmitting.value = false
uni.showToast({
title: res.msg || '提交失败',
icon: 'none'
})
}
}).catch((err) => {
isSubmitting.value = false
uni.hideLoading()
uni.showToast({
title: err.msg || '提交失败',
icon: 'none'
})
})
}
}
const selectItem = (typeStr) => {
if (typeStr === '车辆品牌') {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/selectCarBrand'
})
}
}
const goBack = () => {
uni.removeStorageSync('carNum')
uni.removeStorageSync('carBrand')
uni.removeStorageSync('carModel')
uni.removeStorageSync('carColor')
uni.removeStorageSync('carImageUrl')
uni.removeStorageSync('carBrandId')
uni.removeStorageSync('checkType')
uni.redirectTo({
url: '/pageSubPack/my/myCarIndex'
})
}
</script>
<style lang="scss">
@@ -451,4 +492,4 @@
color: #999 !important;
font-size: 28rpx !important;
}
</style>
</style>

View File

@@ -71,35 +71,18 @@
<text class="item-arrow">\u003E</text>
</view> -->
<!-- 点击这里弹出下拉选择 -->
<view class="form-item" @click="showPopup = true">
<view class="form-item" style="opacity: 0.6;">
<text class="label">房屋状态</text>
<text class="value"
:class="{selected:formData.status}">{{ formData.status.name || '请选择' }}</text>
<text class="item-arrow"></text>
</view>
<!-- 底部弹出下拉选择框全端可用 -->
<view v-if="showPopup" class="mask" @click="showPopup=false"></view>
<view v-if="showPopup" class="popup">
<view class="popup-header">
<text class="title">选择房屋状态</text>
<text class="close" @click="showPopup = false"></text>
</view>
<view class="list">
<view :class="{ active: formData.status.id == item.id }" class="item"
v-for="(item, index) in statusList" :key="upDataListNum" @click="selectClick(item)">
{{ item.name }}
</view>
</view>
</view>
<!-- 设为默认房屋开关 -->
<view class="form-item switch-item">
<!-- <view class="form-item switch-item" v-if="!houseId">
<text class="item-label" style="width: 100px;">设为默认房屋</text>
<switch class="no-cross-switch" :checked="formData.isDefault" color="#1677ff"
@change="onSwitchChange" />
</view>
</view> -->
</view>
<!-- 第二步 -->
<view v-if="currentStep==1">
@@ -107,7 +90,7 @@
<scroll-view scroll-y class="form-box">
<view class="form-item">
<text class="label">住户名称</text>
<input class="input" v-model="form.name" placeholder="请输入真实姓名" placeholder-class="ph" />
<input class="input" v-model="form.name" @input="onNameInput" placeholder="请输入真实姓名" placeholder-class="ph" maxlength="20"/>
</view>
<view class="form-item">
@@ -134,7 +117,7 @@
<view class="form-item">
<text class="label">证件号码</text>
<input class="input" v-model="form.cardNo" placeholder="请输入证件号码" placeholder-class="ph" />
<input class="input" v-model="form.cardNo" @input="onCardNoInput" placeholder="请输入证件号码" placeholder-class="ph" maxlength="18"/>
</view>
<view class="form-item arrow-item" @click="openRelation">
@@ -146,16 +129,19 @@
<view class="form-item phone-item">
<text class="label">手机号码</text>
<input class="input" v-model="form.phone" placeholder="请输入手机号" placeholder-class="ph"
type="tel" />
<button class="code-btn" :disabled="cd>0" @click="sendCode">
type="tel" maxlength="11"/>
<!-- <button class="code-btn" :disabled="cd>0" @click="sendCode">
{{ cd > 0 ? cd+'s' : '获取验证码' }}
</button>
</button> -->
<button class="get-verify-btn" :disabled=" countDown > 0 || !canGetVerify" @click="getVerifyCode">
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
<view class="form-item">
<text class="label">验证码</text>
<input class="input" v-model="form.code" placeholder="请输入6位验证码" placeholder-class="ph"
type="number" />
type="number" maxlength="6"/>
</view>
<!-- 身份证上传 -->
@@ -191,42 +177,58 @@
<!-- 证件类型弹窗 -->
<view v-if="showCardType" class="mask" @click="showCardType=false"></view>
<view v-if="showCardType" class="popup">
<view class="popup-header">
<text>选择证件类型</text>
<text class="close" @click="showCardType=false"></text>
</view>
<view class="popup-list">
<view :class="{ active: form.cardType.id == item.id }" class="item" v-for="item in cardTypes"
:key="item.id" @click="selectCard(item)">
{{item.name}}
</view>
</view>
</view>
<view v-if="showRelation" class="mask" @click="showRelation=false"></view>
<view v-if="showRelation" class="popup">
<view class="popup-header">
<text>选择与业主关系</text>
<text class="close" @click="showRelation=false"></text>
</view>
<view class="popup-list">
<view :class="{ active: form.relation.id == item.id }" class="item" v-for="item,index in relations"
:key="upDataListNum" @click="selectRelation(item)">
{{item.name}}
</view>
</view>
</view>
</scroll-view>
<!-- 底部下一步按钮 -->
<view class="bottom-btn-wrap">
<button class="next-btn" @click="goNext" v-if="currentStep==0">下一步</button>
<button class="next-btn" @click="goSubmit" v-if="currentStep==1">提交审核</button>
</view>
<!-- 房屋状态弹窗 -->
<view v-if="showPopup" class="mask" @click="showPopup=false"></view>
<view v-if="showPopup" class="popup">
<view class="popup-header">
<text class="title">选择房屋状态</text>
<text class="close" @click="showPopup = false"></text>
</view>
<view class="list">
<view :class="{ active: formData.status.id == item.id }" class="item"
v-for="(item, index) in statusList" :key="upDataListNum" @click="selectClick(item)">
{{ item.name }}
</view>
</view>
</view>
<!-- 证件类型弹窗 -->
<view v-if="showCardType" class="mask" @click="showCardType=false"></view>
<view v-if="showCardType" class="popup">
<view class="popup-header">
<text>选择证件类型</text>
<text class="close" @click="showCardType=false"></text>
</view>
<view class="popup-list">
<view :class="{ active: form.cardType.id == item.id }" class="item" v-for="item in cardTypes"
:key="item.id" @click="selectCard(item)">
{{item.name}}
</view>
</view>
</view>
<!-- 与业主关系弹窗 -->
<view v-if="showRelation" class="mask" @click="showRelation=false"></view>
<view v-if="showRelation" class="popup">
<view class="popup-header">
<text>选择与业主关系</text>
<text class="close" @click="showRelation=false"></text>
</view>
<view class="popup-list">
<view :class="{ active: form.relation.id == item.id }" class="item" v-for="item,index in relations"
:key="upDataListNum" @click="selectRelation(item)">
{{item.name}}
</view>
</view>
</view>
</view>
@@ -238,11 +240,14 @@
import {
ref,
onMounted,
reactive
reactive,
onUnmounted,
computed
} from 'vue'
import {
onReachBottom,
onLoad,
onShow,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
@@ -251,6 +256,7 @@
getHouseAuthSendSmsCode,
getMyHouseDetail,
upDateMyHouse,
getHouseDataList,
} from '/pageSubPack/api/apiSub.js'
import {
useImageUpload
@@ -275,7 +281,7 @@
unitId: uni.getStorageSync('unitId'),
floorId: uni.getStorageSync('floorId'),
houseNumberId: uni.getStorageSync('houseNumberId'),
status: uni.getStorageSync('houseStatus'),
status: uni.getStorageSync('houseStatus') || { id: 0, name: '自住' },
isDefault: uni.getStorageSync('houseIsDefault') == true ? true : false, // 开关默认开启
})
const statusList = ref([{
@@ -295,64 +301,22 @@
name: '其他'
}
])
const cardTypes = ref([{
id: 0,
name: '居民身份证'
},
{
id: 1,
name: '护照'
},
{
id: 2,
name: '其他'
}
])
const cardTypes = ref([])
const relations = ref([{
id: 0,
name: '本人'
},
{
id: 1,
name: '配偶'
},
{
id: 2,
name: '父母'
},
{
id: 3,
name: '子女'
},
{
id: 4,
name: '亲属'
},
{
id: 5,
name: '租户'
},
{
id: 6,
name: '其他'
}
])
const relations = ref([])
const upDataListNum = ref(0)
// const idCardFront = ref('')
// const idCardBack = ref('')
const frontSideOfIDcardSrc = ref('http://47.104.199.163:9090/images/propertyImgs/frontSideOfIDcard.png')
const backSideOfIDcardSrc = ref('http://47.104.199.163:9090/images/propertyImgs/backSideOfIDcard.png')
const frontSideOfIDcardSrc = ref('http://wuye.ckdzkj.com/images/propertyImgs/frontSideOfIDcard.png')
const backSideOfIDcardSrc = ref('http://wuye.ckdzkj.com/images/propertyImgs/backSideOfIDcard.png')
const houseId = ref(undefined)
const type = ref('')
const currentStep = ref(0)
const showPopup = ref(false)
const cd = ref(0)
const timer = ref(null)
const countDown = ref(0)
let timer = null
const showCardType = ref(false)
const showRelation = ref(false)
@@ -364,6 +328,54 @@
const idCardBackUpload = useImageUpload({
uploadUrl: '/api/resident/file/uploadImage'
})
// ========== 计算属性 ==========
// 是否可以获取验证码(手机号格式正确)
const canGetVerify = computed(() => {
// 手机号正则11位数字以1开头
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(form.phone);
});
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone:form.phone,
scene:'house_bind'
}
uni.showLoading({ title: '发送中...', mask: true });
getHouseAuthSendSmsCode(params).then(res =>{
if(res.code === 200){
uni.hideLoading();
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
if (timer) clearInterval(timer);
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
}else{
uni.showToast({
title: '验证码发送失败',
icon: 'none'
});
}
}).catch(err => {
uni.hideLoading();
uni.showToast({
title: err.msg||'网络异常',
icon: 'none'
});
})
};
// ==================== 获取列表数据 ====================
const getDetail = async () => {
@@ -373,7 +385,7 @@
})
const data = res
console.log(data);
console.log('列表返回数据',data);
if (data.code === 200) {
uni.setStorageSync('communityName', data.data.villageName)
uni.setStorageSync('buildingName', data.data.buildingName)
@@ -382,7 +394,7 @@
uni.setStorageSync('houseNumberName', data.data.houseNo)
uni.setStorageSync('communityId', data.data.villageId)
uni.setStorageSync('buildingId', data.data.buildingId)
uni.setStorageSync('unitId', data.data.unitNo)
uni.setStorageSync('unitId', data.data.unitNoId)
uni.setStorageSync('floorId', data.data.floorNo)
uni.setStorageSync('houseNumberId', data.data.houseId)
uni.setStorageSync('houseStatus', data.data.houseStatus)
@@ -443,13 +455,13 @@
idCardBackUpload.setPreviewUrl(data.data.cardImageBack?.url || data.data.cardImageBack || '')
} else {
uni.showToast({
title: `${res.msg}`,
icon: 'error'
title: res.msg,
icon: 'none'
});
}
} catch (err) {
uni.showToast({
title: `${err}`,
title: err.msg || '网络异常',
icon: 'none'
})
} finally {
@@ -458,10 +470,46 @@
}
// 清空房屋相关缓存与表单数据(新增房屋时避免回显上一次修改残留的数据)
const clearHouseData = () => {
['communityId', 'communityName', 'buildingId', 'buildingName', 'unitId', 'unitName',
'floorId', 'floorName', 'houseNumberId', 'houseNumberName', 'houseStatus', 'houseIsDefault',
'updateHouseId'
].forEach(key => uni.removeStorageSync(key))
formData.community = ''
formData.building = ''
formData.unit = ''
formData.floor = ''
formData.houseNumber = ''
formData.communityId = ''
formData.buildingId = ''
formData.unitId = ''
formData.floorId = ''
formData.houseNumberId = ''
formData.status = { id: 0, name: '自住' }
formData.isDefault = false
form.name = ''
form.gender = '男'
form.cardType = {}
form.cardNo = ''
form.relation = {}
form.phone = ''
form.code = ''
idCardFrontUpload.reset()
idCardBackUpload.reset()
currentStep.value = 0
}
onUnload(() => {
clearInterval(timer)
})
onLoad((option) => {
onLoad(async (option) => {
// 先加载下拉选项数据,再获取详情回显,避免证件类型/与业主关系因选项未加载而无法匹配
await loadCardTypes()
await loadRelations()
console.log(uni.getStorageSync('updateHouseId'));
if (option.id) {
if (uni.getStorageSync('operationType') == 'update') {
@@ -477,26 +525,89 @@
if (uni.getStorageSync('operationType') == 'add') {
houseId.value = undefined;
type.value = '添加';
// 新增房屋时清空上一次修改残留的数据,避免回显
clearHouseData()
}
}
})
// 页面显示时从 storage 同步选择数据(从选择小区/楼栋等页面返回时)
const syncFormDataFromStorage = () => {
formData.community = uni.getStorageSync('communityName')
formData.communityId = uni.getStorageSync('communityId')
formData.building = uni.getStorageSync('buildingName')
formData.buildingId = uni.getStorageSync('buildingId')
formData.unit = uni.getStorageSync('unitName')
formData.unitId = uni.getStorageSync('unitId')
formData.floor = uni.getStorageSync('floorName')
formData.floorId = uni.getStorageSync('floorId')
formData.houseNumber = uni.getStorageSync('houseNumberName')
formData.houseNumberId = uni.getStorageSync('houseNumberId')
formData.status = uni.getStorageSync('houseStatus') || { id: 0, name: '自住' }
}
onShow(() => {
syncFormDataFromStorage()
})
const chooseIdCard = async (type) => {
try {
const tempPath = await (type === 'front' ? idCardFrontUpload.chooseImage() : idCardBackUpload
.chooseImage())
// 已经由 hook 自动处理了 previewUrl 和 tempFilePath
} catch (err) {
// 用户取消选择,不处理
}
}
// 住户名称输入过滤:只允许中文和字母
const onNameInput = (e) => {
const val = e.detail.value || ''
const filtered = val.replace(/[^一-龥a-zA-Z]/g, '')
if (filtered !== val) {
form.name = filtered
}
}
// 证件号码输入过滤:只允许字母和数字
const onCardNoInput = (e) => {
const val = e.detail.value || ''
const filtered = val.replace(/[^a-zA-Z0-9]/g, '')
if (filtered !== val) {
form.cardNo = filtered
}
}
// 18位居民身份证号码校验含出生日期与校验码
const isValidIdCard = (id) => {
const reg = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/
if (!reg.test(id)) return false
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
let sum = 0
for (let i = 0; i < 17; i++) {
sum += parseInt(id[i], 10) * weights[i]
}
return checkCodes[sum % 11] === id[17].toUpperCase()
}
// 按证件类型校验证件号码格式
const validateCardNo = () => {
const cardNo = (form.cardNo || '').toUpperCase()
const cardTypeId = String(form.cardType.id ?? '')
// 居民身份证18位含校验码
if (cardTypeId === '0') {
return isValidIdCard(cardNo)
}
// 护照:字母开头 + 7~8位数字
if (cardTypeId === '1') {
return /^[A-Z]\d{7,8}$/.test(cardNo)
}
// 港澳通行证H/M/C 开头 + 8位数字
if (cardTypeId === '2') {
return /^[HMC]\d{8}$/.test(cardNo)
}
// 其他类型:字母数字 5~18 位
return /^[A-Z0-9]{5,18}$/.test(cardNo)
}
const openCardType = () => {
showCardType.value = true
@@ -514,53 +625,46 @@
showRelation.value = false
upDataListNum.value++
}
// 获取证件类型下拉数据
const loadCardTypes = async () => {
try {
const res = await getHouseDataList({dictType:"com_id_card_type",dictName:"证件类型"})
if (res.code === 200) {
cardTypes.value = (res.rows || []).map(item => ({
id: item.dictValue,
name: item.dictLabel
}))
}
} catch (err) {
console.error('加载证件类型失败:', err)
}
}
// 获取与业主关系下拉数据
const loadRelations = async () => {
try {
const res = await getHouseDataList({dictType:"com_resident_relationship",dictName:'与业主关系'})
if (res.code === 200) {
relations.value = (res.rows || []).map(item => ({
id: item.dictValue,
name: item.dictLabel
}))
}
} catch (err) {
console.error('加载与业主关系失败:', err)
}
}
const selectClick = (item) => {
formData.status = item;
uni.setStorageSync('houseStatus', item)
showPopup.value = false;
upDataListNum.value++
}
const sendCode = () => {
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
return uni.showToast({
title: '手机号格式错误',
icon: 'none'
})
}
cd.value = 60
timer.value = setInterval(() => {
cd.value--
if (cd.value <= 0) clearInterval(timer)
}, 1000)
let params = {
phone: form.phone,
scene: "house_bind"
}
getHouseAuthSendSmsCode(params).then(res => {
if (res.code === 200) {
uni.showToast({
title: `发送成功!`,
icon: 'error'
});
} else {
uni.showToast({
title: `${res.msg}`,
icon: 'error'
});
}
}).catch(err => {
uni.showToast({
title: err.msg || '发送失败',
icon: 'none'
});
})
}
// 下一步
const goNext = () => {
console.log(typeof(uni.getStorageSync('communityId')));
// 基础校验
if (!formData.community) {
uni.showToast({
title: '请选择小区',
@@ -604,13 +708,11 @@
return
}
currentStep.value = 1
//跳转
}
// 完整校验
const goSubmit = async () => {
// 1. 前端校验
if (!form.name) return uni.showToast({
title: '请输入住户姓名',
icon: 'none'
@@ -627,6 +729,10 @@
title: '请输入证件号',
icon: 'none'
})
if (!validateCardNo()) return uni.showToast({
title: '证件号码格式错误',
icon: 'none'
})
if (!form.relation && form.relation != 0) return uni.showToast({
title: '请选择关系',
icon: 'none'
@@ -647,8 +753,7 @@
title: '请上传身份证国徽面',
icon: 'none'
})
// 2. 确认弹窗
const modalRes = await new Promise((resolve) => {
uni.showModal({
title: '确认提交',
@@ -658,7 +763,6 @@
})
if (!modalRes.confirm) return
// 3. 开始提交
uni.showLoading({
title: '提交中...',
mask: true
@@ -694,7 +798,7 @@
const params = {
villageId: uni.getStorageSync('communityId'),
buildingId: uni.getStorageSync('buildingId'),
unitNo: uni.getStorageSync('unitId'),
unitNoId: uni.getStorageSync('unitId'),
floorNo: uni.getStorageSync('floorId'),
houseNo: uni.getStorageSync('houseNumberName'),
houseId: uni.getStorageSync('houseNumberId'),
@@ -702,11 +806,11 @@
gender: form.gender == '男' ? '0' : '1',
idCardType: form.cardType.id,
idCard: form.cardNo,
houseStatus: uni.getStorageSync('houseStatus').id,
houseStatus: formData.status.id,
ownerRelationship: form.relation.id,
phone: form.phone,
smsCode: form.code,
isDefault: uni.getStorageSync('houseIsDefault'),
isDefault: formData.isDefault,
cardImageFront: cardImageFrontUrl.url,
cardImageBack: cardImageBackUrl.url,
}
@@ -727,25 +831,53 @@
title: '提交成功',
icon: 'success'
})
// 标记需要从绑定房屋返回首页后重新加载用户信息
uni.setStorageSync('needReloadUserInfo', true)
setTimeout(() => {
goBack()
uni.removeStorageSync("communityId");
uni.removeStorageSync("communityName");
uni.removeStorageSync("buildingId");
uni.removeStorageSync("buildingName");
uni.removeStorageSync("unitId");
uni.removeStorageSync("unitName");
uni.removeStorageSync("floorId");
uni.removeStorageSync("floorName");
uni.removeStorageSync("houseNumberId");
uni.removeStorageSync("houseNumberName");
uni.removeStorageSync("houseStatus");
uni.removeStorageSync("houseIsDefault");
uni.removeStorageSync("updateHouseId");
// 在页面栈中找到 myHouseIndex 并精准返回
const pages = getCurrentPages()
let targetIndex = -1
for (let i = pages.length - 1; i >= 0; i--) {
if (pages[i].route && pages[i].route.includes('myHouseIndex')) {
targetIndex = i
break
}
}
if (targetIndex >= 0) {
const delta = pages.length - 1 - targetIndex
uni.navigateBack({ delta })
} else {
uni.redirectTo({ url: "/pageSubPack/my/myHouseIndex" })
}
}, 1500)
} catch (err) {
// 8. 统一错误处理
uni.hideLoading()
console.log('提交失败:', err)
console.log('提交失败11111111111111', err)
uni.showToast({
title: err.msg || '提交失败',
icon: 'none',
duration: 2500
})
}
}
const updateMethod = async (params) => {
await upDateMyHouse(params).then(res => {
console.log('33333', res)
// console.log('33333', res)
if (res.code === 200) {
uni.hideLoading();
uni.showToast({
@@ -757,33 +889,27 @@
}, 1500);
} else {
uni.showToast({
title: `${res.msg}`,
icon: 'error'
title: res.msg,
icon: 'none'
});
}
}).catch(err => {
console.log(err);
uni.showToast({
title: `${err}`,
icon: 'error'
title: err.msg,
icon: 'none'
});
})
}
// 选择项点击事件可替换为picker/弹窗选择)
const selectItem = (type) => {
console.log(type);
console.log(uni.getStorageSync('checkType'));
if (type === '小区') {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/selectResidentialCommunity?sourcePage=' + 'addHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
} else if (type === '楼栋') {
if (!formData.community) {
@@ -794,11 +920,8 @@
} else {
uni.setStorageSync('checkType', '楼栋');
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/selectBuilding',
success: res => {},
fail: () => {},
complete: () => {}
});
}
@@ -811,11 +934,9 @@
} else {
uni.setStorageSync('checkType', '单元');
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/selectBuilding',
success: res => {},
fail: () => {},
complete: () => {}
});
}
} else if (type === '楼层') {
@@ -827,11 +948,9 @@
} else {
uni.setStorageSync('checkType', '楼层');
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/selectBuilding',
success: res => {},
fail: () => {},
complete: () => {}
});
}
} else if (type === '户号') {
@@ -844,42 +963,50 @@
} else {
uni.setStorageSync('checkType', '户号');
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/selectBuilding',
success: res => {},
fail: () => {},
complete: () => {}
});
}
}
}
// 开关切换
const onSwitchChange = (e) => {
/* const onSwitchChange = (e) => {
formData.isDefault = e.detail.value
uni.setStorageSync('houseIsDefault', e.detail.value)
}
} */
// ========== 生命周期 ==========
// 页面卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
const goBack = () => {
uni.removeStorageSync('communityId');
uni.removeStorageSync('communityName');
uni.removeStorageSync('buildingId');
uni.removeStorageSync('buildingName');
uni.removeStorageSync('unitId');
uni.removeStorageSync('unitName');
uni.removeStorageSync('floorId');
uni.removeStorageSync('floorName');
uni.removeStorageSync('houseNumberId');
uni.removeStorageSync('houseNumberName');
uni.removeStorageSync('houseStatus')
uni.removeStorageSync('houseIsDefault')
uni.removeStorageSync('updateHouseId')
uni.redirectTo({
url: '/pageSubPack/my/myHouseIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
// 第二步返回第一步
if (currentStep.value === 1) {
currentStep.value = 0
return
}
uni.removeStorageSync("communityId");
uni.removeStorageSync("communityName");
uni.removeStorageSync("buildingId");
uni.removeStorageSync("buildingName");
uni.removeStorageSync("unitId");
uni.removeStorageSync("unitName");
uni.removeStorageSync("floorId");
uni.removeStorageSync("floorName");
uni.removeStorageSync("houseNumberId");
uni.removeStorageSync("houseNumberName");
uni.removeStorageSync("houseStatus")
uni.removeStorageSync("houseIsDefault")
uni.removeStorageSync("updateHouseId")
uni.navigateBack()
// uni.redirectTo({
// url: '/pageSubPack/my/myHouseIndex',
// });
}
</script>
<style lang="scss">
@@ -1018,6 +1145,32 @@
font-size: 28rpx;
color: #333;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #007aff;
color: #fff;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
margin: 0;
}
/* 获取验证码按钮禁用状态 */
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
border-radius: 50px;
}
.get-verify-btn::after{
border-radius: 50px;
}
/* .item-arrow {
font-size: 40rpx;

View File

@@ -7,11 +7,11 @@
<scroll-view class="page" scroll-y="true">
<view class="form-container">
<view class="form-item" @click="selectItem('选择小区')">
<view class="form-item" :class="{ 'form-item-disabled': type === '修改' }" @click="selectItem('选择小区')">
<text class="item-label">选择小区</text>
<text class="item-placeholder"
:class="{selected: formData.community}">{{ formData.community || '请选择' }}</text>
<text class="item-arrow"></text>
<text class="item-arrow" v-if="type !== '修改'"></text>
</view>
<!-- <view class="form-item" @click="selectItem('绑定房屋')">
@@ -20,18 +20,18 @@
<text class="item-arrow"></text>
</view> -->
<view class="form-item" @click="selectItem('停车场')">
<view class="form-item" :class="{ 'form-item-disabled': type === '修改' }" @click="selectItem('停车场')">
<text class="item-label">停车场</text>
<text class="item-placeholder"
:class="{selected: formData.parkingLot}">{{ formData.parkingLot || '请选择' }}</text>
<text class="item-arrow"></text>
<text class="item-arrow" v-if="type !== '修改'"></text>
</view>
<view class="form-item" @click="selectItem('车位编号')">
<view class="form-item" :class="{ 'form-item-disabled': type === '修改' }" @click="selectItem('车位编号')">
<text class="item-label">车位编号</text>
<text class="item-placeholder"
:class="{selected: formData.parkingSpaceNum}">{{ formData.parkingSpaceNum || '请选择' }}</text>
<text class="item-arrow"></text>
<text class="item-arrow" v-if="type !== '修改'"></text>
</view>
<view class="form-item">
@@ -54,8 +54,15 @@
</view>
</view>
</view>
<view class="form-item" :class="{ 'form-item-disabled': checkStatus === '1' }" @click="selectItem('选择业主')">
<text class="item-label">选择业主</text>
<text class="item-placeholder"
:class="{selected: formData.owner}">{{ formData.owner || '请选择' }}</text>
<text class="item-arrow" v-if="checkStatus !== '1'"></text>
</view>
<view class="form-item" @click="bindCarClick">
<view class="form-item" @click="bindCarClick" v-if="formData.certificationStatus === '1'">
<text class="item-label">绑定车辆</text>
<text class="item-placeholder"
:class="{selected: formData.bindCar}">{{ formData.bindCar || '请选择' }}</text>
@@ -86,8 +93,14 @@
} from '@/pageSubPack/api/apiSub.js'
const statusBarHeight = ref(20)
const goBack = () => {
uni.navigateBack()
}
const id = ref(undefined)
const type = ref('')
const checkStatus = ref('')
const formData = ref({
community: uni.getStorageSync('communityName'),
communityId: uni.getStorageSync('communityId'),
@@ -100,6 +113,9 @@
parkingStatus: uni.getStorageSync('parkingStatus') || '0',
bindCar: uni.getStorageSync('bindCarName'),
bindCarId: uni.getStorageSync('bindCarId'),
owner: uni.getStorageSync('ownerName'),
ownerId: uni.getStorageSync('ownerId'),
certificationStatus:''
})
onShow(() => {
@@ -113,10 +129,13 @@
formData.value.parkingSpaceNum = uni.getStorageSync('parkingSpaceName')
formData.value.parkingSpaceId = uni.getStorageSync('parkingSpaceId')
formData.value.parkingStatus = uni.getStorageSync('parkingStatus') || '0',
formData.value.bindCar = uni.getStorageSync('bindCarName')
formData.value.bindCar = uni.getStorageSync('bindCarName')
formData.value.bindCarId = uni.getStorageSync('bindCarId')
formData.value.owner = uni.getStorageSync('ownerName')
formData.value.ownerId = uni.getStorageSync('ownerId')
})
onLoad((option) => {
checkStatus.value = option.checkStatus || ''
if (uni.getStorageSync('operationType') == 'update') {
if (option.id) {
id.value = option.id
@@ -129,6 +148,18 @@
} else if (uni.getStorageSync('operationType') == 'add') {
id.value = undefined
type.value = '添加'
// 添加模式下清空上次残留的缓存,保证每次进入都是干净的
uni.removeStorageSync('communityName')
uni.removeStorageSync('communityId')
uni.removeStorageSync('parkingLotName')
uni.removeStorageSync('parkingLotId')
uni.removeStorageSync('parkingSpaceName')
uni.removeStorageSync('parkingSpaceId')
uni.removeStorageSync('parkingStatus')
uni.removeStorageSync('bindCarName')
uni.removeStorageSync('bindCarId')
uni.removeStorageSync('ownerName')
uni.removeStorageSync('ownerId')
}
})
@@ -178,7 +209,8 @@
parkingId: formData.value.parkingSpaceId,
parkingStatus: formData.value.parkingStatus,
carId: formData.value.bindCarId,
residentId: uni.getStorageSync('userId')
// residentId: uni.getStorageSync('userId')
residentId: formData.value.ownerId
}).then(res => {
uni.hideLoading()
console.log(res);
@@ -188,7 +220,19 @@
icon: 'success'
})
setTimeout(() => {
goBack()
uni.removeStorageSync("communityId")
uni.removeStorageSync("communityName")
uni.removeStorageSync("checkType")
uni.removeStorageSync("parkingLotId")
uni.removeStorageSync("parkingLotName")
uni.removeStorageSync("parkingSpaceId")
uni.removeStorageSync("parkingStatus")
uni.removeStorageSync("parkingSpaceName")
uni.removeStorageSync("bindCarId")
uni.removeStorageSync("bindCarName")
uni.removeStorageSync("ownerId")
uni.removeStorageSync("ownerName")
uni.navigateBack({ delta: type.value === '修改' ? 2 : 1 })
}, 1500)
} else {
uni.showToast({
@@ -231,21 +275,23 @@
formData.value.parkingSpaceId = data.id
uni.setStorageSync('parkingSpaceId', data.id)
formData.value.parkingStatus = data.parkingStatus,
uni.setStorageSync('parkingStatus', data.parkingStatus)
uni.setStorageSync('parkingStatus', data.parkingStatus)
formData.value.bindCar = data.carNum
uni.setStorageSync('bindCarName', data.carNum)
formData.value.bindCarId = data.carId
uni.setStorageSync('bindCarId', data.carId)
formData.value.owner = data.residentName
uni.setStorageSync('ownerName', data.residentName)
formData.value.ownerId = data.residentId
uni.setStorageSync('ownerId', data.residentId)
formData.value.certificationStatus = data.certificationStatus
} catch (err) {
uni.showToast({
title: `${err}`,
title: err.msg,
icon: 'none'
})
console.error('列表接口报错:', err)
@@ -254,14 +300,18 @@
}
}
const bindCarClick = () => {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/bindCar'
})
}
const selectItem = (typeStr) => {
// 修改模式下,小区、停车场、车位编号禁止修改
if (type.value === '修改' && (typeStr === '选择小区' || typeStr === '停车场' || typeStr === '车位编号')) {
return
}
if (typeStr === '选择小区') {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/selectResidentialCommunity?sourcePage=addParkingSpace'
})
}
@@ -269,7 +319,7 @@
// if (!formData.value.community) {
// return uni.showToast({ title: '请先选择上级', icon: 'none' })
// }
// uni.redirectTo({ url: '/pageSubPack/my/bindHouse' })
// uni.navigateTo({ url: '/pageSubPack/my/bindHouse' })
// }
if (typeStr === '停车场') {
if (!formData.value.community) {
@@ -279,7 +329,7 @@
})
}
uni.setStorageSync('checkType', '停车场')
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/selectParingLot'
})
}
@@ -291,29 +341,25 @@
})
}
uni.setStorageSync('checkType', '车位编号')
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/selectParingLot'
})
}
if (typeStr === '选择业主') {
// 审核通过后选择业主不可编辑
if (checkStatus.value === '1') return
if (!formData.value.community) {
return uni.showToast({
title: '请先选择小区',
icon: 'none'
})
}
uni.navigateTo({
url: '/pageSubPack/my/selectOwner?ownerId=' + (formData.value.ownerId || '')
})
}
}
const goBack = () => {
uni.removeStorageSync('communityId')
uni.removeStorageSync('communityName')
// uni.removeStorageSync('bindHouseId')
// uni.removeStorageSync('bindHouseName')
uni.removeStorageSync('checkType')
uni.removeStorageSync('parkingLotId')
uni.removeStorageSync('parkingLotName')
uni.removeStorageSync('parkingSpaceId')
uni.removeStorageSync('parkingStatus')
uni.removeStorageSync('parkingSpaceName')
uni.removeStorageSync('bindCarId')
uni.removeStorageSync('bindCarName')
uni.redirectTo({
url: '/pageSubPack/my/myParkingSpaceIndex'
})
}
</script>
<style lang="scss">
@@ -354,6 +400,11 @@
border-bottom: 1rpx solid #f0f0f0;
}
.form-item-disabled {
opacity: 0.5;
pointer-events: none;
}
.item-label {
font-size: 28rpx;
color: #000;
@@ -447,4 +498,4 @@
border-radius: 12rpx;
border: none;
}
</style>
</style>

View File

@@ -69,7 +69,7 @@
const res = await getMyCarList({
pageNum: pageNum.value,
pageSize: pageSize.value,
residentId: uni.getStorageSync('userId')
userId: uni.getStorageSync('userId')
})
const data = res
@@ -90,7 +90,7 @@
}
} catch (err) {
uni.showToast({
title: '加载失败',
title: err.msg ||'加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
@@ -134,31 +134,22 @@
const confirmBind = () => {
if (selectedIndex.value === -1) {
return uni.showToast({
title: "请先选择绑定车辆",
title: "请先选择车辆",
icon: "none"
})
}
uni.showModal({
title: "确认选择",
content: `确定绑定该车辆吗?`,
content: `确定选择该车辆吗?`,
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '绑定中...',
mask: true
uni.showToast({
title: '选择成功',
icon: 'success'
})
setTimeout(() => {
uni.showToast({
title: '绑定成功',
icon: 'success'
})
}, 500)
setTimeout(() => {
uni.showToast({
title: '绑定成功',
icon: 'success'
})
uni.setStorageSync('bindCarId', selectedId.value)
uni.setStorageSync('bindCarName', selectedName.value)
goBack()
@@ -190,9 +181,7 @@
}
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace'
})
uni.navigateBack()
}
</script>

View File

@@ -202,7 +202,7 @@
}
}
const goNext = () => {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/selectParingLot',
success: res => {},
fail: () => {},
@@ -211,12 +211,7 @@
}
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
})
uni.navigateBack()
}
</script>

View File

@@ -1,8 +1,5 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar title="车辆详情" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y="true">
<view class="info-card">
@@ -57,8 +54,14 @@
deleteCar
} from '@/pageSubPack/api/apiSub.js'
const goBack = () => {
uni.navigateBack()
}
const statusBarHeight = ref(20)
const currentStatus = ref('')
const isDeleting = ref(false)
const isUpdating = ref(false)
const id = ref(undefined)
const form = ref({
plateNo: '',
@@ -67,6 +70,7 @@
color: '',
carImg: ''
})
let isFirstLoad = true
onLoad((options) => {
console.log(options);
id.value = options.id
@@ -75,7 +79,17 @@
})
onShow(() => {
// 首次加载由 onLoad 处理,仅从编辑页返回时刷新
if (isFirstLoad) {
isFirstLoad = false
return
}
isUpdating.value = false
if (id.value) {
loadCarDetail()
}
})
const loadCarDetail = async () => {
// 防重复请求
@@ -90,22 +104,26 @@
console.log(form.value);
} catch (err) {
uni.showToast({
title: `${err}`,
title: err.msg,
icon: 'none'
})
console.error('列表接口报错:', err)
// console.error('列表接口报错:', err)
} finally {
}
}
const clickUpdate = () => {
if (isUpdating.value) return
isUpdating.value = true
uni.setStorageSync('operationType', 'update')
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/addCar?id=' + id.value
})
}
const onDelete = () => {
if (isDeleting.value) return
isDeleting.value = true
uni.showModal({
title: "确认删除",
content: "确认删除此车辆信息吗?",
@@ -125,25 +143,30 @@
icon: 'success'
})
setTimeout(() => {
goBack()
uni.navigateBack()
}, 1500)
} else {
isDeleting.value = false
uni.showToast({
title: deleteRes.msg || '删除失败',
icon: 'none'
})
}
}).catch(() => {
isDeleting.value = false
uni.hideLoading()
uni.showToast({
title: '删除失败',
icon: 'none'
})
})
} else {
isDeleting.value = false
}
}
})
}
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/my/myCarIndex'
})
}
</script>
<style lang="scss">
@@ -234,7 +257,6 @@
.bottom-btn-wrap {
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
}
.btn-modify {
@@ -246,6 +268,7 @@
font-size: 30rpx;
border-radius: 8rpx;
border: none;
margin-right: 20rpx;
}
.btn-delete {
@@ -257,5 +280,6 @@
border: 1rpx solid #f53f3f;
font-size: 30rpx;
border-radius: 8rpx;
margin-right: 0rpx;
}
</style>

View File

@@ -1,15 +1,5 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar
title="更改手机号码"
left-icon="left"
@click-left="goBack"
background-color="transparent"
:border="false"
>
</uni-nav-bar>
<scroll-view class="page" scroll-y>
<!-- 表单区域 -->
<view class="form-container">
@@ -59,7 +49,6 @@ import { ref, onUnmounted } from 'vue'
import { getAuthSendSmsCode, getUserChangePhone } from '../api/apiSub.js'
// 响应式数据
const statusBarHeight = ref(20)
const phone = ref('')
const code = ref('')
const countdown = ref(0)
@@ -134,11 +123,9 @@ const onSubmit = () => {
}
// 返回
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/my/settingIndex'
})
}
const goBack = () => {
uni.navigateBack()
}
// 页面卸载清除定时器
onUnmounted(() => {
@@ -149,6 +136,7 @@ onUnmounted(() => {
<style lang="scss">
page {
background: #f9f9f9;
height: 100%;
}
</style>
@@ -156,7 +144,7 @@ page {
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
height: 100%;
background-color: #f9f9f9;
}
@@ -210,10 +198,6 @@ page {
}
.code-btn {
position: absolute;
right: 10rpx;
top: 13%;
transform: translateY(-50%);
width: 200rpx;
height: 66rpx;
line-height: 66rpx;
@@ -223,6 +207,7 @@ page {
font-size: 28rpx;
padding: 0;
margin: 0;
flex-shrink: 0;
}
.code-btn[disabled] {
@@ -254,9 +239,4 @@ page {
border: none;
margin-top: 20rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -1,10 +1,5 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="修改密码" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y>
<!-- 表单区域 -->
@@ -38,8 +33,7 @@
import { ref } from 'vue'
import { authChangePassword } from '../api/apiSub.js'
const statusBarHeight = ref(20);
const oldPwd = ref('');
const newPwd = ref('');
const confirmPwd = ref('')
@@ -80,15 +74,14 @@ import { authChangePassword } from '../api/apiSub.js'
}
const goBack = () =>{
uni.redirectTo({
url: '/pageSubPack/my/settingIndex',
});
uni.navigateBack()
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
height: 100%;
}
</style>
@@ -96,7 +89,7 @@ import { authChangePassword } from '../api/apiSub.js'
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
height: 100%;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
@@ -195,9 +188,4 @@ import { authChangePassword } from '../api/apiSub.js'
color: #fff;
font-size: 28rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<view class="contentStyle">
<scroll-view class="page" scroll-y>
<!-- 提示信息 -->
<view class="warning-box">
<text class="warning-title">注销须知</text>
<text class="warning-text">1. 账号注销后所有数据将被永久删除且无法恢复</text>
<text class="warning-text">2. 已绑定的房屋车辆等信息将被清空</text>
<text class="warning-text">3. 如有未结清的费用请先结清后再注销</text>
</view>
<!-- 表单区域 -->
<view class="form-container">
<!-- 密码 -->
<input class="input-item" type="password" v-model="password" placeholder="请输入登录密码"
placeholder-class="input-placeholder" />
</view>
</scroll-view>
<view class="bottom-btn">
<button class="submit-btn" :class="{ 'submit-btn--disabled': countdown > 0 }"
:disabled="countdown > 0 || !password" @click="onDeleteAccount">
{{ countdown > 0 ? `请仔细阅读注销须知(${countdown}s` : '确认注销' }}
</button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onUnload } from '@dcloudio/uni-app'
import { authDeleteAccount } from '../api/apiSub.js'
const password = ref('')
const countdown = ref(0)
let timer = null
// 开始倒计时
const startCountdown = () => {
countdown.value = 10
timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
timer = null
}
}, 1000)
}
// 提交注销
const onDeleteAccount = () => {
if (countdown.value > 0) return
if (!password.value) {
uni.showToast({ title: '请输入登录密码', icon: 'none' })
return
}
uni.showModal({
title: '最后确认',
content: '注销后所有数据将永久删除且无法恢复,确定要注销账号吗?',
confirmText: '确认注销',
confirmColor: '#e74c3c',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '注销中...', mask: true })
authDeleteAccount({
password: password.value
}).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '注销成功', icon: 'success' })
setTimeout(() => {
uni.reLaunch({
url: '/pageSubPack/loginSub/login?mode=pwd'
})
}, 1500)
} else {
uni.showToast({ title: res.msg || '注销失败', icon: 'none' })
}
}).catch((err) => {
uni.hideLoading()
uni.showToast({ title: err.msg || '注销失败', icon: 'none' })
})
}
}
})
}
const goBack = () => {
uni.navigateBack()
}
// 页面进入时开始倒计时
onMounted(() => {
startCountdown()
})
onUnload(() => {
if (timer) {
clearInterval(timer)
timer = null
}
})
</script>
<style lang="scss">
page {
background: #f9f9f9;
height: 100%;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100%;
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
/* 警告提示 */
.warning-box {
background-color: #fff7e6;
border-left: 6rpx solid #faad14;
border-radius: 8rpx;
padding: 30rpx;
margin-top: 30rpx;
}
.warning-title {
font-size: 30rpx;
font-weight: bold;
color: #d48806;
display: block;
margin-bottom: 16rpx;
}
.warning-text {
font-size: 26rpx;
color: #8c6d1f;
line-height: 1.8;
display: block;
}
/* 表单容器 */
.form-container {
margin-top: 40rpx;
}
/* 输入框样式 */
.input-item {
width: 100%;
height: 88rpx;
line-height: 88rpx;
padding: 0 20rpx;
margin-bottom: 30rpx;
border-radius: 8rpx;
font-size: 30rpx;
color: #333;
box-sizing: border-box;
background-color: #fff;
}
/* 占位符样式 */
.input-placeholder {
color: #999;
font-size: 30rpx;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 确认按钮 */
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #e74c3c;
color: #fff;
font-size: 32rpx;
border-radius: 8rpx;
border: none;
}
.submit-btn--disabled{
background-color: #ccc;
color: #999;
}
</style>

View File

@@ -1,8 +1,5 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="房屋详情" left-icon="left" @clickLeft="goBack" :border="false" backgroundColor="transparent">
</uni-nav-bar>
<scroll-view class="page" scroll-y="true">
<!-- 顶部状态区 -->
@@ -36,19 +33,18 @@
</view>
<view class="info-row">
<text class="info-label">房间号</text>
<text class="info-value">{{form.houseNo}}</text>
<text class="info-value">{{form.buildingName+'栋'+form.unitNo+'单元'+form.houseNo}}</text>
</view>
<view class="info-row">
<text class="info-label">业主</text>
<text class="info-value">{{form.ownerName}}</text>
<text class="info-value">{{form.name}}</text>
</view>
<view class="info-row">
<text class="info-label">房间状态</text>
<text class="info-value" :style="{color: form.status === '0' ? '#00aaff' : form.status === '1' ?
'#FF700A' : form.status === '2' ? '#00aa00' : form.status === '3' ? '#00557f' : '#999' }">{{
form.status === '0' ? '自住' : form.status === '1' ?
'闲置' : form.status === '2' ? '租赁' : form.status === '3' ? '其他' : ''
}}</text>
<text class="info-value" :style="{color: form.houseStatus === '0' ? '#00aaff' : form.houseStatus === '1' ?
'#FF700A' : form.houseStatus === '2' ? '#00aa00' : form.houseStatus === '3' ? '#00557f' : '#999' }">
{{form.houseStatus === '0' ? '自住' : form.houseStatus === '1' ?
'闲置' : form.houseStatus === '2' ? '租赁' : form.houseStatus === '3' ? '其他' : ''}}</text>
</view>
</view>
@@ -61,16 +57,16 @@
</view>
<view class="info-row">
<text class="info-label">住户</text>
<text class="info-value">{{form.name}}</text>
<text class="info-value">{{form.residentName}}</text>
</view>
<view class="info-row">
<text class="info-label">房间号</text>
<text class="info-value">{{form.houseNo}}</text>
<text class="info-label">性别</text>
<text class="info-value">{{form.gender === '0'?'男':'女'}}</text>
</view>
<view class="info-row">
<text class="info-label">证件类型</text>
<text
class="info-value">{{form.idCardType=='0'?'居民身份证':form.idCardType=='1'?'护照':form.idCardType=='2'?'其他':''}}</text>
class="info-value">{{form.idCardType=='0'?'居民身份证':form.idCardType=='1'?'护照':form.idCardType=='2'?'港澳通行证':''}}</text>
</view>
<view class="info-row">
<text class="info-label">证件号码</text>
@@ -79,7 +75,7 @@
<view class="info-row">
<text class="info-label">与业主关系</text>
<text
class="info-value">{{form.ownerRelationship=='0'?'本人':form.ownerRelationship=='1'?'配偶':form.ownerRelationship=='2'?'父母':form.ownerRelationship=='3'?'子女':form.ownerRelationship=='4'?'亲属':form.ownerRelationship=='5'?'租户':form.ownerRelationship=='6'?'其他':''}}</text>
class="info-value">{{form.ownerRelationship=='0'?'业主本人':form.ownerRelationship=='1'?'亲属':form.ownerRelationship=='2'?'租户':form.ownerRelationship=='3'?'朋友':''}}</text>
</view>
<view class="info-row">
<text class="info-label">手机号码</text>
@@ -107,7 +103,8 @@
<button v-if="currentStatus === 'fail'" class="btn-modify" @click="clickUpdate()">
修改
</button>
<button class="btn-delete" :class="{ 'delete-only': currentStatus !== 'fail' }" @click="onDelete">
<button class="btn-delete" :class="{ 'delete-only': currentStatus !== 'fail' }"
@click="onDelete" v-if="certificationStatus !=='1' ">
删除
</button>
</view>
@@ -131,12 +128,13 @@
deleteMyHouse
} from '/pageSubPack/api/apiSub.js'
// 响应式数据
const statusBarHeight = ref(20)
const id = ref()
const currentStatus = ref('')
const stepList = ref(["房屋信息", "住户信息", "物业审核", "认证成功"])
const form = ref([])
const certificationStatus = ref('') // 认证状态
const statusMap = ref({
success: {
title: "房屋认证成功",
@@ -180,22 +178,21 @@
})
const getDetail = async () => {
// 防重复请求
try {
const res = await getMyHouseDetail({
id: id.value
})
const data = res.data
form.value = data
console.log(form.value);
// const data =
form.value = res.data
certificationStatus.value = form.value.certificationStatus
console.log('详情数据',form.value);
} catch (err) {
uni.showToast({
title: `${err}`,
title: err.msg || '网络异常',
icon: 'none'
})
console.error('列表接口报错:', err)
// console.error('列表接口报错:', err)
} finally {
}
@@ -203,7 +200,7 @@
// 方法(函数名完全不变)
const clickUpdate = () => {
uni.setStorageSync('operationType', 'update')
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/addHouse?id=' + id.value + '&sourcePage=addHouse'
})
}
@@ -222,24 +219,28 @@
id: id.value
}).then(res => {
if (res.code === 200) {
// 删除后清空小区缓存,避免首页仍显示已删除的小区
uni.setStorageSync('needReloadUserInfo', true)
uni.removeStorageSync('currentVillageId')
uni.removeStorageSync('home_cache_' + (uni.getStorageSync('userId') || ''))
uni.showToast({
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
goBack()
uni.redirectTo({ url: "/pageSubPack/my/myHouseIndex" })
}, 1000)
} else {
uni.showToast({
title: res.msg,
icon: 'error'
icon: 'none'
});
}
}).catch(err => {
uni.showToast({
title: err,
icon: 'error'
title: err.msg,
icon: 'none'
});
})
@@ -248,11 +249,6 @@
})
}
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/my/myHouseIndex'
})
}
</script>
<style lang="scss">
@@ -450,8 +446,9 @@
.photo-list {
display: flex;
flex-direction: column;
gap: 20rpx;
/* gap: 20rpx; */
margin-top: 20rpx;
}
.photo-item {
@@ -459,6 +456,10 @@
height: 170px;
background-color: #f6f6f6;
border-radius: 8rpx;
margin-bottom: 20rpx;
}
.photo-item:last-child{
margin-bottom: 0;
}
.bottom-btn-wrap {
@@ -535,9 +536,4 @@
color: #fff;
font-size: 28rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -1,12 +1,9 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="我的车辆" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y="true" @scrolltolower="getList" refresher-enabled="true" :refresher-triggered="isRefreshing" @refresherrefresh="onRefresh">
<view class="page">
<view v-if="carList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<view class="car-item" v-for="(item, index) in carList" :key="index" @click="handleItemClick(item, index)">
@@ -17,15 +14,15 @@
<view class="car-plate">{{ item.carNum }}</view>
<view class="car-model">{{ item.carBrand }}·{{ item.carColor }}</view>
</view>
<view class="car-bindParkingSpace">{{ item.bindParkingSpace }}</view>
<view class="car-bindParkingSpace">{{ item.villageName }}</view>
</view>
<!-- certificationStatus -->
<view class="status-tag"
<!-- <view class="status-tag"
:class="{ underReview: item.certificationStatus === '审核中', unbound: item.certificationStatus === '未绑定', bounded: item.certificationStatus === ''}">
{{ item.certificationStatus }}
</view>
</view> -->
</view>
</scroll-view>
</view>
<view class="bottom-btn">
<button class="confirm-btn" @click="addCar">添加车辆</button>
</view>
@@ -40,30 +37,41 @@
import {
getMyCarList
} from '@/pageSubPack/api/apiSub.js'
import {
onShow,
onPullDownRefresh,
onReachBottom
} from '@dcloudio/uni-app'
const statusBarHeight = ref(20)
const defaultCarImg = "http://47.104.199.163:9090/images/propertyImgs/defaultCarImg.png"
const carList = ref([])
const loadingMore = ref(false)
const noMoreData = ref(false)
const isRefreshing = ref(false)
const pageNum = ref(1)
const pageSize = ref(10)
// ==================== 下拉刷新 ====================
const onRefresh = () => {
isRefreshing.value = true
onPullDownRefresh(() => {
pageNum.value = 1
noMoreData.value = false
carList.value = []
loadingMore.value = false
getList()
}
})
// 页面加载时请求第一页
onMounted(() => {
getList()
})
onMounted(() => {
getList()
})
// 从子页面返回时刷新列表(如新增/编辑/删除车辆后)
onShow(() => {
pageNum.value = 1
noMoreData.value = false
loadingMore.value = false
carList.value = []
getList()
})
const getList = async () => {
// 防重复请求
if (loadingMore.value || noMoreData.value) return
@@ -72,7 +80,7 @@
const res = await getMyCarList({
pageNum: pageNum.value,
pageSize: pageSize.value,
residentId: uni.getStorageSync('userId')
userId: uni.getStorageSync('userId')
})
const data = res
@@ -99,14 +107,16 @@
console.error('列表接口报错:', err)
} finally {
loadingMore.value = false
// 关闭 refresher 下拉刷新状态
if (isRefreshing.value) {
isRefreshing.value = false
}
uni.stopPullDownRefresh()
}
}
// ==================== 上拉加载更多 ====================
onReachBottom(() => {
getList()
})
const getStatusText = (status) => {
if (status === 0) return '未绑定'
if (status === 1) return '已绑定'
@@ -116,44 +126,37 @@
const handleItemClick = (item, index) => {
uni.setStorageSync('operationType', 'update')
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/carDetail?id=' + item.id + '&status=' + item.status
})
}
const addCar = () => {
uni.setStorageSync('operationType', 'add')
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/addCar'
})
}
const goBack = () => {
uni.switchTab({
url: '/pages/myIndex/myIndex'
})
uni.navigateBack({ delta: 1 })
}
</script>
<style lang="scss">
page {
background: #F3F8FD;
overflow: hidden;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
min-height: 100vh;
background-color: #f5f7fa;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
padding: 0px 40rpx 140rpx;
box-sizing: border-box;
}
@@ -174,7 +177,15 @@
}
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
flex-shrink: 0;
background-color: #f5f7fa;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.car-item {
@@ -226,6 +237,7 @@
overflow: hidden;
text-overflow: ellipsis;
width: 90%;
margin-top: 10rpx;
}
.status-tag {
@@ -270,4 +282,4 @@
width: 100vw;
height: 46px;
}
</style>
</style>

View File

@@ -1,16 +1,11 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="我的房屋" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<!-- -->
<scroll-view class="page" scroll-y="true" @scrolltolower="getList" refresher-enabled="true" :refresher-triggered="isRefreshing" @refresherrefresh="onRefresh">
<view class="page">
<!-- 空状态 -->
<view v-if="houseList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<!-- 房屋列表 v-for 渲染 -->
<view class="house-list">
@@ -18,12 +13,12 @@
<!-- 标题 + 状态 -->
<view class="card-header">
<view class="title-box">
<text class="tag-default" v-if="item.isDefault==1">默认</text>
<text class="tag-default" v-if="item.isDefault === true">默认</text>
<text
class="house-name">{{ item.villageName+item.buildingName+item.unitNo+'单元'+item.floorNo+'楼' }}</text>
class="house-name">{{ item.villageName+"-"+item.buildingName+"-"+item.unitNo }}</text>
</view>
<text class="status-tag"
:class="item.certificationStatus=='0'?'pending':item.certificationStatus=='1'?'success':item.certificationStatus=='2'?'fail':''">{{ item.certificationStatus=='0'?'认证':item.certificationStatus=='1'?'认证成功':item.certificationStatus=='2'?'认证失败':'' }}</text>
:class="item.certificationStatus=='0'?'pending':item.certificationStatus=='1'?'success':item.certificationStatus=='2'?'fail':''">{{ item.certificationStatus=='0'?'认证':item.certificationStatus=='1'?'认证成功':item.certificationStatus=='2'?'认证失败':'' }}</text>
</view>
<!-- 房屋信息 -->
@@ -43,7 +38,7 @@
<text
class="value">{{ item.houseStatus==0?'自住':item.houseStatus==1?'闲置':item.houseStatus==2?'租赁':item.houseStatus==3?'其他':''}}</text>
</view>
<view class="defaultBox" v-if="item.certificationStatus=='1'&&!item.isDefault==1">
<view class="defaultBox" v-if="item.certificationStatus=='1'">
<text class="set-default">设为默认</text>
<switch class="no-cross-switch" :checked="item.isDefault==1" color="#1677ff"
@change="onSwitchChange($event,item)" @click.stop />
@@ -51,8 +46,7 @@
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 底部添加按钮 -->
<view class="bottom-btn">
<button class="add-btn" @click="addHouse">添加房屋</button>
@@ -66,49 +60,50 @@
import {
ref,
onMounted,
} from 'vue'
import {
onShow,
onPullDownRefresh,
onReachBottom
} from '@dcloudio/uni-app'
import {
getMyHouseList,
setMyHouseDefault
} from '/pageSubPack/api/apiSub.js'
const houseList = ref([])
const loadingMore = ref(false)
const noMoreData = ref(false)
const isRefreshing = ref(false)
const loading = ref(false)
const pageNum = ref(1)
const pageSize = ref(10)
const noMoreData = ref(false)
// ==================== 下拉刷新 ====================
onPullDownRefresh(() => {
pageNum.value = 1
noMoreData.value = false
houseList.value = []
loading.value = false
getList()
})
// ==================== 获取列表数据 ====================
const getList = async () => {
// 防重复请求
if (loadingMore.value || noMoreData.value) return
loadingMore.value = true
if (loading.value) return
loading.value = true
try {
const res = await getMyHouseList({
pageNum: pageNum.value,
pageSize: pageSize.value
})
const data = res
const newList = data.rows || []
// 第一页 → 覆盖数据
const newList = res.rows || []
if (pageNum.value === 1) {
houseList.value = newList
} else {
// 后续页 → 追加数据
houseList.value = [...houseList.value, ...newList]
}
// 判断是否没有更多数据
// 不足一页说明没有更多数据
if (newList.length < pageSize.value) {
noMoreData.value = true
} else {
pageNum.value++
}
} catch (err) {
uni.showToast({
@@ -117,73 +112,76 @@
})
console.error('列表接口报错:', err)
} finally {
loadingMore.value = false
// 关闭 refresher 下拉刷新状态
if (isRefreshing.value) {
isRefreshing.value = false
}
loading.value = false
uni.stopPullDownRefresh()
}
}
// ==================== 下拉刷新 ====================
const onRefresh = () => {
isRefreshing.value = true
pageNum.value = 1
noMoreData.value = false
houseList.value = []
loadingMore.value = false
// ==================== 加载更多 ====================
const loadMore = () => {
if (loading.value || noMoreData.value) return
pageNum.value++
getList()
}
// ==================== 上拉加载更多 ====================
onReachBottom(() => {
loadMore()
})
// 页面加载时请求第一页
onMounted(() => {
pageNum.value = 1
noMoreData.value = false
getList()
})
// 页面显示时刷新列表(从添加/编辑房屋页返回时)
onShow(() => {
pageNum.value = 1
noMoreData.value = false
getList()
})
const onSwitchChange = async (e, i) => {
// uni.showLoading({
// title: '设置中...',
// mask: true
// });
try {
const res = await setMyHouseDefault({
houseId: i.houseId
houseId: e.detail.value ? i.houseId : ''
})
const data = res
if (data.code === 200) {
// uni.hideLoading();
loadingMore.value = false
pageNum.value = 1
noMoreData.value = false
getList()
uni.setStorageSync('houseIsDefault', e.detail.value)
// 标记首页需要重新加载用户信息,返回首页后 onShow 会自动刷新小区
uni.setStorageSync('needReloadUserInfo', true)
uni.showToast({
title: '设置默认房屋成功',
icon: 'success'
title: res.msg || '设置默认房屋成功',
icon: 'none'
});
} else {
// uni.hideLoading();
uni.showToast({
title: `${res.msg}`,
icon: 'error'
title: res.msg,
icon: 'none'
});
}
} catch (err) {
// uni.hideLoading();
uni.showToast({
title: `${err}`,
title: err.msg,
icon: 'none'
})
console.error('列表接口报错:', err)
}
}
const tapHouse = (item) => {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/houseDetail?id=' + item.id + '&certificationStatus=' + item.certificationStatus,
success: res => {},
fail: () => {},
@@ -193,7 +191,7 @@
const addHouse = () => {
uni.setStorageSync('operationType', 'add');
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/addHouse',
success: res => {},
fail: () => {},
@@ -204,12 +202,7 @@
}
const goBack = () => {
uni.switchTab({
url: '/pages/myIndex/myIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
uni.navigateBack({ delta: 1 })
}
</script>
@@ -222,17 +215,12 @@
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
min-height: 100vh;
background-color: #f5f7fa;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
padding: 0px 40rpx 120rpx;
box-sizing: border-box;
}
@@ -242,7 +230,8 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
height: 100%;
min-height: 500rpx;
color: #ccc;
}
@@ -253,16 +242,8 @@
margin-bottom: 10rpx;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 列表 */
.house-list {
height: 100%;
overflow-y: scroll;
}
.house-card {
@@ -381,6 +362,15 @@
.bottom-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 30rpx 40rpx;
background-color: #f5f7fa;
}
.add-btn {
width: 100%;
height: 88rpx;

View File

@@ -1,11 +1,11 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<!-- <view class="status-bar"></view>
<uni-nav-bar title="我的车位" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y="true" @scrolltolower="getList" refresher-enabled="true" :refresher-triggered="isRefreshing" @refresherrefresh="onRefresh">
</uni-nav-bar> -->
<view class="page">
<view v-if="parkingSpaceList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<view class="parkingSpace-list">
@@ -16,7 +16,7 @@
<text class="parkingSpace-name">{{ item.villageName }}</text>
</view>
<text class="status-tag"
:class="item.checkStatus=='0'?'pending':item.checkStatus=='1'?'success':item.checkStatus=='2'?'fail':''">{{ item.checkStatus=='0'?'认证':item.checkStatus=='1'?'认证成功':item.checkStatus=='2'?'认证失败':'' }}</text>
:class="item.checkStatus=='0'?'pending':item.checkStatus=='1'?'success':item.checkStatus=='2'?'fail':''">{{ item.checkStatus=='0'?'认证':item.checkStatus=='1'?'认证成功':item.checkStatus=='2'?'认证失败':'' }}</text>
</view>
<view class="card-body">
<view class="left">
@@ -37,7 +37,7 @@
<view class="line">
<view class="row">
<text class="label">车位状态</text>
<text class="value">{{ item.parkingStatus }}</text>
<text class="value">{{ getParkingStatus(item.parkingStatus) }}</text>
</view>
</view>
<view class="line">
@@ -49,12 +49,12 @@
</view>
<view class="right">
<image :src="item.carImageUrl?item.carImageUrl:'http://47.104.199.163:9090/images/propertyImgs/myParkingSpace.png'"
class="imgItem" mode="widthFix"></image>
class="imgItem" mode="aspectFill"></image>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="bottom-btn">
<button class="add-btn" @click="addParkingSpace">添加车位</button>
</view>
@@ -63,9 +63,13 @@
<script setup>
import {
ref,
onMounted
ref
} from 'vue'
import {
onShow,
onPullDownRefresh,
onReachBottom
} from '@dcloudio/uni-app'
import {
getCarList
} from '@/pageSubPack/api/apiSub.js'
@@ -74,26 +78,40 @@
const parkingSpaceList = ref([])
// ==================== 下拉刷新 ====================
const onRefresh = () => {
isRefreshing.value = true
onPullDownRefresh(() => {
pageNum.value = 1
noMoreData.value = false
parkingSpaceList.value = []
loadingMore.value = false
getList()
}
})
// 页面加载时请求第一页
onMounted(() => {
// 页面显示时请求第一页(首次进入和从详情/修改页返回时都会刷新)
onShow(() => {
pageNum.value = 1
noMoreData.value = false
parkingSpaceList.value = []
loadingMore.value = false
getList()
})
const loadingMore = ref(false)
const noMoreData = ref(false)
const isRefreshing = ref(false)
const pageNum = ref(1)
const pageSize = ref(10)
// 车位状态映射
const getParkingStatus =(status) =>{
const statusMap = {
'0':'闲置',
'1':'自用',
'2':'租赁',
'3':'借出',
'4':'其他'
}
return statusMap[status]
}
// ==================== 获取列表数据 ====================
const getList = async () => {
// 防重复请求
@@ -103,7 +121,7 @@
const res = await getCarList({
pageNum: pageNum.value,
pageSize: pageSize.value,
residentId: uni.getStorageSync('userId')
// residentId: uni.getStorageSync('userId')
})
const data = res
@@ -130,31 +148,31 @@
console.error('列表接口报错:', err)
} finally {
loadingMore.value = false
// 关闭 refresher 下拉刷新状态
if (isRefreshing.value) {
isRefreshing.value = false
}
uni.stopPullDownRefresh()
}
}
// ==================== 上拉加载更多 ====================
onReachBottom(() => {
getList()
})
const tapParkingSpace = (item) => {
console.log(item);
uni.redirectTo({
url: '/pageSubPack/my/parkingSpaceDetail?id=' + item.parkingId + '&statusClass=' + item.checkStatus
uni.navigateTo({
url: '/pageSubPack/my/parkingSpaceDetail?id=' + item.id + '&statusClass=' + item.checkStatus
})
}
const addParkingSpace = () => {
uni.setStorageSync('operationType', 'add')
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/addParkingSpace'
})
}
const goBack = () => {
uni.switchTab({
url: '/pages/myIndex/myIndex'
})
uni.navigateBack({ delta: 1 })
}
</script>
@@ -171,16 +189,12 @@
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
min-height: 100vh;
background-color: #f5f7fa;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
padding: 0px 40rpx 140rpx;
box-sizing: border-box;
}
@@ -201,7 +215,14 @@
}
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f5f7fa;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.parkingSpace-card {
@@ -276,17 +297,22 @@
font-size: 28rpx;
font-weight: 600;
color: #333;
flex-shrink: 0;
}
.value-box {
flex: 1;
display: flex;
justify-content: space-between;
min-width: 0;
}
.value {
font-size: 28rpx;
color: #666;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.add-btn {
@@ -300,13 +326,22 @@
border: none;
}
.left {}
.left {
flex: 1;
min-width: 0;
overflow: hidden;
}
.right {
width: 42%;
height: 180rpx;
border-radius: 8rpx;
overflow: hidden;
flex-shrink: 0;
}
.imgItem {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,294 @@
<template>
<view class="page">
<view class="header-area">
<view class="status-bar"></view>
<uni-nav-bar title="我的积分" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false"
color="#ffffff" :iconStyle="{ color: '#ffffff' }">
</uni-nav-bar>
<view class="points-display">
<text class="points-num">{{ totalPoints }}</text>
</view>
</view>
<!-- 三个功能入口 -->
<view class="entry-card">
<view class="entry-item" v-for="(item, index) in entryList" :key="index" @click="goPage(item)">
<image :src="item.icon" class="entry-icon" mode="aspectFit" />
<text class="entry-text">{{ item.title }}</text>
</view>
</view>
<!-- 虚拟兑换 -->
<view class="virtual-exchange">
<text class="virtual-title">虚拟兑换</text>
</view>
<view class="section-header">
<view class="section-line"></view>
<text class="section-title">热门虚拟权益服务</text>
<view class="section-line"></view>
</view>
<!-- 人气兑换商品区域 -->
<view class="goods-card">
<uni-section title="人气兑换" type="line"></uni-section>
<view class="goods-grid">
<view class="goods-item" v-for="(item, index) in goodsList" :key="index">
<image :src="item.image" class="goods-img" mode="aspectFill" />
<text class="goods-name">{{ item.name }}</text>
<text class="goods-desc">{{ item.desc }}</text>
<text class="goods-points">{{ item.points }}</text>
</view>
</view>
<view class="goods-more" @click="goMore">
<text class="more-text">更多积分兑换</text>
<text class="more-arrow">></text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getPointsBalance } from '/pageSubPack/api/apiSub.js'
const totalPoints = ref(780)
const entryList = ref([{
title: '积分商城',
icon: 'https://wuye.ckdzkj.com/images/propertyImgs/pointsMall.png',
url: '/pageSubPack/my/myPointsMall'
}, {
title: '积分明细',
icon: 'https://wuye.ckdzkj.com/images/propertyImgs/pointsRecord.png',
url: '/pageSubPack/my/myPointsRecord'
}, {
title: '积分订单',
icon: 'https://wuye.ckdzkj.com/images/propertyImgs/pointsOrder.png',
url: '/pageSubPack/my/myPointsOrder'
}])
const goodsList = ref([
{ id: 1, name: '植物精油', desc: '温和无刺激 下单即赠试用装', points: 6000, image: 'https://via.placeholder.com/200x200/4CAF50/FFF?text=植物' },
{ id: 2, name: '肯德基香辣劲脆鸡腿堡电子', desc: '下单即赠试用装', points: 6000, image: 'https://via.placeholder.com/200x200/FF5722/FFF?text=KFC' },
{ id: 3, name: '植物精油', desc: '温和无刺激 下单即赠试用装', points: 6000, image: 'https://via.placeholder.com/200x200/4CAF50/FFF?text=植物' },
{ id: 4, name: '肯德基香辣劲脆鸡腿堡电子', desc: '下单即赠试用装', points: 4000, image: 'https://via.placeholder.com/200x200/FF5722/FFF?text=KFC' },
{ id: 5, name: '植物精油', desc: '温和无刺激 下单即赠试用装', points: 6000, image: 'https://via.placeholder.com/200x200/4CAF50/FFF?text=植物' },
{ id: 6, name: '肯德基香辣劲脆鸡腿堡电子', desc: '下单即赠试用装', points: 4000, image: 'https://via.placeholder.com/200x200/FF5722/FFF?text=KFC' }
])
const getBalance = async () => {
try {
const res = await getPointsBalance()
if (res.code === 200 && res.data) {
totalPoints.value = res.data.balance || res.data.points || 0
}
} catch (err) {
console.error('获取积分余额失败:', err)
}
}
const goPage = (item) => {
uni.navigateTo({
url: item.url
})
}
const goBack = () => {
uni.navigateBack({ delta: 1 })
}
const goMore = () => {
uni.navigateTo({
url: '/pageSubPack/my/myPointsMall'
})
}
onMounted(() => {
getBalance()
})
</script>
<style lang="scss" scoped>
.page {
background-color: #f5f5f5;
min-height: 100vh;
}
/* ===== 顶部蓝色区域 ===== */
.header-area {
background: #2E59FD;
padding-bottom: 40rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
.points-display {
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx 0 20rpx;
}
.points-num {
font-size: 64rpx;
font-weight: 700;
color: #ffffff;
line-height: 1.1;
letter-spacing: 4rpx;
}
/* ===== 三个功能入口 ===== */
.entry-card {
display: flex;
justify-content: space-around;
align-items: center;
background: #ffffff;
border-radius: 20rpx;
margin: -30rpx 30rpx 0;
padding: 36rpx 0;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
position: relative;
z-index: 2;
overflow: hidden;
}
.entry-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
&:active {
opacity: 0.7;
}
}
.entry-icon {
width: 72rpx;
height: 72rpx;
margin-bottom: 16rpx;
}
.entry-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
/* ===== 虚拟兑换 ===== */
.virtual-exchange {
display: flex;
justify-content: center;
padding: 40rpx 0 30rpx;
}
.virtual-title {
font-size: 32rpx;
color: #000000;
font-weight: 500;
}
/* ===== 热门虚拟权益服务(横线 + 文字 + 横线) ===== */
.section-header {
display: flex;
align-items: center;
justify-content: center;
padding: 0 110rpx;
margin-bottom: 30rpx;
}
.section-line {
flex: 1;
height: 1rpx;
background: #cccccc;
}
.section-title {
font-size: 28rpx;
color: #999999;
margin: 0 24rpx;
white-space: nowrap;
flex-shrink: 0;
}
/* ===== 人气兑换商品卡片 ===== */
.goods-card {
background: #ffffff;
border-radius: 32rpx;
margin: 0 30rpx;
padding: 20rpx 20rpx 30rpx;
}
.goods-grid {
display: flex;
flex-wrap: wrap;
margin: 0 -10rpx;
}
.goods-item {
width: calc(33.33% - 20rpx);
margin: 0 10rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.goods-img {
width: 210rpx;
height: 210rpx;
border-radius: 16rpx;
background: #f0f0f0;
margin-bottom: 12rpx;
}
.goods-name {
font-size: 26rpx;
color: #333;
font-weight: 500;
text-align: left;
line-height: 1.3;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.goods-desc {
font-size: 22rpx;
color: #999;
text-align: left;
line-height: 1.3;
margin-top: 6rpx;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.goods-points {
font-size: 32rpx;
font-weight: 600;
color: #FF8F40;
margin-top: 8rpx;
text-align: left;
width: 100%;
}
/* ===== 更多积分兑换 ===== */
.goods-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0 10rpx;
border-top: 1rpx solid #eee;
}
.more-text {
font-size: 28rpx;
color: #333333;
}
</style>

View File

@@ -0,0 +1,232 @@
<template>
<view class="contentStyle">
<!-- <view class="status-bar"></view>
<uni-nav-bar title="积分商城" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar> -->
<pull-refresh-scroll
custom-class="page"
:refreshing="isRefreshing"
:loading-more="loading"
:no-more-data="noMoreData"
:empty="list.length === 0"
empty-text="暂无商品"
@refresh="onRefresh"
@loadMore="loadMore"
>
<view class="mall-list">
<view class="mall-card" v-for="(item, index) in list" :key="index" @click="onProductClick(item)">
<image :src="item.image || defaultImg" class="product-img" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<text class="product-desc">{{ item.description || '' }}</text>
<view class="product-bottom">
<view class="product-points">
<text class="points-value">{{ item.points }}</text>
<text class="points-unit">积分</text>
</view>
<view class="exchange-btn">立即兑换</view>
</view>
</view>
</view>
</view>
</pull-refresh-scroll>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getPointsMallList, postPointsExchange } from '/pageSubPack/api/apiSub.js'
import PullRefreshScroll from '/components/pull-refresh-scroll/pull-refresh-scroll.vue'
const defaultImg = 'https://wuye.ckdzkj.com/images/propertyImgs/points.png'
const list = ref([])
const pageNum = ref(1)
const pageSize = ref(10)
const noMoreData = ref(false)
const loading = ref(false)
const isRefreshing = ref(false)
const getList = async () => {
if (loading.value) return
loading.value = true
try {
const res = await getPointsMallList({
pageNum: pageNum.value,
pageSize: pageSize.value
})
const newList = res.rows || res.data || []
if (pageNum.value === 1) {
list.value = newList
} else {
list.value = [...list.value, ...newList]
}
if (newList.length < pageSize.value) {
noMoreData.value = true
}
} catch (err) {
uni.showToast({ title: '加载失败', icon: 'none' })
console.error('积分商城接口报错:', err)
} finally {
loading.value = false
if (isRefreshing.value) isRefreshing.value = false
}
}
const onRefresh = () => {
isRefreshing.value = true
pageNum.value = 1
noMoreData.value = false
loading.value = false
getList()
}
const loadMore = () => {
if (loading.value || noMoreData.value) return
pageNum.value++
getList()
}
const onProductClick = (item) => {
uni.showModal({
title: '确认兑换',
content: `确定使用 ${item.points} 积分兑换「${item.name}」吗?`,
success: async (modalRes) => {
if (!modalRes.confirm) return
try {
uni.showLoading({ title: '兑换中...', mask: true })
const res = await postPointsExchange({ productId: item.id || item.productId })
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '兑换成功', icon: 'success' })
} else {
uni.showToast({ title: res.msg || '兑换失败', icon: 'none' })
}
} catch (err) {
uni.hideLoading()
uni.showToast({ title: err.msg || '兑换失败', icon: 'none' })
}
}
})
}
const goBack = () => {
uni.navigateBack({ delta: 1 })
}
onMounted(() => {
getList()
})
</script>
<style lang="scss">
page {
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
</style>
<style lang="scss" scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f7fa;
}
.status-bar {
width: 100vw;
height: 46px;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0 40rpx;
box-sizing: border-box;
}
.mall-list {
padding: 20rpx 0;
}
.mall-card {
display: flex;
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.product-img {
width: 180rpx;
height: 180rpx;
border-radius: 12rpx;
flex-shrink: 0;
background: #f5f5f5;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 20rpx;
overflow: hidden;
}
.product-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-desc {
font-size: 24rpx;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 4rpx;
}
.product-bottom {
display: flex;
justify-content: space-between;
align-items: center;
}
.product-points {
display: flex;
align-items: baseline;
}
.points-value {
font-size: 36rpx;
font-weight: 700;
color: #ff6b35;
}
.points-unit {
font-size: 24rpx;
color: #ff6b35;
margin-left: 4rpx;
}
.exchange-btn {
padding: 10rpx 28rpx;
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
color: #fff;
font-size: 24rpx;
border-radius: 30rpx;
font-weight: 500;
}
.exchange-btn:active {
opacity: 0.8;
}
</style>

View File

@@ -0,0 +1,229 @@
<template>
<view class="contentStyle">
<!-- <view class="status-bar"></view>
<uni-nav-bar title="积分订单" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar> -->
<pull-refresh-scroll
custom-class="page"
:refreshing="isRefreshing"
:loading-more="loading"
:no-more-data="noMoreData"
:empty="list.length === 0"
empty-text="暂无兑换订单"
@refresh="onRefresh"
@loadMore="loadMore"
>
<view class="order-list">
<view class="order-card" v-for="(item, index) in list" :key="index" @click="onOrderClick(item)">
<view class="order-header">
<text class="order-product">{{ item.productName }}</text>
<text class="order-status" :class="getStatusClass(item.status)">
{{ getStatusText(item.status) }}
</text>
</view>
<view class="order-body">
<view class="order-row">
<text class="order-label">兑换积分</text>
<text class="order-points">{{ item.points }}积分</text>
</view>
<view class="order-row">
<text class="order-label">兑换时间</text>
<text class="order-time">{{ item.createTime }}</text>
</view>
</view>
</view>
</view>
</pull-refresh-scroll>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getPointsOrderList } from '/pageSubPack/api/apiSub.js'
import PullRefreshScroll from '/components/pull-refresh-scroll/pull-refresh-scroll.vue'
const list = ref([])
const pageNum = ref(1)
const pageSize = ref(10)
const noMoreData = ref(false)
const loading = ref(false)
const isRefreshing = ref(false)
const getList = async () => {
if (loading.value) return
loading.value = true
try {
const res = await getPointsOrderList({
pageNum: pageNum.value,
pageSize: pageSize.value
})
const newList = res.rows || res.data || []
if (pageNum.value === 1) {
list.value = newList
} else {
list.value = [...list.value, ...newList]
}
if (newList.length < pageSize.value) {
noMoreData.value = true
}
} catch (err) {
uni.showToast({ title: '加载失败', icon: 'none' })
console.error('积分订单接口报错:', err)
} finally {
loading.value = false
if (isRefreshing.value) isRefreshing.value = false
}
}
const onRefresh = () => {
isRefreshing.value = true
pageNum.value = 1
noMoreData.value = false
loading.value = false
getList()
}
const loadMore = () => {
if (loading.value || noMoreData.value) return
pageNum.value++
getList()
}
const onOrderClick = (item) => {
uni.showToast({ title: `订单号:${item.orderNo || item.id}`, icon: 'none' })
}
const getStatusText = (status) => {
const map = { '0': '待处理', '1': '已发货', '2': '已完成', '3': '已取消' }
return map[status] || '未知'
}
const getStatusClass = (status) => {
const map = { '0': 'status-pending', '1': 'status-shipped', '2': 'status-done', '3': 'status-cancel' }
return map[status] || ''
}
const goBack = () => {
uni.navigateBack({ delta: 1 })
}
onMounted(() => {
getList()
})
</script>
<style lang="scss">
page {
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
</style>
<style lang="scss" scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f7fa;
}
.status-bar {
width: 100vw;
height: 46px;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0 40rpx;
box-sizing: border-box;
}
.order-list {
padding: 20rpx 0;
}
.order-card {
background: #fff;
border-radius: 12rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.order-product {
font-size: 30rpx;
font-weight: 600;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-status {
font-size: 26rpx;
padding: 4rpx 16rpx;
border-radius: 20rpx;
flex-shrink: 0;
}
.status-pending {
color: #fa8c16;
background: #fff7e6;
}
.status-shipped {
color: #1677ff;
background: #e6f4ff;
}
.status-done {
color: #07c160;
background: #e6f9f0;
}
.status-cancel {
color: #999;
background: #f5f5f5;
}
.order-body {
padding-top: 20rpx;
}
.order-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.order-row:last-child {
margin-bottom: 0;
}
.order-label {
font-size: 26rpx;
color: #999;
}
.order-points {
font-size: 28rpx;
color: #ff6b35;
font-weight: 500;
}
.order-time {
font-size: 26rpx;
color: #666;
}
</style>

View File

@@ -0,0 +1,190 @@
<template>
<view class="contentStyle">
<!-- <view class="status-bar"></view>
<uni-nav-bar title="积分明细" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar> -->
<pull-refresh-scroll
custom-class="page"
:refreshing="isRefreshing"
:loading-more="loading"
:no-more-data="noMoreData"
:empty="list.length === 0"
empty-text="暂无积分记录"
@refresh="onRefresh"
@loadMore="loadMore"
>
<view class="record-list">
<view class="record-card" v-for="(item, index) in list" :key="index">
<view class="record-left">
<text class="record-title">{{ item.title }}</text>
<text class="record-time">{{ item.createTime }}</text>
</view>
<view class="record-right">
<text class="record-points" :class="item.type === 'income' ? 'income' : 'expense'">
{{ item.type === 'income' ? '+' : '-' }}{{ item.points }}
</text>
<text class="record-balance">余额{{ item.balance }}</text>
</view>
</view>
</view>
</pull-refresh-scroll>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getPointsRecordList } from '/pageSubPack/api/apiSub.js'
import PullRefreshScroll from '/components/pull-refresh-scroll/pull-refresh-scroll.vue'
const list = ref([])
const pageNum = ref(1)
const pageSize = ref(10)
const noMoreData = ref(false)
const loading = ref(false)
const isRefreshing = ref(false)
const getList = async () => {
if (loading.value) return
loading.value = true
try {
const res = await getPointsRecordList({
pageNum: pageNum.value,
pageSize: pageSize.value
})
const newList = res.rows || res.data || []
if (pageNum.value === 1) {
list.value = newList
} else {
list.value = [...list.value, ...newList]
}
if (newList.length < pageSize.value) {
noMoreData.value = true
}
} catch (err) {
uni.showToast({ title: '加载失败', icon: 'none' })
console.error('积分明细接口报错:', err)
} finally {
loading.value = false
if (isRefreshing.value) isRefreshing.value = false
}
}
const onRefresh = () => {
isRefreshing.value = true
pageNum.value = 1
noMoreData.value = false
loading.value = false
getList()
}
const loadMore = () => {
if (loading.value || noMoreData.value) return
pageNum.value++
getList()
}
const goBack = () => {
uni.navigateBack({ delta: 1 })
}
onMounted(() => {
getList()
})
</script>
<style lang="scss">
page {
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
</style>
<style lang="scss" scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f7fa;
}
.status-bar {
width: 100vw;
height: 46px;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0 40rpx;
box-sizing: border-box;
background: #fff;
margin: 0 40rpx;
border-radius: 12rpx 12rpx 0 0;
}
.record-list {
padding: 20rpx 0;
}
.record-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
background: #fff;
}
.record-card:last-child {
border-bottom: none;
}
.record-left {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.record-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.record-time {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.record-right {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0;
margin-left: 20rpx;
}
.record-points {
font-size: 32rpx;
font-weight: 600;
}
.record-points.income {
color: #07c160;
}
.record-points.expense {
color: #333;
}
.record-balance {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<!-- <view class="status-bar"></view>
<uni-nav-bar title="车位详情" left-icon="left" @clickLeft="goBack" :border="false" backgroundColor="transparent">
</uni-nav-bar>
</uni-nav-bar> -->
<scroll-view class="page" scroll-y="true">
<view class="status-header">
@@ -24,7 +24,7 @@
<view class="info-card">
<view class="card-title">
<view class="title-line"></view>
房屋信息
车位信息
</view>
<view class="info-row">
<text class="info-label">小区</text>
@@ -109,6 +109,7 @@
const statusBarHeight = ref(20)
const id = ref(undefined)
const currentStatus = ref('')
const checkStatus = ref('')
// parkingId
const form = ref({
community: "",
@@ -148,6 +149,7 @@
onLoad((option) => {
console.log(option);
id.value = option.id
checkStatus.value = option.statusClass || ''
currentStatus.value = option.statusClass == '0' ? 'pending' : option.statusClass == '1' ?
'success' : option.statusClass == '2' ? 'fail' : ''
loadParkingSpaceDetail()
@@ -169,10 +171,10 @@
console.log(form.value);
} catch (err) {
uni.showToast({
title: `${err}`,
title: err.msg,
icon: 'none'
})
console.error('列表接口报错:', err)
// console.error('列表接口报错:', err)
} finally {
}
@@ -202,7 +204,7 @@
icon: 'success'
})
setTimeout(() => {
goBack()
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
uni.showToast({
@@ -218,11 +220,15 @@
const onModify = () => {
uni.setStorageSync('operationType', 'update')
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace?id=' + id.value + '&sourcePage=addParkingSpace'
uni.navigateTo({
url: '/pageSubPack/my/addParkingSpace?id=' + id.value + '&checkStatus=' + checkStatus.value + '&sourcePage=addParkingSpace'
})
}
const goBack = () => {
uni.navigateBack()
}
const onDelete = () => {
uni.showModal({
title: "确认删除",
@@ -243,7 +249,7 @@
icon: 'success'
})
setTimeout(() => {
goBack()
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
uni.showToast({
@@ -257,11 +263,6 @@
})
}
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/my/myParkingSpaceIndex'
})
}
</script>
<style lang="scss">
@@ -471,7 +472,6 @@
.bottom-btn-wrap {
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
}
.btn-modify {
@@ -483,6 +483,7 @@
font-size: 30rpx;
border-radius: 8rpx;
border: none;
margin-right: 20rpx;
}
.btn-delete {

View File

@@ -14,7 +14,7 @@
{{ '暂无数据'}}
</text>
</view>
<view class="building-grid">
<view class="building-grid" :class="{ disabled: isNavigating }">
<view class="building-item"
:class="{ active:(checkType=='楼栋'&& selectedBuilding === item.id)||(checkType=='单元'&& selectedUnit === item.id)||(checkType=='楼层'&&selectedFloor === item.id) ||(checkType=='户号'&&selectedHouseNumber === item.id) }"
v-for="(item, index) in dataList" :key="index" @click="selectBuilding(item)">
@@ -52,6 +52,9 @@
// 从缓存获取类型
const checkType = ref(uni.getStorageSync('checkType') || '')
const dataList = ref([])
// 添加防重复点击锁
const isNavigating = ref(false)
@@ -78,96 +81,82 @@
// 选择项方法
const selectBuilding = (item) => {
const modalContent = `确认选择此${checkType.value}吗?`
uni.showModal({
title: '确认选择',
content: modalContent,
success: (res) => {
if (!res.confirm) return
uni.showLoading({
title: '选择中...',
mask: true
})
if (checkType.value === '楼栋') {
selectedBuilding.value = item.id
uni.setStorageSync('buildingId', item.id)
uni.setStorageSync('buildingName', item.name)
uni.removeStorageSync('unitId')
uni.removeStorageSync('unitName')
uni.removeStorageSync('floorId')
uni.removeStorageSync('floorName')
uni.removeStorageSync('houseNumberId')
uni.removeStorageSync('houseNumberName')
uni.setStorageSync('checkType', '单元')
} else if (checkType.value === '单元') {
selectedUnit.value = item.id
uni.setStorageSync('unitId', item.id)
uni.setStorageSync('unitName', item.name)
uni.removeStorageSync('floorId')
uni.removeStorageSync('floorName')
uni.removeStorageSync('houseNumberId')
uni.removeStorageSync('houseNumberName')
uni.setStorageSync('checkType', '楼层')
} else if (checkType.value === '楼层') {
selectedFloor.value = item.id
uni.setStorageSync('floorId', item.id)
uni.setStorageSync('floorName', item.name)
uni.removeStorageSync('houseNumberId')
uni.removeStorageSync('houseNumberName')
uni.setStorageSync('checkType', '户号')
} else if (checkType.value === '户号') {
selectedHouseNumber.value = item.id
uni.setStorageSync('houseNumberId', item.id)
uni.setStorageSync('houseNumberName', item.name)
}
// 防重复点击
if (isNavigating.value) return
isNavigating.value = true
setTimeout(() => {
uni.showToast({
title: '选择成功',
icon: 'success'
})
}, 500)
if (checkType.value === '楼栋') {
selectedBuilding.value = item.id
uni.setStorageSync('buildingId', item.id)
uni.setStorageSync('buildingName', item.name)
uni.removeStorageSync('unitId')
uni.removeStorageSync('unitName')
uni.removeStorageSync('floorId')
uni.removeStorageSync('floorName')
uni.removeStorageSync('houseNumberId')
uni.removeStorageSync('houseNumberName')
uni.setStorageSync('checkType', '单元')
} else if (checkType.value === '单元') {
selectedUnit.value = item.id
uni.setStorageSync('unitId', item.id)
uni.setStorageSync('unitName', item.name)
uni.removeStorageSync('floorId')
uni.removeStorageSync('floorName')
uni.removeStorageSync('houseNumberId')
uni.removeStorageSync('houseNumberName')
uni.setStorageSync('checkType', '楼层')
} else if (checkType.value === '楼层') {
selectedFloor.value = item.id
uni.setStorageSync('floorId', item.id)
uni.setStorageSync('floorName', item.name)
uni.removeStorageSync('houseNumberId')
uni.removeStorageSync('houseNumberName')
uni.setStorageSync('checkType', '户号')
} else if (checkType.value === '户号') {
selectedHouseNumber.value = item.id
uni.setStorageSync('houseNumberId', item.id)
uni.setStorageSync('houseNumberName', item.name)
}
setTimeout(() => {
uni.hideLoading()
goFreash()
}, 2000)
}
uni.showToast({
title: '选择成功',
icon: 'success'
})
}
setTimeout(() => {
goFreash()
}, 1000)
}
// 刷新跳转
const goFreash = () => {
console.log(checkType.value, '刷新页面')
if (checkType.value !== '户号') {
// 楼栋/单元/楼层:替换当前页,链式选择不堆叠
uni.redirectTo({
url: '/pageSubPack/my/selectBuilding'
})
} else {
uni.redirectTo({
url: '/pageSubPack/my/addHouse'
})
// 户号选择完成:返回已有的 addHouse 页面addHouse 通过 onShow 重新读取 storage
uni.navigateBack()
}
}
// 返回新增
const goBackAdd = () => {
if (checkType.value === '楼栋') {
uni.removeStorageSync('buildingId')
uni.removeStorageSync('buildingName')
} else if (checkType.value === '单元') {
uni.removeStorageSync('unitId')
uni.removeStorageSync('unitName')
} else if (checkType.value === '楼层') {
uni.removeStorageSync('floorId')
uni.removeStorageSync('floorName')
} else if (checkType.value === '户号') {
uni.removeStorageSync('houseNumberId')
uni.removeStorageSync('houseNumberName')
if (checkType.value === "楼栋") {
uni.removeStorageSync("buildingId")
uni.removeStorageSync("buildingName")
} else if (checkType.value === "单元") {
uni.removeStorageSync("unitId")
uni.removeStorageSync("unitName")
} else if (checkType.value === "楼层") {
uni.removeStorageSync("floorId")
uni.removeStorageSync("floorName")
} else if (checkType.value === "户号") {
uni.removeStorageSync("houseNumberId")
uni.removeStorageSync("houseNumberName")
}
uni.redirectTo({
url: '/pageSubPack/my/addHouse'
})
uni.navigateBack()
}
// 获取数据
const getList = async (type) => {
@@ -187,8 +176,8 @@
buildingId: selectedBuilding.value
})
const newList = res.data.map(item => ({
id: item,
name: item
id: item.id,
name: item.name
}))
dataList.value = newList
@@ -270,6 +259,11 @@
gap: 30rpx;
}
/* 正在跳转时禁用点击 */
.building-grid.disabled {
pointer-events: none;
}
/* 空状态 */
.empty-state {
display: flex;
@@ -298,6 +292,8 @@
border-radius: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
overflow: hidden;
min-width: 0;
}
/* 选中状态 */
@@ -315,6 +311,9 @@
font-size: 32rpx;
color: #333;
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
/* 成功提示弹窗 */

View File

@@ -143,7 +143,7 @@
uni.setStorageSync('carBrandId', item.id);
uni.setStorageSync('carBrandName', item.name);
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/addCar',
success: res => {},
fail: () => {},
@@ -152,15 +152,9 @@
}
const goBackAdd = () => {
uni.removeStorageSync('carBrandId');
uni.removeStorageSync('carBrandName');
uni.redirectTo({
url: '/pageSubPack/my/addCar',
success: res => {},
fail: () => {},
complete: () => {}
});
uni.removeStorageSync("carBrandId");
uni.removeStorageSync("carBrandName");
uni.navigateBack()
}
onShow(() => {})

View File

@@ -0,0 +1,365 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar title="选择业主" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y>
<!-- 搜索框 -->
<view class="search-box">
<input class="search-input" v-model="searchKey" placeholder="请输入业主名称"
placeholder-class="input-placeholder" @input="onSearch" />
<uni-icons type="search" size="20" color="#999" />
</view>
<view v-if="filteredList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<view class="owner-item" v-for="(item, index) in filteredList" :key="index" @click="handleItemClick(item, index)"
:class="{ checked: selectedIndex === index }">
<view class="owner-avatar">
<text class="avatar-text">{{ item.name ? item.name.charAt(0) : '业' }}</text>
</view>
<view class="owner-info">
<view class="owner-name">{{ item.name }}</view>
<view class="owner-phone" v-if="item.phone">{{ item.phone }}</view>
<view class="owner-house" v-if="item.houseName">{{ item.houseName }}</view>
</view>
<view class="check-icon" :class="{ checked: selectedIndex === index }"></view>
</view>
</scroll-view>
<view class="bottom-btn">
<button class="confirm-btn" @click="confirmBind">确定选择</button>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onReachBottom,
onPullDownRefresh,
onLoad
} from '@dcloudio/uni-app'
import {
getOwnerList
} from '@/pageSubPack/api/apiSub.js'
const searchKey = ref('')
const selectedId = ref('')
const selectedName = ref('')
const selectedIndex = ref(-1)
const ownerList = ref([])
const presetOwnerId = ref('')
const loadingMore = ref(false)
const noMoreData = ref(false)
const pageNum = ref(1)
const pageSize = ref(10)
// 过滤后的列表
const filteredList = computed(() => {
if (!searchKey.value.trim()) {
return ownerList.value
}
return ownerList.value.filter(item =>
item.name && item.name.includes(searchKey.value.trim())
)
})
// 搜索输入事件
const onSearch = () => {
// 实时过滤computed 自动处理
}
// 获取列表数据
const getList = async () => {
if (loadingMore.value || noMoreData.value) return
loadingMore.value = true
try {
const res = await getOwnerList({
pageNum: pageNum.value,
pageSize: pageSize.value,
villageId: uni.getStorageSync('communityId')
})
const newList = res.rows || []
if (pageNum.value === 1) {
ownerList.value = newList
// 预设选中已有业主
preselectOwner()
} else {
ownerList.value = [...ownerList.value, ...newList]
}
if (newList.length < pageSize.value) {
noMoreData.value = true
} else {
pageNum.value++
}
} catch (err) {
uni.showToast({
title: err.msg || '加载失败',
icon: 'none'
})
console.error('业主列表接口报错:', err)
} finally {
loadingMore.value = false
uni.stopPullDownRefresh()
}
}
// 根据传入的 ownerId 预设选中
const preselectOwner = () => {
if (!presetOwnerId.value) return
const idx = ownerList.value.findIndex(item => String(item.id) === String(presetOwnerId.value))
if (idx !== -1) {
selectedIndex.value = idx
selectedId.value = ownerList.value[idx].id
selectedName.value = ownerList.value[idx].name
}
}
// 上拉加载更多
onReachBottom(() => {
getList()
})
// 下拉刷新
const refresh = () => {
pageNum.value = 1
noMoreData.value = false
ownerList.value = []
loadingMore.value = false
getList()
}
onPullDownRefresh(() => {
refresh()
})
// 页面加载时请求第一页
onLoad((option) => {
presetOwnerId.value = option.ownerId || ""
})
onMounted(() => {
getList()
})
const handleItemClick = (item, index) => {
selectedIndex.value = index
selectedId.value = item.id
selectedName.value = item.name
}
const confirmBind = () => {
if (selectedIndex.value === -1) {
return uni.showToast({
title: "请先选择业主",
icon: "none"
})
}
uni.showModal({
title: "确认选择",
content: `确定选择业主 ${selectedName.value} 吗?`,
success: (res) => {
if (res.confirm) {
uni.setStorageSync('ownerId', selectedId.value)
uni.setStorageSync('ownerName', selectedName.value)
uni.showToast({
title: '选择成功',
icon: 'success'
})
setTimeout(() => {
goBack()
}, 500)
}
}
})
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
overflow: hidden;
}
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
/* 搜索框 */
.search-box {
display: flex;
align-items: center;
background-color: #F3F3F3;
border-radius: 20rpx;
margin: 20rpx 0px 20rpx 0px;
padding: 0 30rpx;
height: 70rpx;
}
.search-input {
flex: 1;
height: 100%;
font-size: 30rpx;
border: none;
background: transparent;
}
.input-placeholder {
color: #999;
font-size: 30rpx;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
/* 业主列表项 */
.owner-item {
display: flex;
align-items: center;
padding: 24rpx 20rpx;
margin-bottom: 20rpx;
border: 1rpx solid #e6e9ff;
border-radius: 16rpx;
background-color: #fff;
position: relative;
}
.owner-item.checked {
background: #f4f7ff;
border-color: #1677ff;
}
.owner-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background-color: #e8f0fe;
margin-right: 24rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-text {
font-size: 36rpx;
color: #1677ff;
font-weight: bold;
}
.owner-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.owner-name {
font-size: 32rpx;
font-weight: bold;
color: #000;
line-height: 1.4;
margin-bottom: 6rpx;
}
.owner-phone {
font-size: 26rpx;
color: #666;
line-height: 1.4;
}
.owner-house {
font-size: 26rpx;
color: #999;
line-height: 1.4;
}
/* 选中对勾 */
.check-icon {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
flex-shrink: 0;
position: relative;
}
.check-icon::after {
content: "✓";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 26rpx;
font-weight: bold;
}
.check-icon.checked {
background-color: #1677ff;
border-color: #1677ff;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
.confirm-btn {
width: 100%;
height: 88rpx;
background-color: #1677ff;
color: #fff;
border-radius: 12rpx;
font-size: 34rpx;
border: none;
line-height: 88rpx;
}
</style>

View File

@@ -26,6 +26,9 @@
ref,
onMounted
} from 'vue'
import {
onUnload
} from '@dcloudio/uni-app'
import {
getResidentCarAreaManageAreaListByVillageId,
getResidentCarAreaManageParkingListByAreaId
@@ -36,6 +39,9 @@
const dataList = ref([])
const selectedParkingLot = ref(uni.getStorageSync('parkingLotId'))
const selectedParkingSpace = ref(uni.getStorageSync('parkingSpaceId'))
const isSubmitting = ref(false)
let navTimer = null
let lockTimer = null
onMounted(() => {
checkType.value = uni.getStorageSync('checkType')
@@ -62,7 +68,7 @@
}else {
uni.showToast({
title: `${res.msg}`,
icon: 'error'
icon: 'none'
});
}
})
@@ -86,81 +92,81 @@
}else {
uni.showToast({
title: `${res.msg}`,
icon: 'error'
icon: 'none'
});
}
})
}
const selectedParking = (item) => {
console.log(item);
if (checkType.value == '停车场') {
selectedParkingLot.value = item.id
} else if (checkType.value == '车位编号') {
selectedParkingSpace.value = item.id
}
// 防重复点击锁:处理中直接返回,避免重复选择/重复跳转
if (isSubmitting.value) return
isSubmitting.value = true
uni.showModal({
title: "确认选择",
content: '确认选择此' + checkType.value + '吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '选择中...',
mask: true
})
setTimeout(() => {
uni.showToast({
title: '选择成功',
icon: 'success'
})
console.log(checkType.value);
if (checkType.value == '停车场') {
uni.setStorageSync('parkingLotId', item.id)
uni.setStorageSync('parkingLotName', item.name)
uni.removeStorageSync('parkingSpaceId')
uni.removeStorageSync('parkingSpaceName')
uni.setStorageSync('checkType', '车位编号')
} else if (checkType.value == '车位编号') {
console.log(item.id);
uni.setStorageSync('parkingSpaceId', item.id)
uni.setStorageSync('parkingSpaceName', item.name)
}
}, 1000)
setTimeout(() => {
uni.hideLoading()
goFreash()
}, 2500)
}
try {
if (checkType.value == '停车场') {
selectedParkingLot.value = item.id
uni.setStorageSync('parkingLotId', item.id)
uni.setStorageSync('parkingLotName', item.name)
uni.removeStorageSync('parkingSpaceId')
uni.removeStorageSync('parkingSpaceName')
uni.setStorageSync('checkType', '车位编号')
} else if (checkType.value == '车位编号') {
selectedParkingSpace.value = item.id
uni.setStorageSync('parkingSpaceId', item.id)
uni.setStorageSync('parkingSpaceName', item.name)
}
})
uni.showToast({
title: '选择成功',
icon: 'success'
})
navTimer = setTimeout(() => {
goFreash()
}, 1500)
} finally {
// 正常跳转后页面会销毁,防止异常时锁死
lockTimer = setTimeout(() => {
isSubmitting.value = false
}, 1500)
}
}
const goFreash = () => {
if (checkType.value != '车位编号') {
// 选完停车场后替换当前页进入选车位编号,避免压栈
uni.redirectTo({
url: '/pageSubPack/my/selectParingLot'
})
} else {
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace'
})
// 选完车位编号后返回原来的 addParkingSpace
uni.navigateBack()
}
}
const goBackAdd = () => {
if (checkType.value == '停车场') {
uni.removeStorageSync('parkingLotId')
uni.removeStorageSync('parkingLotName')
} else if (checkType.value == '车位编号') {
uni.removeStorageSync('parkingSpaceId')
uni.removeStorageSync('parkingSpaceName')
if (checkType.value == "停车场") {
uni.removeStorageSync("parkingLotId")
uni.removeStorageSync("parkingLotName")
} else if (checkType.value == "车位编号") {
uni.removeStorageSync("parkingSpaceId")
uni.removeStorageSync("parkingSpaceName")
}
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace'
})
uni.navigateBack()
}
// 页面销毁时清理定时器,避免手势返回后延迟跳转仍触发
onUnload(() => {
if (navTimer) {
clearTimeout(navTimer)
navTimer = null
}
if (lockTimer) {
clearTimeout(lockTimer)
lockTimer = null
}
})
</script>
<style lang="scss">
@@ -190,12 +196,7 @@
box-sizing: border-box;
}
.parkingLot-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30rpx;
}
.empty-state {
display: flex;
@@ -212,6 +213,15 @@
margin-top: 20rpx;
margin-bottom: 10rpx;
}
.parkingLot-grid {
/* display: flex;
flex-wrap: wrap;
gap: 30rpx; */
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30rpx
}
.parkingLot-item {
height: 100rpx;
@@ -223,6 +233,9 @@
border-radius: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
/* padding: 0 24rpx; */
overflow: hidden;
min-width: 0;
}
.parkingLot-item.active {
@@ -238,5 +251,8 @@
font-size: 32rpx;
color: #333;
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
</style>

View File

@@ -125,46 +125,22 @@
uni.removeStorageSync('parkingSpaceName');
console.log(sourcePage.value);
if (sourcePage.value == 'addHouse') {
// 替换当前页进入楼栋选择,避免页面堆叠
uni.redirectTo({
url: '/pageSubPack/my/selectBuilding',
success: res => {},
fail: () => {},
complete: () => {}
url: '/pageSubPack/my/selectBuilding'
});
} else if (sourcePage.value == 'addParkingSpace') {
uni.setStorageSync('checkType', '停车场')
uni.redirectTo({
// url: '/pageSubPack/my/bindHouse',
url: '/pageSubPack/my/selectParingLot',
success: res => {},
fail: () => {},
complete: () => {}
});
uni.navigateBack()
} else {
}
}
const goBackAdd = () => {
uni.removeStorageSync('communityId');
uni.removeStorageSync('communityName');
if (sourcePage.value == 'addHouse') {
uni.redirectTo({
url: '/pageSubPack/my/addHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
} else if (sourcePage.value == 'addParkingSpace') {
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
} else {
}
uni.removeStorageSync("communityId");
uni.removeStorageSync("communityName");
uni.navigateBack()
}
</script>

View File

@@ -1,9 +1,5 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar title="设置" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y>
<view class="setting-item" v-for="(item, index) in settingList" :key="index" @click="onItemClick(item)">
<text class="item-title">{{ item.title }}</text>
@@ -29,21 +25,13 @@
<script setup>
import {
ref,
onMounted,
reactive
onMounted
} from 'vue'
import {
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
authLogout, getUserProfile, getUserNotify, changeUserNotify, getAppVersion, checkAppUpdate, postUpdateLog
} from '/pageSubPack/api/apiSub.js'
//
const statusBarHeight = ref(20)
//
const cacheSize = ref(0)
const currentVersion = ref('1.0.0')
const currentVersionCode = ref(1)
@@ -64,7 +52,11 @@
{
title: '清除缓存',
type: 'text',
value: '9.2M'
value: ''
},
{
title: '注销账号',
type: 'arrow'
},
{
title: '升级版本',
@@ -113,7 +105,9 @@
currentVersion.value = manifest.version
}
// #endif
settingList.value[4].value = '当前版本 ' + currentVersion.value
if (settingList.value[5]) {
settingList.value[5].value = '当前版本 ' + currentVersion.value
}
}
// 版本号比较v1 > v2 返回 1v1 = v2 返回 0v1 < v2 返回 -1
@@ -130,16 +124,51 @@
return 0
}
// 跳转到应用商店更新
const openAppStore = (downloadUrl, platform) => {
// #ifdef APP-PLUS
if (downloadUrl) {
// 优先使用服务端返回的应用商店地址
const pkgName = plus.runtime.appid || ''
console.log('包名---------',pkgName)
// 无论返回哪种 downloadUrl都在后面拼接包名
const finalUrl = downloadUrl + pkgName
plus.runtime.openURL(finalUrl)
} else if (platform === 'ios') {
// iOS: 打开 App Store需在 Apple Developer 后台获取 Apple ID
plus.runtime.openURL('itms-apps://itunes.apple.com/app/id' + plus.runtime.appid)
} else {
// Android: 通过 market 协议唤起手机自带应用商店
const packageName = plus.runtime.appid || ''
plus.runtime.openURL('market://details?id=' + packageName, () => {
// market 协议失败时的回调,提示用户手动前往应用商店
uni.showToast({ title: '请前往应用商店更新', icon: 'none' })
})
}
// #endif
// #ifndef APP-PLUS
uni.showToast({ title: '请在应用商店更新', icon: 'none' })
// #endif
}
// 检查版本更新
const checkVersionUpdate = async () => {
uni.showLoading({ title: '检查中...', mask: true })
// #ifdef APP-PLUS
const channel = plus.runtime.channel; // 云打包自动写入,自定义包为空
// const channel = 'xiaomi'
// console.log('channel',channel)
// #endif
try {
const systemInfo = uni.getSystemInfoSync()
const platform = systemInfo.platform === 'ios' ? 'ios' : 'android'
const channel = 'resident'
const platform = systemInfo.platform === 'android' ? 'android':'ios'
const appChannel = 'resident'
// const channel = 'xiaomi'
// 1. 查询最新版本
const versionRes = await getAppVersion(platform, channel)
const versionRes = await getAppVersion(platform, channel, appChannel)
console.log('查询最新版本',versionRes.data)
if (versionRes.code === 200 && versionRes.data) {
const latestVersion = versionRes.data.versionName || ''
const latestVersionCode = versionRes.data.versionCode || 0
@@ -148,23 +177,31 @@
const checkRes = await checkAppUpdate({
platform: platform,
channel: channel,
appChannel: appChannel,
currentVersionCode: currentVersionCode.value
})
uni.hideLoading()
if (checkRes.code === 200 && checkRes.data) {
console.log('是否需要更新',checkRes.data)
const needUpdate = checkRes.data.needUpdate !== false
if (needUpdate) {
// 如果版本号一致,直接提示已是最新版本
if (compareVersion(currentVersion.value, latestVersion) >= 0) {
uni.showToast({ title: '已是最新版本', icon: 'success' })
return
}
const updateType = checkRes.data.updateType || versionRes.data.updateType || 'optional'
const isForce = updateType === 'force'
const downloadUrl = versionRes.data.downloadUrl || ''
uni.showModal({
title: isForce ? '强制更新' : '发现新版本',
content: `当前版本:${currentVersion.value}\n最新版本${latestVersion}${versionRes.data.releaseNotes || versionRes.data.updateDesc ? '\n\n更新内容' + (versionRes.data.releaseNotes || versionRes.data.updateDesc) : ''}`,
content: `当前版本:${currentVersion.value}\n最新版本${latestVersion}${versionRes.data.releaseNotes ? '\n\n更新内容' + (versionRes.data.releaseNotes) : ''}`,
showCancel: !isForce,
confirmText: '立即更新',
cancelText: '稍后',
success: (modalRes) => {
if (modalRes.confirm && versionRes.data.downloadUrl) {
if (modalRes.confirm) {
// 记录更新日志
const userId = uni.getStorageSync('userId')
if (userId) {
@@ -175,12 +212,8 @@
toVersion: latestVersionCode
})
}
// #ifdef APP-PLUS
plus.runtime.openURL(versionRes.data.downloadUrl)
// #endif
// #ifndef APP-PLUS
uni.showToast({ title: '请在应用商店更新', icon: 'none' })
// #endif
// 跳转到应用商店更新
openAppStore(downloadUrl, platform)
} else if (isForce && !modalRes.confirm) {
// 强制更新时用户点取消,退出应用
// #ifdef APP-PLUS
@@ -193,30 +226,35 @@
uni.showToast({ title: '已是最新版本', icon: 'success' })
}
} else {
uni.showToast({ title: '检查失败', icon: 'none' })
uni.showToast({ title: '检查更新失败1', icon: 'none' })
}
} else {
uni.hideLoading()
uni.showToast({ title: '检查失败', icon: 'none' })
uni.showToast({ title: '检查更新失败2', icon: 'none' })
}
} catch (err) {
uni.hideLoading()
uni.showToast({ title: '检查失败', icon: 'none' })
uni.showToast({ title: err.msg || '检查更新失败', icon: 'none' })
}
}
//
//
const onItemClick = (item) => {
if (item.title == '更改手机号码') {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/changePhone'
});
} else if (item.title == '修改密码') {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/changePwd'
});
} else if (item.title == '清除缓存') {
uni.clearStorageSync();
// 保存登录相关数据,清完恢复
const preserveKeys = ['token', 'userId', 'currentVillageId', 'hasShowGuide', 'push_cid']
const saved = {}
preserveKeys.forEach(key => { saved[key] = uni.getStorageSync(key) })
uni.clearStorageSync()
preserveKeys.forEach(key => { if (saved[key]) uni.setStorageSync(key, saved[key]) })
// #ifdef APP-PLUS
plus.cache.clear(() => {})
// #endif
@@ -234,6 +272,10 @@
setTimeout(() => {
uni.hideLoading();
}, 2500);
} else if (item.title == '注销账号') {
uni.navigateTo({
url: '/pageSubPack/my/deleteAccount'
});
} else if (item.title == '升级版本') {
checkVersionUpdate()
}
@@ -244,40 +286,37 @@
title: '提示',
content: '确定要退出当前账号吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '退出中...',
mask: true
});
authLogout().then(res => {
if (res.code === 200) {
uni.hideLoading();
uni.showToast({
title: '退出成功',
icon: 'success'
});
setTimeout(() => {
uni.hideLoading();
uni.redirectTo({
url: '/pageSubPack/loginSub/pwd-login'
});
}, 1500);
} else {
uni.showToast({
title: `${res.msg}`,
icon: 'error'
});
}
}).catch(err => {
console.log(err);
uni.showToast({
title: `${err}`,
icon: 'error'
});
})
if (!res.confirm) return
uni.showLoading({ title: '退出中...', mask: true })
const clearAndGo = () => {
const uid = uni.getStorageSync('userId') || ''
uni.removeStorageSync('token')
uni.removeStorageSync('userId')
uni.removeStorageSync('currentVillageId')
uni.removeStorageSync('home_cache_' + uid)
uni.removeStorageSync('home_cache')
// 清除小区相关缓存,防止切换账号后显示上一个账号的小区和公告
uni.removeStorageSync('communityName')
uni.removeStorageSync('communityId')
uni.removeStorageSync('needReloadUserInfo')
uni.hideLoading()
uni.hideToast()
uni.reLaunch({ url: '/pageSubPack/loginSub/login?mode=pwd' })
}
authLogout().then(res => {
if (res.code === 200) {
uni.hideLoading()
uni.showToast({ title: '退出成功', icon: 'success' })
setTimeout(clearAndGo, 1500)
} else {
clearAndGo()
}
}).catch(err => {
console.log(err)
clearAndGo()
})
}
})
}
@@ -298,11 +337,6 @@
})
}
const goBack = () => {
uni.switchTab({
url: '/pages/myIndex/myIndex'
});
}
onMounted(async () => {
initCurrentVersion()
@@ -338,14 +372,20 @@
.page {
flex: 1;
overflow-y: auto;
padding: 0 40rpx;
padding: 0 40rpx 140rpx;
box-sizing: border-box;
background-color: #fff;
}
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
}
</style>
@@ -395,11 +435,11 @@
height: 96rpx;
line-height: 96rpx;
text-align: center;
font-size: 36rpx;
font-size: 32rpx;
border-radius: 12rpx;
color: #666;
background-color: #F3F3F3;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
/* box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); */
}
.logout-btn:active {
@@ -407,10 +447,6 @@
opacity: 0.8;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>
<style>
@@ -430,4 +466,4 @@
.no-cross-switch>>>.uni-switch-input-checked {
background-color: #007aff !important;
}
</style>
</style>

View File

@@ -1,30 +1,7 @@
<template>
<view class="page">
<view class="status_bar"></view>
<!-- 固定导航栏 -->
<uni-nav-bar
left-icon="left"
title="通知公告"
background-color="#F9F9F9"
@clickLeft="narBack"
/>
<!-- 滚动区域 -->
<scroll-view
scroll-y
class="scroll-box"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<view class="list-wrap">
<!-- 下拉刷新 -->
<view class="refresh" v-if="refreshing">
<uni-icons type="spinner-cycle" class="spin"/> 刷新中...
</view>
<!-- 列表 -->
<!-- 列表 -->
<view class="list-wrap">
<view
v-for="notice in noticeList"
:key="notice.noticeId"
@@ -48,18 +25,16 @@
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无通知</text>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad ,onShow,onUnload} from '@dcloudio/uni-app'
import { onLoad, onShow, onUnload, onBackPress, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getNoticeList } from '../api/api.js'
const noticeList = ref([])
const refreshing = ref(false)
const loadingMore = ref(false)
const noMoreData = ref(false)
const loading = ref(false)
@@ -90,8 +65,8 @@ const loadNotices = async (isRefresh = false) => {
} catch (e) {}
finally {
loading.value = false
refreshing.value = false
loadingMore.value = false
uni.stopPullDownRefresh()
}
}
// 更新单条已读状态
@@ -107,10 +82,13 @@ onShow(() =>{
loadNotices(true)
})
const onRefresh = () => {
refreshing.value = true
onPullDownRefresh(() => {
loadNotices(true)
}
})
onReachBottom(() => {
loadMore()
})
const loadMore = () => {
if (noMoreData.value || loadingMore.value) return
@@ -119,12 +97,18 @@ const loadMore = () => {
loadNotices()
}
const narBack = () => {
onBackPress(() => {
const page = uni.getStorageSync('sourcePage')
if (page === 'index') uni.switchTab({ url: '/pages/index/index' })
if (page === 'serviceIndex') uni.switchTab({ url: '/pages/serviceIndex/serviceIndex' })
uni.removeStorageSync('sourcePage')
}
if (page === 'index') {
uni.switchTab({ url: '/pages/index/index' })
return true
}
if (page === 'serviceIndex') {
uni.switchTab({ url: '/pages/serviceIndex/serviceIndex' })
return true
}
})
onLoad(() => {
isFirstShow.value = false
@@ -138,24 +122,7 @@ onUnload(()=>{
<style lang="scss">
page {
background: #f9f9f9;
}
.page {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
}
.status_bar {
height: 46px;
width: 100%;
}
.scroll-box {
flex: 1;
height: 0; /* 让滚动区域生效 */
background: linear-gradient(to bottom left, #E2F0FF 0%, #f9f9f9 50%);
}
.list-wrap {
padding: 40rpx;
@@ -174,7 +141,8 @@ page {
.title {
font-size: 32rpx;
font-weight: bold;
width: 500rpx;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -197,13 +165,26 @@ page {
margin: 20rpx 0;
background-color: #f9f9f9;
padding: 24rpx;
word-break: break-all;
/* 按高度截断,避免 line-clamp 导致文字重叠 */
max-height: 160rpx;
overflow: hidden;
/* 富文本里的图片不超出屏幕 */
:deep(img),
:deep(image) {
max-width: 100% !important;
height: auto !important;
display: block;
}
}
.detail {
display: flex;
justify-content: space-between;
align-items: center;
}
.refresh, .load-more, .no-more {
.load-more, .no-more {
text-align: center;
padding: 20rpx;
color: #999;
@@ -231,11 +212,4 @@ page {
padding: 10rpx 30rpx;
margin-top: 20rpx;
}
.spin {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@@ -12,7 +12,7 @@
<view class="notice-meta">
<view class="notice-author">
<text class="author-name">{{noticeDetail.author}}</text>
<text class="separator">·</text>
<text class="separator" v-if="noticeDetail.author">·</text>
<text class="publish-time">{{noticeDetail.publishTime}}</text>
</view>
</view>
@@ -21,8 +21,8 @@
<view class="article-content">
<view class="paragraph" v-html="noticeDetail.noticeContent"></view>
<view class="articleimg">
<image src="https://wuyeadmin.bugtc.com/images/home/notice-1.png" mode="" style="width: 100%;height: 100%;border-radius: 20rpx;"></image>
<image src="https://wuyeadmin.bugtc.com/images/home/notice-2.png" mode="" style="width: 100%;height: 100%;border-radius: 20rpx;"></image>
<!-- <image src="https://wuyeadmin.bugtc.com/images/home/notice-1.png" mode="aspectFill" style="width: 100%;height: 100%;border-radius: 20rpx;"></image> -->
<!-- <image src="https://wuyeadmin.bugtc.com/images/home/notice-2.png" mode="aspectFill" style="width: 100%;height: 100%;border-radius: 20rpx;"></image> -->
</view>
</view>
@@ -103,7 +103,7 @@
}
}catch(err){
uni.showToast({
title: res.msg ||"网络异常",
title: err.msg ||"网络异常",
icon:"none"
})
}
@@ -196,6 +196,8 @@
line-height: 1.4;
margin-bottom: 16px;
text-align: center;
word-break: break-all;
white-space: pre-wrap;
}
.article-meta {
@@ -279,6 +281,17 @@
margin-bottom: 20px;
text-align: justify;
text-indent: 2em;
white-space: pre-wrap;
word-break: break-word;
/* 富文本里的图片不超出屏幕 */
:deep(img),
:deep(image) {
max-width: 100% !important;
height: auto !important;
display: block;
margin: 10px 0;
}
}
.bullet-list {

View File

@@ -1,50 +1,46 @@
<template>
<!-- 页面主容器 -->
<view class="door-open-page">
<view class="status_bar" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 顶部标题栏 -->
<view class="top-bar">
<uni-nav-bar left-icon="left" title="远程开门" backgroundColor ="none" :border="false" @clickLeft="narBack" />
</view>
<view class="top-img">
<image src="https://wuyeadmin.bugtc.com/images/openDoor/remoteOpen.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
<view class="page-content">
<view class="top-img">
<image src="https://wuye.ckdzkj.com/images/openDoor/remoteOpen.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
<!-- 提示文案 -->
<view class="tips-text">
远程开门
<text class="tips-desc">请选择要开的门并保持距离在10米以内单次仅开一门</text>
</view>
<!-- 提示文案 -->
<view class="tips-text">
远程开门
<text class="tips-desc">请选择要开的门并保持距离在10米以内单次仅开一门</text>
</view>
<!-- 开门成功提示默认隐藏 -->
<view v-if="showSuccess" class="success-toast">开门成功</view>
<!-- 开门成功提示默认隐藏 -->
<view v-if="showSuccess" class="success-toast">开门成功</view>
<!-- 开门授权区域 -->
<view class="auth-container">
<view class="auth-title">开门授权</view>
<!-- 网格布局2行2列 -->
<view class="grid-container">
<!-- 每个门选项 -->
<view
v-for="(item, index) in doorList"
:key="index"
:class="['door-item', { active: selectedDoor === item.id }]"
@click="selectDoor(item.id)"
>
<!-- 图标占位 -->
<view class="door-icon">
<image class="icon-text" :src="selectedDoor === item.id ? item.iconActive:item.iconText"></image>
<!-- 开门授权区域 -->
<view class="auth-container">
<view class="auth-title">开门授权</view>
<!-- 网格布局2行2列 -->
<view class="grid-container">
<!-- 每个门选项 -->
<view
v-for="(item, index) in doorList"
:key="index"
:class="['door-item', { active: selectedDoor === item.id }]"
@click="selectDoor(item.id)"
>
<!-- 图标占位 -->
<view class="door-icon">
<image class="icon-text" :src="selectedDoor === item.id ? item.iconActive:item.iconText"></image>
</view>
<!-- 门名称 -->
<text class="door-name" :class="{active :selectedDoor === item.id}">{{ item.deviceName }}</text>
</view>
<!-- 门名称 -->
<text class="door-name" :class="{active :selectedDoor === item.id}">{{ item.deviceName }}</text>
</view>
<!-- 空状态 -->
<view v-if="doorList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无可用门禁</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="doorList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无可用门禁</text>
</view>
</view>
<!-- 确认开门按钮 -->
@@ -60,7 +56,6 @@ import {getAccessDevices,getAccessOpen} from '../api/api.js'
// 响应式数据:选中的门索引、开门成功提示
const selectedDoor = ref(-1) // -1表示未选中
const showSuccess = ref(false)
const statusBarHeight = ref(uni.getSystemInfoSync().statusBarHeight || 0)
// 门列表数据
const doorList = ref([])
@@ -70,47 +65,33 @@ const selectDoor = (id) => {
selectedDoor.value = id
showSuccess.value = false // 选择新门时隐藏成功提示
}
// 返回
const narBack = () =>{
if(uni.getStorageSync('sourcePage') === "index"){
uni.switchTab({
url:"/pages/index/index"
})
}else if(uni.getStorageSync('sourcePage') === "serviceIndex"){
uni.switchTab({
url:"/pages/serviceIndex/serviceIndex"
})
}
uni.removeStorageSync('sourcePage')
}
// 查询门禁列表
const getAccessControl = async () =>{
try{
const res = await getAccessDevices()
if(res.code === 200){
if(res.data){
doorList.value = res.data || []
}else{
doorList.value = []
}
}else{
doorList.value = []
uni.showToast({
title:'门禁查询失败',
icon:"error"
})
}
}catch(err){
doorList.value = []
uni.showToast({
title:err.msg || '查询失败',
icon:"none"
})
}
try{
const res = await getAccessDevices()
if(res.code === 200){
if(res.data){
doorList.value = res.data || []
}else{
doorList.value = []
}
}else{
doorList.value = []
uni.showToast({
title:'门禁查询失败',
icon:"none"
})
}
}catch(err){
doorList.value = []
uni.showToast({
title:err.msg || '查询失败',
icon:"none"
})
}
}
@@ -132,67 +113,62 @@ const openDoorClick = () => {
}
// 开门接口
const getAccessOpenData = async () =>{
uni.showLoading({
title: '开门中...',
mask: true
})
try{
let params = {deviceId:selectedDoor.value}
const res = await getAccessOpen(params)
uni.hideLoading()
if(res.code === 200){
uni.showToast({
title:'开门成功',
icon:'success'
})
}else{
uni.showToast({
title:'开门失败',
icon:"error"
})
}
}catch(err){
uni.hideLoading()
uni.showToast({
title: err.msg || '开门失败',
icon:"none"
})
}
uni.showLoading({
title: '开门中...',
mask: true
})
try{
let params = {deviceId:selectedDoor.value}
const res = await getAccessOpen(params)
uni.hideLoading()
if(res.code === 200){
uni.showToast({
title:'开门成功',
icon:'success'
})
}else{
uni.showToast({
title:'开门失败',
icon:"error"
})
}
}catch(err){
uni.hideLoading()
uni.showToast({
title: err.msg || '开门失败',
icon:"none"
})
}
}
onLoad(() =>{
getAccessControl()
getAccessControl()
})
</script>
<style scoped>
/* 页面全局样式 */
.door-open-page {
display: flex;
flex-direction: column;
min-height: 100%;
overflow: hidden;
background: linear-gradient(180deg, #E2F0FF, #F6FAFF);
min-height: 100vh;
padding: 30rpx 40rpx;
box-sizing: border-box;
}
.status_bar {
/* height: var(--status-bar-height); */
width: 100%;
flex-shrink: 0;
}
/* 顶部标题 */
.top-bar {
text-align: center;
margin-bottom: 20rpx;
}
.title-text {
font-size: 48rpx;
color: #ffffff;
font-weight: bold;
.page-content {
flex: 1;
height: 0;
overflow-y: auto;
padding: 30rpx 40rpx 160rpx;
box-sizing: border-box;
}
.top-img{
width: 460rpx;
height: 290rpx;
float: right;
width: 460rpx;
height: 290rpx;
float: right;
}
/* 提示文案 */
@@ -316,7 +292,12 @@ onLoad(() =>{
/* 确认按钮 */
.confirm-btn {
width: 100%;
position: fixed;
left: 40rpx;
right: 40rpx;
bottom: 20rpx;
bottom: calc(20rpx + constant(safe-area-inset-bottom));
bottom: calc(20rpx + env(safe-area-inset-bottom));
height: 88rpx;
line-height: 88rpx;
background-color: #2F77FD;
@@ -324,8 +305,9 @@ onLoad(() =>{
font-size: 28rpx;
border-radius: 20rpx;
border: none;
z-index: 10;
}
.confirm-btn::after {
border: none; /* 去除uni-app默认按钮边框 */
}
</style>
</style>

View File

@@ -19,13 +19,7 @@
</view>
<!-- 小区列表 -->
<scroll-view
class="community-list"
scroll-y
:scroll-into-view="scrollIntoViewId"
@scrolltolower="loadMoreData"
:lower-threshold="100"
>
<view class="community-list">
<view
v-for="item in displayedCommunities"
:key="item.villageId"
@@ -35,7 +29,7 @@
@tap="selectCommunity(item)"
>
<view class="community-item-left">
<image src="https://wuyeadmin.bugtc.com/images/home/community.png" style="width: 60rpx;height: 60rpx;"></image>
<image src="https://wuye.ckdzkj.com/images/home/community.png" style="width: 60rpx;height: 60rpx;"></image>
<view class="community-info">
<text class="community-name">{{ item.villageName }}</text>
</view>
@@ -74,7 +68,7 @@
<text class="empty-text">未找到相关小区</text>
<text class="empty-tip">请尝试其他关键词</text>
</view>
</scroll-view>
</view>
<!-- 确认按钮 -->
<view class="confirm-area">
@@ -92,7 +86,7 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getVillageList, getVillageSwitch } from "../api/api.js"
// 响应式数据
@@ -100,6 +94,7 @@ const searchText = ref('')
const selectedItem = ref(null)
const scrollIntoViewId = ref('')
const currentVillageIdFromHome = ref('') // 首页传入的当前选中小区ID
const sourcePage = ref('') // 来源页面payment 为缴费页独立切换,其他走首页切换
// 分页相关
const currentPage = ref(1)
@@ -119,7 +114,8 @@ const getCommunityList = async (pageNum = 1, keyword = '') => {
isLoading.value = true
try {
keyword = decodeURIComponent(keyword)
// keyword 来自用户输入时是普通文本,无需 decodeURIComponent
// 仅当 keyword 来自 URL 参数时才需要解码(已在外部处理)
const params = {
pageNum,
pageSize,
@@ -129,8 +125,10 @@ const getCommunityList = async (pageNum = 1, keyword = '') => {
const res = await getVillageList(params)
if (res.code === 200) {
const listData = res.data || []
const total = res.data?.total || listData.length
// 兼容两种返回格式:数组 或 { rows, total }
const resData = res.data || {}
const listData = Array.isArray(resData) ? resData : (resData.rows || [])
const total = Array.isArray(resData) ? resData.length : (resData.total || listData.length)
if (pageNum === 1) {
displayedCommunities.value = listData
@@ -142,20 +140,32 @@ const getCommunityList = async (pageNum = 1, keyword = '') => {
hasMoreData.value = displayedCommunities.value.length < totalCount.value
// 默认选中:优先匹配传入的 villageId否则选第一条仅第一页时
// 搜索场景下不覆盖用户已手动选择的项
if (pageNum === 1 && displayedCommunities.value.length > 0) {
const targetId = currentVillageIdFromHome.value || uni.getStorageSync('currentVillageId') || ''
if (targetId) {
const matched = displayedCommunities.value.find(
item => String(item.villageId) === String(targetId)
const userSelectedInCurrentList = selectedItem.value &&
displayedCommunities.value.some(
item => String(item.villageId) === String(selectedItem.value.villageId)
)
selectedItem.value = matched || displayedCommunities.value[0]
} else {
selectedItem.value = displayedCommunities.value[0]
// 如果用户已选中且该项仍在当前列表中,保留用户选择
if (!isSearching.value || !userSelectedInCurrentList) {
const targetId = currentVillageIdFromHome.value || uni.getStorageSync('currentVillageId') || ''
if (targetId) {
const matched = displayedCommunities.value.find(
item => String(item.villageId) === String(targetId)
)
selectedItem.value = matched || displayedCommunities.value[0]
} else {
selectedItem.value = displayedCommunities.value[0]
}
}
} else if (pageNum === 1) {
// 第一页返回空列表(搜索无结果等场景),清除选中项使确认按钮禁用
selectedItem.value = null
}
} else {
displayedCommunities.value = []
hasMoreData.value = false
selectedItem.value = null
uni.showToast({
title: res.msg || "查询失败",
icon: 'error'
@@ -165,18 +175,21 @@ const getCommunityList = async (pageNum = 1, keyword = '') => {
// console.error('获取小区列表失败:', err)
displayedCommunities.value = []
hasMoreData.value = false
selectedItem.value = null
uni.showToast({
title: err.msg || "网络异常",
icon: 'error'
})
} finally {
isLoading.value = false
uni.stopPullDownRefresh()
}
}
// 页面加载时初始化
onLoad((options) => {
// 接收首页传来得选择的小区id
// 接收来源页面及选中小区id
sourcePage.value = options?.source || ''
currentVillageIdFromHome.value = options?.villageId || ''
getCommunityList(1)
})
@@ -188,6 +201,17 @@ const loadMoreData = () => {
currentPage.value += 1
}
// 下拉刷新
onPullDownRefresh(() => {
currentPage.value = 1
getCommunityList(1, searchText.value)
})
// 上拉加载更多
onReachBottom(() => {
loadMoreData()
})
// 处理搜索输入(防抖)
const handleSearchInput = (e) => {
let val = e.detail.value || ''
@@ -226,42 +250,58 @@ const handleConfirm = async () => {
})
return
}
// 缴费页为独立切换:不调用后端切换小区接口
const isPayment = sourcePage.value === 'payment'
try {
uni.showLoading({
title: '切换小区中...',
title: isPayment ? '加载中...' : '切换小区中...',
mask: true
})
const params = {
villageId: selectedItem.value.villageId,
}
const res = await getVillageSwitch(params)
uni.hideLoading()
if (res.code === 200) {
if (res.data.villageId) {
if (!isPayment) {
const res = await getVillageSwitch({
villageId: selectedItem.value.villageId,
})
uni.hideLoading()
if (res.code !== 200) {
uni.showToast({
title: res.msg || '切换小区失败',
icon: 'none',
duration: 2000
})
return
}
if (res.data?.villageId) {
selectedItem.value.villageId = res.data.villageId
}
uni.showToast({
title: '选择成功',
icon: 'success',
duration: 1500
} else {
uni.hideLoading()
}
uni.showToast({
title: '选择成功',
icon: 'success',
duration: 1500
})
if (isPayment) {
// 缴费页独立缓存 + 独立事件,通知缴费页刷新(按账号隔离)
const userId = uni.getStorageSync('userId') || ''
uni.setStorageSync('paymentVillageId_' + userId, selectedItem.value.villageId)
uni.setStorageSync('paymentVillageName_' + userId, selectedItem.value.villageName)
uni.$emit('confirmPayment', {
name: selectedItem.value.villageName,
villageId: selectedItem.value.villageId
})
} else {
// 更新缓存并通知首页
uni.setStorageSync('currentVillageId', selectedItem.value.villageId)
uni.$emit('confirm', {
name: selectedItem.value.villageName,
villageId: selectedItem.value.villageId
})
setTimeout(() => {
uni.navigateBack()
}, 1000)
} else {
uni.showToast({
title: res.msg || '切换小区失败',
icon: 'error',
duration: 2000
})
}
setTimeout(() => {
uni.navigateBack()
}, 1000)
} catch (err) {
uni.hideLoading()
uni.showToast({
@@ -347,7 +387,6 @@ onMounted(() => {
}
.community-list {
flex: 1;
margin-top: 20rpx;
margin-bottom: 180rpx;
}

View File

@@ -1,18 +1,8 @@
<template>
<view class="repair-page">
<view class="status_bar" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-left" @tap="goBack">
<image src="https://wuyeadmin.bugtc.com/images/home/arr-left.png" style="width: 18px; height: 18px;"></image>
</view>
<view class="nav-title">在线报修</view>
<view class="nav-right"></view>
</view>
<!-- 报修类型Tab -->
<scroll-view class="scroll-container" scroll-y="true"
:lower-threshold="100">
<view class="scroll-container">
<view class="repair-type-tabs">
<view
class="type-tab"
@@ -20,7 +10,7 @@
@click="publicClick"
style="margin-right: 20rpx;"
>
<image src="https://wuyeadmin.bugtc.com/images/repair/publicRepair.png" style="width: 100%;height: 100%;"></image>
<image src="https://wuye.ckdzkj.com/images/repair/publicRepair.png" style="width: 100%;height: 100%;"></image>
<!-- <uni-icons type="staff" size="24" color="#fff"></uni-icons> -->
<!-- <text class="type-text">公共报修</text> -->
</view>
@@ -29,7 +19,7 @@
:class="{ active: activeType === 'personal' }"
@click="personalClick"
>
<image src="https://wuyeadmin.bugtc.com/images/repair/personalRepair.png" style="width: 100%;height: 100%;"></image>
<image src="https://wuye.ckdzkj.com/images/repair/personalRepair.png" style="width: 100%;height: 100%;"></image>
<!-- <uni-icons type="person" size="24" color="#fff"></uni-icons> -->
<!-- <text class="type-text">个人报修</text> -->
</view>
@@ -92,7 +82,7 @@
<!-- 时间和详情 -->
<view class="item-footer">
<text class="item-time">{{ item.orderDate || '' }}</text>
<text class="item-time">{{ item.createTime || '' }}</text>
<text class="detail-btn" @click="goDetail(item)">报修详情</text>
</view>
</view>
@@ -102,13 +92,19 @@
<text class="empty-text">暂无数据</text>
</view>
</view>
</scroll-view>
<!-- 加载更多提示 -->
<view v-if="repairList.length > 0" class="load-more-tip">
<text v-if="loading">加载中...</text>
<text v-else-if="!hasMoreData">没有更多数据了</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch } from 'vue';
import { onLoad,onUnload } from '@dcloudio/uni-app';
import { ref, watch, computed } from 'vue';
import { onLoad, onShow, onUnload, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app';
import { getQueryRepair } from '../api/api.js';
// 报修类型public:公共报修personal:个人报修)
@@ -120,6 +116,8 @@ const repairList = ref([]);
const loading = ref(false);
const pageNum = ref(1);
const pageSize = ref(10);
const total = ref(0);
const hasMoreData = computed(() => repairList.value.length < total.value);
const maintenanceItemId = ref('');
const statusBarHeight = ref(uni.getSystemInfoSync().statusBarHeight || 0)
@@ -138,14 +136,23 @@ const getStatusInfo = (val) => {
};
// 获取报修列表
const fetchRepairList = async () => {
// reset = true 时重置到第一页;否则按当前 pageNum 追加加载
const fetchRepairList = async (reset = false) => {
const villageId = uni.getStorageSync('currentVillageId');
if (!villageId || villageId === '0') {
uni.showToast({ title: '请先选择小区', icon: 'none' });
return;
}
// 防止重复请求
if (loading.value) return;
loading.value = true;
if (reset) {
pageNum.value = 1;
repairList.value = [];
total.value = 0;
}
try {
const params = {
villageId,
@@ -158,14 +165,16 @@ const fetchRepairList = async () => {
// console.log('接口返回:', res);
if (res.code === 200) {
const list = res.rows || [];
repairList.value = list.map(item => {
total.value = Number(res.total) || 0;
const mapped = list.map(item => {
const statusInfo = getStatusInfo(item.problemStatus);
const images = item.problemImageUrl ? item.problemImageUrl.split(',').filter(Boolean) : [];
const images = item.problemImageUrl ? item.problemImageUrl.split(',').filter(Boolean).slice(0, 3) : [];
if (statusInfo) {
return { ...item, status: statusInfo.text, statusClass: statusInfo.class, imageList: images };
}
return { ...item, imageList: images };
});
repairList.value = pageNum.value === 1 ? mapped : [...repairList.value, ...mapped];
} else {
uni.showToast({ title: res.msg || '获取失败', icon: 'none' });
}
@@ -173,12 +182,30 @@ const fetchRepairList = async () => {
uni.showToast({ title: '网络异常', icon: 'none' });
} finally {
loading.value = false;
uni.stopPullDownRefresh();
}
};
// 监听状态变化自动刷新
watch(activeStatus, () => {
// 加载更多(滚动到底部触发)
const loadMore = () => {
if (loading.value || !hasMoreData.value) return;
pageNum.value++;
fetchRepairList();
};
// 下拉刷新
onPullDownRefresh(() => {
fetchRepairList(true);
});
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
// 监听状态变化自动刷新(切换 tab 时重置到第一页)
watch(activeStatus, () => {
fetchRepairList(true);
});
function publicClick(){
@@ -203,6 +230,8 @@ const goBack = () => {
uni.switchTab({
url:"/pages/serviceIndex/serviceIndex"
})
}else{
uni.navigateBack()
}
uni.removeStorageSync('sourcePage')
@@ -216,10 +245,14 @@ const goDetail = (item) => {
onLoad(() => {
// maintenanceItemId.value = option.maintenanceProjectId || '' // 接收维修项目id
fetchRepairList()
fetchRepairList(true)
});
onShow(() => {
// 从其他页面返回时刷新列表
fetchRepairList(true)
});
onUnload(() =>{
})
</script>
@@ -273,17 +306,14 @@ onUnload(() =>{
}
/* 滚动区域 */
.scroll-container {
flex: 1;
width: 100%;
height: calc(100vh - 100px);
overflow-y: auto;
box-sizing: border-box;
}
/* 报修类型Tab */
.repair-type-tabs {
display: flex;
/* background-color: #fff; */
/* padding: 10px; */
padding: 10px 0;
}
.type-tab {
flex: 1;
@@ -291,7 +321,7 @@ onUnload(() =>{
flex-direction: column;
align-items: center;
justify-content: center;
height: 100px;
height: 90px;
border-radius: 8px;
color: #fff;
font-size: 14px;
@@ -349,7 +379,7 @@ onUnload(() =>{
/* 报修列表 */
.repair-list {
padding: 28rpx 0rpx;
padding: 28rpx 0rpx 0;
}
.repair-item {
background-color: #fff;
@@ -357,6 +387,9 @@ onUnload(() =>{
padding: 15px;
margin-bottom: 10px;
}
.repair-item:last-child {
margin-bottom: 0;
}
/* 列表项头部 */
.item-header {
@@ -371,6 +404,10 @@ onUnload(() =>{
color: #333;
padding-right: 20rpx;
width: 77%;
word-break: break-all;
word-wrap: break-word;
white-space: normal;
line-height: 1.4;
}
.item-status {
font-size: 14px;
@@ -423,24 +460,28 @@ onUnload(() =>{
color: #666;
line-height: 1.5;
margin-bottom: 10px;
word-break: break-all;
white-space: normal;
overflow-wrap: break-word;
/* background-color: #d7e4f5; */
}
/* 图片占位 */
.item-imgs {
display: flex;
/* gap: 12px; */
margin-bottom: 10px;
flex-wrap: wrap;
margin: -6rpx;
margin-bottom: 16px;
}
.img-placeholder {
width: 80px;
width: 95px;
height: 80px;
background-color: #f5f5f5;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
margin: 6rpx;
}
.empty-state {
display: flex;
@@ -457,6 +498,16 @@ onUnload(() =>{
margin-bottom: 10rpx;
}
/* 加载更多提示 */
.load-more-tip {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx 0;
color: #999;
font-size: 26rpx;
}
/* 底部时间和详情 */
.item-footer {
display: flex;

View File

@@ -2,7 +2,9 @@
<view class="form-container">
<!-- 报修房屋 -->
<view class="form-item">
<text class="label">报修房屋</text>
<view class="label">
<text class="required">*</text>报修房屋
</view>
<view class="value" @click="goToSelectHouse">
{{ houseName || '请选择房屋' }}
<!-- <text class="switch" v-if="houseName">切换</text> -->
@@ -12,7 +14,9 @@
<!-- 维修项目 -->
<view class="form-item">
<text class="label">维修项目</text>
<view class="label">
<text class="required">*</text>维修项目
</view>
<view class="value" @click="goToSelectProject">
{{ projectName || '请选择维修项目' }}
<uni-icons type="arrowright" size="16"></uni-icons>
@@ -21,33 +25,37 @@
<!-- 标题 -->
<view class="form-item">
<text class="label">标题</text>
<uni-easyinput v-model="form.title" placeholder="请输入内容" :inputBorder="false"></uni-easyinput>
<view class="label">
<text class="required">*</text>标题
</view>
<uni-easyinput v-model="form.title" placeholder="请输入内容最多30个字符" :inputBorder="false" maxlength="30"></uni-easyinput>
</view>
<!-- 问题描述 -->
<view class="form-item-wenti">
<text class="label">问题描述</text>
<uni-easyinput v-model="form.desc" placeholder="请描述你的问题" type="textarea"></uni-easyinput>
<view class="textarea-wrap">
<textarea v-model="form.desc" placeholder="请描述你的问题或位置最多200字" maxlength="200" class="desc-textarea" />
<text class="char-count">{{ form.desc.length }}/200</text>
</view>
<!-- <uni-easyinput v-model="form.desc" placeholder="请描述你的问题" type="textarea" maxlength="200"></uni-easyinput> -->
</view>
<!-- 手机号 -->
<view class="form-item">
<text class="label">手机号</text>
<uni-easyinput type="number" v-model="form.phone" placeholder="请输入手机号" :inputBorder="false"></uni-easyinput>
<view class="label">
<text class="required">*</text>手机号
</view>
<uni-easyinput type="number" v-model="form.phone" placeholder="请输入手机号" :inputBorder="false" maxlength="11"></uni-easyinput>
</view>
<!-- 预约时间 -->
<view class="form-item time-item" @click="openTimePopup">
<text class="label">预约时间</text>
<view class="label">
<text class="required">*</text>预约时间
</view>
<text class="value time-value">{{ showTimeText || '请选择预约时间' }}</text>
</view>
<!-- <view class="form-item">
<text class="label">预约时间</text>
<view class="value" @click="">
<uni-datetime-picker type="datetime" v-model="form.datetime" @change="changeTime" />
</view>
</view> -->
<!-- 问题照片 -->
<view class="form-item-wenti">
@@ -56,10 +64,10 @@
<!-- 已上传图片 -->
<view class="upload-item" v-for="(img, index) in form.images" :key="index">
<image :src="img" mode="aspectFill" class="upload-img" @click="previewImage(img)"></image>
<image src="https://wuyeadmin.bugtc.com/images/imgs/delete.png" class="upload-delete" @click="deleteImage(index)"></image>
<image src="https://wuye.ckdzkj.com/images/imgs/delete.png" class="upload-delete" @click="deleteImage(index)"></image>
</view>
<!-- 上传按钮 -->
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 3">
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 9">
<uni-icons type="plusempty" size="24"></uni-icons>
<!-- <text>上传照片</text> -->
</view>
@@ -67,7 +75,9 @@
</view>
<!-- 提交按钮 -->
<button class="submit-btn" @click="submitForm">提交</button>
<button class="submit-btn" :disabled="submitting" @click="submitForm">{{ submitting ? '提交中...' : '提交' }}</button>
<!-- 提交中遮罩 -->
<view class="submit-mask" v-if="submitting" @touchmove.stop.prevent></view>
<!-- 时间选择弹窗 -->
<uni-popup ref="timePopupRef" type="bottom" :mask-click="false" border-radius="16rpx">
@@ -78,7 +88,7 @@
<text class="popup-title">预约时间({{ tempTime.format('YYYY-MM-DD HH:mm') }})</text>
<text class="confirm-btn" @click="confirmTime">确认</text>
</view>
<!-- 时间选择列日期 | 小时 | 分钟 -->
<view class="time-selector">
<!-- 日期列 -->
@@ -146,14 +156,19 @@
import { ref, computed, onMounted } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { addQueryRepair, uploadFile } from '../api/api.js'
import { getUserProfile } from '../api/apiSub.js'
import moment from 'moment/moment'
// 选中的房屋/项目名称
const houseName = ref('')
const houseId = ref('')
const residentId = ref('')
const projectName = ref('')
const projectId = ref('')
// 提交状态锁
const submitting = ref(false)
// 表单数据
const form = ref({
title: '',
@@ -165,8 +180,21 @@ const form = ref({
// ========== 基础数据 ==========
const timePopupRef = ref(null)
const selectedTime = ref('')
const tempTime = ref(moment().hour(9).minute(30).second(0))
// 默认预约时间:今天(向上取整到下一个整点/半点)
const getDefaultTime = () => {
const t = moment().second(0).millisecond(0)
if (t.minute() > 30) {
t.add(1, 'hour').minute(0)
} else if (t.minute() > 0) {
t.minute(30)
}
return t
}
const selectedTime = ref(getDefaultTime())
const tempTime = ref(getDefaultTime())
form.value.datetime = selectedTime.value.format('YYYY-MM-DD HH:mm')
// ========== 时间选择列数据 ==========
const dateList = ref([
@@ -187,12 +215,15 @@ const minuteList = ref([
])
// ========== 选中状态 ==========
const selectedDate = ref('')
const selectedHour = ref('')
const selectedMinute = ref('')
const selectedDate = ref(selectedTime.value.format('YYYY-MM-DD'))
const selectedHour = ref(selectedTime.value.format('HH'))
const selectedMinute = ref(selectedTime.value.format('mm'))
// ========== 滚动配置 ==========
const itemHeight = 80
// 每项高度 / 可视高度rpx 转 px避免直接用数字导致滚动错位
const itemHeight = uni.upx2px(80)
const visibleHeight = uni.upx2px(400)
const centerOffset = (visibleHeight - itemHeight) / 2
const dateScrollTop = ref(0)
const hourScrollTop = ref(0)
const minuteScrollTop = ref(0)
@@ -219,6 +250,11 @@ const closeTimePopup = () => {
const confirmTime = () => {
const selectedDateTime = `${selectedDate.value} ${selectedHour.value}:${selectedMinute.value}`
const now = moment()
if (moment(selectedDateTime, 'YYYY-MM-DD HH:mm').isBefore(now)) {
uni.showToast({ title: '预约时间不能早于当前时间', icon: 'none' })
return
}
selectedTime.value = moment(selectedDateTime, 'YYYY-MM-DD HH:mm')
form.value.datetime = selectedTime.value.format('YYYY-MM-DD HH:mm')
closeTimePopup()
@@ -233,10 +269,11 @@ const syncScrollPosition = () => {
const dateIdx = dateList.value.findIndex(i => i.value === selectedDate.value)
const hourIdx = hourList.value.findIndex(i => i.time === selectedHour.value)
const minIdx = minuteList.value.findIndex(i => i.minute === selectedMinute.value)
dateScrollTop.value = dateIdx * itemHeight
hourScrollTop.value = hourIdx * itemHeight
minuteScrollTop.value = minIdx * itemHeight
// 让选中项滚动到可视区域居中位置,并限制不小于 0
dateScrollTop.value = dateIdx >= 0 ? Math.max(0, dateIdx * itemHeight - centerOffset) : 0
hourScrollTop.value = hourIdx >= 0 ? Math.max(0, hourIdx * itemHeight - centerOffset) : 0
minuteScrollTop.value = minIdx >= 0 ? Math.max(0, minIdx * itemHeight - centerOffset) : 0
}
// 点击选择日期
@@ -278,10 +315,19 @@ onLoad(() => {
uni.$on('selectHouse', (data) => {
houseName.value = data.name
houseId.value = data.id
residentId.value = data.residentId
})
uni.$on('selectProject', (data) => {
projectName.value = data.name
projectId.value = data.id
form.value.title = data.name
})
// 自动获取当前登录用户手机号
getUserProfile().then(res => {
if (res.code === 200 && res.data && res.data.phone) {
form.value.phone = res.data.phone
}
})
})
@@ -301,10 +347,10 @@ const goToSelectProject = () => {
// 选择图片
const chooseImage = () => {
// 计算还能选几张
const remainCount = 3 - form.value.images.length;
const remainCount = 9 - form.value.images.length;
if (remainCount <= 0) {
uni.showToast({
title: '最多只能上传3张图片',
title: '最多只能上传9张图片',
icon: 'none'
})
return;
@@ -350,17 +396,19 @@ const submitForm = async () => {
if (!houseName.value) return uni.showToast({ title: '请选择报修房屋', icon: 'none' })
if (!projectName.value) return uni.showToast({ title: '请选择维修项目', icon: 'none' })
if (!form.value.title) return uni.showToast({ title: '请输入标题', icon: 'none' })
if (!form.value.desc) return uni.showToast({ title: '请输入问题描述', icon: 'none' })
// if (!form.value.desc) return uni.showToast({ title: '请输入问题描述', icon: 'none' })
if (!/^1[3-9]\d{9}$/.test(form.value.phone)) return uni.showToast({ title: '手机号不正确', icon: 'none' })
if (!form.value.datetime) return uni.showToast({ title: '请选择预约时间', icon: 'none' })
if (moment(form.value.datetime, 'YYYY-MM-DD HH:mm').isBefore(moment())) return uni.showToast({ title: '预约时间不能早于当前时间', icon: 'none' })
const villageId = uni.getStorageSync('currentVillageId')
if (!villageId || villageId === '0') return uni.showToast({ title: '请先选择小区', icon: 'none' })
const userId = uni.getStorageSync('userId')
if (!userId) return uni.showToast({ title: '用户信息失效', icon: 'none' })
submitting.value = true
uni.showLoading({ title: '提交中...' })
uni.showLoading({ title: '提交中...' ,mask:true})
try {
const imageUrls = await uploadImages()
@@ -369,9 +417,9 @@ const submitForm = async () => {
villageId,
houseId: houseId.value,
maintenanceItemId: projectId.value,
title: form.value.title,
item: form.value.title,
problem: form.value.desc,
residentId: userId,
residentId: residentId.value,
orderDate: form.value.datetime,
phone: form.value.phone,
problemImageUrl: imageUrls.join(','),
@@ -382,22 +430,24 @@ const submitForm = async () => {
const res = await addQueryRepair(params)
if (res.code === 200) {
uni.hideLoading();
uni.showToast({ title: '提交成功', icon: 'success' })
form.value = { title: '', desc: '', phone: '', datetime: '', images: [] }
houseName.value = ''
houseId.value = ''
residentId.value = ''
projectName.value = ''
projectId.value = ''
setTimeout(() => {
uni.navigateTo({
url:'/pageSubPack/online-repair/onlineRepair'
})
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
submitting.value = false
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (e) {
submitting.value = false
uni.showToast({ title: '提交失败,请检查网络或重试', icon: 'none' })
} finally {
uni.hideLoading()
@@ -418,11 +468,15 @@ const submitForm = async () => {
border-bottom: 1px solid #eee;
}
.label {
width: 140rpx;
width: 180rpx;
font-size: 28rpx;
color: #333;
line-height: 80rpx;
}
.required {
color: red;
margin-right: 4rpx;
}
.value {
flex: 1;
@@ -434,9 +488,10 @@ const submitForm = async () => {
justify-content: space-between;
}
/* .time-value {
color: #000;
} */
.time-value {
white-space: nowrap;
font-size: 26rpx;
}
.input, .textarea {
flex: 1;
padding: 20rpx;
@@ -454,6 +509,27 @@ const submitForm = async () => {
/* border-bottom: 1px solid #eee; */
padding-bottom: 10px;
}
.textarea-wrap {
flex: 1;
position: relative;
}
.desc-textarea {
width: 100%;
height: 160rpx;
font-size: 28rpx;
padding: 10rpx 0;
box-sizing: border-box;
border: 1px solid #eee;
border-radius: 4px;
padding: 5px;
}
.char-count {
position: absolute;
right: 10rpx;
bottom: 10rpx;
font-size: 24rpx;
color: #999;
}
.switch {
font-size: 12px;
@@ -507,6 +583,16 @@ textarea {
color: #999;
font-size: 12px;
}
/* 提交中遮罩 */
.submit-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 9999;
}
.submit-btn {
width: 100%;
background: #007aff;
@@ -568,9 +654,10 @@ textarea {
.time-item {
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
font-size: 26rpx;
color: #BBBCBE;
position: relative;
white-space: nowrap;
}
/* 选中项样式(高亮) */

View File

@@ -1,20 +1,20 @@
<template>
<view class="project-container">
<view class="select-container">
<!-- 搜索框 -->
<view class="search-bar">
<input type="text" placeholder="请输入关键字搜索" v-model="searchKey" />
<uni-icons type="search" size="18"></uni-icons>
</view>
<!-- 分类+项目 -->
<!-- 分类选择 -->
<view class="category-wrap">
<!-- 左侧分类 -->
<scroll-view scroll-y class="category-left">
<view
class="category-item"
<view
class="category-item"
:class="{ active: activeCategory === item.id }"
@click="selectCategory(item)"
v-for="item in categoryList"
v-for="item in categoryList"
:key="item.id"
>
{{ item.projectName }}
@@ -23,28 +23,16 @@
<!-- 右侧项目 -->
<scroll-view scroll-y class="category-right">
<view
<view
class="project-item"
:class="{ active: selectedProject.id === item.id }"
@click="selectProject(item)"
v-for="item in currentProjectList"
v-for="item in currentProjectList"
:key="item.id"
>
{{ item.projectName }}
</view>
</scroll-view>
</view>
<!-- 空状态 -->
<view v-if="categoryList.length === 0 || currentProjectList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
暂无维修项目
</text>
</view>
<!-- 确认按钮 -->
<button class="confirm-btn" @click="confirmSelect">确认</button>
</view>
</template>
@@ -56,13 +44,11 @@ import { getRepairProjectList } from '../api/api.js'
// 分类列表
const categoryList = ref([])
// 项目列表
// 所有项目数据
const projectList = ref([])
// 选中状态
// 选中的分类
const activeCategory = ref(1)
const selectedProject = ref({}) // 选中的项目
const searchKey = ref('')
// 从接口获取个人报修项目列表
const fetchProjectList = async () => {
@@ -89,7 +75,10 @@ onLoad(() => {
fetchProjectList()
})
// 筛选当前分类的项目
// 搜索关键词
const searchKey = ref('')
// 筛选当前分类的项目(含搜索)
const currentProjectList = computed(() => {
let list = projectList.value.filter(item => item.categoryId === activeCategory.value)
if (searchKey.value) {
@@ -101,41 +90,34 @@ const currentProjectList = computed(() => {
// 选择分类
const selectCategory = (item) => {
activeCategory.value = item.id
selectedProject.value = {} // 切换分类清空选中项目
}
// 选择项目
// 选择项目并返回
const selectProject = (item) => {
selectedProject.value = item
}
// 确认选择
const confirmSelect = () => {
if (!selectedProject.value.id) {
uni.showToast({ title: '请选择维修项目', icon: 'none' })
return
}
// 获取分类名称
const categoryName = categoryList.value.find(c => c.id === activeCategory.value).projectName
// 传递给表单页
const categoryName = categoryList.value.find(c => c.id === item.categoryId)?.projectName || ''
// 用全局事件发送参数
uni.$emit('selectProject', {
name: `${categoryName}-${selectedProject.value.projectName}`,
id: selectedProject.value.id
name: `${categoryName}-${item.projectName}`,
id: item.id
})
// 返回上一页
uni.navigateBack({
delta: 1
})
uni.navigateBack({ delta: 1 })
}
</script>
<style scoped>
.project-container {
padding: 15px;
.select-container {
padding: 10px;
height: 100vh;
box-sizing: border-box;
background-color: #f9f9f9;
display: flex;
flex-direction: column;
background: #fff;
}
.search-bar {
display: flex;
@@ -144,6 +126,7 @@ const confirmSelect = () => {
background: #f5f5f5;
border-radius: 6px;
margin-bottom: 10px;
flex-shrink: 0;
}
.search-bar input {
flex: 1;
@@ -152,64 +135,39 @@ const confirmSelect = () => {
.category-wrap {
display: flex;
flex: 1;
margin-bottom: 20px;
min-height: 0;
}
.category-left {
width: 80px;
height: 100%;
background: #f8f8f8;
width: 120px;
background: #fff;
border-radius: 6px 0 0 6px;
margin-right: 20rpx;
color: #666;
}
.category-item {
padding: 12px 10px;
padding: 15px 10px;
text-align: center;
font-size: 14px;
border-bottom: 1px solid #eee;
}
.category-item.active {
background: #fff;
color: #007aff;
background: #f9f9f9;
color: #222;
font-weight: bold;
}
.category-right {
flex: 1;
height: 100%;
background: #fff;
border-radius: 0 6px 6px 0;
padding: 10px;
text-align: center;
}
.project-item {
padding: 12px 10px;
font-size: 14px;
border-bottom: 1px solid #eee;
}
.project-item.active {
color: #007aff;
font-weight: bold;
.project-item:active {
background: #f5f5f5;
}
.confirm-btn {
width: 100%;
height: 44px;
line-height: 44px;
background: #007aff;
color: #fff;
border-radius: 8px;
font-size: 16px;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
</style>
</style>

View File

@@ -2,7 +2,10 @@
<view class="form-container">
<!-- 维修项目展示 -->
<view class="form-item">
<text class="label">维修项目</text>
<!-- <text class="label">维修项目</text> -->
<view class="label">
<text class="required">*</text>维修项目
</view>
<view class="value" @click="goBackSelect">
<view>
<text v-if="!repairProject">请选择项目</text>
@@ -14,21 +17,26 @@
<!-- 标题输入 -->
<view class="form-item">
<text class="label">标题</text>
<!-- <input type="text" placeholder="请输入问题标题" v-model="form.title" /> -->
<uni-easyinput v-model="form.title" placeholder="请输入内容" :inputBorder="false"></uni-easyinput>
<view class="label">
<text class="required">*</text>标题
</view>
<uni-easyinput v-model="form.title" placeholder="请输入内容最多30个字符" :inputBorder="false" maxlength="30"></uni-easyinput>
</view>
<!-- 问题描述 -->
<view class="form-item-wenti">
<text class="label">问题描述</text>
<!-- <textarea placeholder="请输入问题内容" v-model="form.desc" rows="5" ></textarea> -->
<uni-easyinput v-model="form.desc" placeholder="请描述你的问题" type="textarea"></uni-easyinput>
<view class="textarea-wrap">
<textarea v-model="form.desc" placeholder="请描述你的问题或位置最多200字" maxlength="200" class="desc-textarea" />
<text class="char-count">{{ form.desc.length }}/200</text>
</view>
</view>
<!-- 手机号 -->
<view class="form-item">
<text class="label">手机号</text>
<view class="label">
<text class="required">*</text>手机号
</view>
<uni-easyinput type="number" v-model="form.phone" placeholder="请输入手机号" :inputBorder="false"></uni-easyinput>
</view>
@@ -39,11 +47,11 @@
<!-- 已上传图片 -->
<view class="upload-item" v-for="(img, index) in form.images" :key="index">
<image :src="img" mode="aspectFill" class="upload-img"></image>
<image src="https://wuyeadmin.bugtc.com/images/imgs/delete.png" class="upload-delete" @click="deleteImage(index)"></image>
<image src="https://wuye.ckdzkj.com/images/imgs/delete.png" class="upload-delete" @click="deleteImage(index)"></image>
<!-- <uni-icons type="close" size="16" @click="deleteImage(index)"></uni-icons> -->
</view>
<!-- 上传按钮 -->
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 3">
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 9">
<uni-icons type="plusempty" size="24"></uni-icons>
<!-- <text>上传照片</text> -->
</view>
@@ -51,7 +59,10 @@
</view>
<!-- 提交按钮 -->
<button class="submit-btn" @click="submitForm">提交</button>
<button class="submit-btn" :disabled="submitting" @click="submitForm">{{ submitting ? '提交中...' : '提交' }}</button>
<!-- 提交中遮罩 -->
<view class="submit-mask" v-if="submitting" @touchmove.stop.prevent></view>
</view>
</template>
@@ -59,11 +70,14 @@
import { ref } from 'vue'
import{onLoad,onUnload} from "@dcloudio/uni-app"
import { addQueryRepair, uploadFile } from '../api/api.js'
import { getUserProfile } from '../api/apiSub.js'
// 接收上个页面传的维修项目
const repairProject = ref('')
const projectId = ref('')
// 提交状态锁
const submitting = ref(false)
// 表单数据
const form = ref({
title: '',
@@ -79,12 +93,21 @@ onLoad((options) => {
if (options.category && options.project) {
repairProject.value = `${options.category}-${options.project}`
projectId.value = options.projectId
form.value.title = `${options.category}-${options.project}`
}
// ✅ 监听返回选择的项目
uni.$on('selectRepairProject', (data) => {
repairProject.value = `${data.category}-${data.project}`
projectId.value = data.projectId
form.value.title = `${data.category}-${data.project}`
})
// 自动获取当前登录用户手机号
getUserProfile().then(res => {
if (res.code === 200 && res.data && res.data.phone) {
form.value.phone = res.data.phone
}
})
})
@@ -106,10 +129,10 @@ const goBackSelect = () => {
// 选择图片
const chooseImage = () => {
// 计算还能选几张
const remainCount = 3 - form.value.images.length;
const remainCount = 9 - form.value.images.length;
if (remainCount <= 0) {
uni.showToast({
title: '最多只能上传3张图片',
title: '最多只能上传9张图片',
icon: 'none'
})
return;
@@ -163,10 +186,10 @@ const submitForm = async () => {
uni.showToast({ title: '请输入问题标题', icon: 'none' })
return
}
if (!form.value.desc) {
uni.showToast({ title: '请输入问题描述', icon: 'none' })
return
}
// if (!form.value.desc) {
// uni.showToast({ title: '请输入问题描述', icon: 'none' })
// return
// }
if (!/^1[3-9]\d{9}$/.test(form.value.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
@@ -178,7 +201,8 @@ const submitForm = async () => {
return
}
uni.showLoading({ title: '提交中...' })
submitting.value = true
uni.showLoading({ title: '提交中...' ,mask:true})
try {
// 先上传图片
@@ -200,18 +224,20 @@ const submitForm = async () => {
const res = await addQueryRepair(params)
if (res.code === 200) {
uni.hideLoading();
uni.showToast({ title: '提交成功', icon: 'success' })
// const tempProjectId = projectId.value
form.value = { title: '', desc: '', phone: '', images: [] }
repairProject.value = ''
// uni.navigateBack({ delta: 2 })
setTimeout(() => {
uni.navigateTo({ url: '/pageSubPack/online-repair/onlineRepair' })
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
submitting.value = false
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (e) {
submitting.value = false
uni.showToast({ title: '网络异常', icon: 'none' })
} finally {
uni.hideLoading()
@@ -237,11 +263,15 @@ const submitForm = async () => {
border-bottom: 1px solid #eee;
}
.label {
width: 140rpx;
width: 180rpx;
font-size: 28rpx;
color: #333;
line-height: 80rpx;
}
.required {
color: red;
margin-right: 4rpx;
}
.value {
flex: 1;
@@ -261,6 +291,27 @@ const submitForm = async () => {
.textarea {
resize: none;
}
.textarea-wrap {
flex: 1;
position: relative;
}
.desc-textarea {
width: 100%;
height: 160rpx;
font-size: 28rpx;
padding: 10rpx 0;
box-sizing: border-box;
border: 1px solid #eee;
border-radius: 4px;
padding: 5px;
}
.char-count {
position: absolute;
right: 10rpx;
bottom: 10rpx;
font-size: 24rpx;
color: #999;
}
.upload-wrap {
display: flex;
flex-wrap: wrap;
@@ -299,6 +350,16 @@ const submitForm = async () => {
color: #999;
font-size: 12px;
}
/* 提交中遮罩 */
.submit-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 9999;
}
.submit-btn {
width: 100%;
background: #007aff;

View File

@@ -23,7 +23,7 @@
<!-- 报修信息模块 -->
<view class="info-section">
<view class="section-title">报修信息</view>
<view class="info-item">
<view class="info-item" v-if="!repairInfo.isPublic">
<view class="info-label">报修房屋</view>
<view class="info-value">{{repairInfo.house}}</view>
</view>
@@ -52,18 +52,21 @@
<view class="photo-container" v-if="repairInfo.images && repairInfo.images.length">
<image class="photo-item" v-for="(img, index) in repairInfo.images" :key="index" :src="img" mode="aspectFill" @click="previewImage(img)"></image>
</view>
<view class="photo-container" v-else>
<!-- <view class="photo-container" v-else>
<view class="photo-item">
<uni-icons type="plusempty" size="24" color="#ccc"></uni-icons>
</view>
</view>
</view> -->
</view>
<view class="info-item">
<view class="info-label">预约时间</view>
<view class="info-value">{{repairInfo.orderDate}}</view>
</view>
</view>
<!-- 联系物业按钮 -->
<!-- <view class="btn-container">
<view class="btn-container">
<button class="contact-btn" @click="contactProperty">联系物业</button>
</view> -->
</view>
</view>
</template>
@@ -81,8 +84,10 @@ const repairInfo = ref({
title: '',
description: '',
phone: '',
orderDate:'',
time: '',
images: []
images: [],
isPublic: false
})
// 步骤状态
@@ -108,11 +113,14 @@ const mapData = (data) => {
repairInfo.value = {
house: data.houseName || '',
project: data.maintenanceItemName || '',
title: data.title || data.maintenanceItemName || '',
description: data.problem || '',
title: data.item || '',
description: (data.problem || '').replace(/\\n/g, '\n'),
phone: data.phone || '',
time: data.orderDate || '',
images
orderDate: data.orderDate || '',
time: data.createTime || '',
images,
// 公共报修不展示报修房屋
isPublic: data.projectTypeStr === '公共报修'
}
setStep(data.problemStatus)
// 三个步骤时间赋值
@@ -156,16 +164,17 @@ const previewImage = (url) => {
}
// 联系物业按钮点击事件
// const contactProperty = () => {
// uni.makePhoneCall({
// phoneNumber: '物业电话' // 换成真实电话
// })
// }
const contactProperty = () => {
uni.navigateTo({
url: '/pageSubPack/service-list/contactProperty'
})
}
</script>
<style scoped>
.repair-detail-container {
padding: 16px;
padding-bottom: 100px;
background-color: #f5f5f5;
min-height: 100vh;
}
@@ -251,6 +260,7 @@ const previewImage = (url) => {
.multi-line {
white-space: pre-wrap;
word-break: break-all;
line-height: 1.5;
border: 1px solid #f0f0f0;
border-radius: 8rpx;
@@ -260,23 +270,33 @@ const previewImage = (url) => {
.photo-container {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 4px;
margin-right: -12px;
margin-bottom: -12px;
}
.photo-item {
width: 80px;
width: 95px;
height: 80px;
/* border: 1px solid #ccc; */
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
margin-right: 12px;
margin-bottom: 12px;
}
.btn-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
background-color: #ffffff;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.contact-btn {
@@ -287,6 +307,7 @@ const previewImage = (url) => {
height: 48px;
font-size: 16px;
width: 100%;
line-height: 3;
}
.contact-btn::after {

View File

@@ -117,14 +117,17 @@ const selectProject = (item) => {
height: 100vh;
box-sizing: border-box;
background-color: #f9f9f9;
display: flex;
flex-direction: column;
}
.search-bar {
display: flex;
align-items: center;
padding: 15px 10px;
padding: 8px 10px;
background: #f5f5f5;
border-radius: 6px;
margin-bottom: 10px;
flex-shrink: 0;
}
.search-bar input {
flex: 1;
@@ -132,11 +135,11 @@ const selectProject = (item) => {
}
.category-wrap {
display: flex;
height: calc(100% - 50px);
flex: 1;
min-height: 0;
}
.category-left {
width: 120px;
height: 100%;
background: #fff;
border-radius: 6px 0 0 6px;
margin-right: 20rpx;
@@ -155,7 +158,6 @@ const selectProject = (item) => {
}
.category-right {
flex: 1;
height: 100%;
background: #fff;
border-radius: 0 6px 6px 0;
padding: 10px;

View File

@@ -9,7 +9,7 @@
v-for="item in houseList"
:key="item.id"
>
<image src="https://wuyeadmin.bugtc.com/images/repair/house.png" style="width: 48rpx;height: 48rpx;"></image>
<image src="https://wuye.ckdzkj.com/images/repair/house.png" style="width: 48rpx;height: 48rpx;"></image>
<view class="house-name">
{{ item.villageName }}
<view class="house-detail">{{item.unitNo}}-{{item.floorNo}}-{{item.houseNo}}</view>
@@ -27,8 +27,10 @@
</view>
<!-- 确认按钮 -->
<button class="confirm-btn" @click="confirmSelect">确认</button>
<!-- 确认按钮固定底部不随内容滚动 -->
<view class="confirm-bar">
<button class="confirm-btn" @click="confirmSelect">确认</button>
</view>
</view>
</template>
@@ -55,14 +57,13 @@ const fetchHouseList = async () => {
try {
const res = await getHouseList({ pageNum: pageNum.value, pageSize: pageSize.value })
if (res.code === 200) {
const list = res.rows || []
const list = res.data || []
houseList.value = list
if (houseList.value.length > 0) {
if (preSelectedHouseId.value) {
const found = houseList.value.find(item => String(item.id) === String(preSelectedHouseId.value))
selectedHouse.value = found || houseList.value[0]
} else {
selectedHouse.value = houseList.value[0]
// 仅当从表单页带入了房屋ID时自动选中对应房屋否则不默认选择
if (preSelectedHouseId.value) {
const found = houseList.value.find(item => String(item.id) === String(preSelectedHouseId.value))
if (found) {
selectedHouse.value = found
}
}
}
@@ -85,7 +86,7 @@ const selectHouse = (item) => {
// 确认选择并返回
const confirmSelect = () => {
if (!selectedHouse.value) {
if (!selectedHouse.value.id) {
uni.showToast({ title: '请选择房屋', icon: 'none' })
return
}
@@ -93,7 +94,8 @@ const confirmSelect = () => {
// 把选中的房屋信息传给表单页通过uni.$emit
uni.$emit('selectHouse', {
id: selectedHouse.value.id,
name: `${selectedHouse.value.villageName} ${selectedHouse.value.unitNo}-${selectedHouse.value.floorNo}-${selectedHouse.value.houseNo}`
name: `${selectedHouse.value.villageName} ${selectedHouse.value.unitNo}-${selectedHouse.value.floorNo}-${selectedHouse.value.houseNo}`,
residentId: selectedHouse.value.residentId
})
// 返回上一页
@@ -106,13 +108,12 @@ const confirmSelect = () => {
padding: 15px;
height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
background: #f9f9f9;
}
.house-list {
flex: 1;
height: 100%;
overflow-y: auto;
padding-bottom: 120rpx;
}
.house-item {
display: flex;
@@ -143,6 +144,17 @@ const confirmSelect = () => {
color: #666;
margin-top: 4px;
}
.confirm-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f9f9f9;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.confirm-btn {
width: 100%;
height: 44px;
@@ -151,8 +163,6 @@ const confirmSelect = () => {
color: #fff;
border-radius: 8px;
font-size: 28rpx;
margin-top: 20px;
margin-bottom: 20rpx;
}
/* 空状态 */
.empty-state {

View File

@@ -10,7 +10,7 @@
<!-- 头部物业费A标题+水滴图标 -->
<view class="card-header">
<image class="iconStyle"
:src="paymentType=='水'?'https://wuyeadmin.bugtc.com/images/propertyImgs/water.png':'https://wuyeadmin.bugtc.com/images/propertyImgs/electric.png'">
:src="paymentType=='水'?'https://wuye.ckdzkj.com/images/propertyImgs/water.png':'https://wuye.ckdzkj.com/images/propertyImgs/electric.png'">
</image>
<text class="card-title">{{paymentType}}</text>
</view>
@@ -31,7 +31,7 @@
v-model="deviceNo" />
<!-- 右边扫码图标 text标签直接用 双端不乱码 -->
<image class="scanTheCodeIconStyle"
src="https://wuyeadmin.bugtc.com/images/propertyImgs/scanTheCodeIcon.png" @click="scanCode">
src="https://wuye.ckdzkj.com/images/propertyImgs/scanTheCodeIcon.png" @click="scanCode">
</image>
</view>
</view>
@@ -102,33 +102,35 @@
onMounted(() => {})
// 扫码
const scanCode = async () => {
try {
uni.showLoading({
title: '扫码中...',
mask: true
})
const scanCode = () => {
// uni.showLoading({
// title: '扫码中...',
// mask: true
// })
const res = await uni.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode']
})
// 扫码成功,赋值
deviceNo.value = res.result
uni.hideLoading()
uni.showToast({
title: '扫码成功',
icon: 'success'
})
} catch (err) {
uni.hideLoading()
console.log('扫码失败', err)
uni.showToast({
title: '扫码取消/失败',
icon: 'none'
})
}
uni.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode'],
success: (res) => {
// uni.hideLoading()
console.log('扫码成功', res)
// 过滤模拟器/调试模式返回的 mock 数据
if (!res.result || res.result.toUpperCase() === 'TEST') {
uni.showToast({ title: '扫码识别失败,请手动输入设备号', icon: 'none' })
return
}
deviceNo.value = res.result
uni.showToast({ title: '扫码成功', icon: 'success' })
},
fail: (err) => {
// uni.hideLoading()
console.log('扫码失败', err)
uni.showToast({ title: '扫码取消/失败', icon: 'none' })
},
complete: () => {
// uni.hideLoading()
}
})
}
// 提交缴费
@@ -184,7 +186,7 @@
// return
// }
await getTopUpBindDevice({
deviceType: paymentType.value=='水'?'2':'1',
deviceType: paymentType.value=='水'?'2':'1',
deviceCode: deviceNo.value,
// openid: openid,
residentUserId: uni.getStorageSync('userId')
@@ -200,7 +202,7 @@
} catch (err) {
uni.hideLoading()
uni.showToast({
title: `${err}`,
title: err.msg || '网络异常',
icon: 'none'
})
console.error('绑定设备失败:', err)
@@ -214,17 +216,15 @@
// 跳转成功页
const goNext = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/addPaymentSuccess'
uni.navigateTo({
url: '/pageSubPack/payment-list/addPaymentSuccess?type=bind'
})
}
// 返回
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex'
})
}
uni.navigateBack();
}
</script>
<style lang="scss">
page {

View File

@@ -13,7 +13,7 @@
<view class="header-card">
<view class="header-icon">
<image class="iconStyle"
:src="paymentType=='燃气'?'https://wuyeadmin.bugtc.com/images/propertyImgs/gas.png':'https://wuyeadmin.bugtc.com/images/propertyImgs/heating.png'">
:src="paymentType=='燃气'?'https://wuye.ckdzkj.com/images/propertyImgs/gas.png':'https://wuye.ckdzkj.com/images/propertyImgs/heating.png'">
></image>
</view>
<text class="header-title">{{paymentType+'费'}}</text>
@@ -173,22 +173,22 @@
// 跳转成功页
const goNext = () => {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/payment-list/addPaymentSuccess'
})
}
// 切换(注释保留)
const goToChange = () => {
// uni.redirectTo({
// uni.navigateTo({
// url: '/pageSubPack/payment-list/selectPaymentEntity'
// })
}
// 返回
const goBack = () => {
uni.redirectTo({ url: '/pageSubPack/payment-list/paymentIndex' });
}
const goBack = () => {
uni.navigateBack();
}
</script>
<style lang="scss">
page {

View File

@@ -1,7 +1,7 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="'新增'+paymentType+'缴费'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
<uni-nav-bar :title="navTitle" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
@@ -11,20 +11,18 @@
<!-- 成功图标 -->
<view class="success-icon">
<text class="icon-check"></text>
<!-- <image src="https://wuyeadmin.bugtc.com/images/home/bingo.png" ></image> -->
</view>
<!-- 主标题 -->
<view class="title">绑定成功</view>
<view class="title">{{ pageTitle }}</view>
<!-- 提示文案 -->
<view class="desc">
恭喜您绑定成功费用将在24小时内更新<br>
请您按时查看避免造成费用欠费
</view>
<view class="desc" v-html="descText"></view>
<!-- 操作按钮 -->
<view class="btn-wrap">
<button class="btn-back" @click="goBack">返回生活缴费</button>
<button class="btn-back" @click="handlePrimaryClick">{{ btnText }}</button>
</view>
</view>
</scroll-view>
@@ -50,15 +48,52 @@
// 响应式数据
const statusBarHeight = ref(20)
const paymentType = ref(uni.getStorageSync('addPaymentType'))
const pageType = ref('bind') // 'bind' | 'pay'
// 页面配置:根据类型返回对应文案和跳转
const pageConfig = computed(() => {
const configs = {
bind: {
navTitle: '新增' + paymentType.value + '缴费',
pageTitle: '绑定成功',
descText: '恭喜您绑定成功费用将在24小时内更新<br>请您按时查看,避免造成费用欠费',
btnText: '返回生活缴费',
backUrl: '/pageSubPack/payment-list/paymentIndex'
},
pay: {
navTitle: '缴费支付',
pageTitle: '支付成功',
descText: '恭喜您支付成功,感谢您的使用',
btnText: '返回生活缴费',
backUrl: '/pageSubPack/payment-list/paymentIndex' // TODO: 如有缴费记录页请修改
}
}
return configs[pageType.value] || configs.bind
})
const navTitle = computed(() => pageConfig.value.navTitle)
const pageTitle = computed(() => pageConfig.value.pageTitle)
const descText = computed(() => pageConfig.value.descText)
const btnText = computed(() => pageConfig.value.btnText)
// 生命周期
onLoad((options) => {
if (options?.type) {
pageType.value = options.type
}
})
onShow(() => {})
onMounted(() => {})
onReady(() => {})
// 方法
const goBack = () => {
uni.redirectTo({ url: '/pageSubPack/payment-list/paymentIndex' });
uni.navigateBack();
}
const handlePrimaryClick = () => {
const url = pageConfig.value.backUrl
uni.navigateTo({ url });
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="paymentItem.type || '缴费'" left-icon="left" @clickLeft="goBack"
<uni-nav-bar :title="switchStatus(paymentItem.type) || '缴费'" left-icon="left" @clickLeft="goBack"
backgroundColor="transparent" :border="false">
</uni-nav-bar>
@@ -41,7 +41,7 @@
</view>
<!-- <view style="border-bottom: 2rpx solid #c7c7c7;padding-bottom:20rpx;"></view> -->
<!-- 缴费金额输入 -->
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
<!-- <view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
<view class="input-section" style="display: flex;">
<text class="amount-input" style="width: auto;margin: 0rpx 20rpx;">¥</text>
<input
@@ -52,14 +52,13 @@
placeholder="点击输入缴费金额"
/>
</view>
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view> -->
<view class="arrears-row" v-if="outstandingPayment==true">
<text class="arrears-text">当前欠费金额¥{{ arrearsAmount }}</text>
<text class="auto-fill-btn" @click="handleAutoFill">点击自动填入</text>
</view>
<view class="arrears-row" v-if="outstandingPayment==false">
<!-- <view class="arrears-row" v-if="outstandingPayment==false">
<text class="arrears-text">暂未查询到欠费</text>
</view>
</view> -->
</view>
</view>
</view>
@@ -76,7 +75,7 @@
<!-- 提示文案 -->
<view class="desc">
成功缴纳{{ paymentItem.type || '' }}费用 ¥{{ payNum || payAmount }}
成功缴纳{{ switchStatus(paymentItem.type) || '' }}费用 ¥{{ arrearsAmount || payAmount }}
</view>
@@ -109,9 +108,6 @@
onLoad
} from '@dcloudio/uni-app'
import {
getPushSingle
} from '/pageSubPack/api/api.js'
// 响应式数据
const outstandingPayment = ref(true)
@@ -120,21 +116,13 @@
const currentStep = ref(1)
const payAmount = ref('0.00')
const arrearsAmount = ref('0.00')
const payNum = ref('')
// 页面加载时初始化数据
onLoad(() => {
initPaymentData()
})
// 从支付收银台返回时检查支付结果
onShow(() => {
const paySuccess = uni.getStorageSync('paySuccess')
if (paySuccess) {
uni.removeStorageSync('paySuccess')
currentStep.value = 2
}
})
onShow(() => {})
// 初始化缴费数据
const initPaymentData = () => {
@@ -145,41 +133,75 @@
}
}
// 自动填充欠费金额
const handleAutoFill = () => {
payNum.value = arrearsAmount.value
uni.vibrateShort({
type: 'light'
})
}
// 返回
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex'
});
}
// 立即缴费 — 跳转到支付收银台选择支付方式
const handlePay = () => {
if (!payNum.value || Number(payNum.value) <= 0) {
uni.showToast({
title: '请输入正确的缴费金额',
icon: 'none'
})
return
const goBack = () => {
uni.navigateBack();
}
uni.setStorageSync('payData', {
amount: payNum.value,
deviceCode: paymentItem.deviceCode,
type: paymentItem.type,
name: paymentItem.name,
fullAddress: paymentItem.fullAddress
})
uni.navigateTo({ url: '/pageSubPack/payment-list/yinlianPayTn' })
}
// 缴费成功推送通知
// 立即缴费 — 通过微信拉起日照银行小程序进行支付
const handlePay = () => {
const amount = arrearsAmount.value
try {
uni.showLoading({ title: '加载中...' })
// 设备类型映射电费→1(电表)水费→2(水表)
const deviceTypeMap = { '电费': '1', '水费': '2' }
const mappedDeviceType = deviceTypeMap[paymentItem.type] || paymentItem.deviceType || ''
// 直接从本地参数拼 path 查询字符串传给支付小程序,不走接口
const token = uni.getStorageSync('token') || ''
const params = {
acct: uni.getStorageSync('userId'),
orderType: '20',
deviceCode: paymentItem.deviceCode || '',
deviceType: mappedDeviceType,
billId: paymentItem.billId || '',
residentUserId: uni.getStorageSync('userId'),
// goodName: paymentItem.name || switchStatus(paymentItem.type) || '',
// remark: '服务费',
token: token
}
const queryString = Object.keys(params).map(k =>
encodeURIComponent(k) + '=' + encodeURIComponent(params[k])
).join('&')
uni.hideLoading()
console.log('传入中间小程序的path参数:', queryString)
// #ifdef APP-PLUS
// 原生App通过微信SDK拉起小程序
plus.share.getServices((services) => {
const weixin = services.find(s => s.id === 'weixin')
if (!weixin) {
uni.showToast({ title: '未检测到微信客户端', icon: 'none' })
return
}
if (!weixin.launchMiniProgram) {
uni.showToast({ title: '当前微信版本太低,不支持拉起小程序', icon: 'none' })
return
}
weixin.launchMiniProgram({
id: 'gh_a765aef0172c',
type: 1, // 0 正式版 / 1开发版 / 2体验版
path: 'pages/arrearsPayment/arrearsPayment?' + queryString,
}, (res) => {
console.log('回调---回调结果App', JSON.stringify(res))
}, (err) => {
console.error('err---拉起失败App', JSON.stringify(err))
uni.showToast({ title: '拉起支付小程序失败', icon: 'none' })
})
})
// #endif
} catch (err) {
uni.hideLoading()
console.error('拉起支付小程序异常', err)
uni.showToast({ title: '请求异常,请重试', icon: 'none' })
}
}
// 缴费成功推送通知
const sendPaySuccessPush = () => {
const userId = uni.getStorageSync('userId')
if (userId) {
@@ -187,7 +209,7 @@
userId: userId,
channel: 'resident',
title: '缴费成功',
content: `您已成功缴纳${paymentItem.type || ''}费用 ¥${payNum.value || payAmount.value}`
content: `您已成功缴纳${switchStatus(paymentItem.type) || ''}费用 ¥${arrearsAmount.value || payAmount.value}`
}).catch(err => {
console.log('推送发送失败', err)
})
@@ -200,6 +222,16 @@
url: '/pageSubPack/payment-list/paymentIndex'
});
}
// 类型码转中文名展示
const switchStatus = (type) => {
const typeMap = {
'0': '物业费',
'1': '车位费',
'2': '垃圾处理费'
}
return typeMap[type] || type
}
</script>
<style lang="scss">
page {
@@ -646,4 +678,4 @@
width: 100vw;
height: 46px;
}
</style>
</style>

View File

@@ -0,0 +1,448 @@
<template>
<view class="wrap">
<scroll-view class="page" scroll-y @scrolltolower="onLoadMore">
<view class="empty-state" v-if="!loading && billList.length === 0">
<text class="empty-text">暂无账单数据</text>
</view>
<up-collapse v-if="billList.length" :border="false">
<up-collapse-item
v-for="(item, index) in billList"
:key="index"
:name="String(index)"
>
<!-- 标题勾选框 + 月份 -->
<template #title>
<view class="collapse-header">
<view class="check-circle" :class="{ checked: item.selected }" @click.stop="toggleSelect(index)">
<text v-if="item.selected" class="check-icon"></text>
</view>
<text class="month-text">{{ item.month }}</text>
</view>
</template>
<!-- 右侧金额 -->
<template #value>
<text class="price-text">¥{{ item.totalAmount }}</text>
</template>
<!-- 展开内容每条明细 -->
<view class="collapse-body">
<view class="detail-row fee-row" v-for="(detail, di) in item.details" :key="di">
<text class="detail-label">{{ detail.name }}</text>
<text class="detail-value">¥{{ detail.amount }}</text>
</view>
<view class="detail-row meta-row" v-if="item.unitNo || item.date">
<text class="detail-label">{{ item.unitNo }}</text>
<text class="detail-value">{{ item.date }}</text>
</view>
<view class="detail-row meta-row">
<text class="detail-label">原金额¥{{ item.totalAmount }}</text>
<text class="detail-value">优惠¥{{ formatNum(item.discount) }}</text>
</view>
</view>
</up-collapse-item>
</up-collapse>
<!-- 加载状态 -->
<view class="load-more-status" v-if="loadingMore">
<text class="loading-text">加载中...</text>
</view>
<view class="load-more-status" v-else-if="!hasMore && billList.length >= pageSize">
<text class="no-more-text"> 没有更多了 </text>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar" v-if="billList.length">
<view class="select-all" @click="toggleSelectAll">
<view class="check-circle" :class="{ checked: isAllSelected }">
<text v-if="isAllSelected" class="check-icon"></text>
</view>
<text class="select-all-text">全选</text>
</view>
<view class="total-box">
<text class="total-label">合计</text>
<text class="total-price">¥{{ selectedTotal.toFixed(2) }}</text>
</view>
<button class="pay-btn" :disabled="!canPay" @click="handleBatchPay">立即缴费</button>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getTopUpDetailsByMonth } from '/pageSubPack/api/apiSub.js'
const loading = ref(false)
const billList = ref([])
const pageNum = ref(1)
const pageSize = ref(10)
const total = ref(0)
const hasMore = ref(true)
const loadingMore = ref(false)
// 从 paymentIndex 传来的数据中取 billResidentId
const cacheItem = uni.getStorageSync('arrearsPayment') || {}
const billResidentId = ref(cacheItem.billResidentId || cacheItem.id || '')
onLoad(() => {
fetchBills()
})
// 拉取按月汇总账单
const fetchBills = async (isLoadMore = false) => {
if (!billResidentId.value) return
if (isLoadMore) {
if (!hasMore.value || loadingMore.value) return
pageNum.value++
loadingMore.value = true
} else {
pageNum.value = 1
hasMore.value = true
loading.value = true
}
try {
const res = await getTopUpDetailsByMonth({
billResidentId: billResidentId.value,
villageId: uni.getStorageSync('currentVillageId') || '',
pageNum: pageNum.value,
pageSize: pageSize.value
})
const rows = res.rows || []
const typeNameMap = { '0': '物业管理服务费', '1': '车位管理费', '2': '生活垃圾处理费' }
const mapped = rows.map(r => {
const firstDetail = (r.details && r.details.length) ? r.details[0] : {}
const restDetails = (r.details || []).slice(1)
return {
month: r.month || '',
totalAmount: formatNum(r.totalAmount),
discount: 0,
selected: false,
periodDisplay: firstDetail.periodDisplay || '',
unitNo: cacheItem.unitNo || cacheItem.fullAddress || '',
date: r.month || '',
details: restDetails.map(d => ({
name: typeNameMap[d.type] || d.name || '缴费费用',
amount: formatNum(d.amount),
id: d.id,
payStatus: d.payStatus || '0'
})),
billIds: (r.details || []).map(d => d.id).filter(Boolean)
}
})
if (isLoadMore) {
billList.value = [...billList.value, ...mapped]
} else {
billList.value = mapped
}
total.value = res.total || 0
hasMore.value = billList.value.length < total.value
} catch (e) {
if (isLoadMore) pageNum.value--
console.error('获取账单失败', e)
} finally {
loading.value = false
loadingMore.value = false
}
}
const onLoadMore = () => {
fetchBills(true)
}
const formatNum = (val) => {
return (Number(val || 0)).toFixed(2)
}
const toggleSelect = (index) => {
const current = billList.value[index]
if (!current.selected) {
// 选中:检查前面所有项是否都已选中,不允许跳过
for (let i = 0; i < index; i++) {
if (!billList.value[i].selected) {
uni.showToast({ title: '请按顺序从第一个月开始选择', icon: 'none' })
return
}
}
billList.value[index].selected = true
} else {
// 取消选中:同时取消该项及后面所有已选中的
for (let i = index; i < billList.value.length; i++) {
billList.value[i].selected = false
}
}
}
const isAllSelected = computed(() =>
billList.value.length > 0 && billList.value.every(m => m.selected)
)
const toggleSelectAll = () => {
const val = !isAllSelected.value
billList.value.forEach(m => { m.selected = val })
}
const selectedTotal = computed(() =>
billList.value.filter(m => m.selected).reduce((sum, m) => sum + Number(m.totalAmount || 0), 0)
)
const canPay = computed(() => {
const selected = billList.value.filter(m => m.selected)
if (!selected.length) return false
// 必须从第一个开始且连续
return billList.value[0].selected && selected.length > 0
})
// 批量缴费
const handleBatchPay = () => {
const selected = billList.value.filter(m => m.selected)
if (!selected.length) return
const allIds = selected.flatMap(m => m.billIds)
const token = uni.getStorageSync('token') || ''
const userId = uni.getStorageSync('userId') || ''
const query = [
['acct', userId],
['orderType', '10'],
['billIds', allIds.join(',')],
// ['deviceType', '3'],
['residentUserId', userId],
['token', token]
].map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&')
console.log('往小程序传递的参数',query)
// #ifdef APP-PLUS
uni.showLoading({ title: '加载中...' })
plus.share.getServices((services) => {
const wx = services.find(s => s.id === 'weixin')
uni.hideLoading()
if (!wx) return uni.showToast({ title: '未检测到微信客户端', icon: 'none' })
if (!wx.launchMiniProgram) return uni.showToast({ title: '当前微信版本太低,不支持拉起小程序', icon: 'none' })
wx.launchMiniProgram({
id: 'gh_a765aef0172c',
type: 1,
path: 'pages/arrearsPayment/arrearsPayment?' + query,
}, (res) => {
console.log('小程序回调:', JSON.stringify(res))
}, (err) => {
console.error('拉起失败:', JSON.stringify(err))
uni.showToast({ title: '拉起支付小程序失败', icon: 'none' })
})
})
// #endif
}
</script>
<style lang="scss">
page {
background-color: #f5f5f5;
}
</style>
<style scoped>
.wrap {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f5f5f5;
}
.page {
flex: 1;
overflow-y: auto;
padding: 20rpx 30rpx;
box-sizing: border-box;
padding-bottom: 140rpx;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 0;
}
.empty-text {
font-size: 30rpx;
color: #ccc;
}
.load-more-status {
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx 0;
}
.loading-text {
font-size: 26rpx;
color: #999;
}
.no-more-text {
font-size: 26rpx;
color: #ccc;
}
/* ---- 折叠面板 ---- */
::v-deep .u-collapse-item {
margin-bottom: 20rpx;
border-radius: 8rpx;
overflow: hidden;
background: #fff;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
::v-deep .u-collapse-item__content {
background: #F5FAFF;
padding: 0;
}
::v-deep .u-cell {
padding: 20rpx 18rpx !important;
}
/* 标题区 */
.collapse-header {
display: flex;
align-items: center;
gap: 16rpx;
}
.check-circle {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 3rpx solid #ddd;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.check-circle.checked {
background-color: #1677ff;
border-color: #1677ff;
}
.check-icon {
color: #fff;
font-size: 24rpx;
font-weight: bold;
line-height: 1;
}
.month-text {
font-size: 32rpx;
font-weight: 600;
color: #222;
}
/* 金额 */
.price-text {
font-size: 32rpx;
font-weight: 700;
color: #FF8F40;
}
/* 展开内容 */
.collapse-body {
padding: 20rpx 24rpx 30rpx;
margin: 0 24rpx;
background: #F5FAFF;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14rpx 0;
}
.detail-label {
font-size: 28rpx;
}
.detail-value {
font-size: 28rpx;
}
/* 明细费用名 - 深色 */
.fee-row .detail-label,
.fee-row .detail-value {
color: #666666;
}
/* 日期/原金额/优惠 - 浅色 */
.meta-row .detail-label,
.meta-row .detail-value {
color: #999999;
}
/* ---- 底部操作栏 ---- */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -2rpx 16rpx rgba(0, 0, 0, 0.06);
z-index: 10;
}
.select-all {
display: flex;
align-items: center;
gap: 10rpx;
flex-shrink: 0;
margin-right: 16rpx;
}
.select-all-text {
font-size: 26rpx;
color: #666;
}
.total-box {
flex: 1;
display: flex;
align-items: baseline;
justify-content: center;
}
.total-label {
font-size: 26rpx;
color: #666;
}
.total-price {
font-size: 36rpx;
font-weight: 700;
color: #FF8F40;
}
.pay-btn {
flex-shrink: 0;
width: 180rpx;
height: 72rpx;
line-height: 72rpx;
background-color: #1677ff;
color: #fff;
font-size: 28rpx;
font-weight: 500;
border-radius: 8rpx;
border: none;
padding: 0;
}
.pay-btn[disabled] {
background-color: #ccc;
opacity: 0.7;
}
</style>

View File

@@ -13,7 +13,7 @@
<!-- 未欠费 -->
<!-- v-if="arrearsPayment=='暖气费'" -->
<view class="no-arrears-section">
<image src="https://wuyeadmin.bugtc.com/images/propertyImgs/noArrears.png"
<image src="https://wuye.ckdzkj.com/images/propertyImgs/noArrears.png"
class="background-image-style"></image>
<text class="noArrearsText">暂未查到欠费</text>
<view class="recordText">
@@ -107,9 +107,7 @@
// 方法
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex'
});
uni.navigateBack();
}
// 以下为原代码保留的方法(未使用但保持功能完整)

View File

@@ -1,42 +1,87 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="生活缴费" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<view class="contentStyle">
<view class="topbox">
<image class="titleImg" src="https://wuyeadmin.bugtc.com/images/propertyImgs/payTitle.png"></image>
<image class="payImg" src="https://wuyeadmin.bugtc.com/images/propertyImgs/payImg.png"></image>
<image class="titleImg" src="https://wuye.ckdzkj.com/images/propertyImgs/payTitle.png"></image>
<image class="payImg" src="https://wuye.ckdzkj.com/images/propertyImgs/payImg.png"></image>
<!-- 小区选择 -->
<view class="community-bar" @click="navigateToSelectHome">
<image src="https://wuye.ckdzkj.com/images/home/adress.png" class="location-icon"></image>
<text class="community-name">{{ currentCommunity }}</text>
<u-icon name="arrow-down-fill" size="14" color="#222222"></u-icon>
</view>
</view>
<scroll-view class="page" scroll-y="true">
<view class="page">
<view class="container">
<!-- 已缴费/费列表 -->
<view class="bill-list" v-if="paymentList.length != 0">
<!-- /费列表 -->
<scroll-view class="details-scroll" scroll-y="true" @scrolltolower="onPayLoadMore">
<view class="bill-list">
<template v-if="paymentList.length != 0">
<view class="bill-item" @click="goToBill(item)" v-for="item,index in paymentList" :key="index">
<image class="billIcon" src="https://wuye.ckdzkj.com/images/propertyImgs/water.png"
v-if="item.type=='水费'"></image>
<image class="billIcon" src="https://wuye.ckdzkj.com/images/propertyImgs/electric.png"
v-if="item.type=='电费'"></image>
<view class="bill-info">
<view class="bill-title">{{item.type}}
<view class="bill-status debt" v-if="item.balance<0">已欠费</view>
<text :class="item.balance<0 ? 'balance-debt' : 'balance-normal'">(余额{{item.balance|| 0.00}})</text>
</view>
<view class="bill-desc">{{item.villageName+item.buildingName+item.fullAddress}}</view>
</view>
<view class="bill-item" @click="goToBill(item)" v-for="item,index in paymentList" :key="index">
<image class="billIcon" src="https://wuyeadmin.bugtc.com/images/propertyImgs/water.png"
v-if="item.type=='水费'"></image>
<image class="billIcon" src="https://wuyeadmin.bugtc.com/images/propertyImgs/electric.png"
v-if="item.type=='电费'"></image>
<view class="bill-info">
<view class="bill-title">{{item.type}}
<view class="bill-status debt" v-if="item.balance<0">已欠费</view>
<view class="arrow"></view>
</view>
<view class="bill-desc">{{item.villageName+item.buildingName+item.fullAddress}}</view>
<!-- 滚动加载状态 -->
<view class="load-more-status" v-if="payLoadingMore">
<text class="loading-text">加载中...</text>
</view>
<view class="load-more-status" v-else-if="!payHasMore && paymentList.length >= payPageSize">
<text class="no-more-text"> 没有更多了 </text>
</view>
</template>
<view class="empty-state" v-else>
<text class="empty-text">暂无数据</text>
</view>
<view class="arrow"></view>
</view>
</view>
</scroll-view>
<!-- 物业费/垃圾处理费列表 -->
<scroll-view class="details-scroll-bill" scroll-y="true" @scrolltolower="onLoadMore">
<view class="bill-list">
<template v-if="detailslist.length != 0">
<view class="bill-item" :class="{ 'bill-paid': !item.amount }" @click="!item.amount ? null : goToBill(item)" v-for="item,index in detailslist" :key="index">
<image class="billIcon" src="https://wuye.ckdzkj.com/images/propertyImgs/property.png"></image>
<view class="bill-info">
<view class="bill-title">{{ item.name }}
<view class="bill-status debt" v-if="item.amount">未缴纳</view>
<view class="bill-status done" v-if="!item.amount">已完成</view>
<!-- <view class="bill-amount">({{item.amount}})</view> -->
</view>
<view class="bill-desc">{{ item.unitNo}}</view>
</view>
<view class="arrow"></view>
</view>
<!-- 滚动加载状态 -->
<view class="load-more-status" v-if="loadingMore">
<text class="loading-text">加载中...</text>
</view>
<view class="load-more-status" v-else-if="!hasMore && detailslist.length >= pageSize">
<text class="no-more-text"> 没有更多了 </text>
</view>
</template>
<view class="empty-state" v-else>
<text class="empty-text">暂无数据</text>
</view>
</view>
</scroll-view>
<!-- 新增缴费区域 -->
<view class="add-section">
<view class="section-header">
<text class="section-title">新增缴费</text>
<text class="record-link" @click="goToRecord">缴费记录</text>
</view>
<view class="grid-list">
<view class="grid-item" v-for="(item, index) in addList" :key="index" @click="goToAdd(item)">
<view class="icon-box" :class="item.iconClass">
<image class="iconStyle" :src="item.icon"></image>
@@ -47,422 +92,524 @@
</view>
</view>
<!-- 开启通知按钮 -->
<!-- v-if="subscribeAuth !== 'accepted'" -->
<!-- <view class="subscribe-btn" @click="requestSubscribeMessageClick">
<text class="subscribe-btn-text">开启缴费通知</text>
</view> -->
<!-- <button class="subscribe-btn" @tap="handleWaterElecSubscribe" open-type="subscribeMessage">
开启订单状态通知
</button> -->
<view style="width: 100%;height: 30rpx;"></view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
import { ref } from 'vue'
import { onShow, onLoad, onBackPress, onUnload, onPullDownRefresh } from '@dcloudio/uni-app'
import { getTopUpDetails, getTopUpSelectByResident } from '/pageSubPack/api/apiSub.js'
import { getCurrentVillage } from '/pageSubPack/api/api.js'
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
getTopUpSelectByResident,
postInsertOpenId,
postSubscribeAuth
} from '/pageSubPack/api/apiSub.js'
// 响应式数据
const statusBarHeight = ref(20)
// const subscribeAuth = ref(uni.getStorageSync('subscribeAuth') || '')
const paymentList = ref([
// {
// id: 1,
// name: '物业费',
// status: '已欠费',
// address: '桂花园别墅2号楼2层',
// icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/property.png",
// },
// {
// id: 2,
// name: '暖气费',
// status: '',
// address: '山东省日照市东港区桂花园别墅2号楼2层',
// icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/heating.png",
// },
// {
// id: 3,
// name: '电费',
// status: '已欠费',
// address: '山东省日照市东港区桂花园别墅2号楼2层',
// icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/electric.png",
// }
// 响应式数据
const detailslist = ref([])
const pageNum = ref(1)
const pageSize = ref(5)
const total = ref(0)
const hasMore = ref(true)
const loadingMore = ref(false)
const payPageNum = ref(1)
const payPageSize = ref(5)
const payTotal = ref(0)
const payHasMore = ref(true)
const payLoadingMore = ref(false)
// 小区选择
const currentCommunity = ref('请绑定房屋')
const paymentList = ref([
{
id: 3,
name: '电费',
status: '已欠费',
address: '山东省日照市东港区桂花园别墅2号楼2层',
icon: "https://wuye.ckdzkj.com/images/propertyImgs/electric.png",
}
])
const addList = ref([
// {
// name: "燃气",
// type: "gas",
// icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/gas.png",
// iconClass: "gas"
// },
{
name: "水",
name: "水",
type: "water",
icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/water.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/water.png",
iconClass: "water"
},
{
name: "电",
name: "电",
type: "electric",
icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/electric.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/electric.png",
iconClass: "electric"
},
// {
// name: "物业费",
// type: "property",
// icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/property.png",
// iconClass: "property"
// },
// {
// name: "车位费",
// type: "parking",
// icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/parkingIcon.png",
// iconClass: "parking"
// }
])
const postOpenId = async () => {
// App 端直接从本地存储获取 openid
try {
const openid = uni.getStorageSync('openid')
if (!openid) {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
await postInsertOpenId({
openid: openid,
residentUserId: uni.getStorageSync('userId')
})
} catch (err) {
uni.showToast({
title: `${err}`,
icon: 'none'
})
console.error('列表接口报错:', err)
}
])
// 独立的当前小区ID按账号隔离避免切换账号后残留上个账号的小区
const getPaymentVillageIdKey = () => 'paymentVillageId_' + (uni.getStorageSync('userId') || '')
const getPaymentVillageNameKey = () => 'paymentVillageName_' + (uni.getStorageSync('userId') || '')
const getPaymentVillageId = () => {
return uni.getStorageSync(getPaymentVillageIdKey()) || uni.getStorageSync('currentVillageId') || ''
}
// 获取当前小区
const getCurrentCommunityData = () => {
const paymentVillageId = uni.getStorageSync(getPaymentVillageIdKey())
const paymentVillageName = uni.getStorageSync(getPaymentVillageNameKey())
if (paymentVillageId) {
currentCommunity.value = paymentVillageName || '已选小区'
return
}
onShow(() => {
// 不再自动请求订阅消息,改为用户点击触发
})
// 生命周期
onReady(() => {})
onLoad(() => {
postOpenId()
uni.hideTabBar({
animation: false // 关闭动画,更稳定
});
getPayList()
})
const getPayList = async () => {
// 防重复请求
try {
const res = await getTopUpSelectByResident({
residentUserId: uni.getStorageSync('userId')
})
const data = res.data
paymentList.value = data
} catch (err) {
uni.showToast({
title: `${err}`,
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
getCurrentVillage().then(res => {
if (res.code === 200 && res.data && res.data.villageId) {
currentCommunity.value = res.data.villageName || '未命名小区'
} else {
currentCommunity.value = '请绑定房屋'
}
}).catch(() => {})
}
// 接收小区切换事件
const slelctHomeData = (data) => {
if (data.name) {
currentCommunity.value = data.name
uni.setStorageSync(getPaymentVillageNameKey(), data.name)
}
// 方法
const goToBill = (item) => {
uni.setStorageSync('arrearsPayment', item)
// if (item.balance < 0) {
// uni.redirectTo({
// url: '/pageSubPack/payment-list/arrearsPayment'
// })
// } else {
// uni.redirectTo({
// url: '/pageSubPack/payment-list/noArrearsPayment'
// })
// }
uni.redirectTo({
if (data.villageId) {
uni.setStorageSync(getPaymentVillageIdKey(), data.villageId)
}
getBillDetails()
getPayList()
}
// 跳转小区选择
const navigateToSelectHome = () => {
const cachedVillageId = getPaymentVillageId()
if (!cachedVillageId || cachedVillageId === '0' || cachedVillageId === 0) {
uni.navigateTo({ url: '/pageSubPack/my/myHouseIndex' })
} else {
uni.navigateTo({ url: `/pageSubPack/notice-list/selectHome?villageId=${cachedVillageId}&source=payment` })
}
}
onShow(() => {
getCurrentCommunityData()
getBillDetails()
})
onLoad(() => {
uni.hideTabBar({ animation: false });
uni.$on('confirmPayment', slelctHomeData)
getPayList()
})
onUnload(() => {
uni.$off('confirmPayment', slelctHomeData)
})
// 页面返回拦截
onBackPress(() => {
const pages = getCurrentPages()
if (pages.length <= 1) {
uni.switchTab({ url: "/pages/serviceIndex/serviceIndex" })
return true
}
})
// 跳转账单
const goToBill = (item) => {
uni.setStorageSync('arrearsPayment', item)
// 水费、电费走欠费缴纳页
if (item.type === '水费' || item.type === '电费') {
uni.navigateTo({
url: '/pageSubPack/payment-list/arrearsPayment'
})
}
const goToRecord = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentRecord'
} else {
uni.navigateTo({
url: '/pageSubPack/payment-list/billPayment'
})
}
}
const goToAdd = (type) => {
if (type.name == '燃气' || type.name == '暖气') {
uni.redirectTo({
url: '/pageSubPack/payment-list/selectPaymentEntity'
})
} else if (type.name == '水费' || type.name == '电费') {
uni.redirectTo({
url: '/pageSubPack/payment-list/addHydropower'
})
}
uni.setStorageSync('addPaymentType', type.name)
}
// 跳转缴费记录
const goToRecord = () => {
uni.navigateTo({
url: '/pageSubPack/payment-list/paymentRecord'
})
}
const goBack = () => {
console.log(uni.getStorageSync('sourcePage'))
if (uni.getStorageSync('sourcePage') == 'serviceIndex') {
uni.switchTab({
url: '/pages/serviceIndex/serviceIndex'
})
} else if (uni.getStorageSync('sourcePage') == 'index') {
uni.switchTab({
url: '/pages/index/index'
})
}
uni.removeStorageSync('sourcePage')
// 新增缴费
const goToAdd = (type) => {
uni.setStorageSync('addPaymentType', type.name)
if (type.name == '燃气' || type.name == '暖气') {
uni.navigateTo({
url: '/pageSubPack/payment-list/selectPaymentEntity'
})
} else if (type.name == '水表' || type.name == '电表') {
uni.navigateTo({
url: '/pageSubPack/payment-list/addHydropower'
})
}
}
// 获取物业费/车位费缴费列表
const getBillDetails = (isLoadMore = false) => {
if (isLoadMore) {
if (!hasMore.value || loadingMore.value) return
pageNum.value++
loadingMore.value = true
} else {
pageNum.value = 1
hasMore.value = true
}
getTopUpDetails({
userId: uni.getStorageSync('userId'),
villageId: getPaymentVillageId(),
pageNum: pageNum.value,
pageSize: pageSize.value,
}).then(res => {
if (isLoadMore) {
detailslist.value = [...detailslist.value, ...(res.rows || [])]
loadingMore.value = false
} else {
detailslist.value = res.rows || []
}
total.value = res.total || 0
hasMore.value = detailslist.value.length < total.value
}).catch(() => {
if (isLoadMore) {
pageNum.value--
loadingMore.value = false
}
}).finally(() => {
uni.stopPullDownRefresh();
})
}
const onLoadMore = () => {
getBillDetails(true)
}
// 水电费列表
const getPayList = async (isLoadMore = false) => {
if (isLoadMore) {
if (!payHasMore.value || payLoadingMore.value) return
payPageNum.value++
payLoadingMore.value = true
} else {
payPageNum.value = 1
payHasMore.value = true
}
try {
const res = await getTopUpSelectByResident({
residentUserId: uni.getStorageSync('userId'),
pageNum: payPageNum.value,
pageSize: payPageSize.value,
villageId:getPaymentVillageId()
})
const data = res.rows || res.data
if (isLoadMore) {
paymentList.value = [...paymentList.value, ...(data || [])]
payLoadingMore.value = false
} else {
paymentList.value = data || []
}
payTotal.value = res.total || 0
payHasMore.value = paymentList.value.length < payTotal.value
} catch (err) {
if (isLoadMore) {
payPageNum.value--
payLoadingMore.value = false
}
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
})
} finally {
uni.stopPullDownRefresh();
}
}
const onPayLoadMore = () => {
getPayList(true)
}
// 下拉刷新:同时刷新水电费和物业费列表
onPullDownRefresh(() => {
getBillDetails();
getPayList();
});
</script>
<style lang="scss">
page {
background: linear-gradient(180deg, #dfedfd, #F6FAFF);
}
page {
background: linear-gradient(180deg, #dfedfd, #F6FAFF);
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(180deg, #dfedfd, #F6FAFF);
}
.contentStyle {
display: flex;
flex-direction: column;
min-height: 100vh;
background: linear-gradient(180deg, #dfedfd, #F6FAFF);
overflow-x: hidden;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
.page {
padding: 0px 40rpx;
box-sizing: border-box;
}
.topbox {
height: 150rpx;
width: 100%;
position: relative;
}
.topbox {
height: 180rpx;
width: 100%;
position: relative;
}
.titleImg {
width: 262rpx;
height: 74rpx;
position: absolute;
left: 40rpx;
bottom: 20rpx;
}
.titleImg {
width: 262rpx;
height: 74rpx;
position: absolute;
left: 40rpx;
bottom: 60rpx;
}
.payImg {
width: 349.97rpx;
height: 313.57rpx;
position: absolute;
right: -80rpx;
bottom: -140rpx;
}
.payImg {
width: 349.97rpx;
height: 313.57rpx;
position: absolute;
right: -80rpx;
bottom: -140rpx;
}
.container {}
/* 小区选择栏 */
.community-bar {
position: absolute;
left: 40rpx;
bottom: 10rpx;
display: flex;
align-items: center;
}
.bill-list {
margin-bottom: 40rpx;
background-color: #fff;
padding: 30rpx;
border-radius: 10rpx;
}
.location-icon {
width: 48rpx;
height: 48rpx;
margin-right: 8rpx;
flex-shrink: 0;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
}
.community-name {
font-size: 26rpx;
font-weight: 500;
color: #222;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 260rpx;
margin-right: 6rpx;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
.container {}
.bill-item {
display: flex;
align-items: center;
background-color: #F6F9FE;
border-radius: 12rpx;
padding: 20rpx 20rpx 20rpx 0px;
margin-bottom: 20rpx;
}
.bill-list {
margin-bottom: 40rpx;
background-color: #fff;
padding: 30rpx;
border-radius: 10rpx;
min-height: 550rpx;
box-sizing: border-box;
}
.bill-item:last-child {
margin-bottom: 0rpx;
}
.bill-item {
display: flex;
align-items: center;
background-color: #F6F9FE;
border-radius: 12rpx;
padding: 20rpx 20rpx 20rpx 0px;
margin-bottom: 20rpx;
}
.bill-info {
width: calc(90% - 100rpx);
flex: 1;
}
.bill-item:last-child {
margin-bottom: 0rpx;
}
.billIcon {
width: 64rpx;
height: 64rpx;
margin: 20rpx;
}
.bill-item.bill-paid {
opacity: 0.6;
}
.bill-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
display: flex;
}
.bill-info {
width: calc(90% - 100rpx);
flex: 1;
}
.bill-desc {
font-size: 28rpx;
font-weight: 600;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.billIcon {
width: 64rpx;
height: 64rpx;
margin: 20rpx;
}
.bill-status {
padding: 2rpx 16rpx;
border-radius: 30rpx;
font-size: 24rpx;
font-weight: 500;
color: #fff;
background-color: #E34D59;
margin-left: 10rpx;
}
.bill-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
display: flex;
align-items: center;
}
.arrow {
width: 12rpx;
height: 12rpx;
border-right: 4rpx solid #ccc;
border-bottom: 4rpx solid #ccc;
transform: rotate(-45deg);
}
.bill-desc {
font-size: 24rpx;
font-weight: 600;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.add-section {
background-color: #fff;
padding: 30rpx;
border-radius: 10rpx;
}
.bill-status {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4rpx 14rpx;
border-radius: 30rpx;
font-size: 20rpx;
font-weight: 500;
color: #fff;
margin-left: 10rpx;
line-height: 1.4;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.bill-status.debt {
background-color: #E34D59;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.bill-status.done {
background-color: #999;
}
.record-link {
font-size: 28rpx;
color: #1677ff;
}
.arrow {
width: 12rpx;
height: 12rpx;
border-right: 4rpx solid #ccc;
border-bottom: 4rpx solid #ccc;
transform: rotate(-45deg);
}
.grid-list {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
/* gap: 26rpx; */
}
.details-scroll {
height: 550rpx;
padding: 0 0 20rpx 0;
}
.details-scroll-bill{
height: 550rpx;
padding: 0 0 40rpx 0;
}
.grid-item {
width: calc(25% - 60rpx);
background-color: #F6F9FE;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
margin-left: 20rpx;
display: flex;
align-items: center;
flex-direction: column;
}
/* 每行第 1 个 item 清除左边距,保证对齐 */
.grid-item:nth-child(4n+1) {
margin-left: 0;
}
.load-more-status {
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx 0;
}
.icon-box {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
}
.loading-text {
font-size: 26rpx;
color: #999;
}
.iconStyle {
width: 100%;
height: 100%;
}
.no-more-text {
font-size: 26rpx;
color: #ccc;
}
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.balance-normal {
font-size: 28rpx;
color: #333;
}
.subscribe-btn {
margin-top: 20rpx;
background-color: #1677ff;
padding: 24rpx;
border-radius: 10rpx;
text-align: center;
}
.balance-debt {
font-size: 28rpx;
color: #E34D59;
}
.subscribe-btn-text {
color: #fff;
font-size: 32rpx;
font-weight: 600;
}
.add-section {
background-color: #fff;
padding: 30rpx;
border-radius: 10rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.record-link {
font-size: 28rpx;
color: #1677ff;
}
.grid-list {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
.grid-item {
width: calc(25% - 60rpx);
background-color: #F6F9FE;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
margin-left: 20rpx;
display: flex;
align-items: center;
flex-direction: column;
}
.grid-item:nth-child(4n+1) {
margin-left: 0;
}
.icon-box {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
}
.iconStyle {
width: 100%;
height: 100%;
}
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 490rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
</style>

View File

@@ -1,12 +1,10 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<!-- <view class="status-bar"></view>
<uni-nav-bar title="缴费记录" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
</uni-nav-bar> -->
<view class="page">
<!-- 累计缴费 -->
<view class="total-box">
<view class="top-box">
@@ -16,7 +14,7 @@
<view class="chart-box">
<view v-if="chartData.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
@@ -34,40 +32,35 @@
</view>
<!-- 缴费明细 -->
<view class="list-wrapper">
<!-- 按月份分组渲染 -->
<view v-if="monthGroups.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view>
<view class="month-group" v-for="(item, idx) in monthGroups" :key="idx">
<view class="month-title">
{{ item.month }} 缴费 ¥{{ item.total }}
<!-- 缴费明细分页滚动 -->
<scroll-view class="list-scroll" scroll-y="true" @scrolltolower="loadMore">
<view class="list-wrapper">
<!-- 按月份分组渲染 -->
<view v-if="monthGroups.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<view class="month-group" v-for="(item, idx) in monthGroups" :key="idx">
<view class="month-title">
{{ item.year }}{{ item.month }} 缴费 ¥{{ item.total.toFixed(2) }}
</view>
<view class="item-list">
<view class="bill-item" v-for="bill in item.list" :key="bill.id">
<view class="bill-icon">
<image class="iconStyle" src="https://wuyeadmin.bugtc.com/images/propertyImgs/water.png"
v-if="bill.name=='物业费A'"></image>
<image class="iconStyle" src="https://wuyeadmin.bugtc.com/images/propertyImgs/electric.png"
v-if="bill.name=='物业费B'"></image>
<view class="item-list">
<view class="bill-item" v-for="bill in item.list" :key="bill.id">
<view class="bill-icon">
<image class="iconStyle" :src="bill.icon"></image>
</view>
<view class="bill-info">
<view class="name">{{ bill.name }}</view>
<view class="account">{{ bill.account }}</view>
</view>
<view class="bill-price">-{{ bill.amount.toFixed(2) }}</view>
</view>
<view class="bill-info">
<view class="name">{{ bill.name }}</view>
<view class="account">{{ bill.account }}</view>
</view>
<view class="bill-price">-{{ bill.amount }}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</scroll-view>
</view>
</view>
</template>
@@ -101,18 +94,33 @@
const billList = ref([])
// 图标映射
const iconMap = {
'物业费A': 'https://wuyeadmin.bugtc.com/images/propertyImgs/water.png',
'物业费B': 'https://wuyeadmin.bugtc.com/images/propertyImgs/electric.png',
'物业费': 'https://wuyeadmin.bugtc.com/images/propertyImgs/property.png',
'暖气费': 'https://wuyeadmin.bugtc.com/images/propertyImgs/heating.png',
'燃气费': 'https://wuyeadmin.bugtc.com/images/propertyImgs/gas.png'
// 费用名称映射
// topup水电表充值type 1=电费, 2=水费
// bill物业账单type 0=物业费, 1=车位费, 2=垃圾处理费
const getNameByType = (item) => {
if (item.recordType === 'topup') {
return item.type == '1' ? '电费' : item.type == '2' ? '水费' : '充值'
} else if (item.recordType === 'bill') {
return item.type == '0' ? '物业费' : item.type == '1' ? '车位费' : item.type == '2' ? '垃圾处理费' : '账单'
}
return ''
}
// 获取缴费记录
const getRecords = async () => {
// 图标映射
const iconMap = {
'电费': 'https://wuye.ckdzkj.com/images/propertyImgs/electric.png',
'水费': 'https://wuye.ckdzkj.com/images/propertyImgs/water.png',
'物业费': 'https://wuye.ckdzkj.com/images/propertyImgs/property.png',
'车位费': 'https://wuye.ckdzkj.com/images/propertyImgs/parking.png',
'垃圾处理费': 'https://wuye.ckdzkj.com/images/propertyImgs/garbage.png',
'充值': 'https://wuye.ckdzkj.com/images/propertyImgs/water.png',
'账单': 'https://wuye.ckdzkj.com/images/propertyImgs/property.png'
}
// 获取缴费记录loadMore=true 时分页追加)
const getRecords = async (loadMore = false) => {
try {
if (!loadMore) pageNum.value = 1
const res = await getTopupRecordWithStat({
residentUserId: uni.getStorageSync('userId'),
pageNum: pageNum.value,
@@ -121,39 +129,57 @@
if (res.code === 200) {
const records = res.data.records || []
// 转换记录格式
billList.value = records.map(item => ({
id: item.id,
month: item.rechargeTime && item.rechargeTime.split('-')[1] ? parseInt(item.rechargeTime.split('-')[1], 10) : 1,
name: item.type == 1 ? '物业费B' : item.type == 2 ? '物业费A' : '',
account: item.deviceCode || '-',
amount: Number(item.rechargeAmount) || 0,
icon: iconMap[item.type == 1 ? '物业费B' : item.type == 2 ? '物业费A' : ''] || 'https://wuyeadmin.bugtc.com/images/propertyImgs/water.png'
}))
const mappedList = records.map(item => {
const payDate = item.payTime ? item.payTime.split(' ')[0] : ''
const parts = payDate.split('-')
const name = getNameByType(item)
return {
id: item.id,
year: parts[0] ? parseInt(parts[0], 10) : new Date().getFullYear(),
month: parts[1] ? parseInt(parts[1], 10) : 1,
name,
account: item.deviceCode || '',
amount: Number(item.amount) || 0,
recordType: item.recordType || '',
icon: iconMap[name] || iconMap['账单']
}
})
billList.value = loadMore ? [...billList.value, ...mappedList] : mappedList
// 累计缴费金额
const total = records.reduce((sum, item) => sum + (Number(item.rechargeAmount) || 0), 0)
totalAmount.value = total.toFixed(2)
// 更新图表数据(根据 records 计算最近6个月
updateChartData()
totalAmount.value = res.data.totalAmount || '0.00'
// 柱状图数据使用API返回的monthlyStats
updateChartData(res.data.monthlyStats || {})
}
} catch (err) {
console.error('获取缴费记录失败', err)
}
}
// 更新图表数据(根据 records 计算最近6个月
const updateChartData = () => {
// 按月份汇总金额
const monthTotal = {}
billList.value.forEach(bill => {
const monthKey = bill.month
if (!monthTotal[monthKey]) {
monthTotal[monthKey] = 0
// 加载更多(触底分页
const loadMore = () => {
pageNum.value++
getRecords(true)
}
// 更新图表数据使用API返回的monthlyStats
// monthlyStats格式: { "2026-04": "04月 30.00元", "2026-05": "05月 25.00元", ... }
const updateChartData = (monthlyStats) => {
const statsMap = {}
Object.entries(monthlyStats).forEach(([key, value]) => {
const priceMatch = value.match(/([\d.]+)元/)
if (priceMatch && key.includes('-')) {
const [y, m] = key.split('-')
statsMap[key] = {
year: parseInt(y, 10),
month: parseInt(m, 10),
price: parseFloat(priceMatch[1]) || 0
}
}
monthTotal[monthKey] += bill.amount
})
// 取最近6个月当前月往前
const now = new Date()
const currentMonth = now.getMonth() + 1 // 1-12
const currentMonth = now.getMonth() + 1
const currentYear = now.getFullYear()
const months = []
for (let i = 0; i < 6; i++) {
@@ -163,37 +189,46 @@
month += 12
year -= 1
}
const key = `${year}-${String(month).padStart(2, '0')}`
const data = statsMap[key]
months.push({
month,
year
year,
price: data ? data.price : 0
})
}
chartData.value = months.map((m, i) => ({
month: (m.year + '').slice(-2) + '年' + m.month + '月',
year: m.year,
sortMonth: m.year * 12 + m.month,
price: monthTotal[m.month] || 0,
price: m.price,
index: i
})).sort((a, b) => a.sortMonth - b.sortMonth)
const prices = chartData.value.map(c => c.price)
maxPrice.value = Math.max(...prices, 100)
}
// 计算属性 —— 月份分组
// 计算属性 —— 按年-月分组缴费明细
const monthGroups = computed(() => {
let map = {}
const map = {}
billList.value.forEach(bill => {
if (!map[bill.month]) {
map[bill.month] = {
const key = `${bill.year}-${String(bill.month).padStart(2, '0')}`
if (!map[key]) {
map[key] = {
month: bill.month,
year: bill.year,
total: 0,
list: []
}
}
map[bill.month].list.push(bill)
map[bill.month].total += bill.amount
map[key].list.push(bill)
map[key].total += bill.amount
})
// 按年月降序排列
return Object.values(map).sort((a, b) => {
if (a.year !== b.year) return b.year - a.year
return b.month - a.month
})
return Object.values(map).sort((a, b) => b.month - a.month)
})
// 生命周期
@@ -210,11 +245,9 @@
return `${height}rpx`
}
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex'
});
}
const goBack = () => {
uni.navigateBack();
}
</script>
<style lang="scss">
@@ -233,9 +266,16 @@
.page {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
padding: 0px 40rpx;
box-sizing: border-box;
overflow: hidden;
}
.list-scroll {
flex: 1;
overflow-y: auto;
}
.total-box {

View File

@@ -0,0 +1,674 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="switchStatus(paymentItem.type) || '缴费'" left-icon="left" @clickLeft="goBack"
backgroundColor="transparent" :border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 1. 缴费详情 -->
<view v-if="currentStep === 1" class="pay-page">
<!-- 物业费缴费 -->
<!-- 金额展示 -->
<view class="amount-section" v-if="outstandingPayment">
<text class="amount-text"
:class="{ 'outstandingPaymentClass':outstandingPayment }">{{outstandingPayment?'¥'+payAmount:'¥'+payAmount}}</text>
</view>
<view class="amount-section" v-else>
<text class="amount-text paymentClass">{{'¥'+payAmount}}</text>
</view>
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
<!-- 缴费信息 -->
<view class="info-section" style="">
<view>
<view class="info-item">
<text class="info-label">缴费名称:</text>
<text class="info-value">{{ paymentItem.name || '-' }}</text>
</view>
</view>
</view>
</view>
<!-- 2. 缴费成功页 -->
<view class="container" v-if="currentStep==2">
<!-- 成功图标 -->
<view class="success-icon">
<text class="icon-check"></text>
</view>
<!-- 主标题 -->
<view class="title">缴费成功</view>
<!-- 提示文案 -->
<view class="desc">
成功缴纳{{ switchStatus(paymentItem.type) || '' }}费用 ¥{{ payNum || payAmount }}
</view>
</view>
</view>
</scroll-view>
<!-- 立即缴费按钮 -->
<view class="btn-section" v-if="currentStep === 1">
<button class="pay-btn" @click="handlePay">立即缴费</button>
</view>
<!-- 返回按钮 -->
<view class="btn-section" v-if="currentStep === 2">
<button class="pay-btn" @click="handleBackToList">返回生活缴费</button>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive
} from 'vue'
import {
onShow,
onLoad
} from '@dcloudio/uni-app'
import {
getPushSingle
} from '/pageSubPack/api/api.js'
// 响应式数据
const outstandingPayment = ref(true)
const storageValue = uni.getStorageSync('arrearsPayment')
const paymentItem = reactive(typeof storageValue === 'object' && storageValue !== null ? storageValue : {})
const currentStep = ref(1)
const payAmount = ref('0.00')
const arrearsAmount = ref('0.00')
const payNum = ref('')
// 页面加载时初始化数据
onLoad((options) => {
initPaymentData()
// 若从缴费列表带入 amount则优先展示该金额
if (options && options.amount) {
const amount = Number(options.amount) || 0
payAmount.value = amount
arrearsAmount.value = amount.toFixed(2)
// outstandingPayment.value = amount > 0
}
})
onShow(() => {})
// 初始化缴费数据
const initPaymentData = () => {
if (paymentItem) {
arrearsAmount.value = Math.abs(paymentItem.balance || 0).toFixed(2)
payAmount.value = Math.abs(paymentItem.balance || 0)
outstandingPayment.value = paymentItem.balance < 0
}
}
// 返回
const goBack = () => {
uni.navigateBack();
}
// 立即缴费 — 通过微信拉起日照银行小程序进行支付
const handlePay = () => {
const amount = arrearsAmount.value
console.log('saaaaaaa', paymentItem)
try {
uni.showLoading({ title: '加载中...' })
// 设备类型映射物业费→3车位费→4垃圾处理费→5
const deviceTypeMap = { '物业费': '3', '车位费': '4', '垃圾处理费': '5' }
const mappedDeviceType = deviceTypeMap[switchStatus(paymentItem.type)] || paymentItem.deviceType || ''
// 直接从本地参数拼 path 查询字符串传给支付小程序,不走接口
const token = uni.getStorageSync('token') || ''
const params = {
acct: uni.getStorageSync('userId'),
orderType: '10',
deviceCode: paymentItem.deviceCode || '',
deviceType: mappedDeviceType,
billId: paymentItem.id || '',
residentUserId: uni.getStorageSync('userId'),
// goodName: paymentItem.name || switchStatus(paymentItem.type) || '',
// remark: '服务费',
token: token
}
const queryString = Object.keys(params).map(k =>
encodeURIComponent(k) + '=' + encodeURIComponent(params[k])
).join('&')
uni.hideLoading()
console.log('传入中间小程序的path参数:', queryString)
// #ifdef APP-PLUS
// 原生App通过微信SDK拉起小程序
plus.share.getServices((services) => {
const weixin = services.find(s => s.id === 'weixin')
if (!weixin) {
uni.showToast({ title: '未检测到微信客户端', icon: 'none' })
return
}
if (!weixin.launchMiniProgram) {
uni.showToast({ title: '当前微信版本太低,不支持拉起小程序', icon: 'none' })
return
}
weixin.launchMiniProgram({
id: 'gh_a765aef0172c',
type: 1, // 0 正式版 / 1测试版 / 2预览版
path: 'pages/arrearsPayment/arrearsPayment?' + queryString,
}, (res) => {
console.log('回调---日照银行小程序回调结果App', JSON.stringify(res))
}, (err) => {
console.error('拉起日照银行小程序失败App', JSON.stringify(err))
uni.showToast({ title: '拉起支付小程序失败', icon: 'none' })
})
})
// #endif
} catch (err) {
uni.hideLoading()
console.error('拉起支付小程序异常', err)
uni.showToast({ title: '请求异常,请重试', icon: 'none' })
}
}
// 缴费成功推送通知
const sendPaySuccessPush = () => {
const userId = uni.getStorageSync('userId')
if (userId) {
getPushSingle({
userId: userId,
channel: 'resident',
title: '缴费成功',
content: `您已成功缴纳${switchStatus(paymentItem.type) || ''}费用 ¥${payNum.value || payAmount.value}`
}).catch(err => {
console.log('推送发送失败', err)
})
}
}
// 返回缴费列表
const handleBackToList = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex'
});
}
// 类型码转中文名展示
const switchStatus = (type) => {
const typeMap = {
'0': '物业费',
'1': '车位费',
'2': '垃圾处理费'
}
return typeMap[type] || type
}
</script>
<style lang="scss">
page {
background-color: #f9f9f9;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
background-color: #
/* fff */
;
}
/* 1. 物业费详情页 */
.pay-page {
width: 100%;
/* min-height: 100vh; */
/* background-color: #fff; */
}
.amount-section {
padding: 60rpx 0;
text-align: center;
}
.amount-text {
font-size: 56rpx;
font-weight: 600;
}
.outstandingPaymentClass {
/* color: #E34D59; */
color: #00aaff;
}
.paymentClass {
color: #00aaff;
}
.info-section {
padding-top: 20rpx;
/* margin-bottom: 40rpx; */
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
height: 80rpx;
}
.info-label {
font-size: 30rpx;
color: #666;
flex-shrink: 0;
/* 强制单行 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* 右侧留间距,避免和内容紧贴 */
margin-right: 0.5rem;
}
.info-value {
font-size: 30rpx;
color: #333;
font-weight: 600;
flex: 1;
/* text-align: right; */
}
.info-payAmountItem {
height: 100rpx;
line-height: 100rpx;
margin-bottom: 60rpx;
}
.info-payAmount {
font-size: 50rpx;
color: #333;
font-weight: 600;
}
.input-section {
/* padding: 030rpx; */
}
.amount-input {
width: 100%;
height: 100rpx;
font-size: 32rpx;
line-height: 100rpx;
border: none;
outline: none;
margin-right: 10rpx;
}
.btn-section {
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
}
.pay-btn {
width: 100%;
height: 100rpx;
line-height: 100rpx;
background-color: #007aff;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
border: none;
outline: none;
}
/* 2. 支付密码弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
}
.password-modal {
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: 40rpx;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 100rpx;
padding: 0 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.close-icon {
font-size: 44rpx;
width: 80rpx;
height: 100rpx;
display: flex;
align-items: center;
}
.modal-title {
font-size: 32rpx;
font-weight: 500;
color: #000;
}
.header-right {
width: 80rpx;
}
.pay-info {
padding: 40rpx 30rpx;
text-align: center;
}
.merchant-name {
display: block;
font-size: 32rpx;
color: #333;
margin-bottom: 20rpx;
}
.pay-amount {
font-size: 64rpx;
font-weight: 500;
color: #000;
}
.pay-way {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.way-label {
font-size: 30rpx;
color: #666;
}
.way-content {
display: flex;
align-items: center;
}
.bank-icon {
width: 32rpx;
height: 32rpx;
margin-right: 6rpx;
}
.bank-name {
font-size: 30rpx;
color: #333;
margin-right: 16rpx;
}
.arrow-icon {
font-size: 28rpx;
color: #999;
}
/* 银行卡列表 */
.bank-list {
padding: 030rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.bank-item {
display: flex;
align-items: center;
height: 100rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.bank-item:last-child {
border-bottom: none;
}
.bank-item-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.bank-item-name {
flex: 1;
font-size: 30rpx;
color: #333;
}
.check-icon {
font-size: 36rpx;
color: #007aff;
}
/* 密码输入框 */
.password-input {
display: flex;
justify-content: space-between;
padding: 40rpx 30rpx;
}
.password-item {
width: 100rpx;
height: 100rpx;
/* border: 2rpx solid #dcdcdc; */
background-color: #efefef;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
}
.password-dot {
width: 24rpx;
height: 24rpx;
background-color: #000;
border-radius: 50%;
}
/* 数字键盘 */
.keyboard {
width: 100%;
}
.keyboard-row {
display: flex;
}
.keyboard-item {
flex: 1;
height: 54px;
line-height: 54px;
text-align: center;
font-size: 48rpx;
color: #000;
border: 2rpx solid #f0f0f0;
background-color: #fff;
}
.keyboard-item:active {
background-color: #e5e5e5;
}
.keyboard-item.empty {
background-color: #f5f5f5;
border: none;
}
.keyboard-item.delete {
background-color: #f5f5f5;
border: none;
}
/* 3. 缴费成功页 */
/* 全局容器 */
.container {
display: flex;
flex-direction: column;
align-items: center;
}
/* 成功图标 */
.success-icon {
width: 220rpx;
height: 220rpx;
border-radius: 50%;
background-color: #1677ff;
display: flex;
align-items: center;
justify-content: center;
margin-top: 200rpx;
margin-bottom: 60rpx;
}
.icon-check {
color: #fff;
font-size: 130rpx;
font-weight: bold;
line-height: 1;
}
/* 主标题 */
.title {
font-size: 40rpx;
font-weight: bold;
color: #000;
margin-bottom: 20rpx;
}
/* 提示文案 */
.desc {
font-size: 32rpx;
color: #666;
line-height: 1.6;
text-align: center;
margin-bottom: 150rpx;
}
/* 支付方式选择 */
.pay-method-section {
margin-top: 30rpx;
padding: 0 20rpx;
}
.pay-method-title {
font-size: 30rpx;
color: #333;
font-weight: 600;
margin-bottom: 20rpx;
display: block;
}
.pay-method-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.pay-method-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
background: #fff;
border-radius: 12rpx;
border: 2rpx solid #ededed;
}
.pay-method-item.active {
border-color: #1677ff;
background: #f0f7ff;
}
.pay-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.pay-name {
flex: 1;
font-size: 30rpx;
color: #333;
}
.pay-check {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: #1677ff;
position: relative;
}
.pay-check::after {
content: '';
position: absolute;
left: 10rpx;
top: 6rpx;
width: 8rpx;
height: 14rpx;
border: 2rpx solid #fff;
border-left: none;
border-top: none;
transform: rotate(45deg);
}
/* 核心:欠费金额行样式 */
.arrears-row {
display: flex;
align-items: center;
justify-content: space-between;
/* padding:30rpx; */
margin-bottom: 60rpx;
}
.arrears-text {
font-size: 28rpx;
color: #999;
}
.auto-fill-btn {
font-size: 28rpx;
color: #007aff;
/* 蓝色,匹配设计图 */
font-weight: 500;
padding: 10rpx 0;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -213,7 +213,7 @@
// 方法
const selectItem = (item) => {
console.log(item)
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/payment-list/addPayment'
})
}
@@ -233,7 +233,7 @@
const onSearch = () => {}
const goBack = () => {
uni.redirectTo({ url: '/pageSubPack/payment-list/paymentIndex' });
uni.navigateBack();
}
</script>

View File

@@ -1,56 +1,60 @@
<template>
<view class="container">
<!-- 状态栏 -->
<view class="status-bar"></view>
<!-- 顶部导航 -->
<uni-nav-bar title="支付收银台" left-icon="left" @clickLeft="goBack" />
<uni-nav-bar
title="支付收银台"
left-icon="left"
@clickLeft="goBack"
background-color="#F5F6F9"
color="#000"
/>
<!-- 金额区域 -->
<view class="amount-box">
<text class="amount">¥{{ amount }}</text>
<text class="countdown">剩余支付时间{{ countdownTime }}</text>
<!-- 内容区域 -->
<view class="content">
<!-- 金额区域 -->
<view class="amount-box">
<view class="amount-text">缴费金额</view>
<text class="amount"> <text style="font-size: 32rpx;"> ¥ </text>{{ amount }}</text>
<view class="countdown-wrap">
<text class="countdown">剩余支付时间 {{ countdownTime }}</text>
</view>
</view>
<!-- 支付方式列表 -->
<view class="tip">请选择支付方式</view>
<view class="pay-list">
<view class="pay-item" @click="selectPayType('wx')">
<view class="left">
<image src="https://wuye.ckdzkj.com/images/home/wechat.png" class="icon" mode="widthFix" />
<view class="pay-info">
<text class="pay-name">微信</text>
</view>
</view>
<view class="radio" :class="{ active: selectedType === 'wx' }" />
</view>
<view class="pay-item" @click="selectPayType('alipay')">
<view class="left">
<image src="https://wuye.ckdzkj.com/images/home/alipay.png" class="icon" mode="widthFix" />
<view class="pay-info">
<text class="pay-name">支付宝</text>
</view>
</view>
<view class="radio" :class="{ active: selectedType === 'alipay' }" />
</view>
</view>
</view>
<!-- 支付方式列表 -->
<view class="pay-list">
<view class="pay-item" @click="selectPayType('wx')">
<view class="left">
<image src="/static/wechat.png" class="icon" mode="widthFix" />
<view class="pay-info">
<text class="pay-name">微信支付</text>
<text class="pay-desc">微信快捷支付</text>
</view>
</view>
<view class="radio" :class="{ active: selectedType === 'wx' }" />
</view>
<view class="pay-item" @click="selectPayType('alipay')">
<view class="left">
<image src="/static/alipay.png" class="icon" mode="widthFix" />
<view class="pay-info">
<text class="pay-name">支付宝支付</text>
<text class="pay-desc">支付宝快捷支付</text>
</view>
</view>
<view class="radio" :class="{ active: selectedType === 'alipa··y' }" />
</view>
<view class="pay-item" @click="selectPayType('union')">
<view class="left">
<image src="/static/union.png" class="icon" mode="widthFix" />
<view class="pay-info">
<text class="pay-name">银联支付</text>
<text class="pay-desc">云闪付/银行卡</text>
</view>
</view>
<view class="radio" :class="{ active: selectedType === 'union' }" />
</view>
</view>
<view class="tip">请选择支付方式完成缴费</view>
<!-- 底部支付按钮 -->
<view class="pay-btn" @click="handlePayClick">
<text class="btn-text">立即支付 {{ amount }} </text>
<view class="btn-wrap">
<view class="pay-btn" @click="handlePayClick">
<text class="btn-text">立即支付 {{ amount }} </text>
</view>
</view>
</view>
</template>
@@ -61,16 +65,13 @@
import { getUnionPayTn } from '/pageSubPack/api/apiSub.js'
import { getPushSingle } from '/pageSubPack/api/api.js'
// #ifdef APP
import { startPay, checkWalletInstalled } from '@/uni_modules/unionpay-full'
// #endif
// 支付宝和微信支付
const amount = ref('0.00')
const countdownTime = ref('29:57')
const countdownTime = ref('10:00')
let timer = null
const payData = ref({})
// 读取支付数据
onLoad(() => {
const data = uni.getStorageSync('payData')
if (data) {
@@ -101,37 +102,39 @@
}, 1000)
}
// 选中的支付类型(默认微信)
const selectedType = ref('wx')
// 返回
const goBack = () => uni.navigateBack()
// 选择支付方式
const selectPayType = (type) => {
selectedType.value = type
// #ifndef APP-HARMONY
uni.vibrateShort({ type: 'light' })
// #endif
}
// 点击底部支付按钮
const handlePayClick = () => {
handlePay()
}
// ==================== 统一支付入口 ====================
const handlePay = async () => {
try {
uni.showLoading({ title: '发起支付中' })
const payTypeMap = { wx: '0', alipay: '1' }
const backendPayType = payTypeMap[selectedType.value] || '1'
// 向后端请求获取支付参数
const res = await getUnionPayTn({
amount: amount.value,
orderDesc: (payData.value.type || '缴费') + '缴费',
residentUserId: uni.getStorageSync('userId'),
devicecode: payData.value.deviceCode || '',
deviceType: '2'
payType: backendPayType,
deviceCode: payData.value.deviceCode || '',
deviceType: '2',
chargeTypeId: payData.value.chargeTypeId,
chargeItemId: payData.value.chargeItemId,
billId: payData.value.billId,
houseId: payData.value.houseId
})
console.log('支付接口返回:', JSON.stringify(res.data))
console.log('支付接口返回:', JSON.stringify(res.data))
uni.hideLoading()
if (res.code !== 200 || !res.data) {
@@ -142,16 +145,10 @@
const payType = selectedType.value
const data = res.data
// 根据支付方式调起对应 SDK
if (payType === 'union') {
// 银联UMS SDK
invokeUmsPay('unionpay', data.union?.tn || data.union || data)
} else if (payType === 'wx') {
// 微信plus.payment等申请微信SDK后切到UMS
if (payType === 'wx') {
invokeWxPay(data.wx || data.wxPayInfo || data)
} else if (payType === 'alipay') {
// 支付宝UMS SDK
invokeUmsPay('alipay', data.alipay || data.aliPayInfo || data)
invokeAlipay(data.alipay || data.aliPayInfo || data)
}
} catch (err) {
@@ -160,61 +157,8 @@
uni.showToast({ title: '支付请求异常', icon: 'none' })
}
}
// ==================== UMS 统一支付(银联/支付宝)====================
const invokeUmsPay = (channel, data) => {
// #ifdef APP
const tn = typeof data === 'string' ? data : (data.tn || JSON.stringify(data))
// 银联渠道检查是否安装云闪付
if (channel === 'unionpay') {
const installed = checkWalletInstalled()
if (!installed) {
uni.showModal({
title: '未安装云闪付',
content: '请先下载安装云闪付App后再进行支付',
showCancel: false,
confirmText: '知道了'
})
return
}
}
// 调起 UMS 支付
startPay({
channel: channel,
tn: tn,
mode: '00',
callback: (result) => {
console.log('UMS支付结果[' + channel + ']', JSON.stringify(result))
const code = result?.code
if (code === 0) {
paySuccess()
} else if (code === 1) {
uni.showModal({
title: '支付处理中',
content: '银行正在处理支付结果,请勿重复支付。您可在「缴费记录」中查看最终状态。',
showCancel: false,
confirmText: '我知道了',
success: () => {
uni.redirectTo({ url: '/pageSubPack/payment-list/paymentIndex' })
}
})
} else if (code === -2) {
uni.showToast({ title: '支付已取消', icon: 'none' })
} else {
uni.showToast({ title: result?.msg || '支付失败', icon: 'none' })
}
}
})
// #endif
// #ifndef APP
uni.showToast({ title: '支付仅支持App端', icon: 'none' })
// #endif
}
// ==================== 微信支付uni.requestPayment====================
// 微信支付
const invokeWxPay = (data) => {
const orderInfo = {
appid: data.appId,
@@ -239,11 +183,10 @@
}
})
}
// ==================== 支付宝支付uni.requestPayment====================
// 支付宝支付
const invokeAlipay = (data) => {
const orderInfo = data.orderInfo || data
uni.requestPayment({
provider: 'alipay',
orderInfo: orderInfo,
@@ -258,19 +201,17 @@
})
}
// ==================== 支付成功 ====================
const paySuccess = () => {
sendPaySuccessPush()
uni.showToast({ title: '支付成功', icon: 'success' })
uni.setStorageSync('paySuccess', true)
setTimeout(() => {
uni.redirectTo({
url: '/pageSubPack/payment-list/arrearsPayment'
uni.navigateTo({
url: '/pageSubPack/payment-list/addPaymentSuccess?type=pay'
})
}, 1500)
}
// 缴费成功推送通知
const sendPaySuccessPush = () => {
const userId = uni.getStorageSync('userId')
if (userId) {
@@ -285,48 +226,78 @@
</script>
<style scoped>
/* 全局容器 */
/* 全局背景 */
.container {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
width: 100%;
min-height: 100vh;
background-color: #F5F6F9;
padding: 0 30rpx;
box-sizing: border-box;
}
/* 金额区域 */
/* 状态栏 */
.status-bar {
width: 100%;
height: var(--status-bar-height);
}
/* 内容区域 */
.content {
/* flex: 1; */
}
/* 金额卡片 */
.amount-box {
background: #2F77FD;
color: #fff;
padding: 60rpx 20rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 50rpx 0;
text-align: center;
margin: 30rpx 0;
}
.amount {
font-size: 72rpx;
font-weight: bold;
display: block;
.amount-text {
font-size: 28rpx;
color: #666;
margin-bottom: 16rpx;
}
.countdown {
font-size: 26rpx;
opacity: 0.9;
.amount {
display: block;
font-size: 52rpx;
font-weight: bold;
color: #333;
}
/* 支付方式列表 */
/* 倒计时样式 */
.countdown-wrap {
display: inline-block;
margin-top: 24rpx;
padding: 8rpx 24rpx;
background: #F4F4F4;
border-radius: 30rpx;
}
.countdown {
font-size: 28rpx;
color: #000;
}
/* 支付方式卡片 */
.pay-list {
margin-top: 20rpx;
background-color: #fff;
padding: 0 20rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 0 30rpx;
margin-bottom: 30rpx;
}
.pay-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
padding: 36rpx 0;
border-bottom: 1rpx solid #f2f2f2;
}
.pay-item:last-child {
@@ -341,13 +312,7 @@
.icon {
width: 56rpx;
height: 56rpx;
margin-right: 20rpx;
border-radius: 8rpx;
}
.pay-info {
display: flex;
flex-direction: column;
margin-right: 24rpx;
}
.pay-name {
@@ -356,65 +321,58 @@
font-weight: 500;
}
.pay-desc {
font-size: 24rpx;
color: #999;
margin-top: 4rpx;
}
/* 单选按钮 — 对勾样式 */
.radio {
width: 36rpx;
height: 36rpx;
width: 44rpx;
height: 44rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
border: 3rpx solid #d0d0d0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-sizing: border-box;
}
.radio.active {
border-color: #2F77FD;
background-color: #2F77FD;
border-color: #2F77FD;
}
.radio.active::after {
content: '';
width: 10rpx;
height: 18rpx;
border: 3rpx solid #fff;
border-left: none;
border-top: none;
width: 12rpx;
height: 22rpx;
border: 4rpx solid #fff;
border-left: 0;
border-top: 0;
transform: rotate(45deg) translate(-2rpx, -2rpx);
}
/* 底部提示 */
/* 提示 */
.tip {
font-size: 24rpx;
color: #999;
text-align: center;
padding: 30rpx 0;
margin-bottom: 120rpx;
font-size: 32rpx;
color: #333;
font-weight: 500;
text-align: left;
padding: 20rpx 0;
}
/* 底部按钮 — 圆角胶囊 */
/* 底部按钮区域 */
.btn-wrap {
padding: 30rpx 0 60rpx;
}
/* 底部支付按钮 */
.pay-btn {
position: fixed;
bottom: 40rpx;
left: 30rpx;
right: 30rpx;
height: 90rpx;
line-height: 90rpx;
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: #2F77FD;
color: #fff;
text-align: center;
font-size: 32rpx;
font-size: 34rpx;
font-weight: bold;
border-radius: 45rpx;
border-radius: 8rpx;
box-shadow: 0 6rpx 20rpx rgba(47, 119, 253, 0.3);
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>
</style>

View File

@@ -1,8 +1,5 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar title="投诉建议" backgroundColor="transparent" :border="false" left-icon="left" @clickLeft="goBack">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y>
@@ -46,7 +43,10 @@
<text class="label-text">问题描述</text>
</view>
<view class="row-right desc-right">
<textarea v-model="description" placeholder="请描述您的问题" class="desc-input" auto-height maxlength="500"></textarea>
<view class="desc-input-wrap">
<textarea v-model="description" placeholder="请描述您的问题" class="desc-input" auto-height maxlength="200"></textarea>
<text class="desc-count">{{ description.length }}/200</text>
</view>
</view>
</view>
@@ -56,7 +56,7 @@
<text class="label-text">投诉人姓名</text>
</view>
<view class="row-right">
<input v-model="name" placeholder="请输入姓名" class="text-input" type="text" />
<input v-model="name" placeholder="请输入姓名" class="text-input" type="text" maxlength="20" />
</view>
</view>
@@ -97,7 +97,7 @@
<!-- 底部下一步按钮 -->
<view class="bottom-btn-wrap">
<button class="next-btn" @click="submitForm">提交</button>
<button class="next-btn" :disabled="isSubmitting" @click="submitForm">提交</button>
</view>
<!-- ================== 自定义Popup弹窗纯view实现 ================== -->
@@ -161,7 +161,9 @@ const villageId = ref('')
const tempFilePaths = ref([])
const maxCount = ref(9)
// 关闭所有弹窗
const isSubmitting = ref(false)
// 关闭所有弹窗
const closeAllPopup = () => {
showTypePopup.value = false
showPropertyPopup.value = false
@@ -174,8 +176,9 @@ onLoad(() => {
// 获取投诉类型
const getSelectType = async () => {
let params = {status:'0'}
try {
const res = await getComplainType()
const res = await getComplainType(params)
if (res.code === 200) {
typeOptions.value = res.data || []
} else {
@@ -237,12 +240,14 @@ const submitForm = async () => {
if (!selectedTypeId.value) {
return uni.showToast({ title: '请选择投诉类型', icon: 'none' })
}
if (selectedType.value === '物业服务' && !selectedProperty.value) {
return uni.showToast({ title: '请选择物业', icon: 'none' })
}
// if (selectedType.value === '物业服务' && !selectedProperty.value) {
// return uni.showToast({ title: '请选择物业', icon: 'none' })
// }
if (!description.value.trim()) {
return uni.showToast({ title: '请输入问题描述', icon: 'none' })
}
if (isSubmitting.value) return
isSubmitting.value = true
uni.showLoading({ title: '提交中...', mask: true })
@@ -271,14 +276,14 @@ const submitForm = async () => {
if (res.code === 200) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.redirectTo({
url: '/pageSubPack/service-list/complaintsSuggestionsIndex'
})
uni.navigateBack()
}, 1500)
} else {
isSubmitting.value = false
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (err) {
isSubmitting.value = false
uni.hideLoading()
uni.showToast({
title: err.msg || '提交失败,请重试',
@@ -288,11 +293,9 @@ const submitForm = async () => {
}
// 返回
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/service-list/complaintsSuggestionsIndex'
})
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss">
page {
@@ -392,20 +395,35 @@ const goBack = () => {
.desc-right {
flex: 1;
width: 100%;
}
.desc-input-wrap {
width: 100%;
margin-top: 20rpx;
border: 1rpx solid #f0f0f0;
border-radius: 8rpx;
padding: 24rpx;
box-sizing: border-box;
}
.desc-input {
width: calc(100vw - 84rpx);
width: 100%;
min-height: 80px;
margin-top: 20rpx;
padding: 24rpx;
border: 1rpx solid #f0f0f0;
border-radius: 8rpx;
font-size: 30rpx;
color: #333;
line-height: 1.5;
box-sizing: border-box;
background: transparent;
border: none;
outline: none;
}
.desc-count {
display: block;
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
/* 照片上传区域 */
@@ -557,9 +575,4 @@ const goBack = () => {
font-weight: 600;
background-color: #f8f9fa;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<!-- <view class="status-bar"></view>
<uni-nav-bar title="投诉详情" backgroundColor="transparent" :border="false" left-icon="left" @clickLeft="goBack">
</uni-nav-bar>
</uni-nav-bar> -->
<!-- 表单区域 -->
<scroll-view class="page" scroll-y>
@@ -49,7 +49,7 @@
<view class="photo-section" v-if="photoList.length > 0">
<text class="photo-label">问题照片</text>
<view class="photo-list">
<image v-for="(img, index) in photoList" :key="index" :src="img" mode="aspectFill"
<image v-for="(img, index) in photoList" :key="index" :src="img" mode="aspectFit"
class="photo-item" @click="previewImage(index)"></image>
</view>
</view>
@@ -72,8 +72,8 @@ const photoList = ref([])
// 状态映射
const statusMap = {
'0': { text: '待处理', class: 'pending' },
'1': { text: '处理中', class: 'processing' },
'2': { text: '已完成', class: 'completed' }
// '1': { text: '处理中', class: 'processing' },
'1': { text: '已完成', class: 'completed' }
}
const statusText = computed(() => {
@@ -119,11 +119,9 @@ const previewImage = (index) => {
}
// 返回
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/service-list/complaintsSuggestionsIndex'
})
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss">

View File

@@ -1,17 +1,15 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar title="投诉建议" backgroundColor="transparent" :border="false" left-icon="left" @clickLeft="goBack">
</uni-nav-bar>
<!-- 顶部分段控制器 -->
<view style="width: 60vw;">
<uni-segmented-control :current="currentTab" :values="tabs" @clickItem="onTabClick" style-type="text"
active-color="#007AFF" inactive-color="#999" class="seg-control"></uni-segmented-control>
<view class="tab-wrap">
<view class="tab-item" :class="{ active: currentTab === index }" v-for="(tab, index) in tabs"
:key="index" @click="onTabClick({ currentIndex: index })">
<text class="tab-text">{{ tab }}</text>
</view>
</view>
<!-- 列表区域 -->
<scroll-view class="page" scroll-y>
<view class="page">
<view class="page-container">
<view v-if="dataList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
@@ -23,8 +21,8 @@
<view class="card-header">
<text class="card-title">{{ item.title }}</text>
<text class="card-tag"
:class="item.status === 'pending' ? 'pending' : item.status === 'processing' ? 'processing' : 'completed'">
{{ item.status === 'pending' ? '待处理' : item.status === 'processing' ? '处理中' : '已完成' }}
:class="item.status === 'pending' ? 'pending' : 'completed'">
{{ item.status === 'pending' ? '待处理' : '已完成' }}
</text>
</view>
@@ -33,7 +31,7 @@
<view class="img-scroll-container">
<view v-for="(img, imgIndex) in item.imgList" :key="imgIndex" class="img-item">
<image :src="img" mode="widthFix" class="card-img"></image>
<image :src="img" mode="aspectFit" class="card-img"></image>
</view>
</view>
@@ -44,7 +42,7 @@
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 底部按钮 -->
<view class="bottom-btn-wrap">
@@ -55,11 +53,11 @@
<script setup>
import { ref, onMounted } from 'vue'
import {onLoad} from '@dcloudio/uni-app'
import {onLoad, onShow, onPullDownRefresh} from '@dcloudio/uni-app'
import {getComplainList} from '../api/apiSub.js'
// 分段器
const tabs = ref(["待处理", "处理中", "已完成"])
const tabs = ref(["待处理", "已完成"])
const currentTab = ref(0)
const dataList = ref([])
@@ -71,15 +69,15 @@ const completedList = ref([])
// 状态映射
const statusMap = {
"0":'pending',
'1':'processing',
'2':'completed'
// '1':'processing',
'1':'completed'
}
// 接口数据映射为模板所需格式
const mapItem = (item) => {
// 图片:单 URL 转数组
//列表默认只展示前三张
let imgList = []
if (item.problemImageUrl) {
imgList = [item.problemImageUrl]
imgList = item.problemImageUrl.split(',').filter(url => url.trim()).slice(0, 3)
}
return {
id: item.id,
@@ -113,11 +111,20 @@ const getComplainListData = async () =>{
}catch(err){
// console.log('dess',err)
uni.showToast({title:err.msg || '网络异常',icon:'error'})
}finally{
uni.stopPullDownRefresh();
}
}
onLoad(() =>{
getComplainListData()
})
onShow(() => {
getComplainListData()
});
onPullDownRefresh(() => {
getComplainListData();
});
// 跳转到详情
const turnToDetail = (item) => {
@@ -133,12 +140,6 @@ const addComplaintsSuggestions = () => {
})
}
// 返回
const goBack = () => {
uni.switchTab({
url: '/pages/serviceIndex/serviceIndex'
})
}
// 页面挂载后加载数据
// onMounted(() => {
@@ -154,16 +155,12 @@ const goBack = () => {
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
min-height: 100vh;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
padding: 0px 40rpx 200rpx;
box-sizing: border-box;
}
@@ -187,8 +184,46 @@ const goBack = () => {
margin-bottom: 10rpx;
}
.seg-control {
margin-bottom: 20rpx;
.tab-wrap {
position: sticky;
top: 0;
z-index: 10;
display: flex;
justify-content: flex-start;
align-items: center;
padding: 10rpx 40rpx 20rpx;
}
.tab-item {
position: relative;
padding: 10rpx 0;
margin-right: 80rpx;
}
.tab-item:last-child {
margin-right: 0;
}
.tab-text {
font-size: 30rpx;
color: #999;
}
.tab-item.active .tab-text {
color: #007AFF;
font-weight: 600;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 96rpx;
height: 4rpx;
background: #2F77FD;
border-radius: 32rpx;
}
.card-wrapper {
@@ -202,6 +237,7 @@ const goBack = () => {
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
@@ -243,13 +279,16 @@ const goBack = () => {
color: #666666;
line-height: 40rpx;
margin-bottom: 20rpx;
word-break: break-all;
overflow-wrap: break-word;
white-space: pre-wrap;
}
.img-scroll-container {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin: 10rpx 0px 20rpx 0px;
margin: 10rpx 0px 40rpx 0px;
}
.img-item {
@@ -259,7 +298,7 @@ const goBack = () => {
.card-img {
width: 100%;
height: 52px;
height: 56px;
object-fit: cover;
}
@@ -280,9 +319,15 @@ const goBack = () => {
}
.bottom-btn-wrap {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
background: #f9f9f9;
}
.next-btn {
@@ -296,8 +341,4 @@ const goBack = () => {
border: none;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -1,33 +1,21 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="联系物业" backgroundColor="transparent" :border="false" left-icon="left" @clickLeft="goBack">
</uni-nav-bar>
<view class="contentStyle">
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view v-if="dataList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
暂无数据
</text>
</view>
<view class="property-card" v-for="item,index in dataList" :key="index">
<view class="topBox">
<!-- 左侧头像 -->
<image class="avatar" :src="item.src" mode="aspectFill"></image>
<!-- 右侧信息区 -->
<view class="info-content">
<!-- 物业名称 -->
<view class="property-name">{{item.propertyName}}</view>
<!-- 物业地址 -->
<view class="property-address">{{item.propertyAddress}}</view>
</view>
</view>
<!-- 值班电话 -->
<view class="phone-item" v-for="phoneItems,phoneIndex in item.phoneList" :key="phoneIndex" @click="callPhone(phoneItems.phoneNum)" @longpress="copyPhone(phoneItems.phoneNum)">
<text class="phone-label">值班电话</text>
<text class="phone-num">{{phoneItems.phoneNum}}</text>
@@ -37,19 +25,14 @@
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getManagePhoneData } from '../api/apiSub.js'
const statusBarHeight = ref(20)
const dataList = ref([])
// ====方法=====
function callPhone(phone) {
if (!phone || !phone.trim()) {
uni.showToast({ title: '电话号码为空', icon: 'none' });
@@ -84,11 +67,6 @@ import { getManagePhoneData } from '../api/apiSub.js'
}
});
}
function goBack() {
uni.switchTab({
url: '/pages/serviceIndex/serviceIndex',
});
}
onMounted(() => {
const villageId = uni.getStorageSync('currentVillageId')
@@ -102,7 +80,7 @@ import { getManagePhoneData } from '../api/apiSub.js'
if (res.code === 200 && res.data) {
const d = res.data
dataList.value = [{
src: d.villageImageUrl || 'http://47.104.199.163:9090/images/propertyImgs/community1.png',
src: d.villageImageUrl || 'http://wuye.ckdzkj.com/images/propertyImgs/community1.png',
propertyName: d.villageName || '',
propertyAddress: d.address || '',
phoneList: d.managePhone ? [{ phoneNum: d.managePhone }] : []
@@ -115,8 +93,8 @@ import { getManagePhoneData } from '../api/apiSub.js'
uni.showToast({ title: '获取失败', icon: 'none' })
})
})
</script>
<style lang="scss">
page {
background: #f9f9f9;
@@ -127,10 +105,16 @@ import { getManagePhoneData } from '../api/apiSub.js'
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%); */
height: 100%;
}
.page {
flex: 1;
overflow-y: auto;
padding: 20px 40rpx;
box-sizing: border-box;
}
/* 空状态 */
.empty-state {
display: flex;
@@ -140,25 +124,17 @@ import { getManagePhoneData } from '../api/apiSub.js'
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
/* background-color: #fff; */
}
/* 卡片容器 */
.property-card {
padding: 30rpx;
/* margin: 0px 40rpx 40rpx 40rpx; */
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
@@ -176,6 +152,7 @@ import { getManagePhoneData } from '../api/apiSub.js'
height: 60px;
border-radius: 8rpx;
margin-right: 24rpx;
margin-top: 20rpx;
flex-shrink: 0;
}
@@ -187,7 +164,6 @@ import { getManagePhoneData } from '../api/apiSub.js'
gap: 12rpx;
}
/* 物业名称 */
.property-name {
font-size: 32rpx;
font-weight: 600;
@@ -195,14 +171,13 @@ import { getManagePhoneData } from '../api/apiSub.js'
line-height: 35px;
}
/* 物业地址 */
.property-address {
font-size: 28rpx;
color: #666666;
line-height: 24rpx;
line-height: 35rpx;
}
/* 电话行 */
.phone-item {
display: flex;
align-items: center;
@@ -210,27 +185,19 @@ import { getManagePhoneData } from '../api/apiSub.js'
line-height: 60rpx;
}
/* 电话标签 */
.phone-label {
font-size: 28rpx;
color: #999999;
}
/* 电话号码 */
.phone-num {
font-size: 28rpx;
color: #666666;
}
/* 电话图标 */
.phone-icon {
font-size: 24rpx;
color: #409eff !important;
/* 蓝色,和原图一致 */
margin-left: 8rpx;
}
.status-bar{
width: 100vw;
height: 46px;
}
</style>
</style>

View File

@@ -1,12 +1,11 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar title="问卷调查" backgroundColor="transparent" :border="false" left-icon="left" @clickLeft="goBack">
</uni-nav-bar>
<scroll-view class="page" scroll-y="true">
<view class="survey-title">{{ detailItem.title || '' }}</view>
<view class="survey-desc">{{ detailItem.description || '' }}</view>
<view class="survey-desc" v-if="questionList.length > 0">
为了给您提供更好的服务希望您能抽出几分钟时间将最近的感受和建议告诉我们
我们十分重视每位用户的宝贵意见期待您的参与
</view>
<view v-if="questionList.length === 0" class="empty-tip">
<text>暂无问卷内容</text>
@@ -60,7 +59,7 @@ const questionList = computed(() => {
})
const answerMap = computed(() => {
const answerContent = detailItem.value?.answerContent
const answerContent = detailItem.value?.myAnswer
if (!answerContent) return {}
try {
const list = JSON.parse(answerContent)
@@ -77,8 +76,8 @@ const answerMap = computed(() => {
const getDetailData = async () => {
try {
const params = { questionnairedId: currentId.value }
const res = await getQuestionnaireDetail(params)
const params = { villageId: uni.getStorageSync('currentVillageId') }
const res = await getQuestionnaireDetail(currentId.value, params)
if (res.code === 200) {
const data = Array.isArray(res.data) ? res.data[0] : res.data
detailItem.value = data || {}
@@ -127,12 +126,13 @@ page {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
padding: 20px 40rpx;
box-sizing: border-box;
margin-bottom: 48rpx;
}
@@ -151,6 +151,8 @@ page {
line-height: 1.6;
margin-bottom: 20rpx;
text-align: justify;
word-break: break-all;
word-wrap: break-word;
}
.empty-tip {
@@ -182,6 +184,8 @@ page {
color: #333;
line-height: 1.5;
padding-left: 30rpx;
word-break: break-all;
word-wrap: break-word;
}
.option-list {

View File

@@ -1,8 +1,8 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar title="问卷调查" backgroundColor="transparent" :border="false" left-icon="left" @clickLeft="goBack">
</uni-nav-bar>
<!-- <view class="status-bar"></view>
<uni-nav-bar title="问卷详情" backgroundColor="transparent" :border="false" left-icon="left" @clickLeft="goBack">
</uni-nav-bar> -->
<!-- 表单区域 -->
<scroll-view class="page" scroll-y>
@@ -11,7 +11,11 @@
<view class="survey-title">{{ detailItem.title || '' }}</view>
<!-- 问卷说明 -->
<view class="survey-desc">{{ detailItem.description || '' }}</view>
<!-- <view class="survey-desc">{{ detailItem.description || '' }}</view> -->
<view class="survey-desc" v-if="questionList.length >0">
为了给您提供更好的服务希望您能抽出几分钟时间将最近的感受和建议告诉我们
我们十分重视每位用户的宝贵意见期待您的参与
</view>
<!-- 空状态 -->
<view v-if="questionList.length === 0" class="empty-tip">
@@ -32,7 +36,7 @@
:key="oIdx"
class="radio-item"
:class="{ checked: answers[q.cid] === opt.label }"
@click="selectRadio(q.cid, opt.label)"
@click="!isEndedAndUnjoined && selectRadio(q.cid, opt.label)"
>
<view class="radio-circle" :class="{ active: answers[q.cid] === opt.label }">
<text v-if="answers[q.cid] === opt.label" class="check-icon"></text>
@@ -48,7 +52,7 @@
:key="oIdx"
class="checkbox-item"
:class="{ checked: isChecked(q.cid, opt.label) }"
@click="toggleCheckbox(q.cid, opt.label)"
@click="!isEndedAndUnjoined && toggleCheckbox(q.cid, opt.label)"
>
<view class="checkbox-box" :class="{ active: isChecked(q.cid, opt.label) }">
<text v-if="isChecked(q.cid, opt.label)" class="check-icon"></text>
@@ -61,8 +65,10 @@
<view v-if="q.type === 'input'">
<input
class="text-input"
:class="{ 'input-disabled': isEndedAndUnjoined }"
v-model="answers[q.cid]"
placeholder="请输入"
:disabled="isEndedAndUnjoined"
/>
</view>
@@ -70,9 +76,11 @@
<view v-if="q.type === 'textarea'">
<textarea
class="textarea-input"
:class="{ 'input-disabled': isEndedAndUnjoined }"
v-model="answers[q.cid]"
placeholder="请输入"
maxlength="500"
:disabled="isEndedAndUnjoined"
/>
</view>
</view>
@@ -81,17 +89,23 @@
<!-- 固定底部提交按钮 -->
<view class="submit-btn-wrap">
<button class="submit-btn" @click="handleSubmit">提交</button>
<button
v-if="isEndedAndUnjoined"
class="submit-btn disabled-btn"
disabled
>已结束</button>
<button v-else class="submit-btn" @click="handleSubmit">提交</button>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { getQuestionnaireDetail, submitQuestionnair } from '../api/apiSub.js'
const detailItem = ref({})
const questionnaireId = ref('') // 保存问卷ID用于 onShow 重新获取
const questionList = computed(() => {
const content = detailItem.value?.content
@@ -104,18 +118,34 @@ const questionList = computed(() => {
}
})
// 是否已结束且未参与
const isEndedAndUnjoined = computed(() => {
const endTime = detailItem.value?.endTime
const participated = detailItem.value?.participated
// participated 不存在null/undefined说明接口没返回参与状态按未参与处理
const isNotParticipated = !participated
// endTime 存在且已过期
const isEnded = endTime && new Date(endTime.replace(/-/g, '/')).getTime() < Date.now()
return isEnded && isNotParticipated
})
const answers = ref({})
let hasLoaded = false
// 获取问卷调查
const getDetailData = async (id) => {
try {
const res = await getQuestionnaireDetail({ id })
let params = {
villageId:uni.getStorageSync('currentVillageId')
}
const res = await getQuestionnaireDetail(id,params)
// console.log('接口返回:', JSON.stringify(res))
if (res.code === 200) {
const data = Array.isArray(res.data) ? res.data[0] : res.data
detailItem.value = data || {}
} else {
uni.showToast({ title: res.msg || '获取失败', icon: 'none' })
}
}
} catch (err) {
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
}
@@ -123,14 +153,31 @@ const getDetailData = async (id) => {
onLoad((option) => {
if (option.id) {
questionnaireId.value = option.id
getDetailData(option.id)
} else {
uni.showToast({ title: '缺少问卷ID', icon: 'none' })
}
})
// 切换小区后返回页面时,重新获取数据
onShow(() => {
if (!hasLoaded) {
hasLoaded = true
return
}
if (questionnaireId.value) {
getDetailData(questionnaireId.value)
}
})
const selectRadio = (cid, label) => {
answers.value[cid] = label
// 再次点击已选中的选项时取消选择
if (answers.value[cid] === label) {
delete answers.value[cid]
} else {
answers.value[cid] = label
}
}
const isChecked = (cid, label) => {
@@ -158,9 +205,9 @@ const handleSubmit = async () => {
const ans = answers.value[q.cid]
if (ans === undefined || ans === null || ans === '' || (Array.isArray(ans) && ans.length === 0)) {
uni.showToast({ title: `请填写第${questionList.value.indexOf(q) + 1}`, icon: 'none' })
return
}
}
return
}
}
}
const answerList = []
@@ -168,19 +215,22 @@ const handleSubmit = async () => {
const val = answers.value[q.cid]
if (val !== undefined && val !== null && val !== '' && !(Array.isArray(val) && val.length === 0)) {
answerList.push({ cid: q.cid, value: val })
}
}
uni.showLoading({ title: '提交中...', mask: true })
}
uni.showLoading({ title: "提交中...", mask: true })
try {
const res = await submitQuestionnair(detailItem.value.questionnaireId, answerList)
let params = {
answerList,
villageId: uni.getStorageSync("currentVillageId")
}
const res = await submitQuestionnair(detailItem.value.id,params)
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => { goBack() }, 1500)
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
}
} catch (err) {
console.log('errrr',err)
uni.hideLoading()
@@ -210,7 +260,7 @@ page {
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
padding: 30rpx 40rpx 200rpx;
box-sizing: border-box;
}
@@ -224,6 +274,7 @@ page {
color: #333;
text-align: center;
margin: 0px 0 20rpx 0;
white-space: pre-line;
}
.survey-desc {
@@ -232,6 +283,10 @@ page {
line-height: 1.6;
margin-bottom: 20rpx;
text-align: justify;
white-space: pre-line;
word-break: break-all;
word-wrap: break-word;
overflow-wrap: break-word;
}
.empty-tip {
@@ -255,7 +310,7 @@ page {
color: #ff3b30;
font-size: 36rpx;
margin-right: 8rpx;
}
}
}
}
@@ -287,7 +342,7 @@ page {
&.active {
background-color: #007aff;
border-color: #007aff;
}
}
}
.check-icon {
@@ -330,7 +385,7 @@ page {
&.active {
background-color: #007aff;
border-color: #007aff;
}
}
}
.check-icon {
@@ -372,9 +427,22 @@ page {
margin-left: 30rpx;
}
.input-disabled {
background-color: #f5f5f5;
color: #999;
}
/* 提交按钮 */
.submit-btn-wrap {
padding: 20rpx 30rpx 40rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #ffffff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
.submit-btn {
width: 100%;
@@ -391,7 +459,12 @@ page {
&::after {
border: none;
}
}
&.disabled-btn {
background-color: #ccc;
color: #999;
}
}
}
@@ -399,4 +472,4 @@ page {
width: 100vw;
height: 46px;
}
</style>
</style>

View File

@@ -1,15 +1,12 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar title="问卷调查" backgroundColor="transparent" :border="false" left-icon="left" @click-left="goBack" />
<view style="width: 40vw;">
<uni-segmented-control :current="currentTab" :values="tabs" @click-item="switchTab" style-type="text"
active-color="#007AFF" inactive-color="#999" class="seg-control" />
</view>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y>
<view class="page">
<view class="list-container">
<view v-if="currentList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
@@ -46,16 +43,16 @@
</view>
</view>
<view style="width: 100%;height:2rpx;"></view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed } from 'vue'
import { onShow,onPullDownRefresh } from '@dcloudio/uni-app'
import {getQuestionnaireList} from '../api/apiSub.js'
// 状态栏高度
const statusBarHeight = ref(20)
// 当前选中tab
const currentTab = ref(0)
// tab标题
@@ -105,7 +102,9 @@
}
}catch(err){
uni.showToast({ title: err.msg ||'网络异常', icon: 'none' })
}
}finally{
uni.stopPullDownRefresh();
}
}
// 列表项点击index 控制高亮)
@@ -134,17 +133,13 @@
})
}
// 返回
const goBack = () => {
uni.switchTab({
url: '/pages/serviceIndex/serviceIndex'
})
}
// 生命周期:页面挂载完成
onMounted(() => {
onShow(() => {
getDataList()
})
// 下拉刷新
onPullDownRefresh(() => {
getDataList();
});
</script>
<style lang="scss">
@@ -155,9 +150,10 @@
<style lang="scss" scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
// display: flex;
// flex-direction: column;
min-height: 100%;
// height: 100%;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
@@ -241,10 +237,6 @@
}
}
.status-bar {
width: 100vw;
height: 46px;
}
.empty-state {
display: flex;

View File

@@ -1,5 +1,5 @@
<template>
<view class="visitor-invite">
<view class="visitor-invite" :class="{ 'no-scroll': pageFixed }">
<!-- 标签切换车辆来访/人员来访 -->
<view class="tab-wrap">
<view class="tab-item" :class="{ active: activeTab === 'car' }" @click="switchTab('car')">
@@ -31,8 +31,10 @@
<view class="form-section">
<view class="form-item">
<view class="label">访客姓名</view>
<input class="input" placeholder="请输入访客姓名" v-model="form.name" />
<view class="label">
<text class="required">*</text>访客姓名
</view>
<input class="input" placeholder="请输入访客姓名" v-model="form.name" maxlength="20" @input="onNameInput" />
</view>
<view class="form-item">
<view class="label">
@@ -47,7 +49,7 @@
<view class="label">
<text class="required">*</text>访客联系方式
</view>
<input class="input" placeholder="请输入联系方式" v-model="form.phone" type="number" />
<input class="input" placeholder="请输入联系方式" v-model="form.phone" type="number" maxlength="11" @input="onPhoneInput" />
</view>
<!-- 车牌号仅车辆来访显示 -->
@@ -65,10 +67,8 @@
</view>
<view class="form-item">
<view class="label">
<text class="required">*</text>随行人数
</view>
<input class="input" placeholder="请输入随行人数" v-model="form.peopleNum" type="number" />
<view class="label">随行人数</view>
<input class="input" placeholder="请输入随行人数" v-model="form.peopleNum" type="number" maxlength="2" @input="onPeopleNumInput" />
</view>
</view>
@@ -76,22 +76,22 @@
<view class="section-title">有效期</view>
<view class="form-section">
<view class="form-item">
<view class="label">
<view class="form-item" @click="onPickerOpen">
<view class="label">
<text class="required">*</text>来访时间
</view>
<view class="value">
<uni-datetime-picker type="datetime" v-model="form.visitTime" @change="handleTimeChange"
:border="false" />
<uni-datetime-picker type="datetime" v-model="form.visitTime" @change="handleTimeChange" @maskClick="pickerVisible = false"
:border="false" :hide-second="true"/>
</view>
</view>
<view class="form-item">
<view class="label">
<view class="form-item" @click="onPickerOpen">
<view class="label">
<text class="required">*</text>离开时间
</view>
<view class="value">
<uni-datetime-picker type="datetime" v-model="form.leaveTime" @change="handleTimeChange"
:border="false" />
<uni-datetime-picker type="datetime" v-model="form.leaveTime" @change="handleTimeChange" @maskClick="pickerVisible = false"
:border="false" :hide-second="true"/>
</view>
</view>
<view class="tips">预计来访时间和预计离开时间间隔不能超过24小时</view>
@@ -107,7 +107,7 @@
</view>
<!-- 地址选择弹窗 -->
<uni-popup ref="addressModalRef" type="bottom" :mask-click="false">
<uni-popup ref="addressModalRef" type="bottom" :mask-click="true" @change="onAddressModalChange">
<view class="modal-wrap">
<view class="modal-title">选择来访地址</view>
<view class="address-list">
@@ -121,13 +121,13 @@
</uni-popup>
<!-- 性别选择弹窗 -->
<uni-popup ref="genderModalRef" type="bottom" :mask-click="false">
<uni-popup ref="genderModalRef" type="bottom" :mask-click="true" @change="onGenderModalChange">
<view class="modal-wrap">
<view class="gender-list">
<view class="gender-item" :class="{ active: item === form.gender }" v-for="item in genderList"
:key="item" @click="selectGender(item)">
{{ item }}
</view>
</view>
</view>
</view>
</uni-popup>
@@ -140,7 +140,8 @@
reactive
} from 'vue';
import {
onLoad
onLoad,
onBackPress
} from "@dcloudio/uni-app"
import moment from 'moment';
import{getHouseList,addVisitorData} from '../api/api.js'
@@ -151,7 +152,7 @@
// 表单数据
const form = reactive({
address: '',
villageId: '',
villageId: '',
houseId: '',
name: '',
gender: '',
@@ -172,8 +173,22 @@
const addressModalRef = ref(null);
const genderModalRef = ref(null);
// 页面固定标志
const pageFixed = ref(false);
// 车牌号输入框聚焦索引
const carFocusIndex = ref(0);
const carFocusIndex = ref(-1);
// 日期选择器是否打开
const pickerVisible = ref(false);
// 拦截返回事件:若日期选择器打开则关闭选择器而非返回上一页
onBackPress(() => {
if (pickerVisible.value) {
pickerVisible.value = false;
return true;
}
});
// =========方法===========
// 获取房屋列表
@@ -181,7 +196,7 @@
try {
const res = await getHouseList({ pageNum: 1, pageSize: 10 })
if (res.code === 200) {
addressList.value = (res.rows || []).map(item => ({
addressList.value = (res.data || []).map(item => ({
...item,
label: `${item.buildingName || ''}${item.unitNo}-${item.floorNo || ''}-${item.houseNo || ''}`.trim()
}))
@@ -191,7 +206,7 @@
title:e.msg || '获取房屋列表失败',
icon:'none'
})
// console.error('获取房屋列表失败', e)
// console.error('获取房屋列表失败', e)
}
}
@@ -215,11 +230,16 @@
addressModalRef.value.close();
};
// 地址弹窗状态变化
const onAddressModalChange = (e) => {
pageFixed.value = e.show;
};
// 选择地址
const selectAddress = (item) => {
form.address = item.label;
form.villageId = item.villageId || '';
form.houseId = item.id || '';
form.houseId = item.houseId || '';
closeAddressModal();
};
@@ -234,6 +254,11 @@
genderModalRef.value.close();
};
// 性别弹窗状态变化
const onGenderModalChange = (e) => {
pageFixed.value = e.show;
};
// 车牌号单格输入
const onCarInput = (index, e) => {
const val = (e.detail && e.detail.value !== undefined) ? e.detail.value : (e.target ? e.target.value : '');
@@ -246,36 +271,91 @@
}
};
// 时间校验
const handleTimeChange = () => {
if (!form.visitTime || !form.leaveTime) return true;
const visitMoment = moment(form.visitTime);
const leaveMoment = moment(form.leaveTime);
// 离开时间不能早于来访时间
if (leaveMoment.isBefore(visitMoment)) {
uni.showToast({
title: '离开时间不能早于来访时间',
icon: 'none'
});
form.leaveTime = visitMoment.clone().add(1, 'hours').format('YYYY-MM-DD HH:mm');
return false;
// 姓名输入过滤特殊字符
const onNameInput = (e) => {
const val = e.detail.value;
// 只允许中文、英文字母、数字和中间点(·)
const filtered = val.replace(/[^一-龥a-zA-Z0-9·]/g, '')
if (filtered !== val) {
form.name = filtered;
}
// 不能超过24小时
const diffHours = leaveMoment.diff(visitMoment, 'hours', true);
if (diffHours > 24) {
uni.showToast({
title: '时间间隔不能超过24小时',
icon: 'none'
});
form.leaveTime = visitMoment.clone().add(24, 'hours').format('YYYY-MM-DD HH:mm');
return false;
}
return true;
};
// 手机号输入过滤非数字字符
const onPhoneInput = (e) => {
const val = e.detail.value;
const filtered = val.replace(/\D/g, '');
if (filtered !== val) {
form.phone = filtered;
}
};
// 随行人数输入过滤小数
const onPeopleNumInput = (e) => {
const val = e.detail.value;
// 只保留整数(去除小数点及之后的内容)
const filtered = val.replace(/[^\d]/g, '');
if (filtered !== val) {
form.peopleNum = filtered;
}
};
// 打开选择器时标记状态,供 onBackPress 拦截用
const onPickerOpen = () => {
pickerVisible.value = true;
};
// 时间校验
const handleTimeChange = () => {
pickerVisible.value = false;
if (!form.visitTime || !form.leaveTime) return true;
const visitMoment = moment(form.visitTime);
const leaveMoment = moment(form.leaveTime);
const now = moment();
// 来访时间不能是过去时间
if (visitMoment.isBefore(now)) {
uni.showToast({
title: '来访时间不能早于当前时间',
icon: 'none'
});
return false;
}
// 离开时间不能早于来访时间
if (leaveMoment.isBefore(visitMoment)) {
uni.showToast({
title: '离开时间不能早于来访时间',
icon: 'none'
});
return false;
}
// 离开时间与来访时间间隔不能少于30分钟
const diffMinutes = leaveMoment.diff(visitMoment, 'minutes');
if (diffMinutes < 30) {
uni.showToast({
title: '离开时间与来访时间间隔不能少于30分钟',
icon: 'none'
});
return false;
}
// 不能超过24小时
const diffHours = leaveMoment.diff(visitMoment, 'hours', true);
if (diffHours > 24) {
uni.showToast({
title: '时间间隔不能超过24小时',
icon: 'none'
});
return false;
}
return true;
};
// 随行人数输入过滤小数
// 提交表单
const submitForm = async () => {
// 1. 必填项校验
@@ -321,13 +401,13 @@
});
return;
}
if (!form.peopleNum || form.peopleNum <= 0) {
uni.showToast({
title: '请输入正确的随行人数',
icon: 'none'
});
return;
}
// if (form.peopleNum === '' || form.peopleNum === null || form.peopleNum === undefined) {
// uni.showToast({
// title: '请输入随行人数',
// icon: 'none'
// });
// return;
// }
if (!form.visitTime) {
uni.showToast({
title: '请选择来访时间',
@@ -402,8 +482,20 @@
<style scoped>
.visitor-invite {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
box-sizing: border-box;
background-color: #f5f5f5;
min-height: 100vh;
}
.no-scroll {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* 导航栏 */
@@ -423,6 +515,7 @@
/* 标签切换 */
.tab-wrap {
display: flex;
flex-shrink: 0;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
@@ -443,7 +536,10 @@
/* 表单区域 */
.form-wrap {
padding: 10rpx;
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 10rpx 10rpx 180rpx;
}
.form-section {
@@ -529,7 +625,14 @@
/* 底部按钮 */
.btn-wrap {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
padding: 20rpx 40rpx 50rpx;
padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
background-color: #f5f5f5;
}
.submit-btn {
@@ -567,6 +670,8 @@
.gender-list {
display: flex;
flex-direction: column;
max-height: 60vh;
overflow-y: auto;
}
.address-item,
@@ -589,4 +694,12 @@
background-color: #007aff;
color: #fff;
}
</style>
<style>
/* 页面容器撑满并禁止整体滚动,让表单区域单独滚动 */
page {
height: 100%;
overflow: hidden;
}
</style>

View File

@@ -4,8 +4,8 @@
<view class="info-card">
<view class="info-header">
<text class="title">来访者{{ visitorInfo.name }}</text>
<text class="status {{ visitorInfo.status === 'visited' ? 'visited' : 'waiting' }}">
{{ visitorInfo.status === 'visited' ? '已到访' : '等待来访' }}
<text class="status" :class="{ active: visitorInfo.status === '等待来访' }">
{{ visitorInfo.status}}
</text>
</view>
@@ -20,7 +20,7 @@
<text class="label">联系方式</text>
<text class="value">{{ visitorInfo.phone }}</text>
</view>
<view class="info-item">
<view class="info-item" v-if="visitorInfo.type !== '1'">
<text class="label">车牌号</text>
<text class="value">{{ visitorInfo.carNumber }}</text>
</view>
@@ -30,7 +30,7 @@
</view>
<view class="info-item">
<text class="label">有效期</text>
<text class="value">{{ visitorInfo.validTime }}</text>
<text class="value-valid">{{ visitorInfo.validTime }}</text>
</view>
<view class="info-item">
<text class="label">邀请时间</text>
@@ -46,7 +46,7 @@
<!-- 提示文字 -->
<view class="tips-text">
<view>预计来访时间盒预计离开时间间隔不能超过24小时</view>
<view>预计来访时间离开时间间隔不能超过24小时</view>
车辆停留请勿超出有效期需及时驶出小区
</view>
@@ -65,7 +65,8 @@ import {getVisitorDetail} from '../api/api.js'
// 访客信息(实际项目中从接口获取)
const visitorInfo = ref({
name: '',
status: 'waiting', // waiting:等待来访visited:已到访
status: '', // waiting:等待来访visited:已到访
type: '',
address: '',
phone: '',
carNumber: '',
@@ -83,10 +84,11 @@ const mapDetail = (item) => {
item.unitNo ? item.unitNo + '单元' : '',
item.houseNo
].filter(Boolean)
const status = item.status === 1 || item.status === '1' ? 'visited' : 'waiting'
const status = item.status === '1' ? '已到访' : '等待来访'
return {
name: item.visitorName || '',
status,
type: item.type || '',
address: addressParts.join(''),
phone: item.visitorPhone || '',
carNumber: item.carNum || '',
@@ -151,6 +153,10 @@ const goToQrcode = () => {
padding: 6rpx 16rpx;
border-radius: 20rpx;
}
.status.active{
color: #2f77fd;
font-weight: 500;
}
.waiting {
color: #007aff;
@@ -179,6 +185,13 @@ const goToQrcode = () => {
.value {
color: #222;
flex: 1;
text-align: right;
}
.value-valid{
color: #222;
flex: 1;
text-align: right;
white-space: nowrap;
}
.tips-text {

View File

@@ -1,20 +1,5 @@
<template>
<view class="visitor-page">
<!-- 状态栏占位 -->
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-left" @tap="goBack">
<image
src="https://wuyeadmin.bugtc.com/images/home/arr-left.png"
class="back-icon"
/>
</view>
<view class="nav-title">访客邀请</view>
<view class="nav-right"></view>
</view>
<!-- 标签切换栏 -->
<view class="tab-bar">
<view
@@ -33,8 +18,8 @@
</view>
</view>
<!-- 访客列表 - 固定高度内部滚动 -->
<scroll-view class="list-container" scroll-y :style="{ height: scrollHeight + 'px' }">
<!-- 访客列表 -->
<view class="list-container">
<view
class="visitor-item"
v-for="(item, index) in filterdList"
@@ -66,8 +51,8 @@
<text class="value" :class="item.status">{{ item.phone }}</text>
</view>
<!-- 车牌号 -->
<view class="item-row">
<!-- 车牌号仅车辆来访显示 -->
<view class="item-row" v-if="item.type !== '1'">
<text class="label" :class="item.status">车牌号</text>
<text class="value" :class="item.status">{{ item.carNumber }}</text>
</view>
@@ -92,9 +77,9 @@
</text>
</view>
<!-- 底部占位防止最后一项贴底 -->
<!-- 底部占位 -->
<view class="list-bottom-space"></view>
</scroll-view>
</view>
<!-- 新增邀请按钮 -->
<view class="add-btn-wrapper">
@@ -107,12 +92,10 @@
<script setup>
import { ref, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { onShow, onPullDownRefresh } from "@dcloudio/uni-app";
import { getVisitorList } from "../api/api.js";
const activeTab = ref("waiting");
const statusBarHeight = ref(0);
const scrollHeight = ref(0);
const visitorList = ref([]);
const mapData = (list) => {
@@ -132,30 +115,25 @@ const mapData = (list) => {
};
const fetchVisitorList = async () => {
const userId = uni.getStorageSync("userId");
try {
const res = await getVisitorList({ userId });
const params = {
userId: uni.getStorageSync("userId"),
villageId: uni.getStorageSync('currentVillageId')
}
const res = await getVisitorList(params);
visitorList.value = mapData(res.data || []);
} catch (err) {
uni.showToast({ title: err.msg, icon: "none" });
} finally {
uni.stopPullDownRefresh();
}
};
onLoad(() => {
const info = uni.getSystemInfoSync();
statusBarHeight.value = info.statusBarHeight || 0;
// rpx 转 px 比例
const scale = info.windowWidth / 750;
// 各固定区域高度rpx 转 px
const navPx = 90 * scale; // 导航栏
const tabPx = 88 * scale; // tab栏
const btnPx = 148 * scale; // 底部按钮区域
// 列表区域可用高度 = 窗口高度 - 状态栏 - 导航栏 - tab栏 - 底部按钮区
scrollHeight.value = info.windowHeight - statusBarHeight.value - navPx - tabPx - btnPx;
onShow(() => {
fetchVisitorList();
});
onPullDownRefresh(() => {
fetchVisitorList();
});
@@ -163,16 +141,6 @@ const filterdList = computed(() => {
return visitorList.value.filter((item) => item.status === activeTab.value);
});
const goBack = () => {
const source = uni.getStorageSync("sourcePage");
if (source === "index") {
uni.switchTab({ url: "/pages/index/index" });
} else if (source === "serviceIndex") {
uni.switchTab({ url: "/pages/serviceIndex/serviceIndex" });
}
uni.removeStorageSync("sourcePage");
};
const viewDetail = (item) => {
uni.navigateTo({
url: `/pageSubPack/visitor/visitor-detail?id=${item.id}`,
@@ -187,49 +155,18 @@ const addInvitation = () => {
<style scoped>
/* 页面根容器 */
.visitor-page {
min-height: 100vh;
box-sizing: border-box;
background: linear-gradient(135deg, #e2f0ff 0%, #f9f9f9 50%);
padding: 0 30rpx;
}
.status-bar {
width: 100%;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
height: 90rpx;
}
.nav-left {
width: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 36rpx;
height: 36rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 500;
color: #000;
}
.nav-right {
width: 60rpx;
}
/* 标签切换栏 */
.tab-bar {
position: sticky;
top: 0;
z-index: 10;
display: flex;
/* border-bottom: 1rpx solid rgba(0, 0, 0, 0.04); */
}
.tab-item {
@@ -254,10 +191,13 @@ const addInvitation = () => {
width: 126rpx;
height: 8rpx;
border-radius: 32rpx;
/* background-color: #2f77fd; */
background: linear-gradient(to bottom, #2F77FD, #CEDFFF);
}
/* 列表区域 */
.list-container {
}
/* 访客卡片 */
.visitor-item {
margin-top: 24rpx;
@@ -360,16 +300,21 @@ const addInvitation = () => {
color: #999;
}
/* 列表底部占位 */
/* 列表底部占位(为固定的按钮预留高度,保证最后一项能滚到按钮上方) */
.list-bottom-space {
height: 24rpx;
height: 180rpx;
}
/* 新增邀请按钮 */
.add-btn-wrapper {
height: 148rpx;
display: flex;
align-items: center;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #f9f9f9;
}
.add-btn {
@@ -389,3 +334,4 @@ const addInvitation = () => {
font-size: 30rpx;
}
</style>

View File

@@ -1,5 +1,33 @@
{
"pages": [{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom",
"enablePullDownRefresh": true
// "app-plus": {
// "pullToRefresh": {
// "support": true,
// "style": "circle",
// "offset": "70px"
// }
// }
}
},
{
"path": "pages/serviceIndex/serviceIndex",
"style": {
"navigationBarTitleText": "服务"
}
},
{
"path": "pages/myIndex/myIndex",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/navigation/navigation",
"style": {
"navigationBarTitleText": "引导页",
@@ -7,30 +35,7 @@
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#F8F8F8"
}
},
{
"path": "pages/serviceIndex/serviceIndex",
"style": {
"navigationBarTitleText": "服务",
"navigationStyle": "custom"
}
},
{
"path": "pages/myIndex/myIndex",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
}
],
"subPackages": [{
"root": "pageSubPack/loginSub",
@@ -38,42 +43,41 @@
"path": "register-new-user",
"style": {
"navigationBarTitleText": "注册新用户",
"navigationStyle": "custom"
}
},
{
"path": "pwd-login",
"style": {
"navigationBarTitleText": "账号密码登录",
"navigationStyle": "custom"
"disableScroll": true
}
},
{
"path": "login",
"style": {
"navigationBarTitleText": "验证码登录",
"navigationStyle": "custom"
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "forget-pwd",
"style": {
"navigationBarTitleText": "忘记密码",
"navigationStyle": "custom"
"disableScroll": true
}
},
{
"path": "change-pwd",
"style": {
"navigationBarTitleText": "修改密码",
"navigationStyle": "custom"
"disableScroll": true
}
},
{
"path": "privacy",
"style": {
"navigationBarTitleText": "用户隐私政策"
// "navigationStyle": "custom"
"navigationBarTitleText": "隐私政策"
}
},
{
"path": "user-agreement",
"style": {
"navigationBarTitleText": "用户协议"
}
}
]
@@ -84,7 +88,7 @@
"path": "onlineRepair",
"style": {
"navigationBarTitleText": "在线报修",
"navigationStyle": "custom"
"enablePullDownRefresh": true
}
},
{
@@ -131,7 +135,7 @@
"path": "visitor-list",
"style": {
"navigationBarTitleText": "访客邀请",
"navigationStyle": "custom"
"enablePullDownRefresh": true
}
},
{
@@ -160,7 +164,7 @@
"path": "activityList",
"style": {
"navigationBarTitleText": "活动列表",
"navigationStyle": "custom"
"enablePullDownRefresh": true
}
},
{
@@ -185,7 +189,8 @@
"path": "noticeList",
"style": {
"navigationBarTitleText": "通知公告",
"navigationStyle": "custom"
"navigationBarBackgroundColor": "#F9F9F9",
"enablePullDownRefresh": true
}
},
{
@@ -197,14 +202,14 @@
{
"path": "oneClickOpen",
"style": {
"navigationBarTitleText": "一键开门",
"navigationStyle": "custom"
"navigationBarTitleText": "一键开门"
}
},
{
"path": "selectHome",
"style": {
"navigationBarTitleText": "选择小区"
"navigationBarTitleText": "选择小区",
"enablePullDownRefresh": true
}
}
]
@@ -212,38 +217,66 @@
{
"root": "pageSubPack/my",
"pages": [{
"path": "settingIndex",
"path": "settingIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "设置"
}
},
{
"path": "changePhone",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "更改手机号码"
}
},
{
"path": "changePwd",
"style": {
"navigationBarTitleText": "",
"navigationBarTitleText": "修改密码"
}
},
{
"path": "deleteAccount",
"style": {
"navigationBarTitleText": "注销账号"
}
},
{
"path": "myPointsIndex",
"style": {
"navigationBarTitleText": "我的积分",
"navigationStyle": "custom"
}
},
{
"path": "myPointsMall",
"style": {
"navigationBarTitleText": "积分商城"
}
},
{
"path": "myPointsRecord",
"style": {
"navigationBarTitleText": "积分明细"
}
},
{
"path": "myPointsOrder",
"style": {
"navigationBarTitleText": "积分订单"
}
},
{
"path": "myHouseIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "我的房屋",
"enablePullDownRefresh": true
// "navigationStyle": "custom"
}
},
{
"path": "houseDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "房屋详情"
}
}, {
"path": "addHouse",
@@ -266,15 +299,14 @@
}, {
"path": "myParkingSpaceIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "我的车位",
"enablePullDownRefresh": true
}
},
{
"path": "parkingSpaceDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "车位详情"
}
},
@@ -313,8 +345,8 @@
{
"path": "myCarIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "我的车辆",
"enablePullDownRefresh": true
}
@@ -331,12 +363,19 @@
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "carDetail",
},
{
"path": "selectOwner",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "carDetail",
"style": {
"navigationBarTitleText": "车辆详情"
}
}
]
}, {
@@ -369,8 +408,8 @@
{
"path": "paymentRecord",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "缴费记录"
// "navigationStyle": "custom"
}
},
{
@@ -383,8 +422,15 @@
{
"path": "paymentIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "生活缴费",
"navigationBarBackgroundColor": "#dfedfd",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,
"app-plus": {
"titleNView": {
"autoBackButton": true
}
}
}
}, {
"path": "addHydropower",
@@ -398,6 +444,19 @@
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "propertyFeePayment",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "billPayment",
"style": {
"navigationBarTitleText": "缴费账单"
}
}
]
@@ -407,46 +466,71 @@
"pages": [{
"path": "complaintsSuggestionsIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "投诉建议",
"enablePullDownRefresh": true
}
}, {
"path": "complaintsDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "投诉详情"
}
}, {
"path": "addComplaints",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "投诉建议"
}
}, {
"path": "questionnaireSurveyIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "问卷调查",
"enablePullDownRefresh": true
}
},
{
"path": "questionnaireFillout",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "问卷调查"
}
},
{
"path": "questionnaireDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "问卷调查"
}
}, {
"path": "contactProperty",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "联系物业"
}
}
]
},
{
"root": "pageSubPack/inspection-revenue",
"pages": [{
"path": "inspection",
"style": {
"navigationBarTitleText": "巡检记录",
"enablePullDownRefresh": true
}
},
{
"path": "inspection-details",
"style": {
"navigationBarTitleText": "巡检详情"
}
},
{
"path": "public-revenue",
"style": {
"navigationBarTitleText": "公区收益",
"enablePullDownRefresh": true
}
},
{
"path": "public-revenue-details",
"style": {
"navigationBarTitleText": "收益详情"
}
}
]
@@ -493,4 +577,4 @@
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,9 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="我的" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<view class="contentStyle">
<scroll-view class="page" scroll-y>
<view class="topBox">
<view class="name">
<image :src="avatar" class="img" />
<image src="https://wuye.ckdzkj.com/images/logo-app.png" class="img" />
<span style="padding: 5rpx 5rpx;display: flex;flex-direction: column;line-height: 60rpx;">
<span style="font-size: 36rpx;font-weight: 600;">
{{loginName}}
@@ -28,7 +25,7 @@
style="align-self: flex-end;font-size: 24rpx;vertical-align: baseline;">{{ item.desc }}</span></span>
</view>
<view class="item-arrow"></view>
<!-- <image src="https://wuyeadmin.bugtc.com/images/propertyImgs/arrow-right.png" class="arrow-icon" /> -->
<!-- <image src="https://wuye.ckdzkj.com/images/propertyImgs/arrow-right.png" class="arrow-icon" /> -->
</view>
</view>
@@ -37,6 +34,9 @@
</view>
</scroll-view>
<view class="icp-footer">
<text class="icp-text">ICP备案号鲁ICP备2024131497号-4A</text>
</view>
</view>
@@ -48,27 +48,29 @@
} from 'vue';
import { getUserProfile } from '../../pageSubPack/api/apiSub';
import {onShow} from '@dcloudio/uni-app'
import { requireLogin } from '@/common/checkLogin.js'
const list = ref([{
icon:'https://wuyeadmin.bugtc.com/images/propertyImgs/house.png',
const list = ref([
// {
// icon:'https://wuye.ckdzkj.com/images/propertyImgs/points.png',
// title: "我的积分",
// desc:'999'
// },
{
icon:'https://wuye.ckdzkj.com/images/propertyImgs/house.png',
title: "我的房屋"
},
{
icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/user.png",
title: "住户管理",
// desc: "您可以在这里添加家人、朋友、租客~"
},
{
icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/parking.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/parking.png",
title: "我的车位"
},
{
icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/car.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/car.png",
title: "我的车辆"
},
{
icon: "https://wuyeadmin.bugtc.com/images/propertyImgs/setting.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/setting.png",
title: "设置"
}
])
@@ -97,48 +99,64 @@
// 返回
const onItemClick = (item) => {
if (!requireLogin()) return
const cachedVillageId = uni.getStorageSync('currentVillageId')
// 设置和我的房屋不需要绑定小区(我的房屋用于首次绑定)
if (item.title !== '设置' && item.title !== '我的房屋' && item.title !== '我的积分') {
if (!cachedVillageId || cachedVillageId === '0' || cachedVillageId === 0) {
uni.showToast({ title: '未绑定房屋,没有权限', icon: 'none', duration: 2000 })
return
}
}
if (item.title == '我的房屋') {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/myHouseIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
} else if (item.title == '住户管理') {
// uni.redirectTo({
// uni.navigateTo({
// url: '/pageSubPack/my/changePwd',
// success: res => {},
// fail: () => {},
// complete: () => {}
// });
} else if (item.title == '我的车位') {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/myParkingSpaceIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
} else if (item.title == '我的车辆') {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/myCarIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
} else if (item.title == '设置') {
} else if (item.title == '我的积分') {
uni.navigateTo({
url: '/pageSubPack/my/myPointsIndex'
});
} else if (item.title == '设置') {
goSetting()
}
}
const goSetting = () => {
uni.redirectTo({
if (!requireLogin()) return
uni.navigateTo({
url: '/pageSubPack/my/settingIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
}
let lastUserIdMy = ''
onShow(() =>{
getUserInfoData()
const token = uni.getStorageSync('token')
if (!token) return
const currentUserId = uni.getStorageSync('userId') || ''
// 仅登录状态变化时刷新,切后台回来直接用缓存
if (currentUserId !== lastUserIdMy) {
lastUserIdMy = currentUserId
getUserInfoData()
}
})
</script>
@@ -155,7 +173,7 @@
.contentStyle {
display: flex;
flex-direction: column;
height: calc(100vh - 100rpx);
height: 100%;
.topBox {
width: 100%;
@@ -222,11 +240,19 @@
<style scoped>
.list-container {}
.status-bar {
width: 100vw;
height: 46px;
.icp-footer {
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx 0;
}
.icp-text {
font-size: 22rpx;
color: #999999;
}
.list-item {
display: flex;
align-items: center;

View File

@@ -1,5 +1,5 @@
<template>
<view class="guide-page">
<view class="guide-page" v-if="showContent">
<view class="status-bar"></view>
<!-- 引导内容滑动容器 -->
<swiper
@@ -14,7 +14,7 @@
<swiper-item>
<view class="guide-item">
<view class="guide-img">
<image src="https://wuyeadmin.bugtc.com/images/guide1.png" mode="aspectFit" class="img-content"></image>
<image src="https://wuye.ckdzkj.com/images/guide1.png" mode="aspectFit" class="img-content"></image>
</view>
<view class="guide-text">
<view class="guide-title">便捷服务</view>
@@ -27,7 +27,7 @@
<swiper-item>
<view class="guide-item">
<view class="guide-img">
<image src="https://wuyeadmin.bugtc.com/images/guide2.png" mode="aspectFit" class="img-content"></image>
<image src="https://wuye.ckdzkj.com/images/guide2.png" mode="aspectFit" class="img-content"></image>
</view>
<view class="guide-text">
<view class="guide-title">高效管理</view>
@@ -40,7 +40,7 @@
<swiper-item>
<view class="guide-item">
<view class="guide-img">
<image src="https://wuyeadmin.bugtc.com/images/guide3.png" mode="aspectFit" class="img-content"></image>
<image src="https://wuye.ckdzkj.com/images/guide3.png" mode="aspectFit" class="img-content"></image>
</view>
<view class="guide-text">
<view class="guide-title">安全保障</view>
@@ -71,12 +71,23 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
// ========== 响应式数据 ==========
// ========== 响应式数据 ==========
// 当前引导步骤0/1/2
const currentStep = ref(0);
const mode = ref("default")
// 防重复跳转锁
const isNavigating = ref(false)
// 控制是否展示引导内容
const showContent = ref(false)
// 页面挂载后延迟显示,让过渡更平滑
onMounted(() => {
setTimeout(() => {
showContent.value = true
}, 100)
})
// ========== 方法 ==========
// 滑动swiper切换步骤
@@ -86,20 +97,15 @@ const handleSwiperChange = (e) => {
// 点击按钮操作
const handleBtnClick = () => {
// 前两步:点击下一步切换
if (currentStep.value < 2) {
currentStep.value++;
} else {
// 立即体验,跳转首页
// 1. 记录引导已完成
if (isNavigating.value) return;
isNavigating.value = true;
uni.setStorageSync('hasShowGuide', true);
// 2. 跳转首页
uni.navigateTo({
url: "/pageSubPack/loginSub/pwd-login"
uni.switchTab({
url: '/pages/index/index'
})
// uni.switchTab({
// url: '/pages/index/index'
// });
}
};
</script>
@@ -114,6 +120,19 @@ const handleBtnClick = () => {
flex-direction: column;
align-items: center;
box-sizing: border-box;
animation: guideFadeIn 0.5s ease-out;
}
/* 引导页淡入动画 */
@keyframes guideFadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 状态栏 - 自适应设备状态栏高度 */

View File

@@ -1,14 +1,11 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="服务" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<view class="contentStyle">
<scroll-view class="page" scroll-y>
<view class="container">
<!-- 顶部 Banner 轮播 -->
<view class="banner-box" style="overflow-y: hidden;">
<image class="banner-img" src="http://47.104.199.163:9090/images/propertyImgs/serviceImg2.png" />
<image class="banner-img" src="https://wuye.ckdzkj.com/images/propertyImgs/serviceImg2.png" @tap="bannnerClick"/>
</view>
<!-- 常用功能区 -->
@@ -56,73 +53,84 @@
import {
ref
} from 'vue';
import { requireLogin } from '@/common/checkLogin.js'
const commonFuncList = ref([{
name: "一键开门",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-door.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-door.png",
path: "/pageSubPack/notice-list/oneClickOpen"
},
{
name: "生活缴费",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-pay.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-pay.png",
path: "/pageSubPack/payment-list/paymentIndex"
},
{
name: "在线报修",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-repair.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-repair.png",
path: "/pageSubPack/online-repair/onlineRepair"
},
{
name: "访客邀请",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-visitor.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-visitor.png",
path: "/pageSubPack/visitor/visitor-list"
}
])
const communityFuncList = ref([{
name: "一键开门",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-door.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-door.png",
path: "/pageSubPack/notice-list/oneClickOpen"
},
{
name: "生活缴费",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-pay.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-pay.png",
path: "/pageSubPack/payment-list/paymentIndex"
},
{
name: "在线报修",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-repair.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-repair.png",
path: "/pageSubPack/online-repair/onlineRepair"
},
{
name: "访客邀请",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-visitor.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-visitor.png",
path: "/pageSubPack/visitor/visitor-list"
},
{
name: "投诉意见",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-complaint.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-complaint.png",
path: "/pageSubPack/service-list/complaintsSuggestionsIndex"
},
{
name: "问卷调查",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-survey.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-survey.png",
path: "/pageSubPack/service-list/questionnaireSurveyIndex"
},
{
name: "社区活动",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-activity.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-activity.png",
path: "/pageSubPack/activity-list/activityList"
},
{
name: "公告公示",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-notice.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-notice.png",
path: "/pageSubPack/notice-list/noticeList"
},
{
name: "联系物业",
icon: "http://47.104.199.163:9090/images/propertyImgs/icon-contact.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-contact.png",
path: "/pageSubPack/service-list/contactProperty"
},
{
name: "巡检记录",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-inspection.png",
path: "/pageSubPack/inspection-revenue/inspection"
},
{
name: "公区收益",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/icon-revenue.png",
path: "/pageSubPack/inspection-revenue/public-revenue"
}
])
const loginName = ref("wangh")
@@ -131,15 +139,23 @@
// --方法----
const handleFuncClick = (item) => {
if (!requireLogin()) return
const cachedVillageId = uni.getStorageSync('currentVillageId')
if (!cachedVillageId || cachedVillageId === '0' || cachedVillageId === 0) {
uni.showToast({ title: '未绑定房屋,没有权限', icon: 'none', duration: 2000 })
return
}
uni.setStorageSync('sourcePage', 'serviceIndex')
uni.redirectTo({
uni.navigateTo({
url: item.path,
success: res => {},
fail: () => {},
complete: () => {}
});
}
// banner跳转
function bannnerClick(){
if (!requireLogin()) return
uni.navigateTo({ url: '/pageSubPack/online-repair/onlineRepair'})
}
</script>
@@ -150,15 +166,11 @@
}
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
height: 100%;
/* 关键:占满屏幕高度 */
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
@@ -268,13 +280,17 @@
line-height: 1.4;
}
/* 智慧社区网格适配9个图标最后一个左对齐 */
.community-grid .func-item {
width: 22%;
/* 智慧社区网格 */
.community-grid {
justify-content: flex-start;
}
.community-grid .func-item:nth-child(9) {
margin-right: auto;
margin-left: 0;
.community-grid .func-item {
width: 22%;
margin-right: calc((100% - 22% * 4) / 3);
}
</style>
.community-grid .func-item:nth-child(4n) {
margin-right: 0;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,144 @@
'use strict';
/**
* 推送云函数
* 支持单推cid和群推cids
*/
exports.main = async (event, context) => {
// HTTP 触发器接收的参数在 event.body 中JSON 字符串),需要解析
let requestData;
if (event.body && typeof event.body === 'string') {
try {
requestData = JSON.parse(event.body);
console.log('成功解析 event.body');
} catch (e) {
console.error('JSON 解析失败:', e.message);
return {
success: false,
error: {
code: 'ParseError',
message: '请求体 JSON 格式错误'
}
};
}
} else if (event.body && typeof event.body === 'object') {
// 某些情况下 body 可能已经是对象
requestData = event.body;
console.log('使用 event.body 对象');
} else {
// 兼容直接传递参数的情况(非 HTTP 触发器调用或测试环境)
requestData = event;
console.log('使用 event 对象(兼容模式)');
}
console.log('请求数据:', JSON.stringify(requestData));
// 从解析后的数据中解构参数
const { appId, cids, cid, title, content, payload, request_id } = requestData;
// 参数校验
if (!appId || typeof appId !== 'string') {
console.error('appId 无效:', appId, '类型:', typeof appId);
return {
success: false,
error: {
code: 'ParameterError',
message: '缺少必填参数appId且必须为字符串'
}
};
}
// 统一推送目标cid 优先,其次 cids
const clientIds = cid ? [cid] : cids;
if (!clientIds || (Array.isArray(clientIds) && clientIds.length === 0)) {
console.error('clientIds 无效:', clientIds);
return {
success: false,
error: {
code: 'ParameterError',
message: '缺少必填参数cids 或 cid'
}
};
}
if (!title || typeof title !== 'string') {
console.error('title 无效:', title);
return {
success: false,
error: {
code: 'ParameterError',
message: '缺少必填参数title且必须为字符串'
}
};
}
if (!content || typeof content !== 'string') {
console.error('content 无效');
return {
success: false,
error: {
code: 'ParameterError',
message: '缺少必填参数content且必须为字符串'
}
};
}
// 发送推送
try {
console.log('初始化推送管理器appId:', appId);
const uniPush = uniCloud.getPushManager({ appId });
const sendMessageParams = {
push_clientid: Array.isArray(clientIds) ? clientIds : [clientIds],
title,
content,
force_notification: true // 离线厂商推送必填
};
// 自定义 payload仅当为有效对象时添加
if (
payload &&
typeof payload === 'object' &&
!Array.isArray(payload) &&
Object.keys(payload).length > 0
) {
sendMessageParams.payload = payload;
console.log('添加自定义 payload');
}
// 可选的 requestId
if (request_id) {
sendMessageParams.request_id = request_id;
console.log('添加 request_id:', request_id);
}
console.log('准备发送推送,参数:', JSON.stringify(sendMessageParams));
const res = await uniPush.sendMessage(sendMessageParams);
console.log('推送成功,结果:', JSON.stringify(res));
return {
success: true,
data: res
};
} catch (error) {
// 记录完整错误信息到云函数日志,便于排查
console.error('push-send 推送失败:', JSON.stringify({
message: error.message,
errMsg: error.errMsg,
code: error.code,
stack: error.stack
}));
return {
success: false,
error: {
code: error.code || 'PushError',
message: error.errMsg || error.message || '推送失败,请稍后重试'
}
};
}
};

View File

@@ -0,0 +1,11 @@
{
"name": "push-send",
"cloudfunction-config": {
"path": "/push-send",
"timeout": 60
},
"dependencies": {},
"extensions": {
"uni-cloud-push": {}
}
}

View File

@@ -0,0 +1,12 @@
// 本文件用于使用JQL语法操作项目关联的uniCloud空间的数据库方便开发调试和远程数据库管理
// 编写clientDB的js API也支持常规js语法比如var可以对云数据库进行增删改查操作。不支持uniCloud-db组件写法
// 可以全部运行也可以选中部分代码运行。点击工具栏上的运行按钮或者按下【F5】键运行代码
// 如果文档中存在多条JQL语句只有最后一条语句生效
// 如果混写了普通js最后一条语句需是数据库操作语句
// 此处代码运行不受DB Schema的权限控制移植代码到实际业务中注意在schema中配好permission
// 不支持clientDB的action
// 数据库查询有最大返回条数限制详见https://uniapp.dcloud.net.cn/uniCloud/cf-database.html#limit
// 详细JQL语法请参考https://uniapp.dcloud.net.cn/uniCloud/jql.html
// 下面示例查询uni-id-users表的所有数据
db.collection('uni-id-users').get();

View File

@@ -0,0 +1,6 @@
## 0.0.32022-11-11
- 修复 config 方法获取根节点为数组格式配置时错误的转化为了对象的Bug
## 0.0.22021-04-16
- 修改插件package信息
## 0.0.12021-03-15
- 初始化项目

View File

@@ -0,0 +1,81 @@
{
"id": "uni-config-center",
"displayName": "uni-config-center",
"version": "0.0.3",
"description": "uniCloud 配置中心",
"keywords": [
"配置",
"配置中心"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"directories": {
"example": "../../../scripts/dist"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}

View File

@@ -0,0 +1,93 @@
# 为什么使用uni-config-center
实际开发中很多插件需要配置文件才可以正常运行,如果每个插件都单独进行配置的话就会产生下面这样的目录结构
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ ├─index.js
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b // plugin-b对应的目录
├─index.js
└─config.json // plugin-b对应的配置文件
```
假设插件作者要发布一个项目模板,里面使用了很多需要配置的插件,无论是作者发布还是用户使用都是一个大麻烦。
uni-config-center就是用了统一管理这些配置文件的使用uni-config-center后的目录结构如下
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ └─index.js
├─plugin-b // plugin-b对应的目录
│ └─index.js
└─uni-config-center
├─index.js // config-center入口文件
├─plugin-a
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b
└─config.json // plugin-b对应的配置文件
```
使用uni-config-center后的优势
- 配置文件统一管理,分离插件主体和配置信息,更新插件更方便
- 支持对config.json设置schema插件使用者在HBuilderX内编写config.json文件时会有更好的提示后续HBuilderX会提供支持
# 用法
在要使用uni-config-center的公共模块或云函数内引入uni-config-center依赖请参考[使用公共模块](https://uniapp.dcloud.net.cn/uniCloud/cf-common)
```js
const createConfig = require('uni-config-center')
const uniIdConfig = createConfig({
pluginId: 'uni-id', // 插件id
defaultConfig: { // 默认配置
tokenExpiresIn: 7200,
tokenExpiresThreshold: 600,
},
customMerge: function(defaultConfig, userConfig) { // 自定义默认配置和用户配置的合并规则,不设置的情况侠会对默认配置和用户配置进行深度合并
// defaudltConfig 默认配置
// userConfig 用户配置
return Object.assign(defaultConfig, userConfig)
}
})
// 以如下配置为例
// {
// "tokenExpiresIn": 7200,
// "passwordErrorLimit": 6,
// "bindTokenToDevice": false,
// "passwordErrorRetryTime": 3600,
// "app-plus": {
// "tokenExpiresIn": 2592000
// },
// "service": {
// "sms": {
// "codeExpiresIn": 300
// }
// }
// }
// 获取配置
uniIdConfig.config() // 获取全部配置注意uni-config-center内不存在对应插件目录时会返回空对象
uniIdConfig.config('tokenExpiresIn') // 指定键值获取配置返回7200
uniIdConfig.config('service.sms.codeExpiresIn') // 指定键值获取配置返回300
uniIdConfig.config('tokenExpiresThreshold', 600) // 指定键值获取配置如果不存在则取传入的默认值返回600
// 获取文件绝对路径
uniIdConfig.resolve('custom-token.js') // 获取uni-config-center/uni-id/custom-token.js文件的路径
// 引用文件require
uniIDConfig.requireFile('custom-token.js') // 使用require方式引用uni-config-center/uni-id/custom-token.js文件。文件不存在时返回undefined文件内有其他错误导致require失败时会抛出错误。
// 判断是否包含某文件
uniIDConfig.hasFile('custom-token.js') // 配置目录是否包含某文件true: 文件存在false: 文件不存在
```

View File

@@ -0,0 +1,13 @@
{
"name": "uni-config-center",
"version": "0.0.3",
"description": "配置中心",
"main": "index.js",
"keywords": [],
"author": "DCloud",
"license": "Apache-2.0",
"origin-plugin-dev-name": "uni-config-center",
"origin-plugin-version": "0.0.3",
"plugin-dev-name": "uni-config-center",
"plugin-version": "0.0.3"
}

View File

@@ -0,0 +1,38 @@
## 1.0.192025-12-16
- 增加配置参数缺失时的错误提示,指明配置文件路径
## 1.0.182024-07-08
- checkToken时如果传入的token为空则返回uni-id-check-token-failed错误码以便uniIdRouter能正常跳转
## 1.0.172024-04-26
- 兼容uni-app-x对客户端uniPlatform的调整uni-app-x内uniPlatform区分app-android、app-ios
## 1.0.162023-04-25
- 新增maxTokenLength配置用于限制数据库用户记录token数组的最大长度
## 1.0.152023-04-06
- 修复部分语言国际化出错的Bug
## 1.0.142023-03-07
- 修复 admin用户包含其他角色时未包含在token的Bug
## 1.0.132022-07-21
- 修复 创建token时未传角色权限信息生成的token不正确的bug
## 1.0.122022-07-15
- 提升与旧版本uni-id的兼容性补充读取配置文件时回退平台app-plus、h5但是仍推荐使用新平台名进行配置app、web
## 1.0.112022-07-14
- 修复 部分情况下报`read property 'reduce' of undefined`的错误
## 1.0.102022-07-11
- 将token存储在用户表的token字段内与旧版本uni-id保持一致
## 1.0.92022-07-01
- checkToken兼容token内未缓存角色权限的情况此时将查库获取角色权限
## 1.0.82022-07-01
- 修复clientDB默认依赖时部分情况下获取不到uni-id配置的Bug
## 1.0.72022-06-30
- 修复config文件不合法时未抛出具体错误的Bug
## 1.0.62022-06-28
- 移除插件内的数据表schema
## 1.0.52022-06-27
- 修复使用多应用配置时报`Cannot read property 'appId' of undefined`的Bug
## 1.0.42022-06-27
- 修复使用自定义token内容功能报错的Bug [详情](https://ask.dcloud.net.cn/question/147945)
## 1.0.22022-06-23
- 对齐旧版本uni-id默认配置
## 1.0.12022-06-22
- 补充对uni-config-center的依赖
## 1.0.02022-06-21
- 提供uni-id token创建、校验、刷新接口简化旧版uni-id公共模块

View File

@@ -0,0 +1,101 @@
{
"id": "uni-id-common",
"displayName": "uni-id-common",
"version": "1.0.19",
"description": "包含uni-id token生成、校验、刷新功能的云函数公共模块",
"keywords": [
"uni-id-common",
"uniCloud",
"token",
"权限"
],
"repository": "https://gitcode.net/dcloud/uni-id-common",
"engines": {
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [
"uni-config-center"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": "-",
"vue3": "-"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "-",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
# uni-id-common
文档请参考:[uni-id-common](https://uniapp.dcloud.net.cn/uniCloud/uni-id-common.html)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
{
"name": "uni-id-common",
"version": "1.0.19",
"description": "uni-id token生成、校验、刷新",
"main": "index.js",
"homepage": "https:\/\/uniapp.dcloud.io\/uniCloud\/uni-id-common.html",
"repository": {
"type": "git",
"url": "git+https:\/\/gitee.com\/dcloud\/uni-id-common.git"
},
"author": "DCloud",
"license": "Apache-2.0",
"dependencies": {
"uni-config-center": "file:..\/..\/..\/..\/..\/uni-config-center\/uniCloud\/cloudfunctions\/common\/uni-config-center"
},
"origin-plugin-dev-name": "uni-id-common",
"origin-plugin-version": "1.0.19",
"plugin-dev-name": "uni-id-common",
"plugin-version": "1.0.19"
}

Some files were not shown because too many files have changed in this diff Show More