Compare commits

..

84 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
zhouyanli
d26bd4e86b 银联支付插件 2026-06-04 17:55:18 +08:00
zhouyanli
a8e3d1f875 银联支付集成和购买插件;修改首页小区更改后获取数据;对接我的信息接口 2026-06-04 17:42:29 +08:00
zhouyanli
4d0515e5fe Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side 2026-06-01 11:38:26 +08:00
zhouyanli
3fa853cb4c 对接消息推送 2026-06-01 11:38:03 +08:00
wangyuxin
c740126617 水费缴费部分代码同步 2026-06-01 11:17:04 +08:00
zhouyanli
fbe07e21a6 配置push2.0并测试;修改样式、二维码保存 2026-05-30 16:37:43 +08:00
zhouyanli
7c8a4aba26 修改在修报修标题显示;开通消息推送 2026-05-28 17:50:17 +08:00
zhouyanli
102beeb8bb 修改bug 2026-05-26 17:44:00 +08:00
zhouyanli
1b0e47e79e 设置拨打电话权限、修改定位 2026-05-25 09:50:28 +08:00
zhouyanli
c4d91787b5 修改登录页样式、车牌号输入问题、定位加载问题、联系物业权限问题 2026-05-23 16:44:29 +08:00
zhouyanli
6b520fb70e 对接我的活动接口,修改其他页面样式 2026-05-21 17:00:46 +08:00
zhouyanli
40c9e75b2e 优化首页、访客可视滚动布局 2026-05-20 17:30:16 +08:00
zhouyanli
b7d1037b35 重构首页自适应布局 2026-05-19 10:26:15 +08:00
zhouyanli
aa5453c0be 修改引导页、活动列表自适应样式 2026-05-18 17:48:43 +08:00
zhouyanli
8ee170f78b 替换图片路径 2026-05-14 16:10:56 +08:00
zhouyanli
6341c08703 Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side 2026-05-14 08:33:40 +08:00
zhouyanli
c16f4f6efb 在线报修修改选择项目返回问题 2026-05-14 08:30:37 +08:00
wangyuxin
b644f26521 Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side
# Conflicts:
#	property-uniapp-project/pageSubPack/api/apiSub.js
2026-05-13 17:31:31 +08:00
wangyuxin
3ab6450b8c 我的车位,我的车辆 2026-05-13 17:20:31 +08:00
131 changed files with 14955 additions and 7918 deletions

View File

@@ -0,0 +1,46 @@
{
"permissions": {
"allow": [
"WebFetch(domain:app.apifox.com)",
"Bash(sed -i '545s/.*/\\\\t\\\\t\\\\t.activity-card{\\\\n\\\\t\\\\t\\\\t}\\\\n\\\\t\\\\t\\\\t.uni-card{/' pages/index/index.vue)",
"Bash(sed -n '544,549p' pages/index/index.vue)",
"Bash(sed -i '548d' pages/index/index.vue)",
"Bash(sed -n '544,552p' pages/index/index.vue)",
"Bash(cat)",
"Bash(python /tmp/notice_edit.py)",
"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(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,16 +1,128 @@
<script>
export default {
onLaunch: function() {
// console.log('App Launch')
},
onShow: function() {
// console.log('App Show')
},
onHide: function() {
<script setup>
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
})
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) => {
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" : "uniapp-project-test",
"appid" : "",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"name" : "花开物业",
"appid" : "__UNI__29C3D60",
"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,11 +17,22 @@
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"autoclose" : false,
"delay" : 0
},
/* */
"modules" : {},
"modules" : {
"Geolocation" : {},
"Camera" : {},
"Contacts" : {},
"Gallery" : {},
"Push" : {},
"Barcode" : {},
"Payment" : {},
"Bluetooth" : {},
"Maps" : {},
"Share" : {}
},
/* */
"distribute" : {
/* android */
@@ -37,13 +52,116 @@
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>"
],
"schemes" : "",
"targetSdkVersion" : 30
},
/* ios */
"ios" : {},
"ios" : {
"dSYMs" : false,
"privacyDescription" : {
"NSPhotoLibraryUsageDescription" : "需要访问手机相册,选取本地水电表二维码、故障照片、活动照片用于抄表扫码、提交报修工单、活动实况,仅读取本次业务所需图片,不会获取相册内其他无关照片。",
"NSPhotoLibraryAddUsageDescription" : "需要相册写入权限,将访客通行凭证二维码图片保存至手机相册,方便线下小区出示通行,图片仅本地存储,不会上传至无关服务器",
"NSCameraUsageDescription" : "本应用需要使用相机,用于扫描水电表二维码读取表计账单、拍摄故障现场提交报修工单、拍摄访客登记凭证,拍摄内容仅用于小区物业业务处理,不会私自留存或对外分享。",
"NSBluetoothAlwaysUsageDescription" : "本应用需要蓝牙权限,用于扫描、连接小区蓝牙门禁设备实现一键开门,仅和本小区门禁硬件建立通信,不会扫描、连接无关蓝牙设备。",
"NSBluetoothPeripheralUsageDescription" : "需要蓝牙权限连接小区智能门禁,实现蓝牙一键开门通行,仅用于小区门禁解锁业务,不采集蓝牙设备信息。",
"NSMicrophoneUsageDescription" : "需要麦克风权限,用于拨打物业客服电话、与维修人员通话沟通报修问题,仅通话时采集语音,不会录制、留存通话音频。"
},
"urltypes" : "uppaylyhuts",
"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" : {}
"sdkConfigs" : {
"geolocation" : {
"system" : {
"__platform__" : [ "ios", "android" ]
}
},
"maps" : {
"amap" : {
"name" : "玉米汁穿过三峡人家",
"appkey_ios" : "39a4c28a01c3ec15684b21d4093aad3d",
"appkey_android" : "044055aed47cb305e86419fd82e4c5f3"
}
},
"push" : {
"unipush" : {
"version" : "2",
"offline" : true,
"icons" : {
"small" : {
"mdpi" : "unpackage/res/icons/logo-app.png"
}
}
}
},
"share" : {
"weixin" : {
"appid" : "wx84b02b2ec80715f4",
"UniversalLinks" : "https://verification.ckdzkj.com/uni-universallinks/__UNI__29C3D60/"
}
},
"payment" : {
"appleiap" : {},
"alipay" : {
"__platform__" : [ "ios", "android" ]
},
"weixin" : {
"__platform__" : [ "ios", "android" ]
}
},
"oauth" : {
"weixin" : {
"appid" : "",
"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"
}
}
}
}
},
/* */
@@ -54,10 +172,8 @@
"setting" : {
"urlCheck" : false
},
"requiredPrivateInfos": [
"chooseLocation",
"getLocation"
],
"navigateToMiniProgramAppIdList" : [ "wxc0189436c58b59cd" ],
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ],
"permission" : {
"scope.camera" : {
"desc" : "用于扫码"
@@ -80,5 +196,29 @@
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
"vueVersion" : "3",
"app-harmony" : {
"distribute" : {
"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,19 +1,11 @@
<template>
<view class="container">
<view class="status_bar"></view>
<!-- 顶部标题栏 固定 -->
<view class="top-bar">
<uni-nav-bar left-icon="left" title="活动列表" backgroundColor="none" :border="false"
@clickLeft="narBack" />
</view>
<!-- 滚动区域从banner开始滚动 -->
<scroll-view class="scroll-container" scroll-y="true" @scrolltolower="loadMoreData"
:lower-threshold="100">
<view class="scroll-container">
<!-- 顶部图片 -->
<view class="topImg">
<image src="http://47.104.199.163:9090/images/home/banner.png"
style="width: 100%;height:100%;"></image>
<view class="top-img">
<image class="banner-img" src="http://wuye.ckdzkj.com/images/home/banner.png" mode="aspectFill"></image>
</view>
<!-- 选项卡 -->
@@ -53,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>
@@ -66,8 +55,10 @@
<up-row justify="space-between" customStyle="margin-bottom: 10px">
<up-col span="4">
<view class="info-image-left">
<image :src="activity.coverImage"
style="width: 100%;height: 100%;"></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">
@@ -85,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>
@@ -93,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>
@@ -117,32 +114,32 @@
</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 } from '../api/api.js'
import { getActivityList,getMyActivityList} from '../api/api.js'
// ===================== 响应式数据 =====================
const activeTab = ref('list') // 'list' 或 'my'
const myFilter = ref('reviewing') // 我的活动筛选条件
const myFilter = ref('') // 我的活动筛选条件''全部, '0'未开始, '1'进行中, '2'已结束
const isLoading = ref(false)
const hasMoreData = ref(true)
const currentPage = ref(1)
const pageSize = ref(5)
const pageSize = ref(10)
const total = ref(0)
const displayedActivities = ref([]) // 当前显示的活动列表
// 筛选条件
// 筛选条件status ''全部, '0'未开始, '1'进行中, '2'已结束
const filters = ref([
{ label: '审核中', value: 'reviewing' },
{ label: '已通过', value: 'passed' },
{ label: '未通过', value: 'rejected' },
{ label: '已结束', value: 'ended' }
{ label: '全部', value: '' },
{ label: '未开始', value: '0' },
{ label: '进行中', value: '1' },
{ label: '已结束', value: '2' }
])
// ===================== 核心方法 =====================
@@ -156,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
}
})
/**
* 切换选项卡(活动列表/我的活动)
@@ -179,20 +174,27 @@ const switchTab = (tab) => {
if (tab === 'list') {
resetAndLoad()
} else {
// 暂无审核功能,"我的活动"默认显示暂无数据
displayedActivities.value = []
hasMoreData.value = false
currentPage.value = 1
resetAndLoadMy()
}
}
/**
* 重置并刷新我的活动第一页数据
*/
const resetAndLoadMy = () => {
currentPage.value = 1
displayedActivities.value = []
hasMoreData.value = true
getMyActivityListData(1)
}
/**
* 切换我的活动筛选条件
* @param {string} filter - 筛选值
*/
const switchFilter = (filter) => {
myFilter.value = filter
// resetAndLoad()
resetAndLoadMy()
}
/**
@@ -205,12 +207,11 @@ const getActivityListData = async (pageNum = 1) => {
isLoading.value = true
try {
// 构造请求参数(区分活动列表/我的活动 + 筛选条件
// 构造请求参数(活动列表)
const params = {
pageNum: pageNum,
pageSize: pageSize.value,
//tabType: activeTab.value, // 传给后端list/my
//filterType: activeTab.value === 'my' ? myFilter.value : '' // 我的活动筛选条件
villageId:uni.getStorageSync('currentVillageId')
}
const res = await getActivityList(params)
@@ -234,10 +235,57 @@ 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();
}
}
/**
* 获取我的活动列表数据
* @param {number} pageNum - 页码
*/
const getMyActivityListData = async (pageNum = 1) => {
if (isLoading.value) return
isLoading.value = true
try {
const params = {
pageNum: pageNum,
pageSize: pageSize.value,
villageId:uni.getStorageSync('currentVillageId')
}
// status 为空时不传,返回全部
if (myFilter.value !== '') {
params.status = myFilter.value
}
const res = await getMyActivityList(params)
if (res.code === 200) {
const list = res.data || []
total.value = res.total || 0
if (pageNum === 1) {
displayedActivities.value = list
} else {
displayedActivities.value = [...displayedActivities.value, ...list]
}
hasMoreData.value = displayedActivities.value.length < total.value
currentPage.value = pageNum
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'error' })
if (pageNum === 1) displayedActivities.value = []
}
} catch (err) {
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
if (pageNum === 1) displayedActivities.value = []
} finally {
isLoading.value = false
uni.stopPullDownRefresh();
}
}
@@ -246,7 +294,11 @@ const getActivityListData = async (pageNum = 1) => {
*/
const loadMoreData = () => {
if (isLoading.value || !hasMoreData.value) return
if (activeTab.value === 'list') {
getActivityListData(currentPage.value + 1)
} else {
getMyActivityListData(currentPage.value + 1)
}
}
/**
@@ -259,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 || ''}`
})
}
@@ -275,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 || ''}`
})
}
@@ -285,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 || ''}`
})
}
@@ -295,7 +378,9 @@ const viewActivityDetail = (activity) => {
*/
onShow(() => {
if (activeTab.value === 'list') {
getActivityListData(1) // 强制刷新第一页
getActivityListData(1)
} else {
getMyActivityListData(1)
}
})
@@ -305,7 +390,6 @@ onShow(() => {
onLoad(() => {
// 监听详情页报名/取消报名后的刷新通知
uni.$on('refreshActivityList', (data) => {
// console.log('收到活动列表刷新通知:', data)
getActivityListData(1)
})
})
@@ -315,13 +399,29 @@ onLoad(() => {
*/
onUnload(() => {
uni.$off('refreshActivityList')
})
});
// 下拉刷新
onPullDownRefresh(() => {
if (activeTab.value === 'list') {
resetAndLoad();
} else {
resetAndLoadMy();
}
});
// 上拉加载更多
onReachBottom(() => {
loadMoreData();
});
/**
* 监听我的活动筛选条件变化
*/
watch(() => myFilter.value, () => {
// 暂无审核功能,筛选条件暂不触发加载
if (activeTab.value === 'my') {
resetAndLoadMy()
}
})
</script>
@@ -329,59 +429,50 @@ watch(() => myFilter.value, () => {
/* 页面容器 */
.container {
width: 100%;
height: 100vh;
min-height: 100vh;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 0;
}
.status_bar {
height: 46px;
width: 100%;
}
/* 顶部标题 固定 */
.top-bar {
width: 100%;
flex-shrink: 0;
background: #fff;
}
/* 滚动区域 */
.scroll-container {
flex: 1;
width: 100%;
overflow-y: auto;
padding: 0 40rpx;
padding: 0 30rpx;
box-sizing: border-box;
}
.topImg {
/* 顶部图片 */
.top-img {
width: 100%;
height: 280rpx;
margin: 20rpx 0;
}
.banner-img {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
/* 顶部选项卡 */
.tabs-container {
display: flex;
top: 0;
z-index: 10;
margin-bottom: 20rpx
margin-bottom: 20rpx;
}
.tab-item {
flex: 1;
display: flex;
text-align: center;
padding: 20rpx 0;
padding: 20rpx 32rpx;
font-size: 32rpx;
color: #666;
position: relative;
}
.tab-item.active {
color: #007AFF;
color: #2F77FD;
font-weight: 500;
}
@@ -391,8 +482,9 @@ watch(() => myFilter.value, () => {
left: 50%;
transform: translateX(-50%);
width: 120rpx;
height: 6rpx;
background-color: #007AFF;
height: 8rpx;
background: linear-gradient(to bottom, #2F77FD, #CEDFFF);
/* background-color: #2F77FD; */
border-radius: 4rpx;
}
@@ -400,7 +492,7 @@ watch(() => myFilter.value, () => {
.filter-container {
background-color: #fff;
padding: 20rpx 0;
border-bottom: 1px solid #eee;
border-bottom: 1rpx solid #eee;
}
.filter-scroll {
@@ -410,7 +502,7 @@ watch(() => myFilter.value, () => {
.filter-buttons {
display: inline-flex;
padding: 0 30rpx;
padding: 0 10rpx;
}
.filter-btn {
@@ -421,7 +513,9 @@ watch(() => myFilter.value, () => {
color: #666;
background-color: #f8f8f8;
border-radius: 25rpx;
border: 1px solid #eee;
border: 1rpx solid #eee;
white-space: nowrap;
flex-shrink: 0;
}
.filter-btn.active {
@@ -439,18 +533,18 @@ watch(() => myFilter.value, () => {
.activity-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
padding: 12rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
/* box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); */
position: relative;
border: 1px solid #e1e1e1;
border: 1rpx solid #e1e1e1;
}
/* 活动状态标签 */
.status-badge {
position: absolute;
top: 10rpx;
left: 20rpx;
top: 0;
left: 0;
padding: 6rpx 20rpx;
font-size: 22rpx;
border-radius: 15rpx 0rpx 15rpx 0rpx;
@@ -471,6 +565,7 @@ watch(() => myFilter.value, () => {
font-size: 22rpx;
border-radius: 20rpx;
z-index: 1;
background-color: rgba(0, 122, 255, 0.08);
}
.signed-badge text {
@@ -479,27 +574,34 @@ watch(() => myFilter.value, () => {
/* 活动信息 */
.activity-info {
margin-top: 10rpx;
padding-top: 24rpx;
}
/* 活动信息 */
.info-image-left {
width: 170rpx;
height: 220rpx;
position: relative;
}
.activity-title {
font-size: 30rpx;
font-weight: 500;
color: #222;
margin-bottom: 30rpx;
margin-bottom: 20rpx;
line-height: 1.4;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
padding-right: 100rpx;
}
.activity-detail {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
margin-bottom: 16rpx;
color: #999;
font-size: 24rpx;
line-height: 1.4;
@@ -520,25 +622,30 @@ watch(() => myFilter.value, () => {
flex: 1;
font-size: 20rpx;
}
.detail-text-address {
flex: 1;
font-size: 22rpx;
word-break: break-all;
overflow-wrap: break-word;
white-space: pre-wrap;
}
/* 操作按钮 */
.action-buttons {
margin-top: 30rpx;
margin-top: 20rpx;
display: flex;
justify-content: flex-end;
}
.action-btn {
padding: 0px 26rpx;
height: 50rpx;
line-height: 50rpx;
font-size: 22rpx;
border-radius: 10rpx;
padding: 0 26rpx;
height: 56rpx;
line-height: 56rpx;
font-size: 24rpx;
border-radius: 28rpx;
margin: 0;
min-width: 140rpx;
}
.signup-btn {
@@ -550,6 +657,12 @@ watch(() => myFilter.value, () => {
background-color: #0056cc;
}
.disabled-btn {
background-color: #f2f3f5;
color: #999;
text-align: center;
}
/* 加载更多 */
.load-more-container {
display: flex;
@@ -613,7 +726,7 @@ watch(() => myFilter.value, () => {
}
.empty-text {
font-size: 32rpx;
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;

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>{{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"
})
}
@@ -452,15 +492,17 @@ const showToastMessage = (message) => {
<style scoped>
/* 页面容器 */
.container {
min-height: 100vh;
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(to bottom left, #E2F0FF 0%, #ffffff 50%)
background: linear-gradient(to bottom left, #E2F0FF 0%, #ffffff 50%);
overflow: hidden;
}
/* 状态栏 */
.status_bar {
height: 46px;
width: 100%;
flex-shrink: 0;
}
/* 顶部导航栏 */
@@ -473,6 +515,7 @@ const showToastMessage = (message) => {
position: sticky;
top: 44px;
z-index: 100;
flex-shrink: 0;
}
.nav-left {
@@ -504,7 +547,6 @@ const showToastMessage = (message) => {
width: 100%;
box-sizing: border-box;
overflow-y: auto;
margin-bottom: 120rpx;
}
/* 选项卡 */
@@ -565,6 +607,8 @@ const showToastMessage = (message) => {
font-weight: 500;
line-height: 1.4;
text-align: right;
white-space: pre-wrap;
word-break: break-all;
}
/* 区块样式 */
@@ -585,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 {
@@ -661,6 +708,8 @@ const showToastMessage = (message) => {
display: flex;
flex-direction: column;
justify-content: flex-start;
flex: 1;
min-width: 0;
}
.user-name {
@@ -680,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;
}
/* 实况图片 */
@@ -702,14 +754,10 @@ const showToastMessage = (message) => {
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 30rpx 40rpx;
background-color: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
z-index: 100;
flex-shrink: 0;
}
.action-btn {
@@ -759,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;
@@ -820,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;
@@ -889,3 +948,25 @@ const showToastMessage = (message) => {
}
}
</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,12 +58,17 @@ import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { addActivityLive, signinActivityLive, uploadFile } from '../api/api.js';
// 天地图 Web API Key用于逆地址解析
const TIANDITU_KEY = 'b0201708e26cb56e514d74d1686f6f91';
// 活动ID
const activityId = ref('');
// 实况描述内容
const statusDesc = ref('');
// 输入字符长度
const currentLength = ref(0);
// 提交状态锁(防重复点击)
const submitting = ref(false);
// 图片列表(本地临时路径)
const imgList = ref([]);
// 定位信息
@@ -75,6 +80,8 @@ const latitude = ref('');
// 接收活动ID
onLoad((option) => {
activityId.value = option?.id || '';
// 进入页面自动定位,获取不到时可通过“重新定位”重试
reLocate();
});
// 监听输入框字数变化
@@ -82,6 +89,28 @@ const handleTextInput = (e) => {
currentLength.value = e.detail.value.length;
};
// 逆地址解析(坐标转地址)- 天地图 API
const reverseGeocode = async (lng, lat) => {
if (!TIANDITU_KEY) return null;
try {
const res = await uni.request({
url: 'http://api.tianditu.gov.cn/geocoder',
data: {
type: 'rgc',
postStr: JSON.stringify({ lon: lng, lat: lat, ver: 1 }),
tk: TIANDITU_KEY
}
});
if (res.data?.status === '0' && res.data.result?.formatted_address) {
return res.data.result.formatted_address;
}
return null;
} catch (e) {
console.error('逆地址解析失败:', e);
return null;
}
};
// 选择图片
const chooseImg = () => {
uni.chooseImage({
@@ -92,7 +121,6 @@ const chooseImg = () => {
imgList.value = [...imgList.value, ...res.tempFilePaths];
},
fail: (err) => {
uni.showToast({ title: '选择图片失败', icon: 'none' });
console.error('选择图片失败:', err);
}
});
@@ -103,43 +131,67 @@ const chooseImg = () => {
imgList.value.splice(index, 1);
};
// 重新定位
// 定位并获取具体地点名称
// 跳转系统设置App 端Android/iOSH5/小程序另有处理)
const openAppSettings = () => {
// #ifdef APP-PLUS
try {
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 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);
uni.showToast({ title: '跳转失败,请手动前往系统设置开启定位权限', icon: 'none' });
}
// #endif
// #ifdef H5
uni.showToast({ title: '请手动在浏览器设置中允许定位权限', icon: 'none' });
// #endif
// #ifdef MP-WEIXIN
uni.openSetting({});
// #endif
};
// 重新定位(获取当前位置 + 逆地址解析)
const reLocate = () => {
// 调用微信原生选择位置接口(自带地址解析,无调用上限)
uni.chooseLocation({
success: (res) => {
// res.address = 具体地址XX市XX区XX街道XX小区
// res.name = 地点名称XX小区
uni.showLoading({ title: '正在定位...', mask: true });
uni.getLocation({
type: 'gcj02',
isHighAccuracy: true,
success: async (res) => {
longitude.value = res.longitude;
latitude.value = res.latitude;
location.value = res.address; // 显示具体地址
uni.showToast({ title: `定位成功:${res.name}`, icon: 'success' });
// 逆地址解析
const address = await reverseGeocode(res.longitude, res.latitude);
location.value = address || `${res.longitude.toFixed(6)}, ${res.latitude.toFixed(6)}`;
uni.hideLoading();
},
fail: (err) => {
// 处理权限拒绝/定位失败
if (err.errMsg.includes('auth deny')) {
uni.hideLoading();
const errMsgStr = err?.errMsg || err?.message || JSON.stringify(err);
console.error('定位失败:', err);
if (err?.code === -22 || /权限|授权|拒绝|auth|deny|permission/i.test(errMsgStr)) {
uni.showModal({
title: '需要定位权限',
content: '请允许获取位置信息以显示具体地点',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting({
success: (settingRes) => {
// 用户开启权限后重新定位
if (settingRes.authSetting['scope.userLocation']) {
reLocate();
}
}
});
}
}
success: (res) => { if (res.confirm) openAppSettings(); }
});
} else {
uni.hideLoading();
uni.showToast({ title: '定位失败,请重试', icon: 'none' });
console.error('定位失败:', err);
uni.showToast({ title: `定位失败:${errMsgStr}`, icon: 'none', duration: 3000 });
}
}
});
@@ -155,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' });
@@ -194,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>
@@ -212,28 +270,28 @@ const submitStatus = async () => {
/* 主体内容 */
.content {
flex: 1;
padding: 15px 0px;
max-height: calc(100vh - 120px);
padding: 10px 0px;
}
/* 表单项通用样式 */
.form-item {
background-color: #fff;
padding: 15px;
padding: 20px 15px;
border-radius: 8px;
margin-bottom: 15px;
margin-bottom: 8px;
}
.label {
font-size: 16px;
font-weight: 500;
margin-bottom: 10px;
margin-bottom: 6px;
display: block;
}
/* 输入框样式 */
.textarea {
width: 100%;
min-height: 120px;
height: 180px;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
@@ -244,7 +302,7 @@ const submitStatus = async () => {
text-align: right;
font-size: 12px;
color: #999;
margin-top: 5px;
margin-top: 4px;
display: block;
}
@@ -289,7 +347,8 @@ const submitStatus = async () => {
.upload-btn {
width: 80px;
height: 80px;
border: 1px dashed #ddd;
/* border: 1px dashed #ddd; */
background-color: #F3F3F3;
border-radius: 4px;
display: flex;
flex-direction: column;
@@ -316,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,25 +76,25 @@ 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 = () =>{
return get("/api/resident/activity/my")
export const getMyActivityList = (data) =>{
return get("/api/resident/activity/my",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) =>{
@@ -139,3 +143,7 @@ export const addVisitorData = (data) =>{
export const uploadFile = (filePath) => {
return upload(filePath,'/api/resident/file/uploadImage');
}
// 单推消息推送
export const getPushSingle = (data) =>{
return post("/api/push/single", data)
}

View File

@@ -5,6 +5,10 @@ import request, {
putMethod
} from "./request.js"
// 字典
export const getSystemDictData = (data) => {
return get('/system/dict/data/list', data)
}
// =======公告列表========
export const getResidentNoticeList = (data) => {
@@ -53,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)
@@ -79,14 +89,14 @@ 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)
}
// 修改身份证照片
export const uploadFileUploadImage = (data) => {
return post(`/api/resident/file/uploadImage`, 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) => {
@@ -123,3 +133,181 @@ export const getComplainDetail = (data) => {
export const getManagePhoneData = (data) => {
return get('/api/manage/banner/getManagePhone',data)
}
// 修改身份证照片
export const uploadFileUploadImage = (data) => {
return post(`/api/resident/file/uploadImage`, data)
}
// 水电表
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)
}
// 充值调用
// /${data.rechargeAmount}/${data.type}/${data.deviceCode}
export const getTopUp = (data) => {
return post(`/api/topUp`, data)
}
export const getopenIdByCode = (data) => {
return get(`/api/topUp/getOpenIdByCode?code=${data.code}`, 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(
`/api/topUp/topupRecordWithStat?residentUserId=${data.residentUserId}&pageNum=${data.pageNum}&pageSize=${data.pageSize}`,
data)
}
export const postInsertOpenId = (data) => {
return post(`/api/resident/auth/insertOpenId`, data)
}
export const postSubscribeAuth = (data) => {
return post(`/api/wx/subscribeAuth/auth`, data)
}
// 添加车辆
export const postCar = (data) => {
return post(`/api/resident/car`, data)
}
// 修改车辆
export const putCar = (data) => {
return putMethod(`/api/resident/car`, data)
}
// 获取车辆列表
export const getMyCarList = (data) => {
return get(`/api/resident/car/myList`, data)
}
// 获取车位列表
export const getCarList = (data) => {
return get(`/api/resident/car/list`, data)
}
// 获取车辆详细信息
export const getCarDetail = (data) => {
return get(`/api/resident/car/${data.id}`, data)
}
// 删除车辆信息
export const deleteCar = (data) => {
return deleteMethod(`/api/resident/car/${data.carId}`, data)
}
// 获取用户车位绑定列表
export const getUserParkingSpaceList = (data) => {
return get(`/api/resident/car/parking/list`, data)
}
// 获取区域列表
export const getResidentCarAreaManageAreaListByVillageId = (data) => {
return get(`/ResidentCarAreaManage/areaListByVillageId`, data)
}
// 根据区域id获得车位列表
export const getResidentCarAreaManageParkingListByAreaId = (data) => {
return get(`/ResidentCarAreaManage/parkingListByAreaId`, data)
}
// 绑定车位
export const postCarBind = (data) => {
return post(`/api/resident/car/bind`, data)
}
// 获取用户下车位绑定信息
// export const getCarList = (data) => {
// return get(`/api/resident/car/list`, data)
// }
// 删除车位
export const deleteParking = (data) => {
return deleteMethod(`/ResidentCarAreaManage/parking/${data.parkingId}`, data)
}
// 车位详情
export const getParkingDetail = (data) => {
return get(`/api/resident/car/parking/detail/${data.id}`, 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,appChannel) => {
return get(`/api/app/version/${platform}/${channel}/${appChannel}`)
}
// 检查是否需要更新
export const checkAppUpdate = (data) => {
return post(`/api/app/checkUpdate`, 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' });
if (token) {
uni.clearStorageSync('token');
setTimeout(() => {
uni.reLaunch({ url: '/pageSubPack/loginSub/pwd-login' });
}, 1500);
navigateToLogin({ method: 'reLaunch' });
}
reject({ msg: '登录过期', code: 401 });
return;
}
@@ -53,14 +60,14 @@ export default function request(url, data = {}, method = "GET") {
}
},
fail: (err) => {
uni.hideLoading();
// uni.hideLoading();
reject({ msg: '网络异常', code: -1, raw: err });
}
});
});
}
export const get = (url, data) => request(url, data, 'GET');
export const get = (url, data = {}) => request(url, data, 'GET');
export const post = (url, data) => request(url, data, 'POST');
export const deleteMethod = (url, data) => request(url, data, 'DELETE');
export const putMethod = (url, data) => request(url, data, 'PUT');
@@ -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.224:8080';
const baseUrl = 'https://wuyeapi.ckdzkj.com';
// const baseUrl = "http://192.168.1.6:8080"
/**
* 图片上传 Hook
@@ -7,7 +8,7 @@ const baseUrl = 'http://192.168.0.224:8080';
* 解决编辑回显后重新上传的问题
*/
import { ref } from 'vue'
import { reactive } from 'vue'
/**
* @param {Object} options 配置
@@ -21,14 +22,17 @@ export function useImageUpload(options = {}) {
fileKey = 'file'
} = options
// 使用 reactive 对象存储所有状态
const state = reactive({
// 当前显示的图片(前端展示用,支持 base64/blob URL/网络URL
const previewUrl = ref('')
previewUrl: '',
// 服务器返回的 URL提交时使用
const serverUrl = ref('')
serverUrl: '',
// 临时的文件路径(重新上传时用)
const tempFilePath = ref(null)
tempFilePath: null,
// 上传状态
const uploading = ref(false)
uploading: false
})
/**
* 设置回显图片(编辑时从后端获取)
@@ -36,9 +40,11 @@ export function useImageUpload(options = {}) {
*/
const setPreviewUrl = (url) => {
if (url) {
previewUrl.value = url
serverUrl.value = url
tempFilePath.value = null // 重置临时文件,表示没有重新上传
// 确保 url 是字符串
const urlStr = typeof url === 'string' ? url : (url?.url || String(url))
state.previewUrl = urlStr
state.serverUrl = urlStr
state.tempFilePath = null // 重置临时文件,表示没有重新上传
}
}
@@ -53,14 +59,20 @@ export function useImageUpload(options = {}) {
sizeType: ['compressed'],
sourceType: ['camera', 'album'],
success: (res) => {
const path = res.tempFilePaths[0]
const path = res.tempFilePaths?.[0]
if (!path) {
reject(new Error('选择图片失败'))
return
}
// 确保 path 是字符串
const pathStr = typeof path === 'string' ? path : String(path)
// 设置预览
previewUrl.value = path
state.previewUrl = pathStr
// 记录临时文件路径
tempFilePath.value = path
state.tempFilePath = pathStr
// 置空服务器URL等实际上传后再获取
serverUrl.value = ''
resolve(path)
state.serverUrl = ''
resolve(pathStr)
},
fail: (err) => {
reject(err)
@@ -71,25 +83,32 @@ export function useImageUpload(options = {}) {
/**
* 上传图片到服务器
* @returns {Promise<string>} 返回服务器上的URL
* @returns {Promise<Object>} 返回 { url: string }
*/
const uploadImage = () => {
// 如果没有新选择临时文件直接返回已有的服务器URL
if (!tempFilePath.value) {
return Promise.resolve(serverUrl.value)
if (!state.tempFilePath) {
return Promise.resolve({ url: state.serverUrl })
}
// 如果已经有服务器URL且没有重新选择也直接返回
if (serverUrl.value && !tempFilePath.value) {
return Promise.resolve(serverUrl.value)
if (state.serverUrl && !state.tempFilePath) {
return Promise.resolve({ url: state.serverUrl })
}
uploading.value = true
state.uploading = true
return new Promise((resolve, reject) => {
// 确保 filePath 是字符串
const filePath = state.tempFilePath
if (!filePath || typeof filePath !== 'string') {
state.uploading = false
reject(new Error('文件路径无效'))
return
}
uni.uploadFile({
url: baseUrl + uploadUrl,
filePath: tempFilePath.value,
filePath: filePath,
name: fileKey,
header: {
'Authorization': 'Bearer ' + (uni.getStorageSync('token') || ''),
@@ -101,10 +120,10 @@ export function useImageUpload(options = {}) {
if (data.code === 200 && data.data) {
// 更新服务器URL确保存的是字符串
const url = typeof data.data === 'string' ? data.data : (data.data?.url || '')
serverUrl.value = url
state.serverUrl = url
// 清空临时文件
tempFilePath.value = null
resolve(url)
state.tempFilePath = null
resolve({ url })
} else {
uni.showToast({
title: data.msg || '上传失败',
@@ -128,7 +147,7 @@ export function useImageUpload(options = {}) {
reject(err)
},
complete: () => {
uploading.value = false
state.uploading = false
}
})
})
@@ -138,36 +157,35 @@ export function useImageUpload(options = {}) {
* 获取最终可提交的URL
* 如果有新上传的图片先上传再返回URL
* 如果没有新上传直接返回已有的服务器URL
* @returns {Promise<string>}
* @returns {Promise<Object>}
*/
const getSubmitUrl = async () => {
// 如果重新选择了新图片,先上传
if (tempFilePath.value) {
if (state.tempFilePath) {
return await uploadImage()
}
// 否则返回已有的服务器URL
return serverUrl.value
return { url: state.serverUrl }
}
/**
* 重置状态
*/
const reset = () => {
previewUrl.value = ''
serverUrl.value = ''
tempFilePath.value = null
uploading.value = false
state.previewUrl = ''
state.serverUrl = ''
state.tempFilePath = null
state.uploading = false
}
// 返回 reactive state 和方法
// state 作为 reactive 对象,在模板中通过 idCardFrontUpload.state.previewUrl 访问
return {
previewUrl, // 前端展示用
serverUrl, // 提交时使用的URL
tempFilePath, // 临时文件路径
uploading, // 上传中状态
setPreviewUrl, // 设置回显图片(编辑时用)
chooseImage, // 选择图片(预览用,不上传)
uploadImage, // 上传到服务器
getSubmitUrl, // 获取最终可提交的URL自动处理上传
reset // 重置
state,
setPreviewUrl,
chooseImage,
uploadImage,
getSubmitUrl,
reset
}
}

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,22 +2,25 @@
<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>
<!-- 新密码输入框 -->
<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>
@@ -92,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'
});
})
@@ -113,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{
@@ -157,6 +162,29 @@ const handleConfirm = () => {
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;
}
/* 密码提示 */
.pwd-tips {
font-size: 24rpx;
color: #999;
margin-bottom: 20rpx;
padding-left: 10rpx;
}
/* 确定按钮 */
.confirm-btn {
width: 100%;

View File

@@ -1,28 +1,21 @@
<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>
<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>
<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"
@@ -46,6 +39,9 @@
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;
@@ -187,6 +165,27 @@ const goBack = () => {
color: #333;
height: 100%;
}
/* 原生 input 垂直居中 */
.native-input {
line-height: 88rpx;
}
/* 深度覆盖 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;
}
.verify-input {
padding-right: 20rpx;
}
@@ -231,7 +230,7 @@ const goBack = () => {
color: #fff;
}
/* 禁用按钮边框 */
button:after{
.forget-btn:after{
border-radius: 8px;
}

View File

@@ -1,259 +1,300 @@
<template>
<view class="login-page">
<view class="status_bar"></view>
<!-- 登录标题 -->
<view class="login-title">手机验证码登录</view>
<!-- 登录标题 -->
<view class="login-title">{{ loginMode === 'sms' ? '手机验证码登录' : '账号密码登录' }}</view>
<!-- ====== 验证码登录模式 ====== -->
<template v-if="loginMode === 'sms'">
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="phone" size="24" color="#999"></uni-icons> -->
<image src="http://47.104.199.163:9090/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
<image src="https://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"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<!-- <input
class="input-content"
type="number"
placeholder="请输入手机号"
v-model="phoneNumber"
maxlength="11"
@input="checkForm"
/> -->
<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="https://wuye.ckdzkj.com/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
</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"
>
<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>
</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>
<!-- ====== 账号密码登录模式 ====== -->
<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>
<text class="agreement-text" @click.stop="goToPrivacy">我已阅读并同意<text class="agreePrivacy">用户隐私政策</text></text>
<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;
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 (!/^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) {
uni.showToast({ title: '请勾选协议', icon: 'none' });
return;
}
uni.showLoading({ title: '登录中...', mask: true });
let params = {
phone:phoneNumber.value,
smsCode:verifyCode.value,
scene:'login',
}
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'
// })
// setTimeout(() =>{
// uni.switchTab({ url: '/pages/index/index' });
// },1500)
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 handleLogin = () => {
if (loginMode.value === 'sms') {
handleSmsLogin()
} else {
handlePwdLogin()
}
}
// ========== 验证码登录 ==========
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)
}
}
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' })
}
}
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%;
@@ -291,7 +332,6 @@ onUnmounted(() => {
width: 50rpx;
height: 50rpx;
margin-right: 20rpx;
color: #999;
}
/* 输入框内容 */
@@ -302,6 +342,27 @@ onUnmounted(() => {
height: 100%;
}
/* 原生 input 垂直居中 */
.native-input {
line-height: 88rpx;
}
/* 深度覆盖 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;
}
/* 验证码输入框 */
.verify-input {
padding-right: 20rpx;
@@ -324,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;
}
/* 协议勾选项 */
.agreement-item {
/* 协议和忘记密码行 */
.agreement-wrap {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
margin: 70rpx 0 28rpx 0;
padding-left: 10rpx;
padding: 0 10rpx;
}
.agreement-item {
display: flex;
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.active{
color: #2F77FD;
} */
.forget-pwd {
font-size: 24rpx;
color: #666;
flex-shrink: 0;
margin-left: 16rpx;
line-height: 1.5;
}
/* 登录按钮 */
.login-btn {
width: 100%;
@@ -366,13 +447,11 @@ onUnmounted(() => {
margin-bottom: 60rpx;
}
/* 登录按钮禁用状态 */
.login-btn:disabled {
background-color: #99c8ff;
color: #fff;
/* border-radius: 50px; */
}
/* 禁用按钮边框 */
button:after {
border-radius: 8px;
}
@@ -384,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">
欢迎使用我们的产品/服务以下统称服务我们高度重视用户隐私保护
欢迎使用我们的产品/服务以下统称"服务"我们高度重视用户隐私保护
致力于为您提供安全可靠的服务体验本隐私政策旨在向您说明我们如何收集使用存储保护您的个人信息
以及您享有的相关权利请您在使用我们的服务前仔细阅读并理解本政策全部内容
一旦您使用我们的服务即视为您同意本政策的所有条款
我们收集的个人信息 为了向您提供更优质个性化的服务我们会根据服务功能的需要收集您主动提供或在使用服务过程中产生的个人信息
具体如下 您主动提供的信息 账户注册信息当您注册我们的账户时您可能需要提供姓名手机号码电子邮箱密码等信息用于身份验证账户登录及找回
</view>
</view>
<view class="section">
<view class="section-title">我们收集的个人信息</view>
<view class="section-text">
为了向您提供更优质个性化的服务我们会根据服务功能的需要收集您主动提供或在使用服务过程中产生的个人信息具体如下
</view>
<view class="section-text">
您主动提供的信息账户注册信息当您注册我们的账户时您可能需要提供姓名手机号码电子邮箱密码等信息用于身份验证账户登录及找回
服务相关信息根据您使用的具体服务您可能需要提供相关补充信息如使用客服咨询服务时提供问题描述及相关凭证使用交易类服务时提供收货地址支付相关信息等
自愿提交的信息您主动向我们反馈的意见建议或参与我们的问卷调查活动时提交的相关信息
若您通过第三方平台如微信支付宝QQ等登录我们的服务我们会根据您的授权
从第三方获取您的相关账户信息如昵称头像用于完成登录及账户绑定
我们不会获取第三方平台的其他未授权信息
若您通过第三方平台如微信支付宝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>

View File

@@ -1,279 +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="http://47.104.199.163:9090/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="http://47.104.199.163:9090/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 });
let params = {
username:phoneNumber.value,
password:password.value
}
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 => {
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%;
}
.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,11 +1,6 @@
<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-title">注册用户</view>
@@ -13,24 +8,21 @@
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/phone.png" mode=""
<image src="https://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" placeholder="请输入手机号"
:inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<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 src="https://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" placeholder="请输入验证码"
:inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<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后重新获取` : '获取验证码' }}
@@ -38,41 +30,43 @@
</view>
<!-- 账号输入框 -->
<view class="input-item-wrap">
<view class="input-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/account.png" mode=""
<image src="https://wuye.ckdzkj.com/images/login/account.png" mode=""
style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="person" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content" type="text" v-model="account" placeholder="请输入账号" :inputBorder="false"
:placeholderStyle="placeholderStyle">
</uni-easyinput>
<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="http://47.104.199.163:9090/images/login/password.png" mode=""
<image src="https://wuye.ckdzkj.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>
<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>
<!-- 协议勾选 -->
<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>
<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">
立即注册
@@ -82,6 +76,9 @@
<view class="login-link">
<navigator class="link-item" url="/pageSubPack/loginSub/login">已有账号去登录</navigator>
</view>
<!-- 隐私协议弹窗 -->
<privacy-popup v-model:show="showPrivacyPopup" @agree="agreePrivacy" @disagree="disagreePrivacy" @close="showPrivacyPopup = false" />
</view>
</template>
@@ -89,39 +86,43 @@
import {
ref,
computed,
watch,
onUnmounted
} from 'vue';
import {
getRegisterUser,
getSendCode
} from '../api/api.js'
import PrivacyPopup from '@/components/privacy-popup/privacy-popup.vue'
// 输入框类型:统一用 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;
// ========== 计算属性 ==========
// 是否可获取验证码(手机号格式正确)
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位
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) &&
@@ -130,25 +131,93 @@
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;
};
// 跳转隐私协议
const goToPrivacy = () => {
uni.navigateTo({
url: '/pageSubPack/loginSub/privacy',
})
// 账号失焦校验(禁止颜文字/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' })
}
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 = () => {
@@ -163,7 +232,6 @@
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
@@ -172,21 +240,18 @@
timer = null;
}
}, 1000);
} else {
uni.showToast({
title: '验证码发送失败',
icon: 'error'
icon: 'none'
});
}
}).catch(err => {
uni.showToast({
title: err.msg || '网络异常',
icon: 'error'
icon: 'none'
});
})
};
// 处理注册逻辑
@@ -215,6 +280,14 @@
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;
@@ -227,10 +300,18 @@
uni.showToast({ title: '密码不能超过 16 位', icon: 'none' });
return;
}
if (!agreeAgreement.value) {
uni.showToast({ title: '请勾选协议', icon: 'none' });
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
@@ -249,35 +330,26 @@
icon: 'success'
});
setTimeout(() => {
uni.redirectTo({
url: '/pageSubPack/loginSub/pwd-login'
uni.navigateTo({
url: '/pageSubPack/loginSub/login?mode=pwd'
});
}, 1500);
} else {
uni.showToast({
title: '注册失败',
icon: 'error'
icon: 'none'
});
}
}).catch((err) => {
uni.hideLoading();
uni.showToast({
title: err.msg || '网络异常',
icon: 'error'
icon: 'none'
});
})
// 注册成功后跳转到登录页
// uni.redirectTo({
// url: '/pages/login/pwd-login'
// });
// setTimeout(() => {
// }, 1500);
};
// ========== 生命周期 ==========
// 页面卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer);
@@ -289,8 +361,9 @@
/* 页面容器 */
.register-page {
padding: 20rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
height: 100vh;
overflow: hidden;
box-sizing: border-box;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF);
}
@@ -299,11 +372,9 @@
width: 100%;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
/* height: 88rpx; */
margin-bottom: 70rpx;
}
@@ -316,7 +387,6 @@
margin-right: 100rpx;
}
/* 注册标题 */
.register-title {
font-size: 40rpx;
font-weight: 600;
@@ -324,6 +394,15 @@
margin-bottom: 60rpx;
}
/* 输入框外层包装(用于承载错误提示) */
.input-item-wrap {
margin-bottom: 30rpx;
}
.input-item-wrap .input-item {
margin-bottom: 0;
}
/* 输入框通用样式 */
.input-item {
display: flex;
@@ -358,10 +437,38 @@
height: 100%;
}
/* 原生 input 垂直居中 */
.native-input {
line-height: 88rpx;
}
/* 深度覆盖 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;
}
.verify-input {
padding-right: 20rpx;
}
/* 输入框错误提示(在输入框下方) */
.input-error {
color: #e74c3c;
font-size: 22rpx;
line-height: 1.4;
padding: 10rpx 30rpx 0;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;

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

@@ -1,289 +1,359 @@
<template>
<view class="contentStyle" style="">
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar :title="type + '车辆'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y="true">
<!-- 表单区域 -->
<view class="form-container">
<!-- 车牌号 -->
<view class="form-item">
<view class="item-label">车牌号</view>
<input class="item-input" v-model="formData.plateNo" placeholder="请输入"
placeholder-class="input-placeholder" @blur="inputChange('车牌号')" />
<input class="item-input" v-model="formData.carNum" placeholder="请输入"
placeholder-class="input-placeholder" maxlength="8" @input="onCarNumInput" />
</view>
<!-- 车辆品牌选择 -->
<view class="form-item" @click="selectItem('车辆品牌')">
<view class="form-item">
<text class="item-label">车辆品牌</text>
<input class="item-input" v-model="formData.carBrand" placeholder="请输入"
placeholder-class="input-placeholder" maxlength="32"/>
</view>
<!-- <view class="form-item" @click="selectItem('车辆品牌')">
<text class="item-label">车辆品牌</text>
<text class="item-placeholder"
:class="{selected:formData.carBrandName}">{{ formData.carBrandName || '请选择' }}</text>
:class="{selected: formData.carBrand}">{{ formData.carBrand || '请选择' }}</text>
<text class="item-arrow"></text>
</view>
</view> -->
<!-- 车辆型号 -->
<view class="form-item">
<view class="item-label">车辆型号</view>
<input class="item-input" v-model="formData.model" placeholder="请输入"
placeholder-class="input-placeholder" @blur="inputChange('车辆型号')" />
<input class="item-input" v-model="formData.carModel" placeholder="请输入"
placeholder-class="input-placeholder" maxlength="32"/>
</view>
<!-- 车辆颜色 -->
<view class="form-item">
<view class="item-label">车辆颜色</view>
<input class="item-input" v-model="formData.color" placeholder="请输入"
placeholder-class="input-placeholder" @blur="inputChange('车辆颜色')" />
<input class="item-input" v-model="formData.carColor" placeholder="请输入"
placeholder-class="input-placeholder" maxlength="5"/>
</view>
<!-- 车辆图片上传 -->
<view class="form-item upload-item">
<view class="item-label">车辆图片</view>
<view class="upload-box" @click="chooseImage">
<!-- 未上传显示加号+文字 -->
<view v-if="!formData.carImg" class="upload-tip">
<view v-if="!carImageUpload.state.previewUrl" class="upload-tip">
<text class="plus-icon">+</text>
<text class="upload-text">上传照片</text>
</view>
<!-- 已上传显示图片 -->
<image v-else :src="formData.carImg" class="car-image" mode="aspectFill" />
<image v-else :src="carImageUpload.state.previewUrl" class="car-image" mode="aspectFill" />
</view>
</view>
</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>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
onReachBottom,
onPullDownRefresh,
onLoad,
onShow
} from '@dcloudio/uni-app'
import {
postCar,
putCar,
getCarDetail,
} from '@/pageSubPack/api/apiSub.js'
import {
useImageUpload
} from '/pageSubPack/api/useImageUpload.js'
},
const statusBarHeight = ref(20)
computed: {
const goBack = () => {
uni.navigateBack()
}
},
created() {
},
onShow() {
},
onLoad(option) {
const id = ref(undefined)
const type = ref('')
const isSubmitting = ref(false)
const formData = ref({
carNum: '',
carBrand: '',
carModel: '',
carColor: '',
carImageUrl: '',
carBrandId: ''
})
onLoad((option) => {
if (uni.getStorageSync('operationType') == 'update') {
this.id = option.id;
this.type = '修改';
//根据id获取回显数据
this.formData.plateNo = '京A88888';
this.formData.carBrandName = '宝马';
this.formData.model = 'X5';
this.formData.color = '白色';
this.formData.carImg = 'http://47.104.199.163:9090/images/propertyImgs/carImg.png';
this.formData.carBrandId = '1';
if (option.id) {
id.value = option.id
uni.setStorageSync('parkingSpaceId', option.id)
loadCarDetail()
} else {
id.value = uni.getStorageSync('parkingSpaceId')
}
type.value = '修改'
} else if (uni.getStorageSync('operationType') == 'add') {
this.id = undefined;
this.type = '添加';
id.value = undefined
type.value = '添加'
}
},
})
onMounted(() => {
})
const loadCarDetail = async () => {
// 防重复请求
try {
const res = await getCarDetail({
id: id.value
})
const data = res.data
formData.value.carNum = data.carNum
// uni.setStorageSync('carNum', data.carNum)
formData.value.carBrand = data.carBrand
// uni.setStorageSync('carBrand', data.carBrand)
formData.value.carModel = data.carModel
// uni.setStorageSync('carModel', data.carModel)
formData.value.carColor = data.carColor
// uni.setStorageSync('carColor', data.carColor)
formData.value.carImageUrl = data.carImageUrl
// uni.setStorageSync('carImageUrl', data.carImageUrl)
formData.value.carBrandId = data.carBrandId
// uni.setStorageSync('carBrandId', data.carBrandId)
// 使用 hook 设置回显图片
carImageUpload.setPreviewUrl(data.carImageUrl?.url || data.carImageUrl || '')
} catch (err) {
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
})
} finally {
onUnload() {},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
id: undefined,
type: '',
// 表单数据
formData: {
plateNo: uni.getStorageSync('plateNo'),
carBrandName: uni.getStorageSync('carBrandName'),
model: uni.getStorageSync('model'),
color: uni.getStorageSync('color'),
carImg: uni.getStorageSync('carImg') // 车辆图片路径
}
}
},
methods: {
inputChange(type) {
if (type == '车牌号') {
const carImageUpload = useImageUpload({
uploadUrl: '/api/resident/file/uploadImage'
})
uni.setStorageSync('plateNo', this.formData.plateNo)
} else if (type == '车辆型号') {
uni.setStorageSync('model', this.formData.model)
} else if (type == '车辆颜色') {
uni.setStorageSync('color', this.formData.color)
// 车牌号输入限制:省份简称(中文)+ 城市代码(字母)+ 序号(字母数字)
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
}
},
// 选择图片(上传照片)
chooseImage() {
uni.chooseImage({
count: 1, // 只选1张
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"], // 相册/相机
success: (res) => {
// 赋值图片路径
uni.setStorageSync('carImg', res.tempFilePaths[0]);
this.formData.carImg = res.tempFilePaths[0];
const inputChange = (typeStr) => {
if (typeStr == '车牌号') {
uni.setStorageSync('carNum', formData.value.carNum)
} else if (typeStr == '车辆型号') {
uni.setStorageSync('carModel', formData.value.carModel)
} else if (typeStr == '车辆品牌') {
uni.setStorageSync('brand', formData.value.brand)
} else if (typeStr == '车辆颜色') {
uni.setStorageSync('carColor', formData.value.carColor)
}
}
});
},
// 保存车辆信息
saveCar() {
// 表单校验
if (!this.formData.plateNo) {
const chooseImage = async () => {
try {
await carImageUpload.chooseImage()
} catch (err) {
// 用户取消选择
}
}
// 车牌号正则:省份简称 + 城市代码字母 + 序号
// 蓝牌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 (!this.formData.carBrandName) {
if (!CAR_NUM_REGEX.test(formData.value.carNum)) {
return uni.showToast({
title: "请输入车辆品牌",
title: "请输入正确的车牌号",
icon: "none"
});
}
if (!this.formData.model) {
return uni.showToast({
title: "请输入车辆型号",
icon: "none"
});
}
if (!this.formData.color) {
return uni.showToast({
title: "请输入车辆颜色",
icon: "none"
});
})
}
uni.showModal({
title: '确认提交',
content: '保存后进入审核',
success: (res) => {
isSubmitting.value = true
uni.showLoading({
title: '提交中...',
mask: true
});
setTimeout(() => {
})
try {
let carImageUrl = ''
// 编辑模式
if (id.value) {
if (carImageUpload.state.tempFilePath) {
carImageUrl = await carImageUpload.uploadImage()
} else {
carImageUrl = carImageUpload.state.serverUrl || carImageUpload.state.previewUrl || ''
}
} else {
// 新增模式:必须上传
carImageUrl = await carImageUpload.uploadImage()
}
submitCarData(carImageUrl)
} catch (err) {
isSubmitting.value = false
uni.hideLoading()
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
})
}
}
const submitCarData = (carImageUrl) => {
// 处理 carImageUrl 可能是对象 { url: string } 的情况
const imageUrl = typeof carImageUrl === 'object' ? carImageUrl.url : carImageUrl
const params = {
carNum: formData.value.carNum,
carBrand: formData.value.carBrand,
// carBrandId: formData.value.carBrandId,
carModel: formData.value.carModel,
carColor: formData.value.carColor,
carImageUrl: imageUrl,
villageId:uni.getStorageSync('currentVillageId'),
userId: uni.getStorageSync('userId')
}
// 编辑模式下添加 id
if (id.value) {
params.id = id.value
putCar(params).then(res => {
console.log(res);
uni.hideLoading()
if (res.code === 200) {
uni.showToast({
title: '提交成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
}
})
},
// 选择项点击事件可替换为picker/弹窗选择)
selectItem(type) {
if (type === '车辆品牌') {
uni.redirectTo({
url: '/pageSubPack/my/selectCarBrand',
success: res => {},
fail: () => {},
complete: () => {}
});
setTimeout(() => {
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 {
postCar(params).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({
title: '提交成功',
icon: 'success'
})
setTimeout(() => {
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'
})
})
}
},
goBack() {
uni.removeStorageSync('plateNo');
uni.removeStorageSync('carBrandName');
uni.removeStorageSync('model');
uni.removeStorageSync('color');
uni.removeStorageSync('carImg');
uni.removeStorageSync('carBrandId');
uni.removeStorageSync('checkType');
// if (uni.getStorageSync('operationType') == 'update') {
// uni.redirectTo({
// url: '/pageSubPack/my/houseDetail?id=' + this.id,
// success: res => {},
// fail: () => {},
// complete: () => {}
// });
// } else if (uni.getStorageSync('operationType') == 'add') {
uni.redirectTo({
url: '/pageSubPack/my/myCarIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
// }
},
},
}
const selectItem = (typeStr) => {
if (typeStr === '车辆品牌') {
uni.navigateTo({
url: '/pageSubPack/my/selectCarBrand'
})
}
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
</style>
<style>
/* 全局样式去掉switch的×保证开关纯净 */
.my-switch::before {
display: none !important;
}
.my-switch {
background-color: #e5e5e5 !important;
border: none !important;
width: 90rpx !important;
height: 48rpx !important;
border-radius: 50rpx !important;
}
.my-switch[checked="true"] {
background-color: #1677ff !important;
}
.my-switch::after {
width: 44rpx !important;
height: 44rpx !important;
border-radius: 50% !important;
margin: 2rpx !important;
}
</style>
<style scoped>
.status-bar {
width: 100vw;
@@ -294,7 +364,6 @@
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
@@ -306,66 +375,10 @@
background-color: #fff;
}
/* 步骤条 */
.step-bar {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 30rpx 20rpx;
}
.step-item {
display: flex;
flex-direction: column;
align-items: center;
}
.step-circle {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background-color: #ccc;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
margin-bottom: 16rpx;
}
.step-item.active .step-circle {
background-color: #1677ff;
}
.step-text {
font-size: 28rpx;
color: #999;
}
.step-item.active .step-text {
color: #1677ff;
font-weight: 500;
}
.step-line {
width: 150rpx;
height: 4rpx;
background-color: #ccc;
margin: 0 20rpx;
margin-bottom: 40rpx;
}
/* 表单容器 */
.form-container {
padding: 0 0px;
/* height: calc(100vh - 240rpx); */
/* overflow-y: scroll; */
}
/* 表单项 */
.form-item {
display: flex;
align-items: center;
@@ -392,7 +405,6 @@
color: #333;
}
.item-arrow {
display: inline-block;
width: 10rpx;
@@ -404,12 +416,6 @@
margin-right: 10rpx;
}
/* 开关项 */
.switch-item {
justify-content: space-between;
}
/* 底部按钮 */
.bottom-btn-wrap {
padding: 20rpx 30rpx 40rpx;
display: flex;
@@ -427,174 +433,7 @@
border-radius: 12rpx;
border: none;
}
</style>
<style>
/* 注意:这里不能加 scoped必须写全局样式 */
/* 隐藏 switch 关闭状态的 × 号 */
.no-cross-switch::before {
display: none !important;
}
.no-cross-switch {
transform: scale(0.85);
border-radius: 50rpx !important;
}
.no-cross-switch>>>.uni-switch-input-checked {
background-color: #007aff !important;
}
</style>
<style scoped>
/* 表单项 —— 和你页面保持一致 */
.form-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.label {
font-size: 28rpx;
color: #000;
width: 90px;
font-weight: 600;
}
.value {
flex: 1;
text-align: left;
font-size: 28rpx;
color: #999;
margin-right: 10rpx;
}
.arrow {
font-size: 28rpx;
color: #ccc;
}
/* 遮罩 */
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 99;
}
/* 底部弹出框 */
.popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-radius: 20rpx 20rpx 0 0;
z-index: 100;
max-height: 60vh;
}
.popup-header {
padding: 30rpx;
text-align: center;
border-bottom: 1rpx solid #f0f0f0;
position: relative;
}
.title {
font-size: 32rpx;
font-weight: 500;
}
.close {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
font-size: 36rpx;
color: #666;
}
/* 选项列表 */
.list {
padding: 0 30rpx;
}
.item {
padding: 30rpx 0;
text-align: center;
font-size: 28rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.item:last-child {
border-bottom: none;
}
</style>
<style scoped>
/* 表单 */
.form-item {
display: flex;
align-items: center;
padding: 28rpx 0;
border-bottom: 1rpx solid #f2f2f2;
position: relative;
}
.label {
width: 180rpx;
font-size: 28rpx;
color: #333;
}
.input {
flex: 1;
font-size: 28rpx;
text-align: left;
color: #333;
}
.ph {
color: #999 !important;
}
.arrow-item::after {
content: '>';
position: absolute;
right: 0;
color: #ccc;
font-size: 28rpx;
}
.value {
flex: 1;
text-align: left;
font-size: 28rpx;
color: #999;
padding-right: 40rpx;
}
.item-input {
flex: 1;
height: 40rpx;
font-size: 28rpx;
color: #333;
border: none;
outline: none;
background: transparent;
}
.input-placeholder {
color: #999 !important;
font-size: 28rpx !important;
}
/* ===================== 图片上传区域 ===================== */
.upload-item {
align-items: flex-start;
padding-top: 40rpx;
@@ -639,100 +478,18 @@
border-radius: 8rpx;
}
/* 弹窗遮罩 */
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 99;
}
.popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-radius: 20rpx 20rpx 0 0;
z-index: 100;
max-height: 60vh;
}
.popup-header {
padding: 30rpx;
text-align: center;
border-bottom: 1rpx solid #eee;
font-size: 32rpx;
font-weight: 500;
position: relative;
}
.close {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
font-size: 36rpx;
color: #666;
}
.popup-list {
padding: 0 30rpx;
max-height: 40vh;
overflow-y: auto;
}
.item {
padding: 30rpx 0;
text-align: center;
.item-input {
flex: 1;
height: 40rpx;
font-size: 28rpx;
border-bottom: 1rpx solid #f5f5f5;
color: #333;
border: none;
outline: none;
background: transparent;
}
/* 成功提示弹窗 */
.toast-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 9999;
}
.toast-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 40rpx 60rpx;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.toast-icon {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 50%;
background-color: #fff;
color: #1677ff;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.toast-text {
color: #fff;
font-size: 28rpx;
.input-placeholder {
color: #999 !important;
font-size: 28rpx !important;
}
</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 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>
<!-- 身份证上传 -->
@@ -167,7 +153,7 @@
<!-- 身份证人像面 -->
<view class="upload-item" @click="chooseIdCard('front')">
<!-- 已上传 显示图片 -->
<image :src="idCardFrontUpload.previewUrl || frontSideOfIDcardSrc"
<image :src="idCardFrontUpload.state.previewUrl || frontSideOfIDcardSrc"
class="preview-img" mode="aspectFit">
</image>
@@ -175,7 +161,7 @@
<!-- 身份证国徽面 -->
<view class="upload-item" @click="chooseIdCard('back')">
<image :src="idCardBackUpload.previewUrl || backSideOfIDcardSrc" class="preview-img"
<image :src="idCardBackUpload.state.previewUrl || backSideOfIDcardSrc" class="preview-img"
mode="aspectFit">
</image>
@@ -191,6 +177,29 @@
</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">
@@ -206,6 +215,7 @@
</view>
</view>
<!-- 与业主关系弹窗 -->
<view v-if="showRelation" class="mask" @click="showRelation=false"></view>
<view v-if="showRelation" class="popup">
<view class="popup-header">
@@ -219,14 +229,6 @@
</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>
@@ -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'
@@ -639,16 +745,15 @@
title: '请输入6位验证码',
icon: 'none'
})
if (!idCardFrontUpload.previewUrl) return uni.showToast({
if (!idCardFrontUpload.state.previewUrl) return uni.showToast({
title: '请上传身份证人像面',
icon: 'none'
})
if (!idCardBackUpload.previewUrl) return uni.showToast({
if (!idCardBackUpload.state.previewUrl) return uni.showToast({
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
@@ -672,16 +776,16 @@
// 编辑模式
if (houseId.value) {
// 如果重新选择了图片,需要上传
if (idCardFrontUpload.tempFilePath) {
if (idCardFrontUpload.state.tempFilePath) {
cardImageFrontUrl = await idCardFrontUpload.uploadImage()
} else {
const frontUrl = idCardFrontUpload.serverUrl || idCardFrontUpload.previewUrl
const frontUrl = idCardFrontUpload.state.serverUrl || idCardFrontUpload.state.previewUrl
cardImageFrontUrl = typeof frontUrl === 'string' ? frontUrl : ''
}
if (idCardBackUpload.tempFilePath) {
if (idCardBackUpload.state.tempFilePath) {
cardImageBackUrl = await idCardBackUpload.uploadImage()
} else {
const backUrl = idCardBackUpload.serverUrl || idCardBackUpload.previewUrl
const backUrl = idCardBackUpload.state.serverUrl || idCardBackUpload.state.previewUrl
cardImageBackUrl = typeof backUrl === 'string' ? backUrl : ''
}
} else {
@@ -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;

File diff suppressed because it is too large Load Diff

View File

@@ -1,192 +1,207 @@
<template>
<view class="contentStyle" style="">
<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 v-if="carList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
<text class="empty-text">暂无数据</text>
</view>
<view class="car-item" v-for="(item, index) in carList" :key="index" @click="handleItemClick(item, index)"
:class="{ checked: selectedIndex === index }">
<!-- 左侧圆形占位 -->
<image :src="item.imgSrc" mode="aspectFill" class="car-avatar" @error="item.imgSrc = defaultCarImg" />
<!-- 中间车辆信息 -->
<image :src="item.carImageUrl" mode="aspectFill" class="car-avatar"
@error="item.carImageUrl = defaultCarImg" />
<view class="car-info">
<view class="car-plate">{{ item.plate }}</view>
<view class="car-model">{{ item.model }}·{{ item.color }}</view>
<view class="car-plate">{{ item.carNum }}</view>
<view class="car-model">{{ item.carBrand }}·{{ item.carColor }}</view>
<view class="car-bindParkingSpace">{{ item.bindParkingSpace }}</view>
</view>
<!-- 右侧状态标签 -->
<view class="status-tag" :class="{ bound: item.status === '已绑定', unbound: item.status === '未绑定' }">
{{ item.status }}
<!-- <view class="status-tag"
:class="{ underReview: item.certificationStatus === '审核中', unbound: item.certificationStatus === '未绑定', bounded: item.certificationStatus === ''}">
{{ item.certificationStatus }}
</view> -->
</view>
</view>
</scroll-view>
<!-- 底部确定绑定按钮 -->
<view class="bottom-btn">
<button class="confirm-btn" @click="confirmBind">确定绑定</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted,
},
} from 'vue'
import {
onReachBottom,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
getMyCarList,
postCarBind
} from '@/pageSubPack/api/apiSub.js'
computed: {
const statusBarHeight = ref(20)
const defaultCarImg = "http://47.104.199.163:9090/images/propertyImgs/defaultCarImg.png"
const selectedId = ref('')
const selectedName = ref('')
const selectedIndex = ref(-1)
const carList = ref([])
const loadingMore = ref(false)
const noMoreData = ref(false)
},
const pageNum = ref(1)
const pageSize = ref(10)
// ==================== 获取列表数据 ====================
const getList = async () => {
// 防重复请求
created() {
if (loadingMore.value || noMoreData.value) return
},
onShow() {
loadingMore.value = true
try {
const res = await getMyCarList({
pageNum: pageNum.value,
pageSize: pageSize.value,
userId: uni.getStorageSync('userId')
})
},
onUnload() {
const data = res
const newList = data.rows || []
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
defaultCarImg: "http://47.104.199.163:9090/images/propertyImgs/defaultCarImg.png",
searchKey: "",
selectedId: '',
selectedName: '',
selectedIndex: -1, // 当前选中的索引
// 模拟车辆数据
carList: [{
id: '1',
imgSrc: 'http://47.104.199.163:9090/images/propertyImgs/carImg.png',
plate: "京A88888",
model: "宝马X5",
color: "白色",
status: "已绑定"
},
{
id: '2',
imgSrc: 'https://错误地址2.png',
plate: "京A88888",
model: "宝马X5",
color: "白色",
status: "未绑定"
},
{
id: '3',
imgSrc: 'https://错误地址3.png',
plate: "京A88888",
model: "宝马X5",
color: "白色",
status: "未绑定"
// 第一页 → 覆盖数据
if (pageNum.value === 1) {
carList.value = newList
} else {
// 后续页 → 追加数据
carList.value = [...carList.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() // 关闭下拉刷新
}
}
// ==================== 上拉加载更多(页面生命周期) ====================
onReachBottom(() => {
getList()
})
// ==================== 下拉刷新 ====================
const refresh = () => {
// 重置所有状态
pageNum = 1
pageSize = 10
noMoreData = false
carList = []
loadingMore = false
getList()
}
onPullDownRefresh(() => {
refresh()
})
// 页面加载时请求第一页
onMounted(() => {
getList()
})
methods: {
const handleItemClick = (item, index) => {
selectedIndex.value = index
selectedId.value = item.id
selectedName.value = item.carNum
}
// 选择绑定车辆
handleItemClick(item, index) {
this.selectedIndex = index;
this.selectedId = item.id;
this.selectedName = item.plate;
},
// 确定绑定
confirmBind() {
if (this.selectedIndex === -1) {
const confirmBind = () => {
if (selectedIndex.value === -1) {
return uni.showToast({
title: "请先选择绑定车辆",
icon: "none",
});
title: "请先选择车辆",
icon: "none"
})
}
uni.showModal({
title: "确认选择",
content: `确定绑定该车辆吗?`,
content: `确定选择该车辆吗?`,
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '绑定中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '绑定成功',
title: '选择成功',
icon: 'success'
});
}, 1000);
})
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
uni.setStorageSync('bindCarId', this.selectedId);
uni.setStorageSync('bindCarName', this.selectedName);
this.goBack();
}, 2500);
uni.setStorageSync('bindCarId', selectedId.value)
uni.setStorageSync('bindCarName', selectedName.value)
goBack()
}, 1500)
// postCarBind({
// carId: selectedId.value
// }).then(bindRes => {
// uni.hideLoading()
// if (bindRes.code === 200) {
// uni.showToast({
// title: '绑定成功',
// icon: 'success'
// })
// setTimeout(() => {
// uni.setStorageSync('bindCarId', selectedId.value)
// uni.setStorageSync('bindCarName', selectedName.value)
// goBack()
// }, 1500)
// } else {
// uni.showToast({
// title: bindRes.msg || '绑定失败',
// icon: 'none'
// })
// }
// })
}
}
})
}
},
});
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
overflow: hidden;
// height: 100vh;
}
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background-color: #fff; */
}
.page {
@@ -196,7 +211,6 @@
box-sizing: border-box;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
@@ -213,96 +227,10 @@
margin-bottom: 10rpx;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 通用输入框样式 */
.input-item {
font-size: 30rpx;
color: #333;
}
.input-placeholder {
color: #999;
font-size: 30rpx;
}
/* 确定按钮 */
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #1677ff;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
margin-top: 20rpx;
}
/* 成功提示弹窗 */
.toast-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 9999;
}
.toast-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 40rpx 60rpx;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.toast-icon {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 50%;
background-color: #fff;
color: #1677ff;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.toast-text {
color: #fff;
font-size: 28rpx;
}
</style>
<style scoped>
/* 页面整体 */
/* ====================== 列表容器 ====================== */
.list-container {
/* flex: 1; */
padding: 40rpx;
min-height: calc(100vh - 105px);
overflow-y: scroll;
}
/* ====================== 绑定车辆卡片 ====================== */
/* 单个车辆项 */
.car-item {
display: flex;
align-items: center;
@@ -316,11 +244,9 @@
.car-item.checked {
background: #f4f7ff;
/* 浅蓝色背景 */
border-color: #1677ff;
}
/* 左侧圆形头像占位 */
.car-avatar {
width: 100rpx;
height: 100rpx;
@@ -330,31 +256,6 @@
flex-shrink: 0;
}
/* 中间信息区域 */
.car-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
/* 车牌号 */
.car-plate {
font-size: 32rpx;
font-weight: bold;
color: #000;
line-height: 1.4;
margin-bottom: 8rpx;
}
/* 车型+颜色 */
.car-model {
font-size: 28rpx;
color: #666;
line-height: 1.4;
}
/* 状态标签 */
.status-tag {
font-size: 24rpx;
padding: 6rpx 14rpx;
@@ -366,22 +267,62 @@
top: 30rpx;
}
/* 未绑定状态(浅绿色背景+绿色文字) */
.status-tag.unbound {
/* background-color: #e6f7e6; */
color: #999;
}
.status-tag.bounded {
color: #2F77FD;
}
.status-tag.underReview {
color: #FF8F40;
}
.status-tag.unpass {
color: red;
}
.car-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.car-plate {
font-size: 32rpx;
font-weight: bold;
color: #000;
line-height: 1.4;
margin-bottom: 8rpx;
}
.car-model {
font-size: 28rpx;
color: #666;
line-height: 1.4;
}
.status-tag {
font-size: 24rpx;
padding: 6rpx 14rpx;
border-radius: 6rpx;
font-weight: 500;
flex-shrink: 0;
position: absolute;
right: 30rpx;
top: 30rpx;
}
.status-tag.unbound {
color: #999999;
}
/* 已绑定状态(可扩展,比如蓝色) */
.status-tag.bound {
/* background-color: #e6f0ff; */
color: #1677ff;
}
/* ====================== 底部按钮 ====================== */
.confirm-btn {
width: 100%;
height: 88rpx;
@@ -392,9 +333,4 @@
border: none;
line-height: 88rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -29,7 +29,7 @@
<!-- 小区名称 -->
<view class="community-name">
<text class="title-line"></text>
<text class="title-text">{{ item.community }}</text>
<text class="title-text">{{ item.villageName }}</text>
</view>
<!-- 选中对勾 -->
<view class="check-icon" :class="{ checked: selectedIndex === index }"></view>
@@ -66,162 +66,159 @@
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onReachBottom,
onPullDownRefresh,
onLoad
} from '@dcloudio/uni-app'
import {
getVillageList
} from '/pageSubPack/api/apiSub.js'
},
computed: {
// 过滤后的列表(搜索功能)
filteredList() {
this.selectedIndex = -1
if (!this.searchKey.trim()) {
return this.houseList
}
return this.houseList.filter(item =>
item.community.includes(this.searchKey.trim())
)
},
},
created() {
},
onShow() {
},
onUnload() {
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
searchKey: "",
selectedId: '',
selectedName: '',
selectedIndex: -1, // 当前选中的索引
// 房屋状态颜色映射
statusColor: {
const statusBarHeight = 20
const searchKey = ref("")
const selectedId = ref('')
const selectedName = ref('')
const selectedIndex = ref(-1)
/* 搜索状态 */
const onSearch = () => {
// 实时过滤computed 自动处理
}
onLoad(() => {
getList()
})
/* 房屋状态颜色映射 */
const statusColor = {
自住: "#409EFF",
闲置: "#67C23A",
租赁: "#E6A23C",
其他: "#909399",
},
// 模拟房屋数据
houseList: [{
}
/* 模拟房屋数据 */
const communityList = ref([{
id: 1,
community: "北境碧桂园小区(别墅)",
villageName: "北境碧桂园小区(别墅)",
roomNo: "2号楼2层",
owner: "张三",
status: "自住",
},
{
id: 1,
community: "北境碧桂园小区(别墅)",
villageName: "北境碧桂园小区(别墅)",
roomNo: "2号楼1层",
owner: "张三",
status: "闲置",
},
],
])
/* 过滤后的列表(搜索功能) */
const filteredList = computed(() => {
selectedIndex.value = -1
if (!searchKey.value.trim()) {
return communityList.value
}
},
return communityList.value.filter(item =>
item.villageName.includes(searchKey.value.trim())
)
})
/* 选择房屋 */
const selectHouse = (item, index) => {
selectedIndex.value = index
selectedId.value = item.id
selectedName.value = item.villageName + item.roomNo
}
methods: {
// 搜索输入事件
onSearch() {
// 实时过滤computed 自动处理
},
// 选择房屋
selectHouse(item, index) {
this.selectedIndex = index;
this.selectedId = item.id;
this.selectedName = item.community + item.roomNo;
},
// 确定绑定
confirmBind() {
if (this.selectedIndex === -1) {
/* 确定绑定 */
const confirmBind = () => {
if (selectedIndex.value === -1) {
return uni.showToast({
title: "请先选择房屋",
icon: "none",
});
})
}
uni.showModal({
title: "确认选择",
content: `确定绑定 ${this.selectedName} 吗?`,
content: `确定绑定 ${selectedName.value} 吗?`,
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '绑定中...',
mask: true
});
})
setTimeout(() => {
uni.showToast({
title: '绑定成功',
icon: 'success'
});
}, 1000);
})
}, 1000)
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
uni.setStorageSync('bindHouseId', this.selectedId);
uni.setStorageSync('bindHouseName', this.selectedName);
uni.setStorageSync('checkType', '停车场');
uni.removeStorageSync('paringLotId');
uni.removeStorageSync('paringLotName');
uni.removeStorageSync('parkingSpaceId');
uni.removeStorageSync('parkingSpaceName');
this.goNext();
}, 2500);
uni.hideLoading()
uni.setStorageSync('bindHouseId', selectedId.value)
uni.setStorageSync('bindHouseName', selectedName.value)
uni.setStorageSync('checkType', '停车场')
uni.removeStorageSync('paringLotId')
uni.removeStorageSync('paringLotName')
uni.removeStorageSync('parkingSpaceId')
uni.removeStorageSync('parkingSpaceName')
goNext()
}, 2500)
}
},
});
})
}
const getList = async () => {
try {
const res = await getVillageList({
},
})
console.log(res);
const data = res
communityList.value = data.data || []
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
goNext() {
uni.redirectTo({
}
}
const goNext = () => {
uni.navigateTo({
url: '/pageSubPack/my/selectParingLot',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
})
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
overflow: hidden;
// height: 100vh;
}
</style>
@@ -230,8 +227,6 @@
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background-color: #fff; */
}
.page {
@@ -246,8 +241,6 @@
padding: 20rpx 30rpx 40rpx;
}
/* 通用输入框样式 */
.input-item {
font-size: 30rpx;
@@ -272,55 +265,6 @@
margin-top: 20rpx;
}
/* 成功提示弹窗 */
.toast-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 9999;
}
.toast-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 40rpx 60rpx;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.toast-icon {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 50%;
background-color: #fff;
color: #1677ff;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.toast-text {
color: #fff;
font-size: 28rpx;
}
</style>
<style scoped>
/* 页面整体 */
/* ====================== 搜索框样式1:1还原 ====================== */
/* 搜索框 */
.search-box {
display: flex;
@@ -350,8 +294,9 @@
color: #999;
}
/* ====================== 列表容器 ====================== */
/* 列表容器 */
.list-container {}
/* 空状态 */
.empty-state {
display: flex;
@@ -369,7 +314,7 @@
margin-bottom: 10rpx;
}
/* ====================== 房屋卡片 ====================== */
/* 房屋卡片 */
.house-card {
border: 1rpx solid #e5e5e5;
border-radius: 12rpx;
@@ -418,21 +363,19 @@
.info-label {
width: 160rpx;
font-size: 28rpx;
/* font-weight: 600; */
color: #000;
}
.info-value {
font-size: 28rpx;
color: #333;
/* font-weight: 600; */
}
.status-text {
font-weight: 500;
}
/* ====================== 选中对勾 ====================== */
/* 选中对勾 */
.check-icon {
position: absolute;
right: 20rpx;
@@ -442,7 +385,6 @@
height: 52rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
/* background-color: #ccc; */
}
.check-icon::after {
@@ -473,14 +415,6 @@
border: none;
}
.title-line {
width: 8rpx;
height: 36rpx;
background-color: #007AFF;
margin-right: 12rpx;
border-radius: 4rpx;
}
.status-bar {
width: 100vw;
height: 46px;

View File

@@ -1,11 +1,7 @@
<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">
<scroll-view class="page" scroll-y="true">
<!-- 车辆信息 -->
<view class="info-card">
<view class="card-title">
<view class="title-line"></view>
@@ -13,152 +9,166 @@
</view>
<view class="info-row">
<text class="info-label">车牌号</text>
<text class="info-value">{{form.plateNo}}</text>
<text class="info-value">{{ form.carNum }}</text>
</view>
<view class="info-row">
<text class="info-label">车辆品牌</text>
<text class="info-value">{{form.carBrandName}}</text>
<text class="info-value">{{ form.carBrand }}</text>
</view>
<view class="info-row">
<text class="info-label">车辆型号</text>
<text class="info-value">{{form.model}}</text>
<text class="info-value">{{ form.carModel }}</text>
</view>
<view class="info-row">
<text class="info-label">车身颜色</text>
<text class="info-value">{{form.color}}</text>
<text class="info-value">{{ form.carColor }}</text>
</view>
<!-- 车辆图片区-->
<view class="photo-row">
<text class="info-label" style="width: 200rpx;">车辆图片</text>
<view class="photo-list">
<image class="photo-item" :src="form.carImg"></image>
<image class="photo-item" :src="form.carImageUrl"></image>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作按钮 -->
<view class="bottom-btn-wrap">
<!-- 仅认证失败显示修改按钮 -->
<button class="btn-modify" @click="clickUpdate()">
修改
</button>
<button class="btn-delete" @click="onDelete">
删除
</button>
<button class="btn-modify" @click="clickUpdate">修改</button>
<button class="btn-delete" @click="onDelete">删除</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
onReachBottom,
onPullDownRefresh,
onLoad,
onShow
} from '@dcloudio/uni-app'
import {
getCarDetail,
deleteCar
} from '@/pageSubPack/api/apiSub.js'
},
const goBack = () => {
uni.navigateBack()
}
computed: {
const statusBarHeight = ref(20)
const currentStatus = ref('')
const isDeleting = ref(false)
const isUpdating = ref(false)
const id = ref(undefined)
const form = ref({
plateNo: '',
carBrandName: '',
model: '',
color: '',
carImg: ''
})
let isFirstLoad = true
onLoad((options) => {
console.log(options);
id.value = options.id
currentStatus.value = options.status
loadCarDetail(options.id)
})
},
created() {
onShow(() => {
// 首次加载由 onLoad 处理,仅从编辑页返回时刷新
if (isFirstLoad) {
isFirstLoad = false
return
}
isUpdating.value = false
if (id.value) {
loadCarDetail()
}
})
},
onShow() {
},
onLoad(option) {
this.id = option.id;
this.currentStatus = option.status;
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
currentStatus: '',
// 可通过页面传参动态修改状态success / pending / fail
id: undefined,
form: {
plateNo: '北境碧桂园小区小区',
carBrandName: '101栋2单元402',
model: '张三',
color: '租赁',
carImg: 'http://47.104.199.163:9090/images/propertyImgs/carImg.png',
},
const loadCarDetail = async () => {
// 防重复请求
try {
const res = await getCarDetail({
id: id.value
})
const data = res.data
form.value = data
console.log(form.value);
} catch (err) {
uni.showToast({
title: err.msg,
icon: 'none'
})
// console.error('列表接口报错:', err)
} finally {
}
},
methods: {
// 修改按钮事件(仅认证失败显示)
clickUpdate() {
}
const clickUpdate = () => {
if (isUpdating.value) return
isUpdating.value = true
uni.setStorageSync('operationType', 'update')
uni.redirectTo({
url: '/pageSubPack/my/addCar?id=' + this.id,
success: res => {},
fail: () => {},
complete: () => {}
});
},
uni.navigateTo({
url: '/pageSubPack/my/addCar?id=' + id.value
})
}
// 删除按钮事件
onDelete() {
const onDelete = () => {
if (isDeleting.value) return
isDeleting.value = true
uni.showModal({
title: "确认删除",
content: "确认删除此车辆信息吗?",
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '删除中...',
mask: true
});
setTimeout(() => {
})
deleteCar({
carId: id.value
}).then(deleteRes => {
uni.hideLoading()
if (deleteRes.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'success'
});
}, 1000);
})
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
uni.navigateBack()
}, 1500)
} else {
isDeleting.value = false
uni.showToast({
title: deleteRes.msg || '删除失败',
icon: 'none'
})
}
},
});
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/my/myCarIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}).catch(() => {
isDeleting.value = false
uni.hideLoading()
uni.showToast({
title: '删除失败',
icon: 'none'
})
})
} else {
isDeleting.value = false
}
}
})
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
@@ -166,11 +176,15 @@
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f5f7fa;
}
@@ -181,126 +195,6 @@
box-sizing: border-box;
}
/* 顶部状态区 */
.status-header {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 30rpx 30rpx;
}
.status-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.icon-success {
background-color: #e6f7ee;
border: 4rpx solid #00b42a;
}
.icon-pending {
background-color: #e6f0ff;
border: 4rpx solid #1677ff;
}
.icon-fail {
background-color: #ffe7e7;
border: 4rpx solid #f53f3f;
}
.icon-text {
font-size: 40rpx;
font-weight: bold;
color: inherit;
}
.icon-success .icon-text {
color: #00b42a;
}
.icon-pending .icon-text {
color: #1677ff;
}
.icon-fail .icon-text {
color: #f53f3f;
}
.status-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 40rpx;
}
/* 步骤条 */
.step-bar {
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
padding: 0 20rpx;
}
.step-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
flex: 1;
}
.step-circle {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background-color: #1677ff;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
z-index: 2;
}
.step-item.active .step-circle {
background-color: #1677ff;
}
.step-item:not(.active) .step-circle {
background-color: #e5e5e5;
color: #999;
}
.step-text {
font-size: 24rpx;
color: #666;
margin-top: 10rpx;
text-align: center;
}
.step-line {
position: absolute;
top: 20rpx;
left: 50%;
width: 100%;
height: 4rpx;
background-color: #e5e5e5;
z-index: 1;
}
.step-line.active {
background-color: #1677ff;
}
/* 信息卡片 */
.info-card {
background-color: #fff;
padding: 20rpx;
@@ -329,10 +223,6 @@
align-items: center;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
width: 180rpx;
font-size: 28rpx;
@@ -345,12 +235,6 @@
flex: 1;
}
.status-tag {
color: #ff7d00;
font-weight: 500;
}
/* 身份证照片区 */
.photo-row {
margin-top: 30rpx;
display: flex;
@@ -366,18 +250,13 @@
.photo-item {
width: 100%;
/* height: 170px; */
background-color: #f0f4ff;
border-radius: 8rpx;
}
/* 底部按钮区 */
.bottom-btn-wrap {
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
/* background-color: #fff; */
}
.btn-modify {
@@ -389,6 +268,7 @@
font-size: 30rpx;
border-radius: 8rpx;
border: none;
margin-right: 20rpx;
}
.btn-delete {
@@ -400,58 +280,6 @@
border: 1rpx solid #f53f3f;
font-size: 30rpx;
border-radius: 8rpx;
}
.btn-delete.delete-only {
flex: 1;
}
/* 成功提示弹窗 */
.toast-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 9999;
}
.toast-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 40rpx 60rpx;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.toast-icon {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 50%;
background-color: #fff;
color: #1677ff;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.toast-text {
color: #fff;
font-size: 28rpx;
}
.status-bar {
width: 100vw;
height: 46px;
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)
@@ -135,9 +124,7 @@ const onSubmit = () => {
// 返回
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/my/settingIndex'
})
uni.navigateBack()
}
// 页面卸载清除定时器
@@ -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,13 +198,16 @@ page {
}
.code-btn {
font-size: 24rpx;
width: 200rpx;
height: 66rpx;
line-height: 66rpx;
background-color: #2F77FD;
color: #fff;
background: #2F77FD;
border: none;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
line-height: 88rpx;
width: 100px;
margin: 0;
flex-shrink: 0;
}
.code-btn[disabled] {
@@ -248,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>
<!-- 表单区域 -->
@@ -39,7 +34,6 @@ 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,186 +1,165 @@
<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">
<!-- 我的车辆列表 -->
<view class="page">
<view v-if="carList.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>
<view class="car-item" v-for="(item, index) in carList" :key="index" @click="handleItemClick(item, index)">
<!-- 左侧圆形占位 -->
<image :src="item.imgSrc" mode="aspectFill" class="car-avatar" @error="item.imgSrc = defaultCarImg" />
<!-- 中间车辆信息 -->
<image :src="item.carImageUrl" mode="aspectFill" class="car-avatar"
@error="item.carImageUrl = defaultCarImg" />
<view class="car-info">
<view>
<view class="car-plate">{{ item.plate }}</view>
<view class="car-model">{{ item.model }}·{{ item.color }}</view>
<view class="car-plate">{{ item.carNum }}</view>
<view class="car-model">{{ item.carBrand }}·{{ item.carColor }}</view>
</view>
<view class="car-bindParkingSpace">{{ item.bindParkingSpace }}
<view class="car-bindParkingSpace">{{ item.villageName }}</view>
</view>
<!-- certificationStatus -->
<!-- <view class="status-tag"
:class="{ underReview: item.certificationStatus === '审核中', unbound: item.certificationStatus === '未绑定', bounded: item.certificationStatus === ''}">
{{ item.certificationStatus }}
</view> -->
</view>
</view>
<!-- 右侧状态标签 -->
<view class="status-tag"
:class="{ underReview: item.status === '审核中', unbound: item.status === '未绑定',bounded: item.status === '已绑定' }">
{{ item.status }}
</view>
</view>
</scroll-view>
<!-- 底部添加车辆按钮 -->
<view class="bottom-btn">
<button class="confirm-btn" @click="addCar">添加车辆</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted
} from 'vue'
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 pageNum = ref(1)
const pageSize = ref(10)
computed: {
// ==================== 下拉刷新 ====================
onPullDownRefresh(() => {
pageNum.value = 1
noMoreData.value = false
carList.value = []
loadingMore.value = false
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
loadingMore.value = true
try {
const res = await getMyCarList({
pageNum: pageNum.value,
pageSize: pageSize.value,
userId: uni.getStorageSync('userId')
})
},
created() {
const data = res
const newList = data.rows || []
},
onShow() {
},
onUnload() {
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
defaultCarImg: "http://47.104.199.163:9090/images/propertyImgs/defaultCarImg.png",
searchKey: "",
selectedId: '',
selectedName: '',
selectedIndex: -1, // 当前选中的索引
// 模拟车辆数据
carList: [
{
id: '1',
imgSrc: 'http://47.104.199.163:9090/images/propertyImgs/carImg.png',
plate: "京A88888",
model: "宝马X5",
color: "白色",
status: "未绑定",
bindParkingSpace: "",
},
{
id: '2',
imgSrc: 'https://错误地址2.png',
plate: "京A88888",
model: "宝马X5",
color: "白色",
status: "已绑定",
bindParkingSpace: "北境碧桂园小区小区地下停车场B2A区202",
},
{
id: '3',
imgSrc: 'https://错误地址3.png',
plate: "京A88888",
model: "宝马X5",
color: "白色",
status: "审核中",
bindParkingSpace: "",
// 第一页 → 覆盖数据
if (pageNum.value === 1) {
carList.value = newList
} else {
// 后续页 → 追加数据
carList.value = [...carList.value, ...newList]
}
// 判断是否没有更多数据
if (newList.length < pageSize.value) {
noMoreData.value = true
} else {
pageNum.value++
}
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
loadingMore.value = false
uni.stopPullDownRefresh()
}
]
}
},
methods: {
// 选择我的车辆
handleItemClick(item, index) {
uni.setStorageSync('operationType', 'update');
uni.redirectTo({
url: '/pageSubPack/my/carDetail?id=' + item.id + '&status=' + item.status,
success: res => {},
fail: () => {},
complete: () => {}
});
},
// 添加车辆
addCar() {
uni.setStorageSync('operationType', 'add');
uni.redirectTo({
url: '/pageSubPack/my/addCar',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goBack() {
uni.switchTab({
url: '/pages/myIndex/myIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
// ==================== 上拉加载更多 ====================
onReachBottom(() => {
getList()
})
},
const getStatusText = (status) => {
if (status === 0) return '未绑定'
if (status === 1) return '已绑定'
if (status === 2) return '审核中'
return '未绑定'
}
const handleItemClick = (item, index) => {
uni.setStorageSync('operationType', 'update')
uni.navigateTo({
url: '/pageSubPack/my/carDetail?id=' + item.id + '&status=' + item.status
})
}
const addCar = () => {
uni.setStorageSync('operationType', 'add')
uni.navigateTo({
url: '/pageSubPack/my/addCar'
})
}
const goBack = () => {
uni.navigateBack({ delta: 1 })
}
</script>
<style lang="scss">
page {
background: #F3F8FD;
overflow: hidden;
// height: 100vh;
}
</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;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
@@ -197,95 +176,18 @@
margin-bottom: 10rpx;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 表单容器 */
.form-container {
padding: 60rpx 30rpx;
}
/* 通用输入框样式 */
.input-item {
font-size: 30rpx;
color: #333;
}
.input-placeholder {
color: #999;
font-size: 30rpx;
}
/* 确定按钮 */
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #1677ff;
color: #fff;
font-size: 28rpx;
border-radius: 8rpx;
border: none;
margin-top: 20rpx;
}
/* 成功提示弹窗 */
.toast-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 9999;
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);
}
.toast-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 40rpx 60rpx;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.toast-icon {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 50%;
background-color: #fff;
color: #1677ff;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.toast-text {
color: #fff;
font-size: 28rpx;
}
</style>
<style scoped>
/* 页面整体 */
/* ====================== 列表容器 ====================== */
/* ====================== 我的车辆卡片 ====================== */
/* 单个车辆项 */
.car-item {
display: flex;
align-items: center;
@@ -298,13 +200,6 @@
width: calc(100% - 44rpx);
}
.car-item.checked {
background: #f4f7ff;
/* 浅蓝色背景 */
border-color: #1677ff;
}
/* 左侧圆形头像占位 */
.car-avatar {
width: 100rpx;
height: 100rpx;
@@ -314,7 +209,6 @@
flex-shrink: 0;
}
/* 中间信息区域 */
.car-info {
flex: 1;
display: flex;
@@ -322,7 +216,6 @@
justify-content: center;
}
/* 车牌号 */
.car-plate {
font-size: 32rpx;
font-weight: bold;
@@ -331,7 +224,6 @@
margin-bottom: 8rpx;
}
/* 车型+颜色 */
.car-model {
font-size: 28rpx;
color: #666;
@@ -342,15 +234,12 @@
font-size: 28rpx;
color: #666;
white-space: nowrap;
/* 强制不换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
/* 超出显示省略号 */
width: 90%;
margin-top: 10rpx;
}
/* 状态标签 */
.status-tag {
font-size: 24rpx;
padding: 6rpx 14rpx;
@@ -362,26 +251,21 @@
top: 30rpx;
}
/* 未绑定状态 */
.status-tag.unbound {
/* background-color: #e6f7e6; */
color: #999;
}
/* 已绑定状态) */
.status-tag.bounded {
color: #2F77FD;
}
/* 审核中 */
.status-tag.underReview {
/* background-color: #F8DDBD; */
color: #FF8F40;
}
/* ====================== 底部按钮 ====================== */
.status-tag.unpass {
color: red;
}
.confirm-btn {
width: 100%;

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">
<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,18 +38,15 @@
<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 />
</view>
<!-- <text v-if="item.showSetDefault" class="set-default"
@click="setDefault(item.id)">设为默认</text> -->
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 底部添加按钮 -->
<view class="bottom-btn">
<button class="add-btn" @click="addHouse">添加房屋</button>
@@ -68,52 +60,50 @@
import {
ref,
onMounted,
} from 'vue'
import {
onReachBottom,
onPullDownRefresh
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 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({
@@ -122,79 +112,76 @@
})
console.error('列表接口报错:', err)
} finally {
loadingMore.value = false
uni.stopPullDownRefresh() // 关闭下拉刷新
loading.value = false
uni.stopPullDownRefresh()
}
}
// ==================== 上拉加载更多(页面生命周期) ====================
// ==================== 加载更多 ====================
const loadMore = () => {
if (loading.value || noMoreData.value) return
pageNum.value++
getList()
}
// ==================== 上拉加载更多 ====================
onReachBottom(() => {
getList()
})
// ==================== 下拉刷新 ====================
const refresh = () => {
// 重置所有状态
pageNum = 1
pageSize = 10
noMoreData = false
houseList = []
loadingMore = false
getList()
}
onPullDownRefresh(() => {
refresh()
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: () => {},
@@ -204,7 +191,7 @@
const addHouse = () => {
uni.setStorageSync('operationType', 'add');
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/addHouse',
success: res => {},
fail: () => {},
@@ -215,12 +202,7 @@
}
const goBack = () => {
uni.switchTab({
url: '/pages/myIndex/myIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
uni.navigateBack({ delta: 1 })
}
</script>
@@ -233,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;
}
@@ -253,7 +230,8 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
height: 100%;
min-height: 500rpx;
color: #ccc;
}
@@ -264,16 +242,8 @@
margin-bottom: 10rpx;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 列表 */
.house-list {
height: 100%;
overflow-y: scroll;
}
.house-card {
@@ -392,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,181 +1,181 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<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">
</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>
<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="parkingSpace-list">
<view class="parkingSpace-card" v-for="(item, index) in parkingSpaceList" :key="index"
@click="tapParkingSpace(item)">
<!-- 标题 + 状态 -->
<view class="card-header">
<view class="title-box">
<text class="parkingSpace-name">{{ item.parkingSpaceName }}</text>
<text class="parkingSpace-name">{{ item.villageName }}</text>
</view>
<text class="status-tag" :class="item.statusClass">{{ item.statusText }}</text>
<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>
</view>
<!-- 车位信息 -->
<view class="card-body">
<view class="left">
<view class="line">
<view class="row">
<text class="label">车位楼层</text>
<view class="value-box">
<text class="value">{{ item.floorNo }}</text>
<text class="value">{{ item.areaName }}</text>
</view>
</view>
</view>
<view class="line">
<view class="row">
<text class="label">车位编号</text>
<text class="value">{{ item.parkingSpaceNumber }}</text>
<text class="value">{{ item.parkingCode }}</text>
</view>
</view>
<view class="line">
<view class="row">
<text class="label">车位状态</text>
<text class="value">{{ item.parkingSpaceStatus }}</text>
<text class="value">{{ getParkingStatus(item.parkingStatus) }}</text>
</view>
</view>
<view class="line">
<view class="row">
<text class="label">绑定车辆</text>
<text class="value">{{ item.bindCarNum }}</text>
<text class="value">{{ item.carNum }}</text>
</view>
</view>
</view>
<view class="right">
<image src="http://47.104.199.163:9090/images/propertyImgs/myParkingSpace.png"
class="imgItem" mode="widthFix">
</image>
<image :src="item.carImageUrl?item.carImageUrl:'http://47.104.199.163:9090/images/propertyImgs/myParkingSpace.png'"
class="imgItem" mode="aspectFill"></image>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部添加按钮 -->
<view class="bottom-btn">
<button class="add-btn" @click="addParkingSpace">添加车位</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref
} from 'vue'
import {
onShow,
onPullDownRefresh,
onReachBottom
} from '@dcloudio/uni-app'
import {
getCarList
} from '@/pageSubPack/api/apiSub.js'
},
const statusBarHeight = ref(20)
const parkingSpaceList = ref([])
computed: {
// ==================== 下拉刷新 ====================
onPullDownRefresh(() => {
pageNum.value = 1
noMoreData.value = false
parkingSpaceList.value = []
loadingMore.value = false
getList()
})
// 页面显示时请求第一页(首次进入和从详情/修改页返回时都会刷新)
onShow(() => {
pageNum.value = 1
noMoreData.value = false
parkingSpaceList.value = []
loadingMore.value = false
getList()
})
},
created() {
},
onShow() {
},
onUnload() {
// 页面卸载时清除定时器
if (this.timer) clearInterval(this.timer)
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
parkingSpaceList: [{
id: 1,
parkingSpaceName: "北境碧桂园小区地下停车场",
floorNo: "B2",
parkingSpaceNumber: "202",
parkingSpaceStatus: "租赁",
bindCarNum: '京A88888',
statusText: "认证成功",
statusClass: "success",
},
{
id: 2,
parkingSpaceName: "北境碧桂园小区停车场",
floorNo: "B2",
parkingSpaceNumber: "202",
parkingSpaceStatus: "自用",
bindCarNum: '京A88888',
statusText: "认证中",
statusClass: "pending",
},
{
id: 3,
parkingSpaceName: "北境碧桂园小区停车场",
floorNo: "2号楼1层",
floorNo: "B2",
parkingSpaceNumber: "202",
parkingSpaceStatus: "闲置",
bindCarNum: '京A88888',
statusText: "认证失败",
statusClass: "fail",
},
]
const loadingMore = ref(false)
const noMoreData = 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 () => {
// 防重复请求
if (loadingMore.value || noMoreData.value) return
loadingMore.value = true
try {
const res = await getCarList({
pageNum: pageNum.value,
pageSize: pageSize.value,
// residentId: uni.getStorageSync('userId')
})
const data = res
const newList = data.rows || []
methods: {
tapParkingSpace(item) {
uni.redirectTo({
url: '/pageSubPack/my/parkingSpaceDetail?id=' + item.id + '&statusClass=' + item.statusClass,
success: res => {},
fail: () => {},
complete: () => {}
});
},
// 第一页 → 覆盖数据
if (pageNum.value === 1) {
parkingSpaceList.value = newList
} else {
// 后续页 → 追加数据
parkingSpaceList.value = [...parkingSpaceList.value, ...newList]
}
// 判断是否没有更多数据
if (newList.length < pageSize.value) {
noMoreData.value = true
} else {
pageNum.value++
}
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
loadingMore.value = false
uni.stopPullDownRefresh()
}
}
addParkingSpace() {
uni.setStorageSync('operationType', 'add');
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
// ==================== 上拉加载更多 ====================
onReachBottom(() => {
getList()
})
const tapParkingSpace = (item) => {
console.log(item);
uni.navigateTo({
url: '/pageSubPack/my/parkingSpaceDetail?id=' + item.id + '&statusClass=' + item.checkStatus
})
}
},
goBack() {
uni.switchTab({
url: '/pages/myIndex/myIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
const addParkingSpace = () => {
uni.setStorageSync('operationType', 'add')
uni.navigateTo({
url: '/pageSubPack/my/addParkingSpace'
})
}
const goBack = () => {
uni.navigateBack({ delta: 1 })
}
</script>
<style lang="scss">
page {
background: #F9F9F9;
@@ -183,22 +183,21 @@
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.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;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
@@ -215,14 +214,17 @@
margin-bottom: 10rpx;
}
/* 底部按钮 */
.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-list {}
.parkingSpace-card {
background: #fff;
border-radius: 12rpx;
@@ -230,10 +232,8 @@
overflow: hidden;
}
/* 头部 */
.card-header {
padding: 30rpx;
/* border-bottom: 1rpx solid #f0f0f0; */
display: flex;
justify-content: space-between;
align-items: center;
@@ -251,12 +251,8 @@
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 状态标签 */
.status-tag {
font-size: 24rpx;
padding: 4rpx 16rpx;
@@ -280,7 +276,6 @@
color: #fff;
}
/* 内容行 */
.card-body {
padding: 0rpx 30rpx 30rpx 30rpx;
display: flex;
@@ -292,43 +287,34 @@
margin-bottom: 20rpx;
}
.row {
display: flex;
width: 100%;
}
.label {
width: 80px;
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;
}
.set-default {
color: #007aff;
font-weight: 600;
font-size: 28rpx;
}
.add-btn {
width: 100%;
height: 88rpx;
@@ -340,18 +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%;
}
.status-bar {
width: 100vw;
height: 46px;
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,20 +1,15 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<view class="contentStyle">
<!-- <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">
<view class="status-icon" :class="statusConfig.iconClass">
<text class="icon-text">{{ statusConfig.iconText }}</text>
</view>
<text class="status-title">{{ statusConfig.title }}</text>
<!-- 步骤条 -->
<view class="step-bar">
<view class="step-item" v-for="(step, index) in stepList" :key="index"
:class="{ active: index < currentStep, done: index < currentStep - 1 }">
@@ -26,38 +21,36 @@
</view>
</view>
<!-- 房屋信息 -->
<view class="info-card">
<view class="card-title">
<view class="title-line"></view>
房屋信息
车位信息
</view>
<view class="info-row">
<text class="info-label">小区</text>
<text class="info-value">{{form.community}}</text>
<text class="info-value">{{ form.villageName }}</text>
</view>
<view class="info-row">
<text class="info-label">停车场</text>
<text class="info-value">{{form.parkingSpace}}</text>
<text class="info-value">{{ form.areaName }}</text>
</view>
<view class="info-row">
<text class="info-label">车位编号</text>
<text class="info-value">{{form.parkingSpaceNum}}</text>
<text class="info-value">{{ form.parkingCode }}</text>
</view>
<view class="info-row">
<text class="info-label">车位状态</text>
<text class="info-value"
:style="{color: form.status === '自用' ? '#FF8F40' : form.status === '租赁' ? '#00aa00' : '#999' }">{{form.status}}</text>
:style="{color: form.parkingStatus === '0' ? '#00aa00' : form.parkingStatus === '1' ? '#FF8F40' : '#999' }">{{ form.parkingStatus === '0' ? '闲置' : form.parkingStatus === '1' ? '自用' : '租赁' }}</text>
</view>
</view>
<!-- 车辆信息 -->
<view class="info-card">
<view class="info-card" v-if="form.carId">
<view class="card-title">
<view class="title-line"></view>
车辆信息
<text class="unbind" @click="clickUnbind">取消绑定</text>
</view>
<text class="unbind" @click="clickUnbind(item.id)">取消绑定</text>
<view class="info-row">
<text class="info-label">车牌号</text>
<text class="info-value">{{ form.carNum }}</text>
@@ -74,119 +67,169 @@
<text class="info-label">车辆颜色</text>
<text class="info-value">{{ form.carColor }}</text>
</view>
</view>
<view class="empty-card" v-if="!form.carId">
<view class="card-title">
<view class="title-line"></view>
车辆信息
</view>
<view class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">暂无绑定车辆</text>
</view>
</view>
</scroll-view>
<!-- 底部操作按钮 -->
<view class="bottom-btn-wrap">
<!-- 仅认证失败显示修改按钮 -->
<button class="btn-modify" @click="onModify">
修改
</button>
<button class="btn-delete" :class="{ 'delete-only': currentStatus !== 'fail' }" @click="onDelete">
删除
</button>
<button class="btn-modify" @click="onModify">修改</button>
<button class="btn-delete" :class="{ 'delete-only': currentStatus !== 'fail' }"
@click="onDelete">删除</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
computed,
onMounted
} from 'vue'
import {
onReachBottom,
onPullDownRefresh,
onLoad,
onShow
} from '@dcloudio/uni-app'
import {
getUserParkingSpaceList,
rebindCar,
deleteParking,
getParkingDetail
} from '@/pageSubPack/api/apiSub.js'
},
computed: {
// 动态计算当前状态配置
statusConfig() {
return this.statusMap[this.currentStatus];
},
// 动态计算当前步骤
currentStep() {
return this.statusConfig.currentStep;
},
},
created() {
},
onShow() {
},
onLoad(option) {
this.id = option.id;
this.currentStatus = option.statusClass;
// this.currentStatus = 'success';
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
// 可通过页面传参动态修改状态success / pending / fail
id: undefined,
form: {
community: "北境碧桂园小区小区",
parkingSpace: "地下停车场",
parkingSpaceNum: "02",
status: '自用',
carNum: "京A88888",
carBrand: "宝马",
carModel: "X5",
carColor: "白色",
},
currentStatus: "",
stepList: ["车位信息", "物业审核", "认证成功"],
// 状态配置表
statusMap: {
const statusBarHeight = ref(20)
const id = ref(undefined)
const currentStatus = ref('')
const checkStatus = ref('')
// parkingId
const form = ref({
community: "",
parkingSpace: "",
parkingSpaceNum: "",
status: '',
carNum: "",
carBrand: "",
carModel: "",
carColor: "",
carId:''
})
const stepList = ["车位信息", "物业审核", "认证成功"]
const statusMap = {
success: {
title: "车位认证成功",
iconClass: "icon-success",
iconText: "✓",
currentStep: 3,
currentStep: 3
},
pending: {
title: "车位审核中,请耐心等待",
iconClass: "icon-pending",
iconText: "⏳",
currentStep: 2,
currentStep: 2
},
fail: {
title: "车位认证失败,请重新提交",
iconClass: "icon-fail",
iconText: "✕",
currentStep: 2,
},
currentStep: 2
},
}
const statusConfig = computed(() => statusMap[currentStatus.value] || statusMap.pending)
const currentStep = computed(() => statusConfig.value.currentStep)
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()
})
onMounted(() => {
})
const loadParkingSpaceDetail = async () => {
// 防重复请求
try {
const res = await getParkingDetail({
id: id.value
})
const data = res.data
form.value = data
console.log(form.value);
} catch (err) {
uni.showToast({
title: err.msg,
icon: 'none'
})
// console.error('列表接口报错:', err)
} finally {
}
},
}
methods: {
clickUnbind(id) {
const clickUnbind = () => {
uni.showModal({
title: "确认取消绑定",
content: "确认取消绑定该车辆吗?",
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '解绑中...',
mask: true
})
rebindCar({
parkingId: id.value,
residentId:uni.getStorageSync('userId'),
carId:'',
},
// 修改按钮事件(仅认证失败显示)
onModify() {
uni.setStorageSync('operationType', 'update');
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace?id=' + this.id + '&sourcePage=' + 'addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
},
// 删除按钮事件
onDelete() {
}).then(unbindRes => {
uni.hideLoading()
if (unbindRes.code === 200) {
uni.showToast({
title: '解绑成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
uni.showToast({
title: unbindRes.msg || '解绑失败',
icon: 'none'
})
}
})
}
}
})
}
const onModify = () => {
uni.setStorageSync('operationType', 'update')
uni.navigateTo({
url: '/pageSubPack/my/addParkingSpace?id=' + id.value + '&checkStatus=' + checkStatus.value + '&sourcePage=addParkingSpace'
})
}
const goBack = () => {
uni.navigateBack()
}
const onDelete = () => {
uni.showModal({
title: "确认删除",
content: "确认删除此车位信息吗?",
@@ -195,36 +238,33 @@
uni.showLoading({
title: '删除中...',
mask: true
});
setTimeout(() => {
})
deleteParking({
parkingId: id.value
}).then(deleteRes => {
uni.hideLoading()
if (deleteRes.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'success'
});
}, 1000);
})
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
uni.showToast({
title: deleteRes.msg || '删除失败',
icon: 'none'
})
}
},
});
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/my/myParkingSpaceIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
})
}
}
})
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
@@ -232,11 +272,15 @@
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
@@ -247,9 +291,6 @@
box-sizing: border-box;
}
/* 顶部状态区 */
.status-header {
display: flex;
flex-direction: column;
@@ -282,18 +323,6 @@
.icon-text {
font-size: 40rpx;
font-weight: bold;
color: inherit;
}
.icon-success .icon-text {
color: #fff;
}
.icon-pending .icon-text {
color: #fff;
}
.icon-fail .icon-text {
color: #fff;
}
@@ -304,7 +333,6 @@
margin-bottom: 40rpx;
}
/* 步骤条 */
.step-bar {
display: flex;
align-items: center;
@@ -364,9 +392,7 @@
background-color: #1677ff;
}
/* 信息卡片 */
.info-card {
/* margin: 0 30rpx 30rpx; */
padding: 20rpx;
border: 1rpx solid #f0f0f0;
background-color: #fff;
@@ -374,6 +400,30 @@
margin-bottom: 20rpx;
}
.empty-card {
padding: 20rpx;
border: 1rpx solid #f0f0f0;
background-color: #fff;
border-radius: 8rpx;
margin-bottom: 20rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 70rpx 0;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
.card-title {
display: flex;
align-items: center;
@@ -397,10 +447,6 @@
align-items: center;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
width: 180rpx;
font-size: 28rpx;
@@ -411,39 +457,21 @@
font-size: 28rpx;
color: #333;
flex: 1;
align-items: center;
}
.status-tag {
color: #ff7d00;
font-weight: 500;
.unbind {
color: #1677ff;
font-size: 28rpx;
margin-bottom: 20rpx;
display: inline-block;
margin-left: auto;
}
/* 身份证照片区 */
.photo-row {
margin-top: 30rpx;
display: flex;
flex-direction: column;
}
.photo-list {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-top: 20rpx;
}
.photo-item {
width: 100%;
height: 170px;
background-color: #f0f4ff;
border-radius: 8rpx;
}
/* 底部按钮区 */
.bottom-btn-wrap {
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
}
.btn-modify {
@@ -455,6 +483,7 @@
font-size: 30rpx;
border-radius: 8rpx;
border: none;
margin-right: 20rpx;
}
.btn-delete {
@@ -471,53 +500,4 @@
.btn-delete.delete-only {
flex: 1;
}
/* 成功提示弹窗 */
.toast-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 9999;
}
.toast-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 40rpx 60rpx;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.toast-icon {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 50%;
background-color: #fff;
color: #1677ff;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.toast-text {
color: #fff;
font-size: 28rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

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,16 +81,10 @@
// 选择项方法
const selectBuilding = (item) => {
const modalContent = `确认选择此${checkType.value}吗?`
uni.showModal({
title: '确认选择',
content: modalContent,
success: (res) => {
if (!res.confirm) return
uni.showLoading({
title: '选择中...',
mask: true
})
// 防重复点击
if (isNavigating.value) return
isNavigating.value = true
if (checkType.value === '楼栋') {
selectedBuilding.value = item.id
uni.setStorageSync('buildingId', item.id)
@@ -121,53 +118,45 @@
uni.setStorageSync('houseNumberName', item.name)
}
setTimeout(() => {
uni.showToast({
title: '选择成功',
icon: 'success'
})
}, 500)
setTimeout(() => {
uni.hideLoading()
goFreash()
}, 2000)
}, 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

@@ -33,93 +33,135 @@
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted,
},
computed: {
// 过滤后的列表(搜索功能)
filteredList() {
if (!this.searchKey.trim()) {
return this.carBrandList
}
return this.carBrandList.filter(item =>
item.name.includes(this.searchKey.trim())
)
},
} from 'vue'
import {
onReachBottom,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
getSystemDictData
} from '/pageSubPack/api/apiSub.js'
},
created() {
},
onShow() {
},
onUnload() {
},
onLoad(option) {
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
searchKey: '',
carBrandList: [{
id: 1,
name: '宝马'
},
{
id: 2,
name: '奥迪'
},
]
const statusBarHeight = ref(20)
const searchKey = ref('')
const carBrandList = ref()
const loadingMore = ref(false)
const noMoreData = ref(false)
const pageNum = ref(1)
const pageSize = ref(10)
// ==================== 获取列表数据 ====================
const getList = async () => {
// 防重复请求
if (loadingMore.value || noMoreData.value) return
loadingMore.value = true
try {
const res = await getSystemDictData({
pageNum: pageNum.value,
pageSize: pageSize.value,
dictType: 'user_car_brand'
})
const data = res
const newList = data.rows || []
// 第一页 → 覆盖数据
if (pageNum.value === 1) {
carBrandList.value = newList
} else {
// 后续页 → 追加数据
carBrandList.value = [...carBrandList.value, ...newList]
}
// 判断是否没有更多数据
if (newList.length < pageSize.value) {
noMoreData.value = true
} else {
pageNum.value++
}
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
loadingMore.value = false
uni.stopPullDownRefresh() // 关闭下拉刷新
}
}
},
// ==================== 上拉加载更多(页面生命周期) ====================
onReachBottom(() => {
getList()
})
// ==================== 下拉刷新 ====================
const refresh = () => {
// 重置所有状态
pageNum = 1
pageSize = 10
noMoreData = false
carBrandList = []
loadingMore = false
getList()
}
onPullDownRefresh(() => {
refresh()
})
// 页面加载时请求第一页
onMounted(() => {
getList()
})
// 过滤后的列表(搜索功能)
const filteredList = computed(() => {
if (!searchKey.value.trim()) {
return carBrandList.value
}
return carBrandList.value.filter(item =>
item.name.includes(searchKey.value.trim())
)
})
methods: {
// 搜索输入事件
onSearch() {
const onSearch = () => {
// 实时过滤computed 自动处理
},
}
// 选择小区
selectCarBrand(item) {
const selectCarBrand = (item) => {
uni.setStorageSync('carBrandId', item.id);
uni.setStorageSync('carBrandName', item.name);
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/my/addCar',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goBackAdd() {
uni.removeStorageSync('carBrandId');
uni.removeStorageSync('carBrandName');
uni.redirectTo({
url: '/pageSubPack/my/addCar',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}
const goBackAdd = () => {
uni.removeStorageSync("carBrandId");
uni.removeStorageSync("carBrandName");
uni.navigateBack()
}
onShow(() => {})
onUnload(() => {})
onMounted(() => {})
</script>
<style lang="scss">
page {

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

@@ -1,20 +1,16 @@
<template>
<view class="contentStyle" style="">
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar :title="'选择' + checkType" left-icon="left" @clickLeft="goBackAdd" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y>
<!-- 停车场网格列表 -->
<view class="parkingLot-grid">
<view v-if="dataList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
<text class="empty-text">暂无数据</text>
</view>
<view class="parkingLot-grid">
<view class="parkingLot-item"
:class="{ active: (checkType == '停车场' && selectedParkingLot === item.id) || (checkType == '车位编号' && selectedParkingSpace === item.id) }"
v-for="(item, index) in dataList" :key="index" @click="selectedParking(item)">
@@ -22,233 +18,157 @@
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
onUnload
} from '@dcloudio/uni-app'
import {
getResidentCarAreaManageAreaListByVillageId,
getResidentCarAreaManageParkingListByAreaId
} from '@/pageSubPack/api/apiSub.js'
},
computed: {
},
created() {
},
onShow() {
},
onUnload() {},
mounted() {
if (this.checkType == '停车场') {
this.dataList = this.parkingLotList
} else if (this.checkType == '车位编号') {
this.dataList = this.parkingSpaceList
const statusBarHeight = ref(20)
const checkType = ref('')
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')
if (checkType.value == '停车场') {
loadAreaList()
} else if (checkType.value == '车位编号') {
loadParkingList()
}
},
data() {
})
const loadAreaList = () => {
let communityId = uni.getStorageSync('communityId')
if (!communityId) return
getResidentCarAreaManageAreaListByVillageId({
villageId: communityId
}).then(res => {
if (res.code === 200) {
dataList.value = (res.rows || []).map(item => {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
checkType: uni.getStorageSync('checkType'),
dataList: [],
// 停车场列表(可动态生成/接口返回)
parkingLotList: [{
id: 1,
name: '地上停车场'
},
{
id: 2,
name: '地下停车场'
},
],
parkingSpaceList: [{
id: 13,
name: '01'
},
{
id: 14,
name: '02'
},
{
id: 15,
name: '03'
},
{
id: 16,
name: '04'
}, {
id: 17,
name: '05'
},
{
id: 18,
name: '06'
},
{
id: 19,
name: '07'
},
{
id: 20,
name: '08'
}, {
id: 21,
name: '09'
},
{
id: 22,
name: '10'
},
{
id: 23,
name: '11'
},
{
id: 24,
name: '12'
}, {
id: 25,
name: '13'
},
{
id: 26,
name: '14'
},
{
id: 27,
name: '15'
},
{
id: 28,
name: '16'
},
{
id: 29,
name: '17'
},
{
id: 30,
name: '18'
},
{
id: 31,
name: '19'
},
{
id: 32,
name: '20'
},
],
// 选中的停车场
selectedParkingLot: uni.getStorageSync('parkingLotId'),
// 选中的车位编号
selectedParkingSpace: uni.getStorageSync('parkingSpaceId'),
id: item.id,
name: item.name || item.areaName || ''
}
},
methods: {
selectedParking(item) {
if (this.checkType == '停车场') {
this.selectedParkingLot = item.id
} else if (this.checkType == '车位编号') {
this.selectedParkingSpace = item.id
}
let modalContent = '确认选择此' + this.checkType + '吗?'
uni.showModal({
title: "确认选择",
content: modalContent,
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '选择中...',
mask: true
})
}else {
uni.showToast({
title: `${res.msg}`,
icon: 'none'
});
setTimeout(() => {
}
})
}
const loadParkingList = () => {
let parkingLotId = uni.getStorageSync('parkingLotId')
if (!parkingLotId) return
getResidentCarAreaManageParkingListByAreaId({
areaId: parkingLotId,
}).then(res => {
if (res.code === 200) {
dataList.value = (res.rows || []).map(item => {
console.log(item);
return {
id: item.id,
name: item.name || item.parkingCode || ''
}
})
}else {
uni.showToast({
title: `${res.msg}`,
icon: 'none'
});
}
})
}
const selectedParking = (item) => {
// 防重复点击锁:处理中直接返回,避免重复选择/重复跳转
if (isSubmitting.value) return
isSubmitting.value = true
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'
});
if (this.checkType == '停车场') {
uni.setStorageSync('parkingLotId', item.id);
uni.setStorageSync('parkingLotName', item.name);
uni.removeStorageSync('parkingSpaceId');
uni.removeStorageSync('parkingSpaceName');
uni.setStorageSync('checkType', '车位编号');
} else if (this.checkType == '车位编号') {
uni.setStorageSync('parkingSpaceId', item.id);
uni.setStorageSync('parkingSpaceName', item.name);
})
navTimer = setTimeout(() => {
goFreash()
}, 1500)
} finally {
// 正常跳转后页面会销毁,防止异常时锁死
lockTimer = setTimeout(() => {
isSubmitting.value = false
}, 1500)
}
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goFreash()
}, 2500);
}
},
});
},
goFreash() {
if (this.checkType != '车位编号') {
const goFreash = () => {
if (checkType.value != '车位编号') {
// 选完停车场后替换当前页进入选车位编号,避免压栈
uni.redirectTo({
url: '/pageSubPack/my/selectParingLot',
success: res => {},
fail: () => {},
complete: () => {}
});
url: '/pageSubPack/my/selectParingLot'
})
} else {
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
// 选完车位编号后返回原来的 addParkingSpace
uni.navigateBack()
}
},
goBackAdd() {
if (this.checkType == '停车场') {
uni.removeStorageSync('parkingLotId');
uni.removeStorageSync('parkingLotName');
} else if (this.checkType == '车位编号') {
uni.removeStorageSync('parkingSpaceId');
uni.removeStorageSync('parkingSpaceName');
}
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
const goBackAdd = () => {
if (checkType.value == "停车场") {
uni.removeStorageSync("parkingLotId")
uni.removeStorageSync("parkingLotName")
} else if (checkType.value == "车位编号") {
uni.removeStorageSync("parkingSpaceId")
uni.removeStorageSync("parkingSpaceName")
}
uni.navigateBack()
}
// 页面销毁时清理定时器,避免手势返回后延迟跳转仍触发
onUnload(() => {
if (navTimer) {
clearTimeout(navTimer)
navTimer = null
}
if (lockTimer) {
clearTimeout(lockTimer)
lockTimer = null
}
})
</script>
<style lang="scss">
page {
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
@@ -258,12 +178,15 @@
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background-color: #fff; */
}
.page {
@@ -273,21 +196,8 @@
box-sizing: border-box;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 网格布局3列 */
.parkingLot-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30rpx;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
@@ -303,8 +213,16 @@
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;
display: flex;
@@ -315,9 +233,11 @@
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 {
background-color: #1677ff;
border-color: #1677ff;
@@ -327,59 +247,12 @@
color: #fff;
}
/* 停车场文字 */
.parkingLot-text {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
/* 成功提示弹窗 */
.toast-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 9999;
}
.toast-content {
background-color: rgba(0, 0, 0, 0.8);
padding: 40rpx 60rpx;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.toast-icon {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 50%;
background-color: #fff;
color: #1677ff;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.toast-text {
color: #fff;
font-size: 28rpx;
}
.status-bar {
width: 100vw;
height: 46px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
</style>

View File

@@ -123,46 +123,24 @@
uni.removeStorageSync('parkingLotName');
uni.removeStorageSync('parkingSpaceId');
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.redirectTo({
url: '/pageSubPack/my/bindHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
uni.setStorageSync('checkType', '停车场')
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,22 +25,16 @@
<script setup>
import {
ref,
onMounted,
reactive
onMounted
} from 'vue'
import {
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
authLogout, getUserProfile, getUserNotify, changeUserNotify
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)
const settingList = ref([{
title: '更改手机号码',
type: 'text',
@@ -62,29 +52,213 @@
{
title: '清除缓存',
type: 'text',
value: '9.2M'
value: ''
},
{
title: '注销账号',
type: 'arrow'
},
{
title: '升级版本',
type: 'text',
value: '当前版本 2.9.8',
value: '当前版本 1.0.0',
valueClass: 'item-version'
}
])
// 格式化字节大小
const formatSize = (bytes) => {
if (!bytes || bytes === 0) return '0B'
const k = 1024
const sizes = ['B', 'KB', 'M', 'G']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + sizes[i]
}
// 获取真实缓存大小
const getCacheSizeData = () => {
return new Promise((resolve) => {
// #ifdef APP-PLUS
// App 端获取完整缓存(含图片、文件等)
plus.cache.calculate((size) => {
resolve(formatSize(size))
})
// #endif
// #ifndef APP-PLUS
// 小程序/H5 端只能获取 storage 缓存
const info = uni.getStorageInfoSync()
const size = (info.currentSize || 0) * 1024 // currentSize 单位 KB
resolve(formatSize(size))
// #endif
})
}
// 获取当前运行版本号
const initCurrentVersion = () => {
// #ifdef APP-PLUS
currentVersion.value = plus.runtime.version || '1.0.0'
currentVersionCode.value = plus.runtime.versionCode || 1
// #endif
// #ifndef APP-PLUS
const manifest = uni.getAppAuthorizeSetting ? uni.getAppAuthorizeSetting() : null
if (manifest && manifest.version) {
currentVersion.value = manifest.version
}
// #endif
if (settingList.value[5]) {
settingList.value[5].value = '当前版本 ' + currentVersion.value
}
}
// 版本号比较v1 > v2 返回 1v1 = v2 返回 0v1 < v2 返回 -1
const compareVersion = (v1, v2) => {
const arr1 = v1.split('.').map(Number)
const arr2 = v2.split('.').map(Number)
const len = Math.max(arr1.length, arr2.length)
for (let i = 0; i < len; i++) {
const n1 = arr1[i] || 0
const n2 = arr2[i] || 0
if (n1 > n2) return 1
if (n1 < n2) return -1
}
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 === 'android' ? 'android':'ios'
const appChannel = 'resident'
// const channel = 'xiaomi'
// 1. 查询最新版本
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
// 2. 调 checkAppUpdate 检查是否需要更新
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 ? '\n\n更新内容' + (versionRes.data.releaseNotes) : ''}`,
showCancel: !isForce,
confirmText: '立即更新',
cancelText: '稍后',
success: (modalRes) => {
if (modalRes.confirm) {
// 记录更新日志
const userId = uni.getStorageSync('userId')
if (userId) {
postUpdateLog({
userId: userId,
platform: platform,
fromVersion: currentVersionCode.value,
toVersion: latestVersionCode
})
}
// 跳转到应用商店更新
openAppStore(downloadUrl, platform)
} else if (isForce && !modalRes.confirm) {
// 强制更新时用户点取消,退出应用
// #ifdef APP-PLUS
plus.runtime.quit()
// #endif
}
}
})
} else {
uni.showToast({ title: '已是最新版本', icon: 'success' })
}
} else {
uni.showToast({ title: '检查更新失败1', icon: 'none' })
}
} else {
uni.hideLoading()
uni.showToast({ title: '检查更新失败2', icon: 'none' })
}
} catch (err) {
uni.hideLoading()
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();
settingList.value[3].value = '0M'
// 保存登录相关数据,清完恢复
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
settingList.value[3].value = '0B'
uni.showLoading({
title: '清除中...',
mask: true
@@ -98,6 +272,12 @@
setTimeout(() => {
uni.hideLoading();
}, 2500);
} else if (item.title == '注销账号') {
uni.navigateTo({
url: '/pageSubPack/my/deleteAccount'
});
} else if (item.title == '升级版本') {
checkVersionUpdate()
}
}
@@ -106,40 +286,37 @@
title: '提示',
content: '确定要退出当前账号吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '退出中...',
mask: true
});
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(() => {
uni.hideLoading();
uni.redirectTo({
url: '/pageSubPack/loginSub/pwd-login'
});
}, 1500);
uni.hideLoading()
uni.showToast({ title: '退出成功', icon: 'success' })
setTimeout(clearAndGo, 1500)
} else {
uni.showToast({
title: `${res.msg}`,
icon: 'error'
});
clearAndGo()
}
}).catch(err => {
console.log(err);
uni.showToast({
title: `${err}`,
icon: 'error'
});
console.log(err)
clearAndGo()
})
}
}
})
}
@@ -160,13 +337,10 @@
})
}
const goBack = () => {
uni.switchTab({
url: '/pages/myIndex/myIndex'
});
}
onMounted(() => {
onMounted(async () => {
initCurrentVersion()
settingList.value[3].value = await getCacheSizeData()
getUserProfile().then(res => {
if (res.code === 200 && res.data) {
const phone = res.data.phone || ''
@@ -198,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>
@@ -255,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 {
@@ -267,10 +447,6 @@
opacity: 0.8;
}
.status-bar {
width: 100vw;
height: 46px;
}
</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"
@@ -34,7 +11,7 @@
<view class="dot" v-if="notice.readStatus === '0'"></view>
</view>
<view class="info">{{ notice.author }} · {{ notice.publishTime }}</view>
<view class="content">{{ notice.noticeContent }}</view>
<view class="content" v-html="notice.noticeContent"></view>
<view class="detail" @click="goToDetail(notice)">
<text style="color:#2F77FD;font-size: 24rpx;">详情</text>
<uni-icons type="right" size="13" color="#007AFF"/>
@@ -44,22 +21,20 @@
<!-- 加载中 / 无更多 / 空布局 -->
<view class="load-more" v-if="loadingMore">加载中...</view>
<view class="no-more" v-if="noMoreData && noticeList.length">没有更多通知了</view>
<view class="empty" v-if="noticeList.length === 0 && !loading">
暂无通知
<!-- <button class="refresh-btn" @click="loadNotices">刷新试试</button> -->
<view v-if="noticeList.length === 0 && !loading" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无通知</text>
</view>
</view>
</scroll-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,21 +165,45 @@ 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;
font-size: 24rpx;
}
.empty {
text-align: center;
.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;
}
.refresh-btn {
background: #007AFF;
@@ -220,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,19 +12,17 @@
<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>
<!-- 文章内容 -->
<view class="article-content">
<view class="paragraph">
{{noticeDetail.noticeContent}}
</view>
<view class="paragraph" v-html="noticeDetail.noticeContent"></view>
<view class="articleimg">
<image src="http://47.104.199.163:9090/images/home/notice-1.png" mode="" style="width: 100%;height: 100%;border-radius: 20rpx;"></image>
<image src="http://47.104.199.163:9090/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>
@@ -105,7 +103,7 @@
}
}catch(err){
uni.showToast({
title: res.msg ||"网络异常",
title: err.msg ||"网络异常",
icon:"none"
})
}
@@ -198,6 +196,8 @@
line-height: 1.4;
margin-bottom: 16px;
text-align: center;
word-break: break-all;
white-space: pre-wrap;
}
.article-meta {
@@ -281,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,13 +1,9 @@
<template>
<!-- 页面主容器 -->
<view class="door-open-page">
<view class="status_bar"></view>
<!-- 顶部标题栏 -->
<view class="top-bar">
<uni-nav-bar left-icon="left" title="远程开门" backgroundColor ="none" :border="false" @clickLeft="narBack" />
</view>
<view class="page-content">
<view class="top-img">
<image src="http://47.104.199.163:9090/images/openDoor/remoteOpen.png" mode="" style="width: 100%;height: 100%;"></image>
<image src="https://wuye.ckdzkj.com/images/openDoor/remoteOpen.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
<!-- 提示文案 -->
@@ -41,10 +37,10 @@
</view>
<!-- 空状态 -->
<view v-if="doorList.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>
</view>
<!-- 确认开门按钮 -->
@@ -62,32 +58,13 @@ const selectedDoor = ref(-1) // -1表示未选中
const showSuccess = ref(false)
// 门列表数据
const doorList = ref([
// { iconText: 'http://47.104.199.163:9090/images/openDoor/westDoor.png',iconActive:"http://47.104.199.163:9090/images/openDoor/westDoorWhite.png", name: '西入门闸机' },
// { iconText: 'http://47.104.199.163:9090/images/openDoor/eastDoor.png',iconActive:"http://47.104.199.163:9090/images/openDoor/eastDoorWhite.png", name: '东入门闸机' },
// { iconText: 'http://47.104.199.163:9090/images/openDoor/unitDoor.png',iconActive:"http://47.104.199.163:9090/images/openDoor/unitDoorWhite.png", name: '1号楼1单元门' },
// { iconText: 'http://47.104.199.163:9090/images/openDoor/unitDoor.png',iconActive:"http://47.104.199.163:9090/images/openDoor/unitDoorWhite.png", name: '1号楼2单元门' }
])
const doorList = ref([])
// 选择门的方法
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 () =>{
@@ -103,7 +80,7 @@ const getAccessControl = async () =>{
doorList.value = []
uni.showToast({
title:'门禁查询失败',
icon:"error"
icon:"none"
})
}
@@ -111,7 +88,7 @@ const getAccessControl = async () =>{
doorList.value = []
uni.showToast({
title:err.msg || '查询失败',
icon:"error"
icon:"none"
})
}
@@ -128,13 +105,22 @@ const openDoorClick = () => {
// })
// return
// }
getAccessOpenData()
uni.showToast({
title: '正在对接硬件中...',
icon: 'none'
})
// getAccessOpenData()
}
// 开门接口
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:'开门成功',
@@ -147,9 +133,10 @@ const getAccessOpenData = async () =>{
})
}
}catch(err){
uni.hideLoading()
uni.showToast({
title: err.msg || '开门失败',
icon:"error"
icon:"none"
})
}
}
@@ -162,24 +149,20 @@ onLoad(() =>{
<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: 32px;
width: 100%;
}
/* 顶部标题 */
.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{
@@ -301,7 +284,7 @@ onLoad(() =>{
}
.empty-text {
font-size: 32rpx;
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
@@ -309,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;
@@ -317,6 +305,7 @@ onLoad(() =>{
font-size: 28rpx;
border-radius: 20rpx;
border: none;
z-index: 10;
}
.confirm-btn::after {
border: none; /* 去除uni-app默认按钮边框 */

View File

@@ -8,24 +8,18 @@
class="search-input"
placeholder="请输入关键字进行搜索"
placeholder-class="placeholder"
v-model="searchText"
:value="searchText"
@input="handleSearchInput"
/>
<view v-if="searchText" class="clear-icon" @tap="clearSearch">
<uni-icons type="clear" size="18" color="#999"></uni-icons>
</view>
</view>
<button class="search-btn" @tap="handleSearch">搜索</button>
<!-- <button class="search-btn" @tap="handleSearch">搜索</button> -->
</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="http://47.104.199.163:9090/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>
@@ -64,17 +58,17 @@
<!-- 空状态 -->
<view v-if="displayedCommunities.length === 0 && !isSearching && !isLoading" 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 v-if="displayedCommunities.length === 0 && isSearching && !isLoading" class="empty-state">
<uni-icons type="search" size="60" color="#ccc"></uni-icons>
<uni-icons type="search" size="30" color="#ccc"></uni-icons>
<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,10 +94,11 @@ const searchText = ref('')
const selectedItem = ref(null)
const scrollIntoViewId = ref('')
const currentVillageIdFromHome = ref('') // 首页传入的当前选中小区ID
const sourcePage = ref('') // 来源页面payment 为缴费页独立切换,其他走首页切换
// 分页相关
const currentPage = ref(1)
const pageSize = 5
const pageSize = 10
const totalCount = ref(0)
const displayedCommunities = ref([])
const hasMoreData = ref(true)
@@ -119,6 +114,8 @@ const getCommunityList = async (pageNum = 1, keyword = '') => {
isLoading.value = true
try {
// keyword 来自用户输入时是普通文本,无需 decodeURIComponent
// 仅当 keyword 来自 URL 参数时才需要解码(已在外部处理)
const params = {
pageNum,
pageSize,
@@ -128,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
@@ -141,7 +140,14 @@ const getCommunityList = async (pageNum = 1, keyword = '') => {
hasMoreData.value = displayedCommunities.value.length < totalCount.value
// 默认选中:优先匹配传入的 villageId否则选第一条仅第一页时
// 搜索场景下不覆盖用户已手动选择的项
if (pageNum === 1 && displayedCommunities.value.length > 0) {
const userSelectedInCurrentList = selectedItem.value &&
displayedCommunities.value.some(
item => String(item.villageId) === String(selectedItem.value.villageId)
)
// 如果用户已选中且该项仍在当前列表中,保留用户选择
if (!isSearching.value || !userSelectedInCurrentList) {
const targetId = currentVillageIdFromHome.value || uni.getStorageSync('currentVillageId') || ''
if (targetId) {
const matched = displayedCommunities.value.find(
@@ -152,9 +158,14 @@ const getCommunityList = async (pageNum = 1, keyword = '') => {
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'
@@ -164,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)
})
@@ -187,23 +201,31 @@ const loadMoreData = () => {
currentPage.value += 1
}
// 下拉刷新
onPullDownRefresh(() => {
currentPage.value = 1
getCommunityList(1, searchText.value)
})
// 上拉加载更多
onReachBottom(() => {
loadMoreData()
})
// 处理搜索输入(防抖)
const handleSearchInput = () => {
const handleSearchInput = (e) => {
let val = e.detail.value || ''
searchText.value = val
if (searchTimeout) {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(() => {
handleSearch()
currentPage.value = 1
isSearching.value = !!val.trim()
getCommunityList(1, val.trim())
}, 300)
}
// 处理搜索
const handleSearch = () => {
const keyword = searchText.value.trim()
isSearching.value = !!keyword
currentPage.value = 1
getCommunityList(1, keyword)
}
// 清空搜索
const clearSearch = () => {
@@ -228,42 +250,58 @@ const handleConfirm = async () => {
})
return
}
// 缴费页为独立切换:不调用后端切换小区接口
const isPayment = sourcePage.value === 'payment'
try {
uni.showLoading({
title: '切换小区中...',
title: isPayment ? '加载中...' : '切换小区中...',
mask: true
})
const params = {
if (!isPayment) {
const res = await getVillageSwitch({
villageId: selectedItem.value.villageId,
}
const res = await getVillageSwitch(params)
})
uni.hideLoading()
if (res.code === 200) {
if (res.data.villageId) {
if (res.code !== 200) {
uni.showToast({
title: res.msg || '切换小区失败',
icon: 'none',
duration: 2000
})
return
}
if (res.data?.villageId) {
selectedItem.value.villageId = res.data.villageId
}
} 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
})
}
} catch (err) {
uni.hideLoading()
uni.showToast({
@@ -274,13 +312,6 @@ const handleConfirm = async () => {
}
}
// 监听搜索文本变化
watch(searchText, (newVal) => {
if (!newVal.trim()) {
clearSearch()
}
})
// 初始化
onMounted(() => {
if (displayedCommunities.value.length === 0 && !isLoading.value) {
@@ -356,7 +387,6 @@ onMounted(() => {
}
.community-list {
flex: 1;
margin-top: 20rpx;
margin-bottom: 180rpx;
}
@@ -463,7 +493,7 @@ onMounted(() => {
}
.empty-text {
font-size: 32rpx;
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;

View File

@@ -1,18 +1,8 @@
<template>
<view class="repair-page">
<view class="status_bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-left" @tap="goBack">
<image src="http://47.104.199.163:9090/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="http://47.104.199.163:9090/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="http://47.104.199.163:9090/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,7 +116,10 @@ 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)
// 状态映射
const statusMap = {
@@ -137,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,
@@ -157,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' });
}
@@ -172,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(){
@@ -202,6 +230,8 @@ const goBack = () => {
uni.switchTab({
url:"/pages/serviceIndex/serviceIndex"
})
}else{
uni.navigateBack()
}
uni.removeStorageSync('sourcePage')
@@ -215,7 +245,11 @@ const goDetail = (item) => {
onLoad(() => {
// maintenanceItemId.value = option.maintenanceProjectId || '' // 接收维修项目id
fetchRepairList()
fetchRepairList(true)
});
onShow(() => {
// 从其他页面返回时刷新列表
fetchRepairList(true)
});
onUnload(() =>{
@@ -230,8 +264,9 @@ onUnload(() =>{
padding: 0 40rpx;
}
.status_bar {
height: 32px;
height: var(--status-bar-height);
width: 100%;
flex-shrink: 0;
}
/* 顶部导航栏 */
.nav-bar {
@@ -243,7 +278,7 @@ onUnload(() =>{
position: sticky;
top: 44px;
z-index: 100;
margin-bottom: 20px;
margin-bottom: 60px;
/* box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.2); */
}
@@ -258,7 +293,7 @@ onUnload(() =>{
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 600;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -271,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;
@@ -289,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;
@@ -347,7 +379,7 @@ onUnload(() =>{
/* 报修列表 */
.repair-list {
padding: 28rpx 0rpx;
padding: 28rpx 0rpx 0;
}
.repair-item {
background-color: #fff;
@@ -355,6 +387,9 @@ onUnload(() =>{
padding: 15px;
margin-bottom: 10px;
}
.repair-item:last-child {
margin-bottom: 0;
}
/* 列表项头部 */
.item-header {
@@ -369,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;
@@ -421,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;
@@ -449,12 +492,22 @@ onUnload(() =>{
}
.empty-text {
font-size: 32rpx;
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
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,18 +64,20 @@
<!-- 已上传图片 -->
<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="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>
</view>
<!-- 上传按钮 -->
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 3">
<uni-icons type="plus" size="24"></uni-icons>
<text>上传照片</text>
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 9">
<uni-icons type="plusempty" size="24"></uni-icons>
<!-- <text>上传照片</text> -->
</view>
</view>
</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">
@@ -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()
@@ -234,9 +270,10 @@ const syncScrollPosition = () => {
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,12 +1,12 @@
<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">
@@ -25,7 +25,6 @@
<scroll-view scroll-y class="category-right">
<view
class="project-item"
:class="{ active: selectedProject.id === item.id }"
@click="selectProject(item)"
v-for="item in currentProjectList"
:key="item.id"
@@ -33,18 +32,7 @@
{{ 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;
}
.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;
.project-item:active {
background: #f5f5f5;
}
</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,31 +47,37 @@
<!-- 已上传图片 -->
<view class="upload-item" v-for="(img, index) in form.images" :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>
<!-- <uni-icons type="close" size="16" @click="deleteImage(index)"></uni-icons> -->
</view>
<!-- 上传按钮 -->
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 3">
<uni-icons type="plus" size="24"></uni-icons>
<text>上传照片</text>
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 9">
<uni-icons type="plusempty" size="24"></uni-icons>
<!-- <text>上传照片</text> -->
</view>
</view>
</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>
<script setup>
import { ref } from 'vue'
import{onLoad} from "@dcloudio/uni-app"
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: '',
@@ -72,14 +86,34 @@ const form = ref({
images: [] // 图片列表
})
// 页面加载时获取传参
// 页面加载
onLoad((options) => {
// 第一次进入页面接收参数
if (options.category && options.project) {
repairProject.value = `${options.category}-${options.project}`
}
if (options.projectId) {
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
}
})
})
// ✅ 页面卸载时移除监听(必须加)
onUnload(() => {
uni.$off('selectRepairProject')
})
// 返回选择项目页
@@ -95,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;
@@ -152,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
@@ -167,7 +201,8 @@ const submitForm = async () => {
return
}
uni.showLoading({ title: '提交中...' })
submitting.value = true
uni.showLoading({ title: '提交中...' ,mask:true})
try {
// 先上传图片
@@ -177,7 +212,7 @@ const submitForm = async () => {
const params = {
villageId,
maintenanceItemId:projectId.value,
title: form.value.title,
item: form.value.title,
problem: form.value.desc,
residentId:userId,//人员id
// repairTypeName: repairProject.value,
@@ -189,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()
@@ -226,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;
@@ -250,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;
@@ -288,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 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

@@ -91,16 +91,24 @@ const currentProjectList = computed(() => {
const selectCategory = (item) => {
activeCategory.value = item.id
}
// 选择项目并跳转到维修项目填写页
// 选择项目并返回到维修项目填写页
const selectProject = (item) => {
// 获取分类名称
const categoryName = categoryList.value.find(c => c.id === item.categoryId)?.projectName
// 跳转并传参
uni.navigateTo({
url: `/pageSubPack/online-repair/publicRepair?category=${categoryName}&project=${item.projectName}&projectId=${item.id}`
const categoryName = categoryList.value.find(c => c.id === item.categoryId)?.projectName || '';
// 用全局事件发送参数
uni.$emit('selectRepairProject', {
category: categoryName, // 分类名
project: item.projectName, // 项目名
projectId: item.id // 项目ID
})
// 返回上一页
uni.navigateBack({
delta: 1
})
}
</script>
<style scoped>
@@ -109,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;
@@ -124,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;
@@ -147,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="http://47.104.199.163:9090/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,9 +27,11 @@
</view>
<!-- 确认按钮 -->
<!-- 确认按钮固定底部不随内容滚动 -->
<view class="confirm-bar">
<button class="confirm-btn" @click="confirmSelect">确认</button>
</view>
</view>
</template>
<script setup>
@@ -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) {
// 仅当从表单页带入了房屋ID时自动选中对应房屋否则不默认选择
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]
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 {
@@ -165,7 +175,7 @@ const confirmSelect = () => {
}
.empty-text {
font-size: 32rpx;
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;

View File

@@ -7,18 +7,18 @@
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="card-box">
<!-- 头部水费标题+水滴图标 -->
<!-- 头部物业费A标题+水滴图标 -->
<view class="card-header">
<image class="iconStyle"
:src="paymentType=='水'?'http://47.104.199.163:9090/images/propertyImgs/water.png':'http://47.104.199.163:9090/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>
<text class="card-title">{{paymentType}}</text>
</view>
<!-- 分割线 -->
<view class="line"></view>
<view class="input-item">
<view class="input-label">缴费单位</view>
<text class="company-name">XXXXXXXX燃气有限公司</text>
<text class="company-name">花开物业</text>
</view>
<!-- 分割线 -->
<view class="line"></view>
@@ -31,7 +31,7 @@
v-model="deviceNo" />
<!-- 右边扫码图标 text标签直接用 双端不乱码 -->
<image class="scanTheCodeIconStyle"
src="http://47.104.199.163:9090/images/propertyImgs/scanTheCodeIcon.png" @click="scanCode">
src="https://wuye.ckdzkj.com/images/propertyImgs/scanTheCodeIcon.png" @click="scanCode">
</image>
</view>
</view>
@@ -89,53 +89,52 @@
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
getTopUpBindDevice,
getopenIdByCode
} from '/pageSubPack/api/apiSub.js'
// 响应式数据
const paymentType = ref(uni.getStorageSync('addPaymentType'))
const deviceNo = ref('')
const isAgree = ref(false)
const scanResult = ref('')
// 生命周期
onMounted(() => {})
// 协议勾选
const handleAgreeChange = (e) => {
isAgree.value = e.detail.value.length > 0
}
// 扫码
const scanCode = async () => {
try {
uni.showLoading({
title: '扫码中...',
mask: true
})
const scanCode = () => {
// uni.showLoading({
// title: '扫码中...',
// mask: true
// })
const res = await uni.scanCode({
uni.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode']
})
// 扫码成功,赋值
deviceNo.value = res.result
scanResult.value = res.result
uni.hideLoading()
uni.showToast({
title: '扫码成功',
icon: 'success'
})
} catch (err) {
uni.hideLoading()
console.log('扫码失败', err)
uni.showToast({
title: '扫码取消/失败',
icon: 'none'
})
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()
}
})
}
// 提交缴费
const goPay = () => {
const goPay = async () => {
if (!deviceNo.value) {
uni.showToast({
title: '请输入设备号',
@@ -143,54 +142,88 @@
})
return
}
// if (!isAgree.value) {
// uni.showToast({
// title: '请同意协议',
// icon: 'none'
// })
// return
// }
uni.showModal({
title: '确认提交',
content: '确认要提交吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '提交中...',
mask: true
})
setTimeout(() => {
try {
// // 获取 openid
// const loginRes = await wx.login()
// if (!loginRes.code) {
// uni.hideLoading()
// uni.showToast({
// title: '登录失败',
// icon: 'none'
// })
// return
// }
// // 用 code 换 openid
// const openIdRes = await getopenIdByCode({
// code: loginRes.code
// })
// if (openIdRes.code !== 200 || !openIdRes.data) {
// uni.hideLoading()
// uni.showToast({
// title: '获取openid失败',
// icon: 'none'
// })
// return
// }
const openid = uni.getStorageSync('openid')
// if (!openid) {
// uni.hideLoading()
// uni.showToast({
// title: '获取用户信息失败,请重新登录',
// icon: 'none'
// })
// return
// }
await getTopUpBindDevice({
deviceType: paymentType.value=='水表'?'2':'1',
deviceCode: deviceNo.value,
// openid: openid,
residentUserId: uni.getStorageSync('userId')
})
uni.hideLoading()
uni.showToast({
title: '提交成功',
icon: 'success'
})
}, 1000)
setTimeout(() => {
uni.hideLoading()
goNext()
}, 2500)
}, 1500)
} catch (err) {
uni.hideLoading()
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
})
console.error('绑定设备失败:', err)
}
}
}
})
}
const handleProtocol = () => {
}
// 跳转成功页
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">
@@ -223,7 +256,7 @@
padding: 32rpx;
}
/* 头部水费标题 */
/* 头部物业费A标题 */
.card-header {
display: flex;
align-items: center;
@@ -350,7 +383,7 @@
padding: 32rpx;
}
/* 头部水费标题 */
/* 头部物业费A标题 */
.card-header {
display: flex;
align-items: center;

View File

@@ -13,7 +13,7 @@
<view class="header-card">
<view class="header-icon">
<image class="iconStyle"
:src="paymentType=='燃气'?'http://47.104.199.163:9090/images/propertyImgs/gas.png':'http://47.104.199.163:9090/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,23 +173,21 @@
// 跳转成功页
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'
})
uni.navigateBack();
}
</script>
<style lang="scss">

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,17 +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,8 +1,8 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="arrearsPayment.value" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
<uni-nav-bar :title="switchStatus(paymentItem.type) || '缴费'" left-icon="left" @clickLeft="goBack"
backgroundColor="transparent" :border="false">
</uni-nav-bar>
@@ -14,10 +14,13 @@
<view v-if="currentStep === 1" class="pay-page">
<!-- 物业费缴费 -->
<!-- 金额展示 -->
<view class="amount-section">
<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="">
@@ -26,108 +29,42 @@
<view class="info-item">
<text class="info-label">户号</text>
<text class="info-value">101123456</text>
<text class="info-value">{{ paymentItem.deviceCode || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">周大声</text>
<text class="info-value">{{ paymentItem.name || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">缴费单位</text>
<text class="info-value">HN热力有限公司</text>
<text class="info-label">地址</text>
<text class="info-value">{{ paymentItem.fullAddress || '-' }}</text>
</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 type="number" v-model="payNum" class="amount-input" placeholder="点击输入缴费金额">
</input>
<input
type="digit"
:value="payNum"
@input="(e) => payNum = e.detail.value"
class="amount-input"
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>
<!-- 2. 支付密码弹窗 -->
<view v-if="currentStep === 2" class="modal-overlay" @click="closePasswordModal">
<view class="password-modal" @click.stop>
<!-- 弹窗头部 -->
<view class="modal-header">
<view class="close-icon" @click="closePasswordModal">
<text class="close-text">×</text>
</view>
<view class="modal-title">请输入支付密码</view>
<view class="header-right"></view>
</view>
<!-- 商户信息 & 金额 -->
<view class="pay-info">
<text class="merchant-name">充值金额</text>
<text class="pay-amount">¥{{ payAmount }}</text>
</view>
<!-- 支付方式选择 -->
<view class="pay-way">
<text class="way-label">支付方式</text>
<view class="way-content">
<image class="bank-icon"
src="http://47.104.199.163:9090/images/propertyImgs/bank-logo.png" mode="aspectFit">
</image>
<text class="bank-name">建设银行储蓄卡(8888)</text>
<text class="arrow-icon">></text>
</view>
</view>
<!-- 密码输入框 -->
<view class="password-input">
<view v-for="(item, index) in 6" :key="index" class="password-item">
<text v-if="password.length > index" class="password-dot"></text>
</view>
</view>
<!-- 数字键盘 -->
<view class="keyboard">
<view class="keyboard-row">
<view class="keyboard-item" @click="inputPassword(1)">1</view>
<view class="keyboard-item" @click="inputPassword(2)">2</view>
<view class="keyboard-item" @click="inputPassword(3)">3</view>
</view>
<view class="keyboard-row">
<view class="keyboard-item" @click="inputPassword(4)">4</view>
<view class="keyboard-item" @click="inputPassword(5)">5</view>
<view class="keyboard-item" @click="inputPassword(6)">6</view>
</view>
<view class="keyboard-row">
<view class="keyboard-item" @click="inputPassword(7)">7</view>
<view class="keyboard-item" @click="inputPassword(8)">8</view>
<view class="keyboard-item" @click="inputPassword(9)">9</view>
</view>
<view class="keyboard-row">
<view class="keyboard-item empty"></view>
<view class="keyboard-item" @click="inputPassword(0)">0</view>
<view class="keyboard-item delete" @click="deletePassword">
<text class="icon"></text>
</view>
</view>
</view>
</view>
</view>
<!-- 3. 缴费成功页 -->
<view class="container" v-if="currentStep==3">
<!-- 2. 缴费成功页 -->
<view class="container" v-if="currentStep==2">
<!-- 成功图标 -->
<view class="success-icon">
<text class="icon-check"></text>
@@ -138,7 +75,7 @@
<!-- 提示文案 -->
<view class="desc">
成功缴纳物业费
成功缴纳{{ switchStatus(paymentItem.type) || '' }}费用 ¥{{ arrearsAmount || payAmount }}
</view>
@@ -152,7 +89,7 @@
<button class="pay-btn" @click="handlePay">立即缴费</button>
</view>
<!-- 返回按钮 -->
<view class="btn-section" v-if="currentStep === 3">
<view class="btn-section" v-if="currentStep === 2">
<button class="pay-btn" @click="handleBackToList">返回生活缴费</button>
</view>
</view>
@@ -163,196 +100,137 @@
<script setup>
import {
ref
ref,
reactive
} from 'vue'
import {
onShow,
onLoad
} from '@dcloudio/uni-app'
// 响应式数据
const statusBarHeight = ref(20)
const outstandingPayment = ref(true)
const arrearsPayment = ref(uni.getStorageSync('arrearsPayment'))
const storageValue = uni.getStorageSync('arrearsPayment')
const paymentItem = reactive(typeof storageValue === 'object' && storageValue !== null ? storageValue : {})
const currentStep = ref(1)
const payAmount = ref('1680.00')
const password = ref('')
const showBankList = ref(false)
const arrearsAmount = ref('21.21')
const payNum = ref('')
const payAmount = ref('0.00')
const arrearsAmount = ref('0.00')
// 银行卡列表
const bankList = ref([{
id: 1,
name: '建设银行储蓄卡(8888)',
icon: '/static/bank-logo.png'
},
{
id: 2,
name: '工商银行储蓄卡(6666)',
icon: '/static/icbc-logo.png'
},
{
id: 3,
name: '招商银行储蓄卡(9999)',
icon: '/static/cmb-logo.png'
}
])
// 默认选中的银行卡
const selectedBank = ref({
id: 1,
name: '建设银行储蓄卡(8888)',
icon: '/static/bank-logo.png'
// 页面加载时初始化数据
onLoad(() => {
initPaymentData()
})
// 自动填充欠费金额
const handleAutoFill = () => {
payNum.value = arrearsAmount.value
uni.vibrateShort({
type: 'light'
})
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.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex'
})
uni.navigateBack();
}
// 立即缴费
// 立即缴费 — 通过微信拉起日照银行小程序进行支付
const handlePay = () => {
if (!payAmount.value || Number(payAmount.value) <= 0) {
uni.showToast({
title: '请输入正确的缴费金额',
icon: 'none'
})
return
}
requestPayOrder()
// 原支付注释
// currentStep.value = 2
}
// 统一下单后后端返回的支付参数
const payInfo = ref({
appId: '', // 公众号/小程序ID
timeStamp: '', // 时间戳
nonceStr: '', // 随机串
package: '', // 数据包 如 prepay_id=xxx
signType: 'MD5', // 签名方式
paySign: '' // 签名
})
const amount = arrearsAmount.value
// 1. 先请求后端接口生成预支付订单
const requestPayOrder = async () => {
try {
uni.showLoading({
title: '发起支付中'
})
uni.showLoading({ title: '加载中...' })
const res = await getMyHouseList({
deviceNo: '设备号',
money: 0.01, // 金额
openid: '用户openid'
})
// 设备类型映射电费→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()
// 后端返回支付所需参数
const data = res.data
if (data.code !== 0) {
uni.showToast({
title: data.msg || '下单失败',
icon: 'none'
})
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
}
// 赋值支付参数
payInfo.value = {
appId: data.appId,
timeStamp: data.timeStamp,
nonceStr: data.nonceStr,
package: data.package,
signType: data.signType,
paySign: data.paySign
}
// 2. 调起微信支付
wx.requestPayment({
...payInfo.value,
success(res) {
console.log('支付成功', res)
uni.showToast({
title: '支付成功'
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' })
})
currentStep.value == 3
},
fail(err) {
console.log('支付失败', err)
uni.showToast({
title: '支付取消或失败',
icon: 'none'
})
}
})
// #endif
} catch (err) {
uni.hideLoading()
console.log('请求异常', err)
uni.showToast({
title: '支付请求异常',
icon: 'none'
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) || ''}费用 ¥${arrearsAmount.value || payAmount.value}`
}).catch(err => {
console.log('推送发送失败', err)
})
}
}
// 关闭密码弹窗
const closePasswordModal = () => {
currentStep.value = 1
password.value = ''
showBankList.value = false
}
// 选择银行卡
const selectBank = (item) => {
selectedBank.value = item
showBankList.value = false
}
// 输入密码
const inputPassword = (num) => {
if (password.value.length >= 6) return
password.value += num.toString()
// 满6位自动提交
if (password.value.length === 6) {
handleConfirmPay()
}
}
// 删除密码
const deletePassword = () => {
password.value = password.value.slice(0, -1)
}
// 确认支付(模拟)
const handleConfirmPay = () => {
uni.showLoading({
title: '支付中...',
mask: true
})
setTimeout(() => {
uni.hideLoading()
currentStep.value = 3
password.value = ''
showBankList.value = false
}, 1500)
}
// 返回缴费列表
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">
@@ -402,6 +280,10 @@
color: #E34D59;
}
.paymentClass {
color: #00aaff;
}
.info-section {
padding-top: 20rpx;
/* margin-bottom: 40rpx; */
@@ -711,6 +593,65 @@
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;

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

@@ -11,52 +11,64 @@
<!-- 1. 物业费详情页 -->
<view class="pay-page">
<!-- 未欠费 -->
<view class="amount-section">
<image src="http://47.104.199.163:9090/images/propertyImgs/noArrears.png" class="background-image-style"></image>
<!-- v-if="arrearsPayment=='暖气费'" -->
<view class="no-arrears-section">
<image src="https://wuye.ckdzkj.com/images/propertyImgs/noArrears.png"
class="background-image-style"></image>
<text class="noArrearsText">暂未查到欠费</text>
<view class="recordText">
<text>{{'最近缴费'+lastPayTime}}</text>
<text>{{'最近充值'+paymentItem.LastRechargeTime || '-'}}</text>
<text class="lineStyle">{{'|'}}</text>
<text>{{'缴费金额:'+lastPayNum+'元'}}</text>
<text>{{'充值金额:'+paymentItem.LastRechargeAmount+'元' || '-'}}</text>
</view>
</view>
<!-- <view>
<view class="amount-section">
<text class="amount-text">{{'¥'+ Math.abs(paymentItem.balance || 0).toFixed(2)}}</text>
</view>
<view style="border-bottom: 2rpx solid #ededed;margin:10rpx 0rpx;"></view>
</view> -->
<!-- 缴费信息 -->
<view class="info-section" style="">
<view v-if="arrearsPayment=='物业费'">
<view class="info-item">
<text class="info-label">户名</text>
<text class="info-value">桂花园别墅2号楼2层</text>
<text
class="info-value">{{paymentItem.villageName+ paymentItem.fullAddress || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">周大声</text>
<text class="info-value">{{ paymentItem.name || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">缴费单位</text>
<text class="info-value">CT物业</text>
<text class="info-value">花开物业</text>
</view>
<view class="info-item">
<text class="info-label">可用余额</text>
<text class="info-value">0.00</text>
<text class="info-value">{{ Math.abs(paymentItem.balance || 0).toFixed(2) }}</text>
</view>
</view>
<view v-else>
<view class="info-item" v-if="arrearsPayment=='暖气费'">
<view class="info-item">
<text class="info-label">房屋</text>
<text class="info-value">桂花园别墅2号楼2层</text>
<text class="info-value">{{ paymentItem.fullAddress || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户号</text>
<text class="info-value">101123456</text>
<text class="info-value">{{ paymentItem.deviceCode || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">周大声</text>
<text class="info-value">{{ paymentItem.name || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">缴费单位</text>
<text class="info-value">HN热力有限公司</text>
<text class="info-value">花开物业</text>
</view>
<view class="info-item">
<text class="info-label">可用余额</text>
<text class="info-value">{{ Math.abs(paymentItem.balance || 0).toFixed(2) }}</text>
</view>
</view>
</view>
@@ -69,6 +81,7 @@
<script setup>
import {
ref,
reactive,
onMounted,
computed
@@ -83,10 +96,9 @@ import {
} from '@dcloudio/uni-app'
// 响应式数据
const lastPayTime = ref('2022-11-29')
const lastPayNum = ref('2022.00')
const statusBarHeight = ref(20)
const arrearsPayment = ref(uni.getStorageSync('arrearsPayment'))
const paymentItem = reactive(uni.getStorageSync('arrearsPayment') || {})
const arrearsPayment = computed(() => paymentItem.type || '缴费')
// 生命周期
onShow(() => {})
@@ -95,9 +107,7 @@ onReady(() => {})
// 方法
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex'
})
uni.navigateBack();
}
// 以下为原代码保留的方法(未使用但保持功能完整)
@@ -136,6 +146,11 @@ const handleBackToList = () => {}
}
.amount-section {
padding: 60rpx 0;
text-align: center;
}
.no-arrears-section {
padding: 30rpx 0;
text-align: center;
display: flex;
@@ -144,6 +159,12 @@ const handleBackToList = () => {}
color: #999999;
}
.amount-text {
font-size: 56rpx;
font-weight: 600;
color: #007aff;
}
.background-image-style {
width: 466rpx;
height: 250rpx;

View File

@@ -1,36 +1,78 @@
<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="http://47.104.199.163:9090/images/propertyImgs/payTitle.png"></image>
<image class="payImg" src="http://47.104.199.163:9090/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>
<scroll-view class="page" scroll-y="true">
</view>
<view class="page">
<view class="container">
<!-- 已缴费/费列表 -->
<view class="bill-list" v-if="paymentList.length != 0">
<!-- <view v-if="paymentList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view> -->
<!-- /费列表 -->
<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="item.icon"></image>
<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.name}}
<view class="bill-status debt" v-if="item.status=='已欠费'">已欠费</view>
<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.address}}</view>
<view class="bill-desc">{{item.villageName+item.buildingName+item.fullAddress}}</view>
</view>
<view class="arrow"></view>
</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>
</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">
@@ -40,147 +82,264 @@
<view class="grid-list">
<view class="grid-item" v-for="(item, index) in addList" :key="index"
@click="goToAdd(item)">
<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>
</view>
<text class="item-name">{{ item.name +'费'}}</text>
<!-- <text class="item-name">{{ item.name +'费'}}</text> -->
<text class="item-name">{{ item.name}}</text>
</view>
</view>
</view>
<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'
// 响应式数据
const statusBarHeight = ref(20)
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([
const paymentList = ref([{
id: 1,
name: '物业费',
status: '已欠费',
address: '桂花园别墅2号楼2层',
icon: "http://47.104.199.163:9090/images/propertyImgs/property.png",
},
{
id: 2,
name: '暖气费',
status: '',
address: '山东省日照市东港区桂花园别墅2号楼2层',
icon: "http://47.104.199.163:9090/images/propertyImgs/heating.png",
},
{
id: 3,
name: '电费',
status: '已欠费',
address: '山东省日照市东港区桂花园别墅2号楼2层',
icon: "http://47.104.199.163:9090/images/propertyImgs/electric.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/electric.png",
}
])
const addList = ref([{
name: "燃气",
type: "gas",
icon: "http://47.104.199.163:9090/images/propertyImgs/gas.png",
iconClass: "gas"
},
const addList = ref([
{
name: "水",
name: "水",
type: "water",
icon: "http://47.104.199.163:9090/images/propertyImgs/water.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/water.png",
iconClass: "water"
},
{
name: "物业",
type: "property",
icon: "http://47.104.199.163:9090/images/propertyImgs/property.png",
iconClass: "property"
},
{
name: "电",
name: "电表",
type: "electric",
icon: "http://47.104.199.163:9090/images/propertyImgs/electric.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/electric.png",
iconClass: "electric"
},
{
name: "车位",
type: "parking",
icon: "http://47.104.199.163:9090/images/propertyImgs/parkingIcon.png",
iconClass: "parking"
}
])
// 生命周期
onReady(() => {})
onShow(() => {})
onLoad(() => {})
// 独立的当前小区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
}
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)
}
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.name)
if (item.status == '已欠费') {
uni.redirectTo({
uni.setStorageSync('arrearsPayment', item)
// 水费、电费走欠费缴纳页
if (item.type === '水费' || item.type === '电费') {
uni.navigateTo({
url: '/pageSubPack/payment-list/arrearsPayment'
})
} else {
uni.redirectTo({
url: '/pageSubPack/payment-list/noArrearsPayment'
uni.navigateTo({
url: '/pageSubPack/payment-list/billPayment'
})
}
}
// 跳转缴费记录
const goToRecord = () => {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/payment-list/paymentRecord'
})
}
// 新增缴费
const goToAdd = (type) => {
uni.setStorageSync('addPaymentType', type.name)
if (type.name == '燃气' || type.name == '暖气') {
uni.redirectTo({
uni.navigateTo({
url: '/pageSubPack/payment-list/selectPaymentEntity'
})
} else if (type.name == '水' || type.name == '电') {
uni.redirectTo({
} else if (type.name == '水' || type.name == '电') {
uni.navigateTo({
url: '/pageSubPack/payment-list/addHydropower'
})
}
uni.setStorageSync('addPaymentType', type.name)
}
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'
// 获取物业费/车位费缴费列表
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();
})
}
uni.removeStorageSync('sourcePage')
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">
@@ -193,19 +352,18 @@
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
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;
}
.topbox {
height: 150rpx;
height: 180rpx;
width: 100%;
position: relative;
}
@@ -215,7 +373,7 @@
height: 74rpx;
position: absolute;
left: 40rpx;
bottom: 20rpx;
bottom: 60rpx;
}
.payImg {
@@ -226,6 +384,33 @@
bottom: -140rpx;
}
/* 小区选择栏 */
.community-bar {
position: absolute;
left: 40rpx;
bottom: 10rpx;
display: flex;
align-items: center;
}
.location-icon {
width: 48rpx;
height: 48rpx;
margin-right: 8rpx;
flex-shrink: 0;
}
.community-name {
font-size: 26rpx;
font-weight: 500;
color: #222;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 260rpx;
margin-right: 6rpx;
}
.container {}
.bill-list {
@@ -233,22 +418,8 @@
background-color: #fff;
padding: 30rpx;
border-radius: 10rpx;
}
.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;
min-height: 550rpx;
box-sizing: border-box;
}
.bill-item {
@@ -264,6 +435,10 @@
margin-bottom: 0rpx;
}
.bill-item.bill-paid {
opacity: 0.6;
}
.bill-info {
width: calc(90% - 100rpx);
flex: 1;
@@ -276,15 +451,16 @@
}
.bill-title {
font-size: 32rpx;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
display: flex;
align-items: center;
}
.bill-desc {
font-size: 28rpx;
font-size: 24rpx;
font-weight: 600;
color: #999;
white-space: nowrap;
@@ -293,13 +469,24 @@
}
.bill-status {
padding: 2rpx 16rpx;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4rpx 14rpx;
border-radius: 30rpx;
font-size: 24rpx;
font-size: 20rpx;
font-weight: 500;
color: #fff;
background-color: #E34D59;
margin-left: 10rpx;
line-height: 1.4;
}
.bill-status.debt {
background-color: #E34D59;
}
.bill-status.done {
background-color: #999;
}
.arrow {
@@ -310,6 +497,42 @@
transform: rotate(-45deg);
}
.details-scroll {
height: 550rpx;
padding: 0 0 20rpx 0;
}
.details-scroll-bill{
height: 550rpx;
padding: 0 0 40rpx 0;
}
.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;
}
.balance-normal {
font-size: 28rpx;
color: #333;
}
.balance-debt {
font-size: 28rpx;
color: #E34D59;
}
.add-section {
background-color: #fff;
padding: 30rpx;
@@ -338,7 +561,6 @@
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 26rpx;
}
.grid-item {
@@ -347,14 +569,19 @@
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: 88rpx;
height: 88rpx;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
display: flex;
align-items: center;
@@ -373,8 +600,16 @@
color: #333;
}
.status-bar {
width: 100vw;
height: 46px;
.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,22 +1,20 @@
<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">
<view class="total-title">累计缴费</view>
<view class="total-price">¥800.00</view>
<view class="total-price">¥{{ totalAmount }}</view>
</view>
<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,18 +32,17 @@
</view>
<!-- 缴费明细 -->
<!-- 缴费明细分页滚动 -->
<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="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>
<view class="month-group" v-for="(item, idx) in monthGroups" :key="idx">
<view class="month-title">
{{ item.month }} 缴费 ¥{{ item.total }}
{{ item.year }}{{ item.month }} 缴费 ¥{{ item.total.toFixed(2) }}
</view>
<view class="item-list">
@@ -57,14 +54,14 @@
<view class="name">{{ bill.name }}</view>
<view class="account">{{ bill.account }}</view>
</view>
<view class="bill-price">-{{ bill.amount }}</view>
<view class="bill-price">-{{ bill.amount.toFixed(2) }}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
@@ -83,108 +80,164 @@
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
getTopupRecordWithStat
} from '/pageSubPack/api/apiSub.js'
// 响应式数据
const statusBarHeight = ref(20)
const chartData = ref([{
month: '1月',
price: 250
},
{
month: '2月',
price: 130
},
{
month: '3月',
price: 220
},
{
month: '4月',
price: 250
},
{
month: '5月',
price: 100
},
{
month: '6月',
price: 130
}
])
const maxPrice = ref(250)
const pageNum = ref(1)
const pageSize = ref(10)
const totalAmount = ref('0.00')
const chartData = ref([])
const maxPrice = ref(100)
const barMaxHeight = ref(300)
const chart = ref({
months: ['1月', '2月', '3月', '4月', '5月', '6月'],
values: [100, 140, 230, 100, 130, 100]
const billList = ref([])
// 费用名称映射
// 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 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,
pageSize: pageSize.value
})
if (res.code === 200) {
const records = res.data.records || []
// 转换记录格式
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
// 累计缴费金额
totalAmount.value = res.data.totalAmount || '0.00'
// 柱状图数据使用API返回的monthlyStats
updateChartData(res.data.monthlyStats || {})
}
} catch (err) {
console.error('获取缴费记录失败', err)
}
}
// 加载更多(触底分页)
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
}
}
})
const billList = ref([{
id: 1,
month: 5,
name: '燃气费',
account: '123123123123',
amount: 25,
icon: "http://47.104.199.163:9090/images/propertyImgs/gas.png"
},
{
id: 2,
month: 5,
name: '水费',
account: '123123123123',
amount: 25,
icon: "http://47.104.199.163:9090/images/propertyImgs/water.png"
},
{
id: 3,
month: 6,
name: '燃气费',
account: '123123123123',
amount: 30,
icon: "http://47.104.199.163:9090/images/propertyImgs/gas.png"
},
{
id: 4,
month: 6,
name: '水费',
account: '123123123123',
amount: 10,
icon: "http://47.104.199.163:9090/images/propertyImgs/water.png"
},
{
id: 5,
month: 6,
name: '电费',
account: '123123123123',
amount: 30,
icon: "http://47.104.199.163:9090/images/propertyImgs/electric.png"
// 取最近6个月当前月往前
const now = new Date()
const currentMonth = now.getMonth() + 1
const currentYear = now.getFullYear()
const months = []
for (let i = 0; i < 6; i++) {
let month = currentMonth - i
let year = currentYear
if (month <= 0) {
month += 12
year -= 1
}
const key = `${year}-${String(month).padStart(2, '0')}`
const data = statsMap[key]
months.push({
month,
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: 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)
})
// 生命周期
onShow(() => {})
onMounted(() => {})
onReady(() => {
drawLineChart()
onLoad(() => {
getRecords()
})
onReady(() => {})
// 方法
const barHeight = (price) => {
@@ -192,81 +245,8 @@
return `${height}rpx`
}
const drawLineChart = () => {
// #ifdef MP-WEIXIN
const ctx = uni.createCanvasContext('chargeChart')
// #endif
// #ifndef MP-WEIXIN
const ctx = uni.createCanvasContext('chargeChart', this)
// #endif
const canvasPage = uni.getSystemInfoSync();
const w = canvasPage.windowWidth - 30;
const h = canvasPage.windowHeight * 0.3;
const padding = 40
const stepX = (w - 60) / (chart.value.months.length - 1)
const maxVal = 250
ctx.setFillStyle('#999')
ctx.setFontSize(12)
ctx.setTextAlign('center')
ctx.beginPath()
ctx.setStrokeStyle('#d8d8d8')
ctx.setLineWidth(1)
for (let i = 0; i <= 5; i++) {
let y = padding + (h - padding * 2) * (1 - i / 5)
ctx.moveTo(40, y)
ctx.lineTo(w - 20, y)
ctx.fillText(i * 50 + '元', 20, y + 4)
}
ctx.stroke()
chart.value.months.forEach((m, i) => {
let x = 40 + i * stepX
ctx.fillText(m, x, h - 20)
})
let points = []
chart.value.values.forEach((val, i) => {
let x = 40 + i * stepX
let y = padding + (h - padding * 2) * (1 - val / maxVal)
points.push({
x,
y
})
})
ctx.beginPath()
ctx.setStrokeStyle('#4080FF')
ctx.setLineWidth(2)
points.forEach((p, i) => {
if (i === 0) ctx.moveTo(p.x, p.y)
else ctx.lineTo(p.x, p.y)
})
ctx.stroke()
points.forEach((p, i) => {
ctx.beginPath()
ctx.arc(p.x, p.y, 3, 0, 2 * Math.PI)
ctx.setFillStyle('#fff')
ctx.fill()
ctx.setStrokeStyle('#4080FF')
ctx.setLineWidth(2)
ctx.stroke()
ctx.setFillStyle('#4080FF')
ctx.setFontSize(13)
ctx.fillText(chart.value.values[i] + '元', p.x, p.y - 10)
})
ctx.draw()
}
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex'
})
uni.navigateBack();
}
</script>
@@ -286,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 {
@@ -443,7 +430,7 @@
}
.chart-month {
font-size: 32rpx;
font-size: 26rpx;
color: #666;
margin-top: 24rpx;
}

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,9 +233,7 @@
const onSearch = () => {}
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex'
})
uni.navigateBack();
}
</script>

View File

@@ -0,0 +1,378 @@
<template>
<view class="container">
<!-- 状态栏 -->
<view class="status-bar"></view>
<!-- 顶部导航 -->
<uni-nav-bar
title="支付收银台"
left-icon="left"
@clickLeft="goBack"
background-color="#F5F6F9"
color="#000"
/>
<!-- 内容区域 -->
<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="btn-wrap">
<view class="pay-btn" @click="handlePayClick">
<text class="btn-text">立即支付 {{ amount }} </text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getUnionPayTn } from '/pageSubPack/api/apiSub.js'
import { getPushSingle } from '/pageSubPack/api/api.js'
// 支付宝和微信支付
const amount = ref('0.00')
const countdownTime = ref('10:00')
let timer = null
const payData = ref({})
onLoad(() => {
const data = uni.getStorageSync('payData')
if (data) {
payData.value = data
amount.value = Number(data.amount || 0).toFixed(2)
}
startCountdown()
})
onUnmounted(() => {
if (timer) clearInterval(timer)
})
const startCountdown = () => {
let [min, sec] = countdownTime.value.split(':')
let totalSec = parseInt(min) * 60 + parseInt(sec)
timer = setInterval(() => {
totalSec--
if (totalSec <= 0) {
clearInterval(timer)
uni.showToast({ title: '支付超时', icon: 'none' })
setTimeout(() => uni.navigateBack(), 1500)
return
}
const newMin = Math.floor(totalSec / 60).toString().padStart(2, '0')
const newSec = (totalSec % 60).toString().padStart(2, '0')
countdownTime.value = `${newMin}:${newSec}`
}, 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'),
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))
uni.hideLoading()
if (res.code !== 200 || !res.data) {
uni.showToast({ title: res.msg || '获取支付参数失败', icon: 'none' })
return
}
const payType = selectedType.value
const data = res.data
if (payType === 'wx') {
invokeWxPay(data.wx || data.wxPayInfo || data)
} else if (payType === 'alipay') {
invokeAlipay(data.alipay || data.aliPayInfo || data)
}
} catch (err) {
uni.hideLoading()
console.log('支付异常', err)
uni.showToast({ title: '支付请求异常', icon: 'none' })
}
}
// 微信支付
const invokeWxPay = (data) => {
const orderInfo = {
appid: data.appId,
partnerid: data.partnerId || data.partnerid || '',
prepayid: data.prepayId || data.prepayid || (data.package || '').replace('prepay_id=', ''),
package: 'Sign=WXPay',
noncestr: data.nonceStr,
timestamp: data.timeStamp,
sign: data.paySign
}
uni.requestPayment({
provider: 'wxpay',
orderInfo: orderInfo,
success: (res) => {
console.log('微信支付成功', res)
paySuccess()
},
fail: (err) => {
console.error('微信支付失败或取消', err)
uni.showToast({ title: '支付取消或失败', icon: 'none' })
}
})
}
// 支付宝支付
const invokeAlipay = (data) => {
const orderInfo = data.orderInfo || data
uni.requestPayment({
provider: 'alipay',
orderInfo: orderInfo,
success: (res) => {
console.log('支付宝支付成功', res)
paySuccess()
},
fail: (err) => {
console.error('支付宝支付失败或取消', err)
uni.showToast({ title: '支付取消或失败', icon: 'none' })
}
})
}
const paySuccess = () => {
sendPaySuccessPush()
uni.showToast({ title: '支付成功', icon: 'success' })
uni.setStorageSync('paySuccess', true)
setTimeout(() => {
uni.navigateTo({
url: '/pageSubPack/payment-list/addPaymentSuccess?type=pay'
})
}, 1500)
}
const sendPaySuccessPush = () => {
const userId = uni.getStorageSync('userId')
if (userId) {
getPushSingle({
userId: userId,
channel: 'resident',
title: '缴费成功',
content: `您已成功缴纳${payData.value.type || ''}费用 ¥${amount.value}`
}).catch(err => console.log('推送发送失败', err))
}
}
</script>
<style scoped>
/* 全局背景 */
.container {
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: #ffffff;
border-radius: 20rpx;
padding: 50rpx 0;
text-align: center;
margin: 30rpx 0;
}
.amount-text {
font-size: 28rpx;
color: #666;
margin-bottom: 16rpx;
}
.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 {
background: #ffffff;
border-radius: 20rpx;
padding: 0 30rpx;
margin-bottom: 30rpx;
}
.pay-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 36rpx 0;
border-bottom: 1rpx solid #f2f2f2;
}
.pay-item:last-child {
border-bottom: none;
}
.left {
display: flex;
align-items: center;
}
.icon {
width: 56rpx;
height: 56rpx;
margin-right: 24rpx;
}
.pay-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
/* 单选按钮 — 对勾样式 */
.radio {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
border: 3rpx solid #d0d0d0;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.radio.active {
background-color: #2F77FD;
border-color: #2F77FD;
}
.radio.active::after {
content: '';
width: 12rpx;
height: 22rpx;
border: 4rpx solid #fff;
border-left: 0;
border-top: 0;
transform: rotate(45deg) translate(-2rpx, -2rpx);
}
/* 提示 */
.tip {
font-size: 32rpx;
color: #333;
font-weight: 500;
text-align: left;
padding: 20rpx 0;
}
/* 底部按钮区域 */
.btn-wrap {
padding: 30rpx 0 60rpx;
}
/* 底部支付按钮 */
.pay-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: #2F77FD;
color: #fff;
text-align: center;
font-size: 34rpx;
font-weight: bold;
border-radius: 8rpx;
box-shadow: 0 6rpx 20rpx rgba(47, 119, 253, 0.3);
}
</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,6 +161,8 @@ const villageId = ref('')
const tempFilePaths = ref([])
const maxCount = ref(9)
const isSubmitting = ref(false)
// 关闭所有弹窗
const closeAllPopup = () => {
showTypePopup.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 || '提交失败,请重试',
@@ -289,9 +294,7 @@ const submitForm = async () => {
// 返回
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/service-list/complaintsSuggestionsIndex'
})
uni.navigateBack()
}
</script>
<style lang="scss">
@@ -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(() => {
@@ -120,9 +120,7 @@ const previewImage = (index) => {
// 返回
const goBack = () => {
uni.redirectTo({
url: '/pageSubPack/service-list/complaintsSuggestionsIndex'
})
uni.navigateBack()
}
</script>

View File

@@ -1,20 +1,18 @@
<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="60" color="#ccc"></uni-icons>
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
@@ -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;
}
@@ -181,14 +178,52 @@ const goBack = () => {
}
.empty-text {
font-size: 32rpx;
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
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,67 +1,70 @@
<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="60" color="#ccc"></uni-icons>
<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)">
<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>
<!-- <text class="phone-icon"></text> -->
<uni-icons type="phone" size="16" color="#2F77FD"></uni-icons>
</view>
</view>
</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' });
return;
}
const cleanPhone = phone.replace(/\D/g, '');
if (!/^1[3-9]\d{9}$/.test(cleanPhone) && !/^0\d{2,3}\d{7,8}$/.test(cleanPhone)) {
uni.showToast({ title: '电话号码格式有误', icon: 'none' });
return;
}
uni.makePhoneCall({
phoneNumber: phone.replace(/-/g, ""),
phoneNumber: cleanPhone,
fail: (err) => {
console.log('用户取消拨号', err)
console.log('拨号失败或用户取消', err);
uni.showToast({ title: '拨号失败,请长按复制号码', icon: 'none' });
}
});
}
function goBack() {
uni.switchTab({
url: '/pages/serviceIndex/serviceIndex',
function copyPhone(phone) {
if (!phone || !phone.trim()) {
uni.showToast({ title: '电话号码为空', icon: 'none' });
return;
}
uni.setClipboardData({
data: phone.replace(/\D/g, ''),
success: () => {
uni.showToast({ title: '号码已复制', icon: 'none' });
},
fail: () => {
uni.showToast({ title: '复制失败', icon: 'none' });
}
});
}
@@ -77,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 }] : []
@@ -90,8 +93,8 @@ import { getManagePhoneData } from '../api/apiSub.js'
uni.showToast({ title: '获取失败', icon: 'none' })
})
})
</script>
<style lang="scss">
page {
background: #f9f9f9;
@@ -102,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;
@@ -117,23 +126,15 @@ import { getManagePhoneData } from '../api/apiSub.js'
}
.empty-text {
font-size: 32rpx;
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);
@@ -151,6 +152,7 @@ import { getManagePhoneData } from '../api/apiSub.js'
height: 60px;
border-radius: 8rpx;
margin-right: 24rpx;
margin-top: 20rpx;
flex-shrink: 0;
}
@@ -162,7 +164,6 @@ import { getManagePhoneData } from '../api/apiSub.js'
gap: 12rpx;
}
/* 物业名称 */
.property-name {
font-size: 32rpx;
font-weight: 600;
@@ -170,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;
@@ -185,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>

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,11 +11,15 @@
<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">
<text>问卷内容加载中请稍后...</text>
<text>暂无数据</text>
</view>
<!-- 动态问题列表 -->
@@ -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,11 +118,27 @@ 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
@@ -123,15 +153,32 @@ 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) => {
// 再次点击已选中的选项时取消选择
if (answers.value[cid] === label) {
delete answers.value[cid]
} else {
answers.value[cid] = label
}
}
const isChecked = (cid, label) => {
const arr = answers.value[cid]
@@ -170,10 +217,13 @@ const handleSubmit = async () => {
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' })
@@ -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 {
@@ -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%;
@@ -392,6 +460,11 @@ page {
&::after {
border: none;
}
&.disabled-btn {
background-color: #ccc;
color: #999;
}
}
}

View File

@@ -1,18 +1,15 @@
<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="60" color="#ccc"></uni-icons>
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
@@ -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,6 +102,8 @@
}
}catch(err){
uni.showToast({ title: err.msg ||'网络异常', icon: 'none' })
}finally{
uni.stopPullDownRefresh();
}
}
@@ -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;
@@ -256,7 +248,7 @@
}
.empty-text {
font-size: 32rpx;
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;

View File

@@ -17,7 +17,7 @@
</view>
<view class="btn-wrap">
<button class="send-btn" open-type="share" :image-url="qrcodeUrl">发送给好友</button>
<button class="send-btn" @click="saveToAlbum">保存图片到相册</button>
</view>
</view>
</template>
@@ -41,22 +41,71 @@ onLoad((option) => {
}
});
// 分享给好友
// const sendToFriend = () => {
// if (!qrcodeImage.value) {
// uni.showToast({ title: '二维码生成中...', icon: 'none' });
// return;
// }
// 保存图片到相册
const saveToAlbum = () => {
if (!qrcodeUrl.value) {
uni.showToast({ title: '二维码加载中...', icon: 'none' });
return;
}
// uni.share({
// provider: 'weixin',
// type: 0,
// title: '访客通行二维码',
// imageUrl: qrcodeImage.value,
// success: () => uni.showToast({ title: '分享成功' }),
// fail: () => uni.showToast({ title: '分享失败', icon: 'none' })
// });
// };
uni.showLoading({ title: '保存中...', mask: true });
const doSave = (filePath) => {
uni.saveImageToPhotosAlbum({
filePath,
success: () => {
uni.hideLoading();
uni.showToast({ title: '保存成功', icon: 'success' });
},
fail: (err) => {
uni.hideLoading();
console.log('保存失败', err);
const errMsg = (err.errMsg || err.message || '').toLowerCase();
const isAuthError = errMsg.includes('auth') ||
errMsg.includes('deny') ||
errMsg.includes('permission') ||
errMsg.includes('拒绝') ||
err.errCode === '1' ||
err.errCode === 1;
if (isAuthError) {
uni.showModal({
title: '提示',
content: '保存图片需要相册权限,请在设置中开启',
confirmText: '去设置',
success: (res) => {
if (res.confirm) uni.openSetting();
}
});
} else {
uni.showToast({ title: '保存失败', icon: 'none' });
}
}
});
};
if (qrcodeUrl.value.startsWith('http')) {
uni.downloadFile({
url: qrcodeUrl.value,
success: (res) => {
if (res.statusCode === 200) {
doSave(res.tempFilePath);
} else {
uni.hideLoading();
uni.showToast({ title: '图片下载成功', icon: 'none' });
}
},
fail: (err) => {
uni.hideLoading();
console.log('下载失败', err);
uni.showToast({ title: '图片下载失败', icon: 'none' });
}
});
} else {
doSave(qrcodeUrl.value);
}
};
</script>
<style scoped>

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>
<!-- 车牌号仅车辆来访显示 -->
@@ -58,15 +60,15 @@
<view class="car-number-wrap">
<input class="car-input" v-for="(item, index) in 8" :key="index"
:value="form.carNumber[index]" @input="(e) => onCarInput(index, e)"
:focus="carFocusIndex === index"
@focus="carFocusIndex = index"
type="text" />
</view>
</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>
@@ -74,22 +76,22 @@
<view class="section-title">有效期</view>
<view class="form-section">
<view class="form-item">
<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="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>
@@ -105,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">
@@ -119,7 +121,7 @@
</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"
@@ -138,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'
@@ -170,19 +173,40 @@
const addressModalRef = ref(null);
const genderModalRef = ref(null);
// 页面固定标志
const pageFixed = ref(false);
// 车牌号输入框聚焦索引
const carFocusIndex = ref(-1);
// 日期选择器是否打开
const pickerVisible = ref(false);
// 拦截返回事件:若日期选择器打开则关闭选择器而非返回上一页
onBackPress(() => {
if (pickerVisible.value) {
pickerVisible.value = false;
return true;
}
});
// =========方法===========
// 获取房屋列表
const fetchHouseList = async () => {
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()
}))
}
} catch (e) {
console.error('获取房屋列表失败', e)
uni.showToast({
title:e.msg || '获取房屋列表失败',
icon:'none'
})
// console.error('获取房屋列表失败', e)
}
}
@@ -206,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();
};
@@ -225,18 +254,75 @@
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 : '');
form.carNumber[index] = (val || '').toUpperCase().slice(0, 1);
const upperVal = (val || '').toUpperCase().slice(0, 1);
form.carNumber[index] = upperVal;
// 输入一位后自动聚焦到下一个
if (upperVal && index < 7) {
carFocusIndex.value = index + 1;
}
};
// 姓名输入过滤特殊字符
const onNameInput = (e) => {
const val = e.detail.value;
// 只允许中文、英文字母、数字和中间点(·)
const filtered = val.replace(/[^一-龥a-zA-Z0-9·]/g, '')
if (filtered !== val) {
form.name = filtered;
}
};
// 手机号输入过滤非数字字符
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)) {
@@ -244,7 +330,16 @@
title: '离开时间不能早于来访时间',
icon: 'none'
});
form.leaveTime = visitMoment.clone().add(1, 'hours').format('YYYY-MM-DD HH:mm');
return false;
}
// 离开时间与来访时间间隔不能少于30分钟
const diffMinutes = leaveMoment.diff(visitMoment, 'minutes');
if (diffMinutes < 30) {
uni.showToast({
title: '离开时间与来访时间间隔不能少于30分钟',
icon: 'none'
});
return false;
}
@@ -255,12 +350,12 @@
title: '时间间隔不能超过24小时',
icon: 'none'
});
form.leaveTime = visitMoment.clone().add(24, 'hours').format('YYYY-MM-DD HH:mm');
return false;
}
return true;
};
// 随行人数输入过滤小数
// 提交表单
const submitForm = async () => {
// 1. 必填项校验
@@ -306,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: '请选择来访时间',
@@ -365,7 +460,7 @@
}
} catch (e) {
uni.hideLoading();
uni.showToast({ title: '网络异常,请重试', icon: 'none' });
uni.showToast({ title: e.msg || '网络异常,请重试', icon: 'none' });
}
};
@@ -387,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;
}
/* 导航栏 */
@@ -408,6 +515,7 @@
/* 标签切换 */
.tab-wrap {
display: flex;
flex-shrink: 0;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
@@ -428,7 +536,10 @@
/* 表单区域 */
.form-wrap {
padding: 10rpx;
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 10rpx 10rpx 180rpx;
}
.form-section {
@@ -456,7 +567,7 @@
}
.label {
width: 205rpx;
width: 225rpx;
font-size: 32rpx;
color: #333;
}
@@ -514,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 {
@@ -552,7 +670,8 @@
.gender-list {
display: flex;
flex-direction: column;
gap: 20rpx;
max-height: 60vh;
overflow-y: auto;
}
.address-item,
@@ -562,6 +681,12 @@
border-radius: 8rpx;
text-align: center;
font-size: 30rpx;
margin-bottom: 20rpx;
}
.address-item:last-child,
.gender-item:last-child {
margin-bottom: 0;
}
.address-item.active,
@@ -570,3 +695,11 @@
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,15 +1,5 @@
<template>
<view class="visitor-page">
<view class="status_bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-left" @tap="goBack">
<image src="http://47.104.199.163:9090/images/home/arr-left.png" style="width: 18px; height: 18px;"></image>
</view>
<view class="nav-title">访客邀请</view>
<view class="nav-right"></view>
</view>
<!-- 标签切换栏 -->
<view class="tab-bar">
<view
@@ -29,8 +19,7 @@
</view>
<!-- 访客列表 -->
<scroll-view class="list-container" scroll-y>
<!-- 列表项根据标签切换显示对应数据 -->
<view class="list-container">
<view
class="visitor-item"
v-for="(item, index) in filterdList"
@@ -38,14 +27,22 @@
>
<!-- 访客基本信息 -->
<view class="item-header">
<text class="visitor-name" :class="item.status">来访者{{ item.visitorName }}</text>
<text class="status-tag" :class="item.status">{{ item.statusText }}</text>
<text class="visitor-name" :class="item.status">
来访者{{ item.visitorName }}
</text>
<text class="status-tag" :class="item.status">
{{ item.statusText }}
</text>
</view>
<!-- 有效期/到访时间 -->
<view class="item-row">
<text class="label" :class="item.status">{{ item.status === 'waiting' ? '有效期' : '到访时间' }}</text>
<text class="value" :class="item.status">{{ item.startTime }}{{item.endTime}}</text>
<text class="label" :class="item.status">
{{ item.status === "waiting" ? "有效期" : "到访时间" }}
</text>
<text class="value" :class="item.status">
{{ item.startTime }}{{ item.endTime }}
</text>
</view>
<!-- 联系方式 -->
@@ -54,317 +51,287 @@
<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>
<!-- 时间+查看详情 -->
<view class="item-footer">
<text class="time-text" :class="item.status">时间{{ item.createTime }}</text>
<text class="time-text" :class="item.status">
时间{{ item.createTime }}
</text>
<view class="detail-btn" @click="viewDetail(item)">
<text>查看详情</text>
<uni-icons type="arrowright" size="14"></uni-icons>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="filterdList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
{{ activeTab === 'waiting' ? '暂无等待到访数据' : '暂无已到访数据' }}
{{ activeTab === "waiting" ? "暂无等待到访数据" : "暂无已到访数据" }}
</text>
</view>
</scroll-view>
<!-- 底部占位 -->
<view class="list-bottom-space"></view>
</view>
<!-- 新增邀请按钮 -->
<view class="add-btn-wrapper">
<view class="add-btn" @click="addInvitation">
<!-- <uni-icons type="plus" size="18" color="#fff"></uni-icons> -->
<text class="add-text">新增邀请+</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getVisitorList } from '../api/api.js'
import { ref, computed } from "vue";
import { onShow, onPullDownRefresh } from "@dcloudio/uni-app";
import { getVisitorList } from "../api/api.js";
// 激活的标签waiting=等待来访visited=已到访)
const activeTab = ref('waiting')
const activeTab = ref("waiting");
const visitorList = ref([]);
// 访客列表数据
const visitorList = ref([])
// 后端数据映射到前端
const mapData = (list) => {
if (!Array.isArray(list)) return []
return list.map(item => {
const status = item.status === 1 || item.status === '1' ? 'visited' : 'waiting'
if (!Array.isArray(list)) return [];
return list.map((item) => {
const status =
item.status === 1 || item.status === "1" ? "visited" : "waiting";
return {
...item,
visitorName: item.visitorName,
status,
statusText: status === 'visited' ? '已到访' : '等待来访',
startTime: item.startTime,
endTime: item.endTime,
statusText: status === "visited" ? "已到访" : "等待来访",
phone: item.visitorPhone,
carNumber: item.carNum,
createTime: item.createTime || ''
}
})
}
createTime: item.createTime || "",
};
});
};
// 获取访客列表
const fetchVisitorList = async () => {
const userId = uni.getStorageSync('userId')
let params = {userId:userId}
try {
const res = await getVisitorList(params)
visitorList.value = mapData(res.data || [])
// console.log('222222',visitorList.value)
} catch (e) {
console.error('获取访客列表失败:', e)
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(() => {
fetchVisitorList()
})
onShow(() => {
fetchVisitorList();
});
onPullDownRefresh(() => {
fetchVisitorList();
});
// 根据标签筛选列表
const filterdList = computed(() => {
return visitorList.value.filter(item => item.status === activeTab.value)
})
// 返回上一页
const goBack = () => {
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')
return visitorList.value.filter((item) => item.status === activeTab.value);
});
}
// 查看详情
const viewDetail = (item) => {
uni.navigateTo({
url: `/pageSubPack/visitor/visitor-detail?id=${item.id}` // 传 id 到详情页
})
}
url: `/pageSubPack/visitor/visitor-detail?id=${item.id}`,
});
};
// 新增邀请
const addInvitation = () => {
uni.navigateTo({
url: '/pageSubPack/visitor/visitor-add'
})
}
uni.navigateTo({ url: "/pageSubPack/visitor/visitor-add" });
};
</script>
<style scoped>
/* 页面整体样式 */
/* 页面根容器 */
.visitor-page {
background: linear-gradient(to bottom left, #E2F0FF 0%, #f9f9f9 50%);
min-height: 100vh;
padding: 0 40rpx;
}
.status_bar {
height: 32px;
width: 100%;
}
/* 顶部导航栏 */
.nav-bar {
height: 90rpx;
display: flex;
align-items: center;
/* padding: 0 30rpx; */
color: #fff;
position: sticky;
top: 44px;
z-index: 100;
margin-bottom: 20px;
}
.nav-left {
width: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0 20rpx;
color: #000;
}
.nav-right {
width: 60rpx;
box-sizing: border-box;
background: linear-gradient(135deg, #e2f0ff 0%, #f9f9f9 50%);
padding: 0 30rpx;
}
/* 标签切换栏 */
.tab-bar {
position: sticky;
top: 0;
z-index: 10;
display: flex;
}
.tab-item {
/* flex: 1; */
display: flex;
text-align: left;
padding: 20px 40px 0px 0px;
font-size: 16px;
color: #666;
position: relative;
padding: 24rpx 0;
margin-right: 48rpx;
font-size: 30rpx;
color: #666;
}
.tab-item.active {
color: #007aff;
color: #2f77fd;
font-weight: 500;
}
/* 激活标签下划线 */
.tab-item.active::after {
content: '';
content: "";
position: absolute;
bottom: -7px;
left: 33%;
bottom: 8rpx;
left: 50%;
transform: translateX(-50%);
width: 64px;
height: 4px;
border-radius: 6rpx;
background-color: #2F77FD;
/* background-color: linear-gradient(to bottom, #2F77FD, #CEDFFF); */
width: 126rpx;
height: 8rpx;
border-radius: 32rpx;
background: linear-gradient(to bottom, #2F77FD, #CEDFFF);
}
/* 列表容器 */
/* 列表区域 */
.list-container {
height: calc(100vh - 220px); /* 预留导航、标签、按钮高度 */
}
/* 访客列表项 */
/* 访客卡片 */
.visitor-item {
margin-top: 24rpx;
padding: 24rpx;
background-color: #fff;
margin: 25px 0px;
padding: 15px;
border-radius: 8px;
border-radius: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
/* 列表项头部(姓名+状态) */
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
margin-bottom: 16rpx;
}
.visitor-name {
font-size: 16px;
color: #333;
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.visitor-name.visited {
color: #999;
}
.status-tag {
font-size: 14px;
padding: 2px 8px;
border-radius: 12px;
}
.status-tag.waiting {
color: #2F77FD;
/* background-color: #fff8e6; */
}
.status-tag.visited {
color: #999999;
/* background-color: #e6fffa; */
font-size: 26rpx;
}
/* 列表项行数据 */
.item-row {
display: flex;
margin-bottom: 8px;
font-size: 14px;
.status-tag.waiting {
color: #2f77fd;
}
.label {
color: #222;
min-width: 80px;
}
.value {
color: #333;
flex: 1;
}
.label.visited{
.status-tag.visited {
color: #999;
}
/* 信息行 */
.item-row {
display: flex;
margin-bottom: 12rpx;
font-size: 26rpx;
line-height: 1.6;
}
.label {
min-width: 140rpx;
color: #666;
}
.value {
flex: 1;
color: #333;
}
.label.visited,
.value.visited {
color: #999;
}
.time-text.visited{
color: #999;
}
/* 列表项底部(时间+查看详情) */
/* 底部行 */
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-top: 10px;
/* border-top: 1px solid #f5f5f5; */
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f5f5f5;
}
.time-text {
font-size: 28rpx;
/* color: #666; */
font-size: 24rpx;
color: #999;
}
.time-text.visited {
color: #bbb;
}
.detail-btn {
display: flex;
align-items: center;
font-size: 14px;
font-size: 26rpx;
color: #666;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
padding: 120rpx 0;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
font-size: 28rpx;
color: #999;
}
/* 列表底部占位(为固定的按钮预留高度,保证最后一项能滚到按钮上方) */
.list-bottom-space {
height: 180rpx;
}
/* 新增邀请按钮 */
.add-btn {
.add-btn-wrapper {
position: fixed;
bottom: 20px;
left: 15px;
right: 15px;
height: 44px;
line-height: 44px;
background-color: #007aff;
color: #fff;
border-radius: 8px;
text-align: center;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
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 {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #2f77fd;
color: #fff;
border-radius: 16rpx;
text-align: center;
font-size: 30rpx;
font-weight: 500;
box-shadow: 0 8rpx 24rpx rgba(47, 119, 253, 0.3);
}
.add-text {
margin-left: 5px;
font-size: 28rpx;
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
}
}
]
@@ -214,36 +219,64 @@
"pages": [{
"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",
@@ -392,6 +438,25 @@
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "yinlianPayTn",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "propertyFeePayment",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "billPayment",
"style": {
"navigationBarTitleText": "缴费账单"
}
}
]
@@ -401,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": "收益详情"
}
}
]

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="http://47.104.199.163:9090/images/propertyImgs/touxiang.jpg" 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="http://47.104.199.163:9090/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>
@@ -46,32 +46,37 @@
import {
ref
} from 'vue';
import { getUserProfile } from '../../pageSubPack/api/apiSub';
import {onShow} from '@dcloudio/uni-app'
import { requireLogin } from '@/common/checkLogin.js'
const list = ref([{
icon: "http://47.104.199.163:9090/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: "http://47.104.199.163:9090/images/propertyImgs/user.png",
title: "住户管理",
// desc: "您可以在这里添加家人、朋友、租客~"
},
{
icon: "http://47.104.199.163:9090/images/propertyImgs/parking.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/parking.png",
title: "我的车位"
},
{
icon: "http://47.104.199.163:9090/images/propertyImgs/car.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/car.png",
title: "我的车辆"
},
{
icon: "http://47.104.199.163:9090/images/propertyImgs/setting.png",
icon: "https://wuye.ckdzkj.com/images/propertyImgs/setting.png",
title: "设置"
}
])
const loginName = ref("wangh")
const phoneNum = ref('156*****1212')
const loginName = ref("")
const phoneNum = ref('')
const avatar = ref('')
// --方法----
const goToDetail = () => {
@@ -79,48 +84,80 @@
url: '/pageSubPack/notice-list/noticeListDetails'
})
}
// 获取用户账号信息
function getUserInfoData(){
getUserProfile().then(res => {
if (res.code === 200 && res.data) {
avatar.value = res.data.avatar
loginName.value = res.data.username
phoneNum.value = res.data.phone || ''
}
})
}
// 返回
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 == '我的积分') {
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(() =>{
const token = uni.getStorageSync('token')
if (!token) return
const currentUserId = uni.getStorageSync('userId') || ''
// 仅登录状态变化时刷新,切后台回来直接用缓存
if (currentUserId !== lastUserIdMy) {
lastUserIdMy = currentUserId
getUserInfoData()
}
})
</script>
@@ -136,7 +173,7 @@
.contentStyle {
display: flex;
flex-direction: column;
height: calc(100vh - 100rpx);
height: 100%;
.topBox {
width: 100%;
@@ -203,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,6 +1,6 @@
<template>
<view class="guide-page">
<view class="status_bar"></view>
<view class="guide-page" v-if="showContent">
<view class="status-bar"></view>
<!-- 引导内容滑动容器 -->
<swiper
class="guide-swiper"
@@ -14,36 +14,39 @@
<swiper-item>
<view class="guide-item">
<view class="guide-img">
<image src="http://47.104.199.163:9090/images/guide1.png" style="width: 100%;height: 100%;"></image>
<image src="https://wuye.ckdzkj.com/images/guide1.png" mode="aspectFit" class="img-content"></image>
</view>
<view style="height: 200rpx;width: 100%;"></view>
<view class="guide-text">
<view class="guide-title">便捷服务</view>
<view class="guide-subtitle">报修缴费预约</view>
</view>
</view>
</swiper-item>
<!-- 高效管理 -->
<swiper-item>
<view class="guide-item">
<view class="guide-img">
<image src="http://47.104.199.163:9090/images/guide2.png" style="width: 100%;height: 100%;"></image>
<image src="https://wuye.ckdzkj.com/images/guide2.png" mode="aspectFit" class="img-content"></image>
</view>
<view style="height: 200rpx;width: 100%;"></view>
<view class="guide-text">
<view class="guide-title">高效管理</view>
<view class="guide-subtitle">自动化流程和智能设备</view>
</view>
</view>
</swiper-item>
<!--安全保障 -->
<swiper-item>
<view class="guide-item">
<view class="guide-img">
<image src="http://47.104.199.163:9090/images/guide3.png" style="width: 100%;height: 100%;"></image>
<image src="https://wuye.ckdzkj.com/images/guide3.png" mode="aspectFit" class="img-content"></image>
</view>
<view style="height: 200rpx;width: 100%;"></view>
<view class="guide-text">
<view class="guide-title">安全保障</view>
<view class="guide-subtitle">智能巡检和智慧访客</view>
</view>
</view>
</swiper-item>
</swiper>
@@ -68,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切换步骤
@@ -83,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>
@@ -106,40 +115,74 @@ const handleBtnClick = () => {
.guide-page {
width: 100%;
height: 100vh;
/* background-color: #f8f9ff; */
background: linear-gradient(to bottom, #E2F0FF, #F6FAFF);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 120rpx 40rpx 80rpx;
box-sizing: border-box;
animation: guideFadeIn 0.5s ease-out;
}
.status_bar {
height: 148px;
/* 引导页淡入动画 */
@keyframes guideFadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 状态栏 - 自适应设备状态栏高度 */
.status-bar {
height: var(--status-bar-height);
width: 100%;
flex-shrink: 0;
}
/* 滑动容器 */
.guide-swiper {
flex: 1;
width: 100%;
}
/* 单个引导项 */
/* 单个引导项 - 全屏自适应 */
.guide-item {
width: 360px;
height: 480px;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 60rpx;
box-sizing: border-box;
}
/* 图片区域 - 自适应 */
.guide-img {
width: 80%;
max-width: 600rpx;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
}
.img-content {
width: 100%;
height: 50%;
margin-bottom: 80rpx;
height: 100%;
max-height: 50vh;
}
/* 文字区域 */
.guide-text {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40rpx;
}
/* 引导标题 */
@@ -161,7 +204,8 @@ const handleBtnClick = () => {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 60rpx;
margin-bottom: 40rpx;
flex-shrink: 0;
}
/* 单个进度点 */
@@ -171,6 +215,7 @@ const handleBtnClick = () => {
border-radius: 8rpx;
background-color: #ddd;
margin: 0 8rpx;
transition: all 0.3s ease;
}
/* 激活的进度点 */
@@ -182,6 +227,9 @@ const handleBtnClick = () => {
/* 按钮容器 */
.guide-btn-wrap {
width: 80%;
max-width: 600rpx;
margin-bottom: 80rpx;
flex-shrink: 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);
}
.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,30 @@
## 1.1.22026-05-25
【文档】增加uniappx项目引用插件的说明
## 1.1.12026-04-07
升级云闪付SDK版本`Androidv3.5.18``IOSv3.6.3``HarmonyOSv3.1.3`
## 1.1.02026-02-27
【优化】文档增加已知问题说明
## 1.0.92026-02-03
【优化】HarmonyOS完善module.json5文件文档增加对鸿蒙平台的特殊说明
## 1.0.82025-09-22
修复频繁调用插件函数报回调被回收的问题
## 1.0.72025-09-03
【修复】修复鸿蒙打包报错问题
## 1.0.6.12025-08-29
【优化】移除插件中module.json5的配置
## 1.0.62025-07-15
【修复】修复鸿蒙打包报错的问题
## 1.0.52025-05-08
修复IOS端modules丢失打包报错的问题
## 1.0.42025-05-08
升级云闪付SDK版本`Androidv3.5.16``IOSv3.6.2``HarmonyOSv3.1.2`
## 1.0.32025-04-25
IOS端修复支持成功之后App闪退问题
## 1.0.22025-03-25
* Android端修复打包冲突问题
## 1.0.12025-03-11
* 代码优化修复bug
## 1.0.02025-01-10
* 第一版发布,同时支持`Android``IOS``HarmonyOS`三端
* 云闪付SDK版本`Androidv3.5.15``IOSv3.6.0``HarmonyOSv3.1.0`
* 初次发布,如有问题请欢迎大家指出,会及时调整

View File

@@ -0,0 +1 @@
Îà^wïøòR<>£LËA ËœÞÂôz…aY¿@6y­»Ióµ¡î?¿mpDqÚ%ÃCc)“­ô²/CÓl7vʲ$ùi|ËꄟQuXsõœä}˜ÌâPíó-q:Ù.?hZ™ZcÃÄdXgµ>ñåøpþR5dÖQß躵 Þœ£­<C2A3>[×üDð

View File

@@ -0,0 +1,132 @@
{
"id": "liyahong-unionpay",
"displayName": "【Android+IOS+harmonyOS】银联云闪付支付",
"version": "1.1.2",
"description": "银联云闪付支付UTS插件全方位支持Android/IOS/harmonyOS支持自定义支付",
"keywords": [
"云闪付",
"云闪付支付",
"银联",
"银联支付",
"支付"
],
"repository": "",
"engines": {
"HBuilderX": "^3.6.8",
"uni-app": "^4.36",
"uni-app-x": "^4.36"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "89.90"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "1299259684"
},
"declaration": {
"ads": "无",
"data": "插件使用的云闪付支付SDK会采集数据详情可参考https://wallet.95516.com/s/wl/WebAPP/helpAgree/page/agreement/regPrivacy.html",
"permissions": "安卓访问如下权限: <uses-permission android:name=\"android.permission.INTERNET\" /> <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" /> <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />"
},
"npmurl": "",
"darkmode": "√",
"i18n": "√",
"widescreen": "√"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": {
"extVersion": "1.0.0",
"minVersion": ""
},
"vue3": {
"extVersion": "1.0.0",
"minVersion": ""
}
},
"web": {
"safari": "x",
"chrome": "x"
},
"app": {
"vue": {
"extVersion": "1.0.0",
"minVersion": ""
},
"nvue": {
"extVersion": "1.0.0",
"minVersion": ""
},
"android": {
"extVersion": "1.0.0",
"minVersion": "21"
},
"ios": {
"extVersion": "1.0.0",
"minVersion": "12"
},
"harmony": {
"extVersion": "1.0.0",
"minVersion": "12"
}
},
"mp": {
"weixin": "x",
"alipay": "x",
"toutiao": "x",
"baidu": "x",
"kuaishou": "x",
"jd": "x",
"harmony": "x",
"qq": "x",
"lark": "x",
"xhs": "x"
},
"quickapp": {
"huawei": "x",
"union": "x"
}
},
"uni-app-x": {
"web": {
"safari": "x",
"chrome": "x"
},
"app": {
"android": {
"extVersion": "1.0.0",
"minVersion": "21"
},
"ios": {
"extVersion": "1.0.0",
"minVersion": "12"
},
"harmony": {
"extVersion": "1.0.0",
"minVersion": "12"
}
},
"mp": {
"weixin": "x"
}
}
}
}
}
}

View File

@@ -0,0 +1,260 @@
##插件功能概述
* 支持拉起云闪付并完成支付
* 支持检查是否安装云闪付
* 支持获取已安装的云闪付直通App列表
* 支持配置各个直通银行及手机支付(如小米支付、华为支付等)
* 支持Android/IOS/harmonyOS三端`本插件为原生SDK仅支持App端`
* 待完善功能。。。
##插件接入文档
### 关于更新
有空闲时间会不断更新维护,有问题随便提,有时间一定会修改!
### 特别推荐
其他插件请参考主页如人脸采集、百度OCR等深受广大用户青睐可以一试
* 作者主页:[https://ext.dcloud.net.cn/publisher?id=214114](https://ext.dcloud.net.cn/publisher?id=214114)
### 鸿蒙特殊说明`重要授权说明`
* uni-app项目: 由于官方目前不支持`试用以及普通授权`,建议`新建uni-appx项目`引用插件测通后购买源码授权使用
* uni-appx项目: 可直接使用
###准备工作
1. Android、ios、harmonyOS端证书准备工作
* 1) Android端准备应用的Android打包文件.jks文件。(`如使用官方证书忽略`)参考:[https://ask.dcloud.net.cn/article/35777](https://ask.dcloud.net.cn/article/35777)
* 2) IOS端这里只简单介绍一下windows系统下如何创建ios证书如果你是mac系统直接百度搜索就行网上一堆教程windows证书申请教程参考[https://www.pianshen.com/article/59121497532/](https://www.pianshen.com/article/59121497532/)
* 3) harmonyOS端usb连接真机 ---> 运行 ---> 运行手机或模拟器 ---> 运行到鸿蒙 ---> 配置证书(可以自动生成) ---> 运行
###接入步骤
#### 最简单的接入步骤基本10分钟搞定
* step1插件页右边有个`试用`的按钮,点它,然后弹出来一个模态框,选择`试用的项目`,点击`确认`按钮,点击`继续导入HBuilderX`
* step2将demo里面`index.vue`文件里面的函数方法复制过来
* step3打开你项目的`manifest.json`文件,找到`安卓/iOS常用其他设置`,找到`IOS设置` ---> `UrlSchemes`,将`uppaylyhuts`填写进去。如果你本来就有(比如你原来有XXX)则是:`XXX,uppaylyhuts`
* step4同上`安卓/iOS常用其他设置`中找到`IOS设置` ---> `应用访问白名单`,将`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`填写进去
* step5Android/IOS打自定义基座包然后选择以自定义基座包运行
* step6harmonyOS执行完成准备工作`moudle.json5文件拷贝至harmony-configs -> entry -> src -> main目录下`
* step6.1`module.json5`路径:`unpackage -> dist -> dev -> app-harmony -> entry -> src -> main`
* step6.2:修改`module.json5`文件,新增以下代码
```javascript
"module": {
...
"pages": "$profile:main_pages",
"querySchemes": [
"uppaywallet",
"uppaysdk",
],
"abilities": [
{
...
"skills": [
{
...
"uris": [
{
"scheme": "uppaylyhuts",
}
]
}
]
}
]
}
```
* step7运行项目
#### 详细接入步骤及其他细则
1. 使用插件
* 1) 插件方法说明HX其实都有提示再赘述一遍吧别嫌我啰嗦哈
+ checkWalletInstalled() 检查是否安装云闪付App返回true或者false
+ startPay(object) 拉起云闪付app并发起支付具体说明见下
+ getDirectApps(function) 检查已安装的支持直通模式的银行app列表。`用途是在服务端生成tn时保留域传入"supportBank": "银行标识"``感谢用户cha***@126.com的修正`
---
> `startPay`方法说明
>
> 参数说明入参为object
>
> | 属性 | 类型 | 默认值 | 必填 | 说明 |
> | ------------ | ------------ | ------------ | ------------ | ------------ |
> |mode | string | 无 | 是 | 银联环境可选项00/0100正式环境01测试环境 |
> |tn | string | 无 | 是 | 服务端返回的交易流水号即tn |
> |seType | string | 无 | 否 | `【IOS无效】`手机pay支付类别传入会展示对应的手机支付华为支付就传04具体说明见`seType说明表` |
> |callback | function | 无 | 是 | 支付结果回调,回参为`object`,属性见`返回值说明` |
>
> 返回值说明
>
> | 属性 | 返回值说明 |
> | ------------ | ------------ |
> |code | 【必填】状态码0支付成功、1前台支付成功存在不确定性需要后端查询订单实际支付结果(基于data中的签名判断)、-1支付失败、-2支付取消 |
> |msg | 【必填】提示信息 |
> |data | 【选填】支付签名信息,只有`code=1`的时候返回 |
---
> `getDirectApps`方法说明
>
> 参数说明入参为function函数回参为`object`,属性见`返回值说明`
>
> 返回值说明
>
> | 属性 | 返回值说明 |
> | ------------ | ------------ |
> |code | 【必填】状态码。0请求成功、1请求失败(会将银联的错误code和错误msg都返回在data中组成一个对象) |
> |msg | 【必填】提示信息 |
> |data | 【必填】响应结果。code=0时为Array<string>对象,其中的值代表`直通银行标识`,详见`直通银行标识对照表`、code=1是为object对象是银联的错误对象 |
2. seType说明表`注意seType是string类型不是number`
| 手机品牌 | seType |
| ------------ | ------------ |
| Samsung Pay | 02 |
| Huawei Pay | 04 |
| Meizu Pay | 27 |
| Le Pay | 30 |
| Mi Pay | 25 |
| OPPO Pay | 29 |
| vivo Pay | 33 |
| Smartisan Pay | 32 |
| realme Pay | 35 |
3. 代码示例
```javascript
// uniapp项目引入方式
import { checkWalletInstalled, getDirectApps, startPay } from '../../uni_modules/liyahong-unionpay'
// uniappx项目引用方式注意uniappx项目必须使用@符号的方式引用,否则会报路径找不到的问题)
import { checkWalletInstalled, getDirectApps, startPay } from '@/uni_modules/liyahong-unionpay'
/**
* 检查是否安装云闪付App
*/
checkInstalled() {
const result = checkWalletInstalled()
this.installResult = `是否安装云闪付:${result}`
},
/**
* 【鸿蒙不支持此功能】获取直通银行App列表
*/
getBankAppList() {
getDirectApps((result) => {
console.log(JSON.stringify(result))
})
},
/**
* 拉起云闪付进行支付
*/
startPay() {
startPay({
tn: '',
mode: '00',
callback: (result) => {
console.log(JSON.stringify(result))
}
})
}
```
4. 直通银行标识对照表`服务端生成tn时使用举例supportBank:CQP`
| 银行标识 | 银行 APP 名称 |
| ------------ | ------------ |
| CQP | 云闪付 |
| CMBCC | 全民生活 |
| PAB | 平安口袋银行 |
| CCB | 中国建设银行 |
| BoCom | 交通银行 |
| ABC | 中国农业银行 |
| CMB | 招商银行 |
| CMBLIFE | 掌上生活 |
| ICBC | 中国工商银行 |
| SPDB | 浦发银行 |
| SPDBC | 浦大喜奔 |
| CNCB | 中信银行手机银行 |
| GDBC | 发现精彩 |
| CIBC | 兴业生活(好兴动) |
| HXBC | 华彩生活 |
| BCCBC | 掌上京彩 |
| BOCC | 中国银行缤纷生活 |
| CEBC | 阳光惠生活 |
| ICBCC | 工银 e 生活 |
| CCBC | 建行生活 |
| BoComC | 买单吧 |
| GDB | 广发银行手机银行 |
| PSBC | 邮储银行 |
| PSBCC | 邮储信用卡 |
| CEB | 光大银行手机银行 |
| BOS | 上海银行手机银行 |
| SHBANK | 上银美好生活 |
| CNCBC | 动卡空间 |
| CMBC | 民生银行手机银行 |
| CIB | 兴业银行手机银行 |
| BOC | 中国银行 |
| BCCB | 京彩生活 |
| ABCC | 农银 e 管家 |
| HXB | 华夏手机银行 |
5. iOS访问白名单对照表
| 支付 APP scheme | 对应支付 APP |
| ------------ | ------------ |
| uppaysdk | 云闪付 APP |
| uppaywallet | 云闪付 APP |
| uppayx1 | 全民生活 |
| uppayx2 | 预留 |
| uppayx3 | 预留 |
| paesuperbank | 平安口袋银行 |
| ccbmbsylunionpay | 中国建设银行 |
| bocom | 交通银行 |
| bankabc | 中国农业银行 |
| cmbmobilebank | 招商银行 |
| cmblife | 掌上生活 |
| com.icbc.iphoneclient | 中国工商银行 |
| spdbbank | 浦发银行 |
| spdbcccUnionPay | 浦大喜奔 |
| citicbankpay | 中信银行手机银行 |
| credit | 发现精彩 |
| uppayxingyecredit | 兴业生活(好兴动) |
| uppayhuaxiacredit | 华彩生活 |
| uppaybeijingcredit | 掌上京彩 |
| uppayzhonghangcredit | 中国银行缤纷生活 |
| uppayguangdacredit | 阳光惠生活 |
| uppaygonghangcredit | 工银 e 生活 |
| uppayjianhangcredit | 建行生活 |
| uppayjiaohangcredit | 买单吧 |
| uppayguangfa | 广发银行手机银行 |
| psbccashier | 邮储银行 |
| uppayyouchucredit | 邮储信用卡 |
| com.cebbank.ebank | 光大银行手机银行 |
| BankOfShangHai | 上海银行手机银行 |
| boscgdlf | 上银美好生活 |
| citicbankdkkj | 动卡空间 |
| cmbc | 民生银行手机银行 |
| cibmb | 兴业银行手机银行 |
| bocmbankpsn | 中国银行 |
| bankofbjpay | 京彩生活 |
| uppaynonghangcredit | 农银 e 管家 |
| uppayhuaxia | 华夏手机银行 |
| zytuppay | 中银通 |
| LZYHSJYHAUTHSSOAPP | 兰州银行 |
| bnbpay | 宁波银行 |
| dlrcb | 大连农商行 |
| czbank | 浙商银行 |
| uppayCbhb | 渤海银行 |
6. 其他附属
* 1) 银联测试卡链接:[测试卡直达](https://open.unionpay.com/tjweb/support/faq/mchlist?id=4)
* 2) 商户需按照以下链接及表格内容配置 IP 及地址白名单。[配置服务端白名单链接直达](https://open.unionpay.com/tjweb/support/faq/mchlist?id=228)
| 测试 | 生产 |
| ------------ | ------------ |
| https://appcashier.test.cup.com.cn | https://conf.cup.com.cn |
| https://appcashier.test.95516.com | https://conf.95516.com |
| | https://acpstatic.cup.com.cn |
| | https://acpstatic.95516.com |
| | https://appcashier.95516.com |
* 3) Apple Pay需要自行接入接入参考[链接直达](https://open.unionpay.com/tjweb/support/faq/mchlist?id=741)
* 4) 其他问题请参见 [https://open.unionpay.com](https://open.unionpay.com) 帮助中心-FAQ
7. 已知问题
* Q频繁调用`getDirectApps``startPay`函数会导致callback回调触发多次`感谢用户:[爱心]GLL`
* A多页面可以通过页面路径来判断是否是当前页面触发如果是再执行下一步操作否则拦截

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="com.liyahong.unionpay"
>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc.hce"/>
<uses-permission android:name="org.simalliance.openmobileapi.SMARTCARD" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application>
<uses-library android:name="org.simalliance.openmobileapi" android:required="false"/>
<activity
android:name="com.unionpay.UPPayWapActivity"
android:configChanges="orientation|keyboardHidden|fontScale"
android:screenOrientation="portrait"
android:exported="false"
android:windowSoftInputMode="adjustResize" />
</application>
</manifest>

View File

@@ -0,0 +1,3 @@
{
"minSdkVersion": "21"
}

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