Compare commits

...

101 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
zhouyanli
fa017d657f 优化登录提示语,优化request错误返回提示语 2026-05-12 17:33:46 +08:00
zhouyanli
f2f5c5e050 对接设置接口 2026-05-12 14:28:40 +08:00
zhouyanli
78c69cdb10 对接问卷调查、投诉建议接口 2026-05-11 17:29:57 +08:00
zhouyanli
10505e8000 修改登录页接口返回异常文字 2026-05-08 11:52:59 +08:00
zhouyanli
b68766791d 修改首页、公告样式、在线报修字段回显等 2026-05-07 18:05:36 +08:00
zhouyanli
0fb361ad64 修改访客邀请、一键开门bug 2026-05-07 11:57:57 +08:00
zhouyanli
231de9cde8 对接访客邀请接口 2026-05-06 17:48:39 +08:00
zhouyanli
e030bb7cb9 修改登录页样式 2026-05-05 16:21:56 +08:00
zhouyanli
d0a0b2dbaf 对接报修列表和报修详情、访客列表 2026-04-30 17:30:34 +08:00
zhouyanli
ba9ed8d700 request里的upload方法 2026-04-30 14:31:20 +08:00
zhouyanli
2fe56bf94b Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side 2026-04-30 14:28:39 +08:00
zhouyanli
95ccb1e3bc 对接在线报修的新增报修和报修详情接口 2026-04-30 14:28:22 +08:00
wangyuxin
ccacc323e6 1 2026-04-30 09:15:14 +08:00
wangyuxin
eef3a20959 1 2026-04-30 09:00:47 +08:00
wangyuxin
1a9c468460 Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side
# Conflicts:
#	property-uniapp-project/pageSubPack/payment-list/addHydropower.vue
#	property-uniapp-project/pageSubPack/payment-list/paymentIndex.vue
2026-04-30 09:00:19 +08:00
wangyuxin
630b1ee1d2 Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side
# Conflicts:
#	property-uniapp-project/manifest.json
#	property-uniapp-project/pageSubPack/api/request.js
2026-04-30 08:33:11 +08:00
wangyuxin
2c528b7973 我的房屋流程对接接口,缴费页面功能修改一版 2026-04-30 08:24:22 +08:00
132 changed files with 18810 additions and 11981 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() {
console.log('App Hide')
},
<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>
@@ -33,7 +145,7 @@
/* line-height: 1.6 !important; */
padding: 0 !important;
}
/* button:after{
border-radius: 50px;
} */

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,113 +0,0 @@
<template>
<canvas v-if="value" :type="type" :style="{width: size + 'px', height: size + 'px'}" :canvas-id="canvasId"
ref="qrcodeCanvas"></canvas>
</template>
<script setup>
import {
ref,
onMounted,
watch
} from 'vue';
import {
uqrcode
} from 'uqrcodejs'
const props = defineProps({
// 二维码内容(必填,如链接/文本)
value: {
type: String,
default: ''
},
// 尺寸默认300px
size: {
type: [Number, String],
default: 300
},
// 二维码颜色
color: {
type: String,
default: '#000000'
},
// 背景色
backgroundColor: {
type: String,
default: '#ffffff'
},
// logo图片地址
logo: {
type: String,
default: ''
},
// logo大小占二维码比例
logoSize: {
type: Number,
default: 0.2
},
// canvas 类型(小程序需指定)
type: {
type: String,
default: '2d'
},
// canvas ID避免重复
canvasId: {
type: String,
default: 'uqrcode'
}
});
const emit = defineEmits(['success', 'fail']);
const qrcodeCanvas = ref(null);
// 生成二维码核心方法
const createQRCode = async () => {
if (!props.value) return;
try {
// 引入uQRCode核心库
const uQRCode = require('uqrcodejs');
// 获取canvas上下文
const query = uni.createSelectorQuery().in(getCurrentInstance());
const res = await query.select(`#${props.canvasId}`).fields({
node: true,
size: true
}).exec();
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
// 设置canvas尺寸
const dpr = uni.getSystemInfoSync().pixelRatio;
canvas.width = props.size * dpr;
canvas.height = props.size * dpr;
ctx.scale(dpr, dpr);
// 生成二维码
await uQRCode.default.draw({
canvas: canvas,
componentInstance: getCurrentInstance(),
text: props.value,
width: props.size,
color: props.color,
background: props.backgroundColor,
logo: props.logo,
logoSize: props.logoSize
});
// 生成临时图片(可选,用于分享/保存)
uni.canvasToTempFilePath({
canvasId: props.canvasId,
success: (res) => emit('success', res.tempFilePath),
fail: (err) => emit('fail', err)
}, getCurrentInstance());
} catch (err) {
emit('fail', err);
}
};
// 监听value变化重新生成
watch(() => props.value, () => createQRCode(), {
immediate: true
});
onMounted(() => createQRCode());
</script>

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,16 +172,17 @@
"setting" : {
"urlCheck" : false
},
"usingComponents" : true,
"requiredPrivateInfos": [
"getLocation", // 定位接口(必填)
"chooseLocation" // 如果用到选择位置也加
],
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于活动地点定位、签到验证等功能"
}
}
"navigateToMiniProgramAppIdList" : [ "wxc0189436c58b59cd" ],
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ],
"permission" : {
"scope.camera" : {
"desc" : "用于扫码"
},
"scope.userLocation" : {
"desc" : "你的位置信息将用于定位选择"
}
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
@@ -77,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>
<!-- 选项卡 -->
@@ -44,7 +36,7 @@
<view class="activity-list">
<!-- 空状态 -->
<view v-if="displayedActivities.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">
{{ activeTab === 'list' ? '暂无活动' : '暂无参与的活动' }}
</text>
@@ -53,21 +45,20 @@
<!-- 活动卡片 -->
<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>
</view>
<view class="activity-info">
<view class="u-demo-block__content ">
<view class="u-demo-block__content">
<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
}
}
})
/**
* 切换选项卡(活动列表/我的活动)
@@ -176,7 +171,21 @@ const narBack = () => {
*/
const switchTab = (tab) => {
activeTab.value = tab
resetAndLoad()
if (tab === 'list') {
resetAndLoad()
} else {
resetAndLoadMy()
}
}
/**
* 重置并刷新我的活动第一页数据
*/
const resetAndLoadMy = () => {
currentPage.value = 1
displayedActivities.value = []
hasMoreData.value = true
getMyActivityListData(1)
}
/**
@@ -185,7 +194,7 @@ const switchTab = (tab) => {
*/
const switchFilter = (filter) => {
myFilter.value = filter
resetAndLoad()
resetAndLoadMy()
}
/**
@@ -198,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)
@@ -211,7 +219,7 @@ const getActivityListData = async (pageNum = 1) => {
if (res.code === 200) {
const list = res.rows || []
total.value = res.total || 0
// 第一页覆盖数据,下一页追加数据
if (pageNum === 1) {
displayedActivities.value = list
@@ -227,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();
}
}
@@ -239,7 +294,11 @@ const getActivityListData = async (pageNum = 1) => {
*/
const loadMoreData = () => {
if (isLoading.value || !hasMoreData.value) return
getActivityListData(currentPage.value + 1)
if (activeTab.value === 'list') {
getActivityListData(currentPage.value + 1)
} else {
getMyActivityListData(currentPage.value + 1)
}
}
/**
@@ -252,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 || ''}`
})
}
@@ -268,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 || ''}`
})
}
@@ -278,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 || ''}`
})
}
@@ -287,7 +377,11 @@ const viewActivityDetail = (activity) => {
* 页面显示时刷新数据
*/
onShow(() => {
getActivityListData(1) // 强制刷新第一页
if (activeTab.value === 'list') {
getActivityListData(1)
} else {
getMyActivityListData(1)
}
})
/**
@@ -296,7 +390,6 @@ onShow(() => {
onLoad(() => {
// 监听详情页报名/取消报名后的刷新通知
uni.$on('refreshActivityList', (data) => {
console.log('收到活动列表刷新通知:', data)
getActivityListData(1)
})
})
@@ -306,14 +399,28 @@ onLoad(() => {
*/
onUnload(() => {
uni.$off('refreshActivityList')
})
});
// 下拉刷新
onPullDownRefresh(() => {
if (activeTab.value === 'list') {
resetAndLoad();
} else {
resetAndLoadMy();
}
});
// 上拉加载更多
onReachBottom(() => {
loadMoreData();
});
/**
* 监听我的活动筛选条件变化
*/
watch(() => myFilter.value, () => {
if (activeTab.value === 'my') {
resetAndLoad()
resetAndLoadMy()
}
})
</script>
@@ -322,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;
}
@@ -384,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;
}
@@ -393,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 {
@@ -403,7 +502,7 @@ watch(() => myFilter.value, () => {
.filter-buttons {
display: inline-flex;
padding: 0 30rpx;
padding: 0 10rpx;
}
.filter-btn {
@@ -414,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 {
@@ -432,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;
@@ -464,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 {
@@ -472,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;
@@ -513,25 +622,30 @@ watch(() => myFilter.value, () => {
flex: 1;
font-size: 20rpx;
}
.detail-text-address{
.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: 0 40rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0 26rpx;
height: 56rpx;
line-height: 56rpx;
font-size: 24rpx;
border-radius: 35rpx;
border-radius: 28rpx;
margin: 0;
min-width: 140rpx;
}
.signup-btn {
@@ -543,6 +657,12 @@ watch(() => myFilter.value, () => {
background-color: #0056cc;
}
.disabled-btn {
background-color: #f2f3f5;
color: #999;
text-align: center;
}
/* 加载更多 */
.load-more-container {
display: flex;
@@ -606,9 +726,9 @@ watch(() => myFilter.value, () => {
}
.empty-text {
font-size: 32rpx;
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
</style>
</style>

View File

@@ -1,6 +1,6 @@
<template>
<view class="container">
<view class="status_bar"></view>
<view class="status_bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-left" @tap="goBack">
@@ -9,21 +9,21 @@
<view class="nav-title">{{navTitle}}</view>
<view class="nav-right"></view>
</view>
<!-- 内容区域 -->
<scroll-view class="scroll-content" scroll-y="true">
<!-- 选项卡 -->
<view class="tabs">
<view
class="tab-item"
<view
class="tab-item"
:class="{ 'active': activeTab === 'detail' }"
@tap="switchTab('detail')"
>
活动详情
</view>
<view
class="tab-item"
<view
class="tab-item"
:class="{ 'active': activeTab === 'status' }"
@tap="switchTab('status')"
>
@@ -39,66 +39,65 @@
<view class="info-label">报名截止时间</view>
<view class="info-value">{{formatTime(activityConent.applyDeadline)}}</view>
</view>
<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>
<view class="info-item">
<view class="info-label">活动地点</view>
<view class="info-value">{{activityConent.address}}</view>
</view>
<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">
<view class="info-label">主办方</view>
<view class="info-value">{{activityConent.organizer}}</view>
</view>
<view class="info-item">
<view class="info-label">联系人</view>
<view class="info-value">{{activityConent.contactName}}</view>
</view>
<view class="info-item">
<view class="info-label">联系电话</view>
<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,9 +106,9 @@
<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>
现场实况
</view>
<image src="http://wuye.ckdzkj.com/images/activity/liveScene.png" style="width: 38rpx;height: 23rpx;"></image>
现场实况
</view>
<view class="status-count">{{ liveRecords.length }}</view>
</view>
@@ -119,29 +118,30 @@
<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">
<view
v-for="(img, imgIndex) in record.images"
:key="imgIndex"
<view
v-for="(img, imgIndex) in record.images"
:key="imgIndex"
class="image-item"
:style="{ backgroundImage: `url(${img})` }"
>
<image :src="img" style="width: 100%;height: 100%;"></image>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
@@ -149,26 +149,38 @@
<!-- 底部操作栏 -->
<view class="bottom-bar">
<!-- 活动已结束显示不可点击的结束按钮 -->
<button
v-if="isActivityEnded && activeTab === 'detail'"
class="action-btn ended-btn"
disabled
>
报名已结束
</button>
<!-- 未报名状态显示报名按钮 -->
<button
v-if="!hasSigned && activeTab === 'detail'"
<button
v-if="!isActivityEnded && !hasSigned && activeTab === 'detail'"
class="action-btn signup-btn"
@tap="handleSignup"
>
报名
</button>
<!-- 已报名状态显示取消报名按钮 -->
<button
v-if="hasSigned && activeTab === 'detail'"
<button
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,50 +270,71 @@ 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
// console.log('hasSigned.value',hasSigned.value)
// 2. 初始化数据
if (curActivityId.value) {
getActivityDetailsData(curActivityId.value) // 获取活动详情
getLiveActivityList() // 获取实况列表
}
// 3. 监听实况列表刷新事件
uni.$on('refreshLiveList', () => {
getLiveActivityList()
})
navTitle.value = option?.title
curActivityId.value = option?.id
statusTextFromParam.value = option?.statusText || ''
hasSigned.value = option?.applied === 'true'
// console.log('hasSigned.value',hasSigned.value)
// 2. 初始化数据
if (curActivityId.value) {
getActivityDetailsData(curActivityId.value) // 获取活动详情
// getLiveActivityList() // 获取实况列表
}
// 3. 监听实况列表刷新事件
uni.$on('refreshLiveList', () => {
getLiveActivityList()
})
})
// 获取列表详情
const getActivityDetailsData = async (id) =>{
try{
const res = await getActivityDetails(id)
if(res.code === 200){
activityConent.value = res.data || {}
}else{
activityConent.value = {}
uni.showToast({
title: res.msg ||"详情获取失败",
icon:"error"
})
}
}catch(err){
uni.showToast({
title: "网络异常",
icon:"none"
})
// console.error('获取活动详情失败:', err)
}
try{
let params = {villageId:uni.getStorageSync('currentVillageId')}
const res = await getActivityDetails(id,params)
if(res.code === 200){
activityConent.value = res.data || {}
}else{
activityConent.value = {}
uni.showToast({
title: res.msg ||"详情获取失败",
icon:"error"
})
}
}catch(err){
uni.showToast({
title: "网络异常",
icon:"none"
})
// console.error('获取活动详情失败:', err)
}
}
// 切换选项卡
const switchTab = (tab) => {
activeTab.value = tab
if (tab === 'status') {
getLiveActivityList()
}
}
// 返回上一页
@@ -321,34 +360,35 @@ const confirmSignup = () => {
// 确认报名接口
const confirmRegist = async () =>{
try{
const res = await confirmResident(curActivityId.value)
if(res.code === 200){
hasSigned.value = true // 报名成功后更新状态
uni.showToast({
title: '报名成功',
icon:"success"
})
setTimeout(() =>{
uni.$emit('refreshActivityList', {
activityId: curActivityId.value,
isSigned: true
})
uni.navigateBack()
},1500)
}else{
uni.showToast({
title: res.msg || "报名失败",
icon:"error"
})
}
}catch(err){
uni.showToast({
title: "网络异常",
icon:"none"
})
// console.error('报名失败:', err)
}
try{
let params = {villageId:uni.getStorageSync('currentVillageId')}
const res = await confirmResident(curActivityId.value,params)
if(res.code === 200){
hasSigned.value = true // 报名成功后更新状态
uni.showToast({
title: '报名成功',
icon:"success"
})
setTimeout(() =>{
uni.$emit('refreshActivityList', {
activityId: curActivityId.value,
isSigned: true
})
uni.navigateBack()
},1500)
}else{
uni.showToast({
title: res.msg || "报名失败",
icon:"error"
})
}
}catch(err){
uni.showToast({
title: err.msg ||"网络异常",
icon:"none"
})
// console.error('报名失败:', err)
}
}
// 处理取消报名
@@ -361,85 +401,85 @@ const hideCancelModal = () => {
showCancelModal.value = false
}
// 确认取消报名
// 确认取消报名
const confirmCancel = () => {
hasSigned.value = false // 取消报名后更新状态
cancelActivityData()
hideCancelModal()
// showToastMessage('已取消报名')
}
// 确认取消报名接口
const cancelActivityData = async () =>{
try{
const res = await cancelActivity(curActivityId.value)
if(res.code === 200){
hasSigned.value = false // 报名成功后更新状态
uni.showToast({
title: '取消报名成功',
icon:"success"
})
setTimeout(() =>{
uni.$emit('refreshActivityList', {
activityId: curActivityId.value,
isSigned: false
})
uni.navigateBack()
},1500)
}else{
uni.showToast({
title: res.msg || "取消报名失败",
icon:"error"
})
}
}catch(err){
uni.showToast({
title: "网络异常",
icon:"none"
})
}
try{
let params = {villageId:uni.getStorageSync('currentVillageId')}
const res = await cancelActivity(curActivityId.value,params)
if(res.code === 200){
hasSigned.value = false // 报名成功后更新状态
uni.showToast({
title: '取消报名成功',
icon:"success"
})
setTimeout(() =>{
uni.$emit('refreshActivityList', {
activityId: curActivityId.value,
isSigned: false
})
uni.navigateBack()
},1500)
}else{
uni.showToast({
title: res.msg || "取消报名失败",
icon:"error"
})
}
}catch(err){
uni.showToast({
title: err.msg ||"网络异常",
icon:"none"
})
}
}
// -----获取活动实况列表----
const getLiveActivityList = async () =>{
try{
const res = await getActivityLive(curActivityId.value)
if(res.code === 200){
liveRecords.value = res.rows || []
}else{
liveRecords.value = []
uni.showToast({
title: res.msg ||"实况列表获取失败",
icon:"error"
})
}
}catch(err){
uni.showToast({
title: "网络异常",
icon:"none"
})
}
try{
let params = {villageId:uni.getStorageSync('currentVillageId')}
const res = await getActivityLive(curActivityId.value,params)
if(res.code === 200){
liveRecords.value = res.rows || []
}else{
liveRecords.value = []
uni.showToast({
title: res.msg ||"实况列表获取失败",
icon:"error"
})
}
}catch(err){
uni.showToast({
title: err.msg ||"网络异常",
icon:"none"
})
}
}
// 添加实况跳转
const liveClick = () =>{
uni.navigateTo({
url:`/pageSubPack/activity-list/live-add?id=${curActivityId.value}`
})
uni.navigateTo({
url:`/pageSubPack/activity-list/live-add?id=${curActivityId.value}`
})
}
// 监听实况列表刷新
// ---------
onUnload(() => {
uni.$off('refreshLiveList')
uni.$off('refreshLiveList')
})
// 显示提示信息
const showToastMessage = (message) => {
toastMessage.value = message
showToast.value = true
setTimeout(() => {
showToast.value = false
}, 2000)
@@ -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;
@@ -888,4 +947,26 @@ const showToastMessage = (message) => {
margin: 0 auto;
}
}
</style>
<style lang="scss">
/* 富文本v-html内图片自适应 — 非 scoped否则穿透不到 v-html 内容 */
.section-content {
img {
max-width: 100% !important;
height: auto !important;
display: block;
}
}
.rich-content {
white-space: normal !important;
word-break: break-all !important;
overflow-wrap: break-word !important;
overflow: hidden;
/* 富文本内嵌的所有元素不超出容器 */
* {
max-width: 100% !important;
box-sizing: border-box !important;
}
}
</style>

View File

@@ -21,8 +21,9 @@
<view class="upload-area">
<!-- 已上传图片 -->
<view class="uploaded-img" v-for="(img, index) in imgList" :key="index">
<image :src="img" mode="aspectFill"></image>
<text class="del-img" @click="deleteImg(index)">×</text>
<image :src="img" mode="aspectFill" class="upload-img"></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张 -->
<view class="upload-btn" @click="chooseImg" v-if="imgList.length < 3">
@@ -47,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>
@@ -55,7 +56,10 @@
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { addActivityLive, signinActivityLive, uploadImage } from '../api/api.js';
import { addActivityLive, signinActivityLive, uploadFile } from '../api/api.js';
// 天地图 Web API Key用于逆地址解析
const TIANDITU_KEY = 'b0201708e26cb56e514d74d1686f6f91';
// 活动ID
const activityId = ref('');
@@ -63,10 +67,12 @@ const activityId = ref('');
const statusDesc = ref('');
// 输入字符长度
const currentLength = ref(0);
// 提交状态锁(防重复点击)
const submitting = ref(false);
// 图片列表(本地临时路径)
const imgList = ref([]);
// 定位信息
const location = ref('山东省日照市东港区学院路');
const location = ref('');
// 经纬度
const longitude = ref('');
const latitude = ref('');
@@ -74,6 +80,8 @@ const latitude = ref('');
// 接收活动ID
onLoad((option) => {
activityId.value = option?.id || '';
// 进入页面自动定位,获取不到时可通过“重新定位”重试
reLocate();
});
// 监听输入框字数变化
@@ -81,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({
@@ -91,54 +121,77 @@ const chooseImg = () => {
imgList.value = [...imgList.value, ...res.tempFilePaths];
},
fail: (err) => {
uni.showToast({ title: '选择图片失败', icon: 'none' });
console.error('选择图片失败:', err);
}
});
};
// 删除图片
const deleteImg = (index) => {
const deleteImage = (index) => {
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 });
}
}
});
@@ -147,44 +200,49 @@ const reLocate = () => {
// 上传所有图片
const uploadAllImages = async () => {
if (imgList.value.length === 0) return [];
const uploadPromises = imgList.value.map(img => uploadImage(img));
const uploadPromises = imgList.value.map(img => uploadFile(img));
const results = await Promise.all(uploadPromises);
return results.map(res => res.data?.url || res.data);
return results.map(res => res.url);
};
// 提交实况
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' });
return;
}
// 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' });
@@ -193,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>
@@ -211,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;
@@ -243,7 +302,7 @@ const submitStatus = async () => {
text-align: right;
font-size: 12px;
color: #999;
margin-top: 5px;
margin-top: 4px;
display: block;
}
@@ -258,11 +317,20 @@ const submitStatus = async () => {
height: 80px;
position: relative;
}
.uploaded-img image {
.uploaded-img .upload-img{
width: 100%;
height: 100%;
border-radius: 4px;
}
.uploaded-img .upload-delete{
position: absolute;
top: -5px;
right: -5px;
width: 32rpx;
height: 32rpx;
border-radius: 3rpx;
padding: 2px;
}
.del-img {
position: absolute;
top: -5px;
@@ -279,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;
@@ -306,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

@@ -22,9 +22,13 @@ export const getResetPassword = (data) =>{
return post('/api/resident/auth/resetPassword',data)
}
// =========首页=======
// 获取bannar图
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)
}
// 查询已认证的小区
@@ -40,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')
}
// ========公告列表======
@@ -68,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) =>{
@@ -111,14 +119,31 @@ 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) =>{
return get('/repairOrder/queryRepairDetail',data)
}
// =========访客邀请===========
// 获取访客列表
export const getVisitorList = (data) =>{
return get('/api/visitor/list',data)
}
// 获取详情信息
export const getVisitorDetail = (id) =>{
return get(`/api/visitor/${id}`)
}
// 新增访客
export const addVisitorData = (data) =>{
return post("/api/visitor", data)
}
// 通用文件上传
export const uploadImage = () => {
return upload('');
}
// 上传单张图片(请根据后端实际地址修改)
// 上传单张图片
export const uploadFile = (filePath) => {
return upload(filePath, '/common/upload');
}
return upload(filePath,'/api/resident/file/uploadImage');
}
// 单推消息推送
export const getPushSingle = (data) =>{
return post("/api/push/single", data)
}

View File

@@ -0,0 +1,313 @@
import request, {
get,
post,
deleteMethod,
putMethod
} from "./request.js"
// 字典
export const getSystemDictData = (data) => {
return get('/system/dict/data/list', data)
}
// =======公告列表========
export const getResidentNoticeList = (data) => {
return get('/api/resident/notice/list', data)
}
// 我的房屋列表
export const getMyHouseList = (data) => {
return get('/api/resident/my/house/list', data)
}
// 提交房屋注册认证申请
export const myHouseApply = (data) => {
return post('/api/resident/my/house/apply', data)
}
// 查询小区列表
export const getVillageList = (data) => {
return get('/api/resident/village/list', data)
} // 查询楼栋列表
export const getHouseBuildings = (data) => {
return get('/api/resident/house/buildings', data)
}
// 查询单元列表
export const getHouseUnits = (data) => {
return get('/api/resident/house/units', data)
}
// 查询楼层列表
export const getHouseFloors = (data) => {
return get('/api/resident/house/floors', data)
}
// 查询户号列表
export const getHouseHouses = (data) => {
return get('/api/resident/house/houses', data)
}
// 房屋绑定手机获取验证码
export const getHouseAuthSendSmsCode = (data) => {
return post('/api/resident/auth/sendSmsCode', data)
}
// 房屋详情
export const getMyHouseDetail = (data) => {
return get(`/api/resident/my/house/${data.id}`, data)
}
// 删除房屋
export const deleteMyHouse = (data) => {
return deleteMethod(`/api/resident/my/house/${data.id}`, 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)
}
// 发送新手机号验证码
export const getAuthSendSmsCode = (data) => {
return post(`/api/resident/auth/sendSmsCode`, data)
}
// 更改手机号
export const getUserChangePhone = (data) => {
return post(`/api/resident/user/changePhone`, data)
}
// 获取消息通知开关
export const getUserNotify = (data) => {
return get(`/api/resident/user/notify`, data)
}
// 更新消息通知开关
export const changeUserNotify = (data) => {
return post(`/api/resident/user/notify`, data)
} // 修改密码(需登录)
export const authChangePassword = (data) => {
return post(`/api/resident/auth/changePassword`, 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 getQuestionnaireList = (data) => {
return get('/api/resident/questionnaire/list',data)
}
// 问卷详情
export const getQuestionnaireDetail = (id,data) => {
return get(`/api/resident/questionnaire/${id}`,data)
}
// 提交问卷
export const submitQuestionnair = (id,data) => {
return post(`/api/resident/questionnaire/submit/${id}`, data)
}
// =====投诉意见=======
// 获取投诉类型
export const getComplainType = (data) => {
return post('/complain/getComplainType',data)
}
// 获取投诉列表
export const getComplainList = (data) => {
return get('/complain/getComplainList',data)
}
// 提交投诉建议
export const addComplain = (data) => {
return post('/complain/add',data)
}
// 投诉详情
export const getComplainDetail = (data) => {
return get('/complain/detail',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,111 +1,104 @@
import { navigateToLogin } from '@/common/checkLogin.js'
let baseUrl = '';
let baseUrl = '';
if (process.env.NODE_ENV === 'development') {
baseUrl = 'http://192.168.1.215:8080'; // 开发环境
baseUrl = 'http://192.168.1.6:8080'; // 开发环境
// baseUrl = 'http://192.168.1.222:8080';
} else {
baseUrl = 'http://192.168.1.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: {
// "token": uni.getStorageSync("token") || '',
// 'content-type': 'application/json'
// },
header,
success: (res) => {
uni.hideLoading(); // 关闭加载提示
// uni.hideLoading();
// 处理HTTP状态码(网络层面)
// HTTP 失败
if (res.statusCode !== 200) {
const errMsg = `请求失败(${res.statusCode}`;
uni.showToast({ title: errMsg, icon: "none", duration: 2000 });
reject(errMsg);
return;
}
// 处理业务状态码后端返回的data里
const resData = res.data || {}; // 兼容后端返回空的情况
// 优先处理token过期401
if (resData.code === 401) {
uni.showToast({ title: '登录过期,请重新登录', icon: 'none', duration: 2000 });
uni.clearStorageSync('token');
setTimeout(() => {
uni.reLaunch({ url: '/pageSubPack/loginSub/pwd-login' });
}, 1500);
reject('token 过期');
reject({ msg: `请求异常 ${res.statusCode}`, code: res.statusCode });
return;
}
// 业务成功code=200
const resData = res.data || {};
// token 过期:仅在有 token 且过期时跳转登录页(无 token 用户正常浏览首页,由点击拦截处理)
if (resData.code === 401) {
if (token) {
uni.clearStorageSync('token');
navigateToLogin({ method: 'reLaunch' });
}
reject({ msg: '登录过期', code: 401 });
return;
}
// 成功
if (resData.code === 200) {
resolve(resData); // 只返回核心数据,简化页面调用
}
// 业务失败code=500
else if (resData.code === 500) {
const errMsg = resData.msg || '请求失败';
uni.showToast({ title: errMsg, icon: "none", duration: 2000 });
// resolve(null)
reject(errMsg);
resolve(resData);
}
// 业务失败
else {
const errMsg = resData.msg || '业务异常';
uni.showToast({ title: errMsg, icon: "none", duration: 2000 });
reject(errMsg);
reject({ msg: resData.msg || '请求失败', code: resData.code, data: resData });
}
},
fail: (err) => {
uni.hideLoading(); // 关闭加载提示
const errMsg = err.msg || '网络请求失败';
uni.showToast({ title: errMsg, icon: "none", duration: 2000 });
reject(err);
// uni.hideLoading();
reject({ msg: '网络异常', code: -1, raw: err });
}
});
});
}
export const get = (url, data) => request(url, data, 'GET');
export const post = (url, data) => request(url, data, 'POST');
// 通用文件上传
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');
// 上传
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) => {
if (res.statusCode === 200) {
const data = JSON.parse(res.data);
if (data.code === 200) {
resolve(data);
} else {
reject(data.msg || '上传失败');
try {
const data = JSON.parse(res.data);
resolve(data.data || ''); // 直接返回图片地址
} catch (e) {
resolve('');
}
} else {
reject('上传失败');
}
},
fail: (err) => {
reject(err);
}
});
});
};
};

View File

@@ -0,0 +1,191 @@
// 服务器地址(与 request.js 保持一致)
const baseUrl = 'https://wuyeapi.ckdzkj.com';
// const baseUrl = "http://192.168.1.6:8080"
/**
* 图片上传 Hook
* 支持 uniapp 和微信小程序
* 解决编辑回显后重新上传的问题
*/
import { reactive } from 'vue'
/**
* @param {Object} options 配置
* @param {string} options.uploadUrl - 上传接口地址
* @param {string} options.fileKey - 上传文件的字段名,默认 'file'
* @returns {Object}
*/
export function useImageUpload(options = {}) {
const {
uploadUrl = '/api/resident/file/uploadImage',
fileKey = 'file'
} = options
// 使用 reactive 对象存储所有状态
const state = reactive({
// 当前显示的图片(前端展示用,支持 base64/blob URL/网络URL
previewUrl: '',
// 服务器返回的 URL提交时使用
serverUrl: '',
// 临时的文件路径(重新上传时用)
tempFilePath: null,
// 上传状态
uploading: false
})
/**
* 设置回显图片(编辑时从后端获取)
* @param {string} url - 后端返回的图片URL
*/
const setPreviewUrl = (url) => {
if (url) {
// 确保 url 是字符串
const urlStr = typeof url === 'string' ? url : (url?.url || String(url))
state.previewUrl = urlStr
state.serverUrl = urlStr
state.tempFilePath = null // 重置临时文件,表示没有重新上传
}
}
/**
* 选择图片并预览(不上传)
* @returns {Promise<string>} 返回临时文件路径
*/
const chooseImage = () => {
return new Promise((resolve, reject) => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera', 'album'],
success: (res) => {
const path = res.tempFilePaths?.[0]
if (!path) {
reject(new Error('选择图片失败'))
return
}
// 确保 path 是字符串
const pathStr = typeof path === 'string' ? path : String(path)
// 设置预览
state.previewUrl = pathStr
// 记录临时文件路径
state.tempFilePath = pathStr
// 置空服务器URL等实际上传后再获取
state.serverUrl = ''
resolve(pathStr)
},
fail: (err) => {
reject(err)
}
})
})
}
/**
* 上传图片到服务器
* @returns {Promise<Object>} 返回 { url: string }
*/
const uploadImage = () => {
// 如果没有新选择临时文件直接返回已有的服务器URL
if (!state.tempFilePath) {
return Promise.resolve({ url: state.serverUrl })
}
// 如果已经有服务器URL且没有重新选择也直接返回
if (state.serverUrl && !state.tempFilePath) {
return Promise.resolve({ url: state.serverUrl })
}
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: filePath,
name: fileKey,
header: {
'Authorization': 'Bearer ' + (uni.getStorageSync('token') || ''),
'clientid': 'resident'
},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.code === 200 && data.data) {
// 更新服务器URL确保存的是字符串
const url = typeof data.data === 'string' ? data.data : (data.data?.url || '')
state.serverUrl = url
// 清空临时文件
state.tempFilePath = null
resolve({ url })
} else {
uni.showToast({
title: data.msg || '上传失败',
icon: 'none'
})
reject(new Error(data.msg || '上传失败'))
}
} catch (e) {
uni.showToast({
title: '上传失败',
icon: 'none'
})
reject(e)
}
},
fail: (err) => {
uni.showToast({
title: err.message || '上传失败',
icon: 'none'
})
reject(err)
},
complete: () => {
state.uploading = false
}
})
})
}
/**
* 获取最终可提交的URL
* 如果有新上传的图片先上传再返回URL
* 如果没有新上传直接返回已有的服务器URL
* @returns {Promise<Object>}
*/
const getSubmitUrl = async () => {
// 如果重新选择了新图片,先上传
if (state.tempFilePath) {
return await uploadImage()
}
// 否则返回已有的服务器URL
return { url: state.serverUrl }
}
/**
* 重置状态
*/
const reset = () => {
state.previewUrl = ''
state.serverUrl = ''
state.tempFilePath = null
state.uploading = false
}
// 返回 reactive state 和方法
// state 作为 reactive 对象,在模板中通过 idCardFrontUpload.state.previewUrl 访问
return {
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>
@@ -51,7 +54,7 @@ const smsCode = ref('');
// ========== 计算属性 ==========
// 是否可点击确定按钮(密码格式正确 + 两次输入一致)
const canConfirm = computed(() => {
// 密码格式规则6-16位任意字符(可根据需求调整)
// 密码格式规则6-16位任意字符
const pwdReg = /^(?=.*[a-zA-Z])(?=.*\d).{6,16}$/;
// 校验条件:新密码符合格式 + 确认密码和新密码一致 + 均不为空
return pwdReg.test(newPassword.value)
@@ -92,36 +95,20 @@ 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: '网络异常',
icon: 'error'
title: err.msg || '网络异常',
icon: 'none'
});
})
// 模拟修改密码请求
// uni.showLoading({
// title: '修改中...'
// });
// setTimeout(() => {
// uni.hideLoading();
// uni.showToast({
// title: '密码修改成功',
// icon: 'success'
// });
// // 密码修改成功后返回上一页(或跳转到登录页)
// uni.navigateTo({
// url:"/pages/login/pwd-login"
// });
// }, 1500);
};
</script>
@@ -129,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{
@@ -173,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%;
@@ -180,9 +192,10 @@ const handleConfirm = () => {
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
border-radius: 8rpx;
font-size: 28rpx;
margin-top: 20rpx;
margin-top: 66rpx;
}
/* 确定按钮禁用状态 */
@@ -192,6 +205,6 @@ const handleConfirm = () => {
}
/* 禁用按钮边框 */
button:after{
border-radius: 50px;
border-radius: 8px;
}
</style>

View File

@@ -1,29 +1,22 @@
<template>
<view class="forget-page">
<view class="status_bar"></view>
<view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">忘记密码</view>
</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
<image src="http://wuye.ckdzkj.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber" trim="all"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<input class="input-content native-input" :type="inputType" v-model="phoneNumber"
placeholder="请输入手机号" placeholder-style="font-size:24rpx" maxlength="11"/>
</view>
<!-- 验证码输入 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
<image src="http://wuye.ckdzkj.com/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode" trim="all"
placeholder="请输入验证码" :inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<button
<input class="input-content verify-input native-input" :type="inputType" v-model="verifyCode"
placeholder="请输入验证码" maxlength="6" placeholder-style="font-size:24rpx"/>
<button
class="get-verify-btn"
:disabled="!canGetVerify || countDown > 0"
@click="getVerifyCode"
@@ -43,9 +36,12 @@
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import { ref, computed, onUnmounted } from 'vue';
import {getSendCode} from '../api/api.js'
// 输入框类型:统一用 text避免 number 在真机上把 -/. 过滤掉导致手机号被误判合法;非法手机号由 canGetVerify 禁用获取验证码按钮
const inputType = 'text'
// ====响应式数据======
const phoneNumber = ref('')
const verifyCode = ref('')
@@ -87,9 +83,14 @@ const getVerifyCode = () => {
}else{
uni.showToast({
title: '验证码发送失败',
icon: 'error'
icon: 'none'
});
}
}).catch(err =>{
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
});
})
@@ -126,28 +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;
}
/* 输入框通用样式 */
.input-item {
display: flex;
@@ -181,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;
}
@@ -205,6 +210,9 @@ const goBack = () => {
background-color: #ccc;
color: #fff;
}
.get-verify-btn::after{
border-radius: 50px;
}
/* 下一步按钮 */
.forget-btn{
width: 100%;
@@ -212,17 +220,18 @@ const goBack = () => {
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 40rpx;
margin-top: 66rpx;
}
.forget-btn:disabled{
background-color: #99c8ff;
color: #fff;
}
/* 禁用按钮边框 */
button:after{
border-radius: 50px;
.forget-btn:after{
border-radius: 8px;
}

View File

@@ -1,265 +1,312 @@
<template>
<view class="login-page">
<view class="status_bar"></view>
<view class="status_bar"></view>
<!-- 登录标题 -->
<view class="login-title">手机验证码登录</view>
<view class="login-title">{{ loginMode === 'sms' ? '手机验证码登录' : '账号密码登录' }}</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="phone" size="24" color="#999"></uni-icons> -->
<image src="http://47.104.199.163:9090/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- ====== 验证码登录模式 ====== -->
<template v-if="loginMode === 'sms'">
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<input class="input-content native-input" :type="inputType" v-model="phoneNumber"
placeholder="请输入手机号" placeholder-style="font-size:24rpx" maxlength="11" />
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<!-- <input
class="input-content"
type="number"
placeholder="请输入手机号"
v-model="phoneNumber"
maxlength="11"
@input="checkForm"
/> -->
</view>
<!-- 验证码输入框 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- 验证码输入框 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<input class="input-content verify-input native-input" :type="inputType" v-model="verifyCode"
placeholder="请输入验证码" maxlength="6" placeholder-style="font-size:24rpx"/>
<button class="get-verify-btn" :disabled="!canGetVerify || countDown > 0" @click="getVerifyCode">
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode"
placeholder="请输入验证码" :inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<!-- <input
class="input-content verify-input"
type="number"
placeholder="请输入验证码"
v-model="verifyCode"
maxlength="6"
@input="checkForm"
/> -->
<button
class="get-verify-btn"
:disabled="!canGetVerify || countDown > 0"
@click="getVerifyCode"
>
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
</template>
<!-- 协议勾选 -->
<view class="agreement-item" >
<view @click="toggleAgreement">
<uni-icons
type="checkbox-filled"
size="24"
color="#007aff"
v-if="agreeAgreement"
></uni-icons>
<uni-icons
type="checkbox"
size="24"
color="#999"
v-else
></uni-icons>
</view>
<text class="agreement-text" @click.stop="goToPrivacy">我已阅读并同意<text class="agreePrivacy">用户隐私政策</text></text>
<!-- ====== 账号密码登录模式 ====== -->
<template v-if="loginMode === 'pwd'">
<!-- 账号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="text" v-model="account"
placeholder="请输入帐号" :inputBorder="false" maxlength="20" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 密码输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/password.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="password" v-model="password"
placeholder="请输入密码" :inputBorder="false" maxlength="16" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
</template>
<!-- 协议勾选 + 忘记密码 -->
<view class="agreement-wrap">
<view class="agreement-item">
<view @click="toggleAgreement" class="checkbox-icon">
<uni-icons type="checkbox-filled" size="24" color="#007aff" v-if="agreeAgreement"></uni-icons>
<uni-icons type="checkbox" size="24" color="#999" v-else></uni-icons>
</view>
<text class="agreement-text">我已阅读并同意
<text class="agreePrivacy" @click.stop="goToUserAgreement">用户协议</text>
<text class="agreePrivacy" @click.stop="goToPrivacy">隐私政策</text>
</text>
</view>
<text class="forget-pwd" v-if="loginMode === 'pwd'" @click="goForgetPwd">忘记密码</text>
</view>
<!-- 登录按钮 -->
<button
class="login-btn"
:disabled="!canLogin"
@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 isClicked = ref(false)
// 验证码倒计时
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 = () => {
// 点击变色
// isClicked.value = true
uni.navigateTo({
url:'/pageSubPack/loginSub/privacy',
uni.navigateTo({ url: '/pageSubPack/loginSub/privacy' })
}
const openPrivacyPopup = (action) => {
pendingAction.value = action || ''
showPrivacyPopup.value = true
}
const agreePrivacy = () => {
showPrivacyPopup.value = false
agreeAgreement.value = true
if (pendingAction.value === 'login') {
doLogin()
}
pendingAction.value = ''
}
const disagreePrivacy = () => {
showPrivacyPopup.value = false
pendingAction.value = ''
}
// 切换登录模式
const switchMode = () => {
if (loginMode.value === 'sms') {
loginMode.value = 'pwd'
} else {
loginMode.value = 'sms'
}
// 重置相关字段
agreeAgreement.value = false
if (timer) { clearInterval(timer); timer = null }
countDown.value = 0
}
// 忘记密码跳转
const goForgetPwd = () => {
uni.navigateTo({ url: '/pageSubPack/loginSub/forget-pwd' })
}
// ========== 验证码登录:获取验证码 ==========
const getVerifyCode = () => {
if (!canGetVerify.value) return
uni.showLoading({ title: '发送中...', mask: true })
getSendCode({ phone: phoneNumber.value, scene: 'login' }).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '验证码已发送', icon: 'success' })
countDown.value = 60
timer = setInterval(() => {
countDown.value--
if (countDown.value <= 0) {
clearInterval(timer)
timer = null
}
}, 1000)
} else {
uni.showToast({ title: '验证码发送失败', icon: 'none' })
}
}).catch(err => {
uni.hideLoading()
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
})
}
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone:phoneNumber.value,
scene:'login'
}
uni.showLoading({ title: '发送中...', mask: true });
getSendCode(params).then(res =>{
if(res.code === 200){
uni.hideLoading();
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
}else{
uni.showToast({
title: '验证码发送失败',
icon: 'error'
});
}
}).catch(err => {
uni.showToast({
title: '网络异常',
icon: 'error'
});
})
};
// 登录处理
// ========== 登录处理(分发) ==========
const handleLogin = () => {
if (!canLogin.value) return;
uni.showLoading({ title: '登录中...', mask: true });
let params = {
phone:phoneNumber.value,
smsCode:'123456',
scene:'login',
if (loginMode.value === 'sms') {
handleSmsLogin()
} else {
handlePwdLogin()
}
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' });
}
// ========== 验证码登录 ==========
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()
}
}).catch(err => {
uni.showToast({
title: '网络异常,请重试',
icon: 'error'
});
})
// ========== 密码登录 ==========
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')
// 模拟登录请求
// uni.showLoading({
// title: '登录中...'
// });
// uni.showToast({
// title:"登录成功",
// icon:'success'
// })
// setTimeout(() =>{
// uni.switchTab({ url: '/pages/index/index' });
// },1500)
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%;
}
.status_bar {
height: 160px;
width: 100%;
}
/* 登录标题 */
.login-title {
font-size: 48rpx;
font-size: 52rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 80rpx;
margin-bottom: 142rpx;
}
/* 输入框项 */
@@ -282,10 +329,9 @@ onUnmounted(() => {
/* 输入框图标 */
.input-icon {
width: 50rpx;
height: 50rpx;
width: 50rpx;
height: 50rpx;
margin-right: 20rpx;
color: #999;
}
/* 输入框内容 */
@@ -296,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;
@@ -318,33 +385,56 @@ onUnmounted(() => {
margin: 0;
}
/* 获取验证码按钮禁用状态 */
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
border-radius: 50px;
}
/* 协议勾选项 */
.agreement-item {
display: flex;
align-items: center;
margin: 40rpx 0;
padding-left: 10rpx;
.get-verify-btn::after {
border-radius: 50px;
}
/* 协议和忘记密码行 */
.agreement-wrap {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin: 70rpx 0 28rpx 0;
padding: 0 10rpx;
}
.agreement-item {
display: flex;
align-items: flex-start;
flex: 1;
min-width: 0;
}
.checkbox-icon {
flex-shrink: 0;
margin-top: 2rpx;
}
/* 协议文本 */
.agreement-text {
font-size: 28rpx;
font-size: 24rpx;
color: #666;
margin-left: 10rpx;
line-height: 1.5;
}
.agreePrivacy{
color: #2F77FD;
.agreePrivacy {
color: #2F77FD;
}
/* .agreePrivacy.active{
color: #2F77FD;
} */
.forget-pwd {
font-size: 24rpx;
color: #666;
flex-shrink: 0;
margin-left: 16rpx;
line-height: 1.5;
}
/* 登录按钮 */
.login-btn {
width: 100%;
@@ -352,20 +442,18 @@ onUnmounted(() => {
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 60rpx;
}
/* 登录按钮禁用状态 */
.login-btn:disabled {
background-color: #99c8ff;
color: #fff;
/* border-radius: 50px; */
}
/* 禁用按钮边框 */
button:after{
border-radius: 50px;
button:after {
border-radius: 8px;
}
/* 底部链接 */
@@ -375,7 +463,6 @@ button:after{
padding: 0 20rpx;
}
/* 链接项 */
.link-item {
font-size: 28rpx;
color: #007aff;

View File

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

View File

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

View File

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

View File

@@ -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"
<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()
}
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') {
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') {
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 {
}
}
},
const carImageUpload = useImageUpload({
uploadUrl: '/api/resident/file/uploadImage'
})
created() {
// 车牌号输入限制:省份简称(中文)+ 城市代码(字母)+ 序号(字母数字)
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
}
},
onShow() {
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)
}
}
},
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';
const chooseImage = async () => {
try {
await carImageUpload.chooseImage()
} catch (err) {
// 用户取消选择
}
}
} else if (uni.getStorageSync('operationType') == 'add') {
this.id = undefined;
this.type = '添加';
// 车牌号正则:省份简称 + 城市代码字母 + 序号
// 蓝牌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 (!CAR_NUM_REGEX.test(formData.value.carNum)) {
return uni.showToast({
title: "请输入正确的车牌号",
icon: "none"
})
}
isSubmitting.value = true
uni.showLoading({
title: '提交中...',
mask: true
})
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'
})
}
}
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') // 车辆图片路径
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'
})
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'
})
}
}
},
methods: {
inputChange(type) {
if (type == '车牌号') {
uni.setStorageSync('plateNo', this.formData.plateNo)
} else if (type == '车辆型号') {
uni.setStorageSync('model', this.formData.model)
} else if (type == '车辆颜色') {
uni.setStorageSync('color', this.formData.color)
}
},
// 选择图片(上传照片)
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];
}
});
},
// 保存车辆信息
saveCar() {
// 表单校验
if (!this.formData.plateNo) {
return uni.showToast({
title: "请输入车牌号",
icon: "none"
});
}
if (!this.formData.carBrandName) {
return uni.showToast({
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) => {
uni.showLoading({
title: '提交中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '提交成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
}
}).catch((err) => {
isSubmitting.value = false
uni.hideLoading()
uni.showToast({
title: err.msg || '提交失败',
icon: 'none'
})
},
// 选择项点击事件可替换为picker/弹窗选择)
selectItem(type) {
if (type === '车辆品牌') {
uni.redirectTo({
url: '/pageSubPack/my/selectCarBrand',
success: res => {},
fail: () => {},
complete: () => {}
});
})
} 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'
})
}
},
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: () => {}
});
// }
},
},
}).catch((err) => {
isSubmitting.value = false
uni.hideLoading()
uni.showToast({
title: err.msg || '提交失败',
icon: 'none'
})
})
}
}
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,24 +405,17 @@
color: #333;
}
.item-arrow {
display: inline-block;
width:10rpx;
height:10rpx;
width: 10rpx;
height: 10rpx;
border-top: 2rpx solid #999;
border-right: 2rpx solid #999;
transform: rotate(45deg);
margin-left: 10rpx;
margin-right:10rpx;
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;
.input-placeholder {
color: #999 !important;
font-size: 28rpx !important;
}
.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>

File diff suppressed because it is too large Load Diff

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)"
<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>
<!-- 右侧状态标签 -->
<view class="status-tag" :class="{ bound: item.status === '已绑定', unbound: item.status === '未绑定' }">
{{ item.status }}
<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="{ underReview: item.certificationStatus === '审核中', unbound: item.certificationStatus === '未绑定', bounded: item.certificationStatus === ''}">
{{ item.certificationStatus }}
</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() {
},
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: "已绑定"
},
{
id: '2',
imgSrc: 'https://错误地址2.png',
plate: "京A88888",
model: "宝马X5",
color: "白色",
status: "未绑定"
},
{
id: '3',
imgSrc: 'https://错误地址3.png',
plate: "京A88888",
model: "宝马X5",
color: "白色",
status: "未绑定"
}
]
loadingMore.value = true
try {
const res = await getMyCarList({
pageNum: pageNum.value,
pageSize: pageSize.value,
userId: uni.getStorageSync('userId')
})
const data = res
const newList = data.rows || []
// 第一页 → 覆盖数据
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;
},
const confirmBind = () => {
if (selectedIndex.value === -1) {
return uni.showToast({
title: "请先选择车辆",
icon: "none"
})
}
// 确定绑定
confirmBind() {
if (this.selectedIndex === -1) {
return uni.showToast({
title: "请先选择绑定车辆",
icon: "none",
});
uni.showModal({
title: "确认选择",
content: `确定选择该车辆吗?`,
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '选择成功',
icon: 'success'
})
setTimeout(() => {
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'
// })
// }
// })
}
}
})
}
uni.showModal({
title: "确认选择",
content: `确定绑定该车辆吗?`,
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '绑定中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '绑定成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
uni.setStorageSync('bindCarId', this.selectedId);
uni.setStorageSync('bindCarName', this.selectedName);
this.goBack();
}, 2500);
}
},
});
},
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,34 +256,57 @@
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: 6rpx14rpx;
padding: 6rpx 14rpx;
border-radius: 6rpx;
font-weight: 500;
flex-shrink: 0;
position: absolute;
right: 30rpx;
top: 30rpx;
}
.status-tag.unbound {
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;
@@ -366,22 +315,14 @@
top: 30rpx;
}
/* 未绑定状态(浅绿色背景+绿色文字) */
.status-tag.unbound {
/* background-color: #e6f7e6; */
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'
/* 设定状态栏默认高度 */
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",
}
/* 模拟房屋数据 */
const communityList = ref([{
id: 1,
villageName: "北境碧桂园小区(别墅)",
roomNo: "2号楼2层",
owner: "张三",
status: "自住",
},
computed: {
// 过滤后的列表(搜索功能)
filteredList() {
this.selectedIndex = -1
if (!this.searchKey.trim()) {
return this.houseList
{
id: 1,
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
}
/* 确定绑定 */
const confirmBind = () => {
if (selectedIndex.value === -1) {
return uni.showToast({
title: "请先选择房屋",
icon: "none",
})
}
uni.showModal({
title: "确认选择",
content: `确定绑定 ${selectedName.value} 吗?`,
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '绑定中...',
mask: true
})
setTimeout(() => {
uni.showToast({
title: '绑定成功',
icon: 'success'
})
}, 1000)
setTimeout(() => {
// 提交成功后跳转列表页
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)
}
return this.houseList.filter(item =>
item.community.includes(this.searchKey.trim())
)
},
})
}
const getList = async () => {
},
try {
const res = await getVillageList({
})
console.log(res);
const data = res
communityList.value = data.data || []
created() {
},
onShow() {
},
onUnload() {
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
searchKey: "",
selectedId: '',
selectedName: '',
selectedIndex: -1, // 当前选中的索引
// 房屋状态颜色映射
statusColor: {
自住: "#409EFF",
闲置: "#67C23A",
租赁: "#E6A23C",
其他: "#909399",
},
// 模拟房屋数据
houseList: [{
id: 1,
community: "北境碧桂园小区(别墅)",
roomNo: "2号楼2层",
owner: "张三",
status: "自住",
},
{
id: 1,
community: "北境碧桂园小区(别墅)",
roomNo: "2号楼1层",
owner: "张三",
status: "闲置",
},
],
}
},
methods: {
// 搜索输入事件
onSearch() {
// 实时过滤computed 自动处理
},
// 选择房屋
selectHouse(item, index) {
this.selectedIndex = index;
this.selectedId = item.id;
this.selectedName = item.community + item.roomNo;
},
// 确定绑定
confirmBind() {
if (this.selectedIndex === -1) {
return uni.showToast({
title: "请先选择房屋",
icon: "none",
});
}
uni.showModal({
title: "确认选择",
content: `确定绑定 ${this.selectedName} 吗?`,
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '绑定中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '绑定成功',
icon: 'success'
});
}, 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);
}
},
});
},
goNext() {
uni.redirectTo({
url: '/pageSubPack/my/selectParingLot',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
}
}
const goNext = () => {
uni.navigateTo({
url: '/pageSubPack/my/selectParingLot',
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;
@@ -361,7 +306,7 @@
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
color: #999;
@@ -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>
</view>
<view class="bottom-btn-wrap">
<button class="btn-modify" @click="clickUpdate">修改</button>
<button class="btn-delete" @click="onDelete">删除</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
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',
},
}
},
methods: {
// 修改按钮事件(仅认证失败显示)
clickUpdate() {
uni.setStorageSync('operationType', 'update')
uni.redirectTo({
url: '/pageSubPack/my/addCar?id=' + this.id,
success: res => {},
fail: () => {},
complete: () => {}
});
},
// 删除按钮事件
onDelete() {
uni.showModal({
title: "确认删除",
content: "确认删除此车辆信息吗?",
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '删除中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '删除成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
}
},
});
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/my/myCarIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
<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()
}
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)
})
onShow(() => {
// 首次加载由 onLoad 处理,仅从编辑页返回时刷新
if (isFirstLoad) {
isFirstLoad = false
return
}
isUpdating.value = false
if (id.value) {
loadCarDetail()
}
})
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 {
}
}
const clickUpdate = () => {
if (isUpdating.value) return
isUpdating.value = true
uni.setStorageSync('operationType', 'update')
uni.navigateTo({
url: '/pageSubPack/my/addCar?id=' + id.value
})
}
const onDelete = () => {
if (isDeleting.value) return
isDeleting.value = true
uni.showModal({
title: "确认删除",
content: "确认删除此车辆信息吗?",
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '删除中...',
mask: true
})
deleteCar({
carId: id.value
}).then(deleteRes => {
uni.hideLoading()
if (deleteRes.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
isDeleting.value = false
uni.showToast({
title: deleteRes.msg || '删除失败',
icon: 'none'
})
}
}).catch(() => {
isDeleting.value = false
uni.hideLoading()
uni.showToast({
title: '删除失败',
icon: 'none'
})
})
} else {
isDeleting.value = false
}
}
})
}
</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,296 +1,242 @@
<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>
<!-- 表单区域 -->
<view class="form-container">
<!-- 手机号输入框+86前缀 -->
<view class="input-group phone-input">
<image class="iconStyle" src="http://47.104.199.163:9090/images/propertyImgs/phoneIcon.png"></image>
<input class="input-item" type="number" v-model="phone" placeholder="请输入手机号码"
placeholder-class="input-placeholder" maxlength="11" />
<input
class="input-item"
type="number"
v-model="phone"
placeholder="请输入手机号码"
placeholder-class="input-placeholder"
maxlength="11"
/>
</view>
<!-- 验证码输入框带获取验证码按钮 -->
<view class="input-group code-input" style="padding: 0 0 0 20rpx;">
<image class="iconStyle" src="http://47.104.199.163:9090/images/propertyImgs/codeIcon.png"></image>
<input class="input-item code-item" type="number" v-model="code" placeholder="请输入验证码"
placeholder-class="input-placeholder" maxlength="6" />
<button class="code-btn" :disabled="countdown > 0" @click="getCode">
<image class="iconStyle" src="http://47.104.199.163:9090/images/propertyImgs/codeIcon.png"></image>
<input
class="input-item code-item"
type="number"
v-model="code"
placeholder="请输入验证码"
placeholder-class="input-placeholder"
maxlength="6"
/>
<button
class="code-btn"
:disabled="countdown > 0"
@click="getCode"
>
{{ countdown > 0 ? `${countdown}s后重发` : '获取验证码' }}
</button>
</view>
</view>
</scroll-view>
<view class="bottom-btn">
<button class="submit-btn" @click="onSubmit">确定</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
onShow() {
},
onUnload() {
// 页面卸载时清除定时器
if (this.timer) clearInterval(this.timer)
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
phone: '',
code: '',
countdown: 0,
timer: null,
}
},
methods: {
// 获取验证码
getCode() {
// 1. 手机号校验
const phoneReg = /^1[3-9]\d{9}$/
if (!this.phone) {
uni.showToast({
title: '请输入手机号',
icon: 'none'
})
return
}
if (!phoneReg.test(this.phone)) {
uni.showToast({
title: '手机号格式不正确',
icon: 'none'
})
return
}
// 2. 模拟发送验证码(替换为实际接口)
uni.showLoading({
title: '发送中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '验证码已发送',
icon: 'none'
})
// 3. 开启60秒倒计时
this.countdown = 60
this.timer = setInterval(() => {
if (this.countdown <= 1) {
clearInterval(this.timer)
this.countdown = 0
return
}
this.countdown--
}, 1000)
}, 800)
},
// 提交修改
onSubmit() {
// 1. 基础校验
const phoneReg = /^1[3-9]\d{9}$/
if (!this.phone) {
uni.showToast({
title: '请输入手机号',
icon: 'none'
})
return
}
if (!phoneReg.test(this.phone)) {
uni.showToast({
title: '手机号格式不正确',
icon: 'none'
})
return
}
if (!this.code) {
uni.showToast({
title: '请输入验证码',
icon: 'none'
})
return
}
uni.showLoading({
title: '更换中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '更换成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/my/settingIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
<script setup>
import { ref, onUnmounted } from 'vue'
import { getAuthSendSmsCode, getUserChangePhone } from '../api/apiSub.js'
// 响应式数据
const phone = ref('')
const code = ref('')
const countdown = ref(0)
let timer = null // 定时器无需响应式,用普通变量
// 获取验证码
const getCode = () => {
const phoneReg = /^1[3-9]\d{9}$/
if (!phone.value) {
uni.showToast({ title: '请输入手机号', icon: 'none' })
return
}
if (!phoneReg.test(phone.value)) {
uni.showToast({ title: '手机号格式不正确', icon: 'none' })
return
}
uni.showLoading({ title: '发送中...' })
getAuthSendSmsCode({ phone: phone.value, scene: 'register' }).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '验证码已发送', icon: 'none' })
// 60秒倒计时
countdown.value = 60
timer = setInterval(() => {
if (countdown.value <= 1) {
clearInterval(timer)
countdown.value = 0
return
}
countdown.value--
}, 1000)
} else {
uni.showToast({ title: res.msg || '发送失败', icon: 'none' })
}
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '发送失败', icon: 'none' })
})
}
// 提交修改
const onSubmit = () => {
const phoneReg = /^1[3-9]\d{9}$/
if (!phone.value) {
uni.showToast({ title: '请输入手机号', icon: 'none' })
return
}
if (!phoneReg.test(phone.value)) {
uni.showToast({ title: '手机号格式不正确', icon: 'none' })
return
}
if (!code.value) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
uni.showLoading({ title: '更换中...', mask: true })
getUserChangePhone({ newPhone: phone.value, smsCode: code.value,scene:'change_phone' }).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '更换成功', icon: 'success' })
setTimeout(() => goBack(), 1500)
} else {
uni.showToast({ title: res.msg || '更换失败', icon: 'none' })
}
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '更换失败', icon: 'none' })
})
}
// 返回
const goBack = () => {
uni.navigateBack()
}
// 页面卸载清除定时器
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
page {
background: #f9f9f9;
height: 100%;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100%;
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
/* background-color: #fff; */
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 表单容器 */
.form-container {
/* padding: 60rpx 30rpx; */
}
/* 表单容器 */
.form-container {}
/* 输入框组(手机号+验证码行) */
.input-group {
display: flex;
align-items: center;
height: 88rpx;
/* border: 1rpx solid #dcdcdc; */
border-radius: 8rpx;
margin-bottom: 30rpx;
padding: 0 20rpx;
box-sizing: border-box;
background-color: #fff;
}
/* 输入框组(手机号+验证码行) */
.input-group {
display: flex;
align-items: center;
height: 88rpx;
border-radius: 8rpx;
margin-bottom: 30rpx;
padding: 0 20rpx;
box-sizing: border-box;
background-color: #fff;
}
.iconStyle {
width:50rpx;
height: 50rpx;
}
.iconStyle {
width: 50rpx;
height: 50rpx;
}
.phone-input .input-item {
flex: 1;
height: 100%;
line-height: 88rpx;
border: none;
padding-left: 20rpx;
}
.phone-input .input-item {
flex: 1;
height: 100%;
line-height: 88rpx;
border: none;
padding-left: 20rpx;
}
/* 验证码输入框 */
.code-input .code-item {
flex: 1;
height: 100%;
line-height: 88rpx;
border: none;
font-size: 30rpx;
}
/* 验证码输入框 */
.code-input .code-item {
flex: 1;
height: 100%;
line-height: 88rpx;
border: none;
font-size: 30rpx;
}
.code-btn {
font-size: 24rpx;
color: #fff;
background: #2F77FD;
border: none;
padding: 0;
line-height: 88rpx;
width: 100px;
}
.code-btn {
width: 200rpx;
height: 66rpx;
line-height: 66rpx;
background-color: #2F77FD;
color: #fff;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
margin: 0;
flex-shrink: 0;
}
.code-btn[disabled] {
color: #999;
}
.code-btn[disabled] {
color: #999;
}
/* 通用输入框样式 */
.input-item {
font-size: 30rpx;
color: #333;
background-color: #fff;
padding-left: 20rpx;
}
/* 通用输入框样式 */
.input-item {
font-size: 30rpx;
color: #333;
background-color: #fff;
padding-left: 20rpx;
}
.input-placeholder {
color: #999;
font-size: 30rpx;
}
.input-placeholder {
color: #999;
font-size: 30rpx;
}
/* 确定按钮 */
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #1677ff;
color: #fff;
font-size: 32rpx;
border-radius: 8rpx;
border: none;
margin-top: 20rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
/* 确定按钮 */
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #1677ff;
color: #fff;
font-size: 32rpx;
border-radius: 8rpx;
border: none;
margin-top: 20rpx;
}
</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>
<!-- 表单区域 -->
@@ -34,102 +29,59 @@
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
<script setup>
import { ref } from 'vue'
import { authChangePassword } from '../api/apiSub.js'
},
created() {
},
onShow() {
},
onLoad() {
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
oldPwd: '',
newPwd: '',
confirmPwd: '',
const oldPwd = ref('');
const newPwd = ref('');
const confirmPwd = ref('')
// 方法
const onSubmit = () => {
if (!oldPwd.value) {
uni.showToast({ title: '请输入原密码', icon: 'none' })
return
}
if (!newPwd.value) {
uni.showToast({ title: '请输入新密码', icon: 'none' })
return
}
if (newPwd.value !== confirmPwd.value) {
uni.showToast({ title: '两次密码不一致', icon: 'none' })
return
}
uni.showLoading({ title: '修改中...', mask: true })
authChangePassword({
oldPassword: oldPwd.value,
newPassword: newPwd.value,
confirmPassword: confirmPwd.value
}).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '修改成功', icon: 'success' })
setTimeout(() => goBack(), 1500)
} else {
uni.showToast({ title: res.msg || '修改失败', icon: 'none' })
}
},
methods: {
// 提交修改
onSubmit() {
// 1. 基础校验
if (!this.oldPwd) {
uni.showToast({
title: '请输入原密码',
icon: 'none'
})
return
}
if (!this.newPwd) {
uni.showToast({
title: '请输入新密码',
icon: 'none'
})
return
}
if (this.newPwd !== this.confirmPwd) {
uni.showToast({
title: '两次密码不一致',
icon: 'none'
})
return
}
uni.showLoading({
title: '修改中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '修改成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/my/settingIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}).catch((err) => {
uni.hideLoading()
uni.showToast({ title: err.msg || '修改失败', icon: 'none' })
})
}
const goBack = () =>{
uni.navigateBack()
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
height: 100%;
}
</style>
@@ -137,7 +89,7 @@
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
height: 100%;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
@@ -236,9 +188,4 @@
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">
<!-- 顶部状态区 -->
@@ -32,21 +29,22 @@
</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.houseNumber}}</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.owner}}</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 === '自住' ? '#00aaff' : form.status === '闲置' ?
'#FF700A' : form.status === '租赁' ? '#00aa00' : form.status === '其他' ? '#00557f' : '#999' }">{{form.status}}</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>
@@ -59,35 +57,42 @@
</view>
<view class="info-row">
<text class="info-label">住户</text>
<text class="info-value">{{form.resident}}</text>
<text class="info-value">{{form.residentName}}</text>
</view>
<view class="info-row">
<text class="info-label">房间号</text>
<text class="info-value">{{form.gender}}</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.documentType}}</text>
<text
class="info-value">{{form.idCardType=='0'?'居民身份证':form.idCardType=='1'?'护照':form.idCardType=='2'?'港澳通行证':''}}</text>
</view>
<view class="info-row">
<text class="info-label">证件号码</text>
<text class="info-value">{{form.documentNumber}}</text>
<text class="info-value">{{form.idCard}}</text>
</view>
<view class="info-row">
<text class="info-label">与业主关系</text>
<text class="info-value">{{form.relationship}}</text>
<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>
<text class="info-value">{{form.phoneNum}}</text>
<text class="info-value">{{form.phone}}</text>
</view>
<!-- 身份证照片区 -->
<view class="photo-row">
<text class="info-label" style="width: 200rpx;">本人身份证照片</text>
<view class="photo-list">
<view class="photo-item"></view>
<view class="photo-item"></view>
<view class="photo-item"
:style="{backgroundImage: `url(${form.cardImageFront||''})`,backgroundSize: 'cover', backgroundPosition: 'center', backgroundRepeat: 'no-repeat'}">
</view>
<view class="photo-item"
:style="{backgroundImage: `url(${form.cardImageBack||''})`,backgroundSize: 'cover', backgroundPosition: 'center', backgroundRepeat: 'no-repeat'}">
</view>
</view>
</view>
</view>
@@ -98,148 +103,154 @@
<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>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onReachBottom,
onPullDownRefresh,
onLoad,
onShow
} from '@dcloudio/uni-app'
import {
getMyHouseDetail,
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: "房屋认证成功",
iconClass: "icon-success",
iconText: "✓",
currentStep: 4,
},
computed: {
// 动态计算当前状态配置
statusConfig() {
return this.statusMap[this.currentStatus];
},
// 动态计算当前步骤
currentStep() {
return this.statusConfig.currentStep;
},
pending: {
title: "房屋审核中,请耐心等待",
iconClass: "icon-pending",
iconText: "⏳",
currentStep: 3,
},
created() {
fail: {
title: "房屋认证失败,请重新提交",
iconClass: "icon-fail",
iconText: "✕",
currentStep: 3,
},
onShow() {
})
},
onLoad(option) {
this.id = option.id;
this.currentStatus = option.statusClass;
// 计算属性
const statusConfig = computed(() => {
return statusMap.value[currentStatus.value]
})
const currentStep = computed(() => {
return statusConfig.value.currentStep
})
// 生命周期
onLoad((option) => {
id.value = option.id
currentStatus.value = option.certificationStatus == '0' ? 'pending' : option.certificationStatus == '1' ?
'success' : option.certificationStatus == '2' ? 'fail' : ''
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
// 可通过页面传参动态修改状态success / pending / fail
id: undefined,
currentStatus: "",
stepList: ["房屋信息", "住户信息", "物业审核", "认证成功"],
form: {
community: '北境碧桂园小区小区',
houseNumber: '101栋2单元402',
owner: '张三',
status: '租赁',
resident: '蒋依依',
gender: '女',
documentType: '身份证',
documentNumber: '371100200001010888',
relationship: '亲属',
phoneNum: '16599999999',
},
// 状态配置表
statusMap: {
success: {
title: "房屋认证成功",
iconClass: "icon-success",
iconText: "✓",
currentStep: 4,
},
pending: {
title: "房屋审核中,请耐心等待",
iconClass: "icon-pending",
iconText: "⏳",
currentStep: 3,
},
fail: {
title: "房屋认证失败,请重新提交",
iconClass: "icon-fail",
iconText: "✕",
currentStep: 3,
},
},
})
onShow(() => {})
onMounted(() => {
getDetail()
})
}
},
methods: {
// 修改按钮事件(仅认证失败显示)
clickUpdate() {
uni.setStorageSync('operationType', 'update')
uni.redirectTo({
url: '/pageSubPack/my/addHouse?id=' + this.id + '&sourcePage=' + 'addHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
},
// 删除按钮事件
onDelete() {
uni.showModal({
title: "确认删除",
content: "确认删除此房屋信息吗?",
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '删除中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '删除成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
}
},
});
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/my/myHouseIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
const getDetail = async () => {
try {
const res = await getMyHouseDetail({
id: id.value
})
// const data =
form.value = res.data
certificationStatus.value = form.value.certificationStatus
console.log('详情数据',form.value);
} catch (err) {
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
})
// console.error('列表接口报错:', err)
} finally {
}
}
// 方法(函数名完全不变)
const clickUpdate = () => {
uni.setStorageSync('operationType', 'update')
uni.navigateTo({
url: '/pageSubPack/my/addHouse?id=' + id.value + '&sourcePage=addHouse'
})
}
const onDelete = () => {
uni.showModal({
title: "确认删除",
content: "确认删除此房屋信息吗?",
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '删除中...',
mask: true
})
deleteMyHouse({
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(() => {
uni.redirectTo({ url: "/pageSubPack/my/myHouseIndex" })
}, 1000)
} else {
uni.showToast({
title: res.msg,
icon: 'none'
});
}
}).catch(err => {
uni.showToast({
title: err.msg,
icon: 'none'
});
})
}
}
})
}
</script>
<style lang="scss">
page {
background: #fff;
@@ -251,7 +262,6 @@
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f5f7fa;
}
@@ -262,13 +272,10 @@
box-sizing: border-box;
}
/* 顶部状态区 */
.status-header {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0px 20rpx;
}
@@ -284,17 +291,14 @@
.icon-success {
background-color: #2F77FD;
/* border: 4rpx solid #00b42a; */
}
.icon-pending {
background-color: #2F77FD;
/* border: 4rpx solid #1677ff; */
}
.icon-fail {
background-color: #E34D59;
/* border: 4rpx solid #f53f3f; */
}
.icon-text {
@@ -322,7 +326,6 @@
margin-bottom: 40rpx;
}
/* 步骤条 */
.step-bar {
display: flex;
align-items: center;
@@ -382,7 +385,6 @@
background-color: #1677ff;
}
/* 信息卡片 */
.info-card {
padding: 20rpx;
border: 1rpx solid #f0f0f0;
@@ -435,7 +437,6 @@
font-weight: 500;
}
/* 身份证照片区 */
.photo-row {
margin-top: 30rpx;
display: flex;
@@ -445,8 +446,9 @@
.photo-list {
display: flex;
flex-direction: column;
gap: 20rpx;
/* gap: 20rpx; */
margin-top: 20rpx;
}
.photo-item {
@@ -454,15 +456,16 @@
height: 170px;
background-color: #f6f6f6;
border-radius: 8rpx;
margin-bottom: 20rpx;
}
.photo-item:last-child{
margin-bottom: 0;
}
/* 底部按钮区 */
.bottom-btn-wrap {
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
/* background-color: #fff; */
}
.btn-modify {
@@ -491,7 +494,6 @@
flex: 1;
}
/* 成功提示弹窗 */
.toast-mask {
position: fixed;
top: 0;
@@ -534,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" />
<!-- 中间车辆信息 -->
<view class="car-item" v-for="(item, index) in carList" :key="index" @click="handleItemClick(item, index)">
<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>
<view class="car-bindParkingSpace">{{ item.bindParkingSpace }}
<view class="car-plate">{{ item.carNum }}</view>
<view class="car-model">{{ item.carBrand }}·{{ item.carColor }}</view>
</view>
<view class="car-bindParkingSpace">{{ item.villageName }}</view>
</view>
<!-- 右侧状态标签 -->
<view class="status-tag"
:class="{ underReview: item.status === '审核中', unbound: item.status === '未绑定',bounded: item.status === '已绑定' }">
{{ item.status }}
</view>
<!-- certificationStatus -->
<!-- <view class="status-tag"
:class="{ underReview: item.certificationStatus === '审核中', unbound: item.certificationStatus === '未绑定', bounded: item.certificationStatus === ''}">
{{ item.certificationStatus }}
</view> -->
</view>
</scroll-view>
<!-- 底部添加车辆按钮 -->
</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() {
},
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: "",
}
]
const data = res
const newList = data.rows || []
// 第一页 → 覆盖数据
if (pageNum.value === 1) {
carList.value = newList
} else {
// 后续页 → 追加数据
carList.value = [...carList.value, ...newList]
}
},
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: () => {}
});
// 判断是否没有更多数据
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()
}
}
},
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,18 +234,15 @@
font-size: 28rpx;
color: #666;
white-space: nowrap;
/* 强制不换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
/* 超出显示省略号 */
width: 90%;
margin-top: 10rpx;
}
/* 状态标签 */
.status-tag {
font-size: 24rpx;
padding: 6rpx14rpx;
padding: 6rpx 14rpx;
border-radius: 6rpx;
font-weight: 500;
flex-shrink: 0;
@@ -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%;
@@ -398,4 +282,4 @@
width: 100vw;
height: 46px;
}
</style>
</style>

View File

@@ -1,16 +1,11 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="我的房屋" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<!-- -->
<scroll-view class="page" scroll-y="true">
<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,10 +13,12 @@
<!-- 标题 + 状态 -->
<view class="card-header">
<view class="title-box">
<text class="tag-default" v-if="item.isDefault">默认</text>
<text class="house-name">{{ item.houseName }}</text>
<text class="tag-default" v-if="item.isDefault === true">默认</text>
<text
class="house-name">{{ item.villageName+"-"+item.buildingName+"-"+item.unitNo }}</text>
</view>
<text class="status-tag" :class="item.statusClass">{{ item.statusText }}</text>
<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>
</view>
<!-- 房屋信息 -->
@@ -29,29 +26,27 @@
<view class="row">
<text class="label">房间号</text>
<view class="value-box">
<text class="value">{{ item.roomNo }}</text>
<text class="value">{{ item.houseNo }}</text>
</view>
</view>
<view class="row">
<text class="label">业主</text>
<text class="value">{{ item.owner }}</text>
<text class="value">{{ item.name }}</text>
</view>
<view class="row">
<text class="label">房屋状态</text>
<text class="value">{{ item.houseStatus }}</text>
<text
class="value">{{ item.houseStatus==0?'自住':item.houseStatus==1?'闲置':item.houseStatus==2?'租赁':item.houseStatus==3?'其他':''}}</text>
</view>
<view class="defaultBox" v-if="item.statusText=='认证成功'&&!item.isDefault">
<view class="defaultBox" v-if="item.certificationStatus=='1'">
<text class="set-default">设为默认</text>
<switch class="no-cross-switch" :checked="item.isDefault" color="#1677ff"
@change="onSwitchChange($event,index)" @click.stop />
<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>
@@ -61,147 +56,156 @@
</template>
<script setup>
import {
ref,
onMounted,
} from 'vue'
import {
onShow,
onPullDownRefresh,
onReachBottom
} from '@dcloudio/uni-app'
import {
getMyHouseList,
setMyHouseDefault
} from '/pageSubPack/api/apiSub.js'
const houseList = ref([])
const loading = ref(false)
const pageNum = ref(1)
const pageSize = ref(10)
const noMoreData = ref(false)
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
onShow() {
},
onUnload() {
// 页面卸载时清除定时器
if (this.timer) clearInterval(this.timer)
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
houseList: [{
id: 1,
houseName: "北境碧桂园小区(别墅)",
roomNo: "2号楼2层",
owner: "张三",
houseStatus: "自住",
isDefault: true,
statusText: "认证成功",
statusClass: "success",
showSetDefault: false,
},
{
id: 2,
houseName: "北境碧桂园小区(别墅)",
roomNo: "3号楼2层",
owner: "张三",
houseStatus: "自住",
isDefault: false,
statusText: "认证成功",
statusClass: "success",
showSetDefault: true,
},
{
id: 3,
houseName: "北境碧桂园小区(别墅)",
roomNo: "2号楼1层",
owner: "张三",
houseStatus: "闲置",
isDefault: false,
statusText: "认证失败",
statusClass: "fail",
showSetDefault: false,
},
{
id: 4,
houseName: "北境碧桂园小区(别墅)",
roomNo: "2号楼1层",
owner: "张三",
houseStatus: "闲置",
isDefault: false,
statusText: "认证中",
statusClass: "pending",
showSetDefault: false,
},
]
// ==================== 下拉刷新 ====================
onPullDownRefresh(() => {
pageNum.value = 1
noMoreData.value = false
houseList.value = []
loading.value = false
getList()
})
// ==================== 获取列表数据 ====================
const getList = async () => {
if (loading.value) return
loading.value = true
try {
const res = await getMyHouseList({
pageNum: pageNum.value,
pageSize: pageSize.value
})
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
}
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
loading.value = false
uni.stopPullDownRefresh()
}
}
// ==================== 加载更多 ====================
const loadMore = () => {
if (loading.value || noMoreData.value) return
pageNum.value++
getList()
}
// ==================== 上拉加载更多 ====================
onReachBottom(() => {
loadMore()
})
// 页面加载时请求第一页
onMounted(() => {
pageNum.value = 1
noMoreData.value = false
getList()
})
// 页面显示时刷新列表(从添加/编辑房屋页返回时)
onShow(() => {
pageNum.value = 1
noMoreData.value = false
getList()
})
methods: {
// 开关切换
onSwitchChange(e, i) {
uni.showLoading({
title: '设置中...',
mask: true
const onSwitchChange = async (e, i) => {
try {
const res = await setMyHouseDefault({
houseId: e.detail.value ? i.houseId : ''
})
const data = res
if (data.code === 200) {
pageNum.value = 1
noMoreData.value = false
getList()
uni.setStorageSync('houseIsDefault', e.detail.value)
// 标记首页需要重新加载用户信息,返回首页后 onShow 会自动刷新小区
uni.setStorageSync('needReloadUserInfo', true)
uni.showToast({
title: res.msg || '设置默认房屋成功',
icon: 'none'
});
setTimeout(() => {
uni.showToast({
title: '设置默认房屋成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
this.houseList.forEach((item, index) => {
if (index != i) {
item.isDefault = false
} else {
this.houseList[i].isDefault = e.detail.value
}
})
uni.setStorageSync('houseIsDefault', e.detail.value)
}, 2500);
},
tapHouse(item) {
uni.redirectTo({
url: '/pageSubPack/my/houseDetail?id=' + item.id + '&statusClass=' + item.statusClass,
success: res => {},
fail: () => {},
complete: () => {}
});
},
addHouse() {
uni.setStorageSync('operationType', 'add');
uni.redirectTo({
url: '/pageSubPack/my/addHouse',
success: res => {},
fail: () => {},
complete: () => {}
} else {
uni.showToast({
title: res.msg,
icon: 'none'
});
}
} catch (err) {
// uni.hideLoading();
uni.showToast({
title: err.msg,
icon: 'none'
})
}
}
},
goBack() {
uni.switchTab({
url: '/pages/myIndex/myIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
const tapHouse = (item) => {
uni.navigateTo({
url: '/pageSubPack/my/houseDetail?id=' + item.id + '&certificationStatus=' + item.certificationStatus,
success: res => {},
fail: () => {},
complete: () => {}
});
}
},
const addHouse = () => {
uni.setStorageSync('operationType', 'add');
uni.navigateTo({
url: '/pageSubPack/my/addHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
}
const goBack = () => {
uni.navigateBack({ delta: 1 })
}
</script>
<style lang="scss">
page {
// background: #F3F8FD;
@@ -211,29 +215,26 @@
<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;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
height: 100%;
min-height: 500rpx;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
color: #999;
@@ -241,16 +242,8 @@
margin-bottom: 10rpx;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 列表 */
.house-list {
height: 100%;
overflow-y: scroll;
}
.house-card {
@@ -273,19 +266,25 @@
display: flex;
align-items: center;
gap: 16rpx;
width: 100%;
overflow-x: hidden;
}
.house-name {
font-size: 32rpx;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tag-default {
background: #007aff;
color: #fff;
font-size: 24rpx;
padding:2rpx 16rpx;
padding: 2rpx 16rpx;
border-radius: 30rpx;
min-width: 48rpx;
}
/* 状态标签 */
@@ -294,6 +293,8 @@
padding: 6rpx 14rpx;
font-weight: 600;
border-radius: 6rpx;
min-width: 120rpx;
text-align: center;
}
.success {
@@ -345,7 +346,7 @@
}
.defaultBox {
margin-top:10rpx;
margin-top: 10rpx;
display: flex;
justify-content: space-between;
align-items: center;
@@ -361,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>
</scroll-view>
<!-- 底部添加按钮 -->
</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() {
const loadingMore = ref(false)
const noMoreData = ref(false)
},
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 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 || []
// 第一页 → 覆盖数据
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()
}
}
// ==================== 上拉加载更多 ====================
onReachBottom(() => {
getList()
})
methods: {
tapParkingSpace(item) {
uni.redirectTo({
url: '/pageSubPack/my/parkingSpaceDetail?id=' + item.id + '&statusClass=' + item.statusClass,
success: res => {},
fail: () => {},
complete: () => {}
});
},
addParkingSpace() {
uni.setStorageSync('operationType', 'add');
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goBack() {
uni.switchTab({
url: '/pages/myIndex/myIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
const tapParkingSpace = (item) => {
console.log(item);
uni.navigateTo({
url: '/pageSubPack/my/parkingSpaceDetail?id=' + item.id + '&statusClass=' + item.checkStatus
})
}
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,205 +21,250 @@
</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>
<view class="info-row">
<text class="info-label">车牌号</text>
<text class="info-value">{{ form.carNum }}</text>
</view>
<view class="info-row">
<text class="info-label">车辆品牌</text>
<text class="info-value">{{ form.carBrand }}</text>
</view>
<view class="info-row">
<text class="info-label">车辆型号</text>
<text class="info-value">{{ form.carModel }}</text>
</view>
<view class="info-row">
<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>
<text class="unbind" @click="clickUnbind(item.id)">取消绑定</text>
<view class="info-row">
<text class="info-label">车牌号</text>
<text class="info-value">{{form.carNum}}</text>
<view class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">暂无绑定车辆</text>
</view>
<view class="info-row">
<text class="info-label">车辆品牌</text>
<text class="info-value">{{form.carBrand}}</text>
</view>
<view class="info-row">
<text class="info-label">车辆型号</text>
<text class="info-value">{{form.carModel}}</text>
</view>
<view class="info-row">
<text class="info-label">车辆颜色</text>
<text class="info-value">{{form.carColor}}</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'
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
},
computed: {
// 动态计算当前状态配置
statusConfig() {
return this.statusMap[this.currentStatus];
},
// 动态计算当前步骤
currentStep() {
return this.statusConfig.currentStep;
},
pending: {
title: "车位审核中,请耐心等待",
iconClass: "icon-pending",
iconText: "⏳",
currentStep: 2
},
created() {
fail: {
title: "车位认证失败,请重新提交",
iconClass: "icon-fail",
iconText: "✕",
currentStep: 2
},
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: {
success: {
title: "车位认证成功",
iconClass: "icon-success",
iconText: "✓",
currentStep: 3,
},
pending: {
title: "车位审核中,请耐心等待",
iconClass: "icon-pending",
iconText: "⏳",
currentStep: 2,
},
fail: {
title: "车位认证失败,请重新提交",
iconClass: "icon-fail",
iconText: "✕",
currentStep: 2,
},
},
}
},
methods: {
clickUnbind(id) {
},
// 修改按钮事件(仅认证失败显示)
onModify() {
uni.setStorageSync('operationType', 'update');
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace?id=' + this.id + '&sourcePage=' + 'addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
},
// 删除按钮事件
onDelete() {
uni.showModal({
title: "确认删除",
content: "确认删除此车位信息吗?",
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '删除中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '删除成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
}
},
});
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/my/myParkingSpaceIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}
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 {
}
}
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:'',
}).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: "确认删除此车位信息吗?",
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '删除中...',
mask: true
})
deleteParking({
parkingId: id.value
}).then(deleteRes => {
uni.hideLoading()
if (deleteRes.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
uni.showToast({
title: deleteRes.msg || '删除失败',
icon: 'none'
})
}
})
}
}
})
}
</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

@@ -8,17 +8,17 @@
<scroll-view class="page" scroll-y>
<!-- 楼栋网格列表 -->
<view class="building-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>
</view>
<view v-if="dataList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view>
<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)">
<text class="building-text">{{ item.name }}</text>
<text class="building-text">{{item.name }}</text>
</view>
</view>
</scroll-view>
@@ -29,339 +29,198 @@
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted,
} from 'vue'
import {
onUnload,
onShow
},
} from '@dcloudio/uni-app'
import {
getHouseBuildings,
getHouseUnits,
getHouseFloors,
getHouseHouses
} from '/pageSubPack/api/apiSub.js'
computed: {
// 状态栏高度
const statusBarHeight = ref(20)
},
created() {
},
onShow() {
},
onUnload() {},
mounted() {
if (this.checkType == '楼栋') {
this.dataList = this.buildingList
} else if (this.checkType == '单元') {
this.dataList = this.unitList
} else if (this.checkType == '楼层') {
this.dataList = this.floorList
} else if (this.checkType == '户号') {
this.dataList = this.houseNumberList
}
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
checkType: uni.getStorageSync('checkType'),
dataList: [],
// 楼栋列表(可动态生成/接口返回)
buildingList: [{
id: 1,
name: '01栋'
},
{
id: 2,
name: '02栋'
},
{
id: 3,
name: '03栋'
},
{
id: 4,
name: '04栋'
},
{
id: 5,
name: '05栋'
},
{
id: 6,
name: '06栋'
},
{
id: 7,
name: '07栋'
},
{
id: 8,
name: '08栋'
},
{
id: 9,
name: '09栋'
},
{
id: 10,
name: '10栋'
},
{
id: 11,
name: '11栋'
},
{
id: 12,
name: '12栋'
}
],
floorList: [{
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层'
},
],
unitList: [{
id: 33,
name: '01单元'
},
{
id: 34,
name: '02单元'
},
{
id: 35,
name: '03单元'
},
{
id: 36,
name: '04单元'
},
],
houseNumberList: [{
id: 37,
name: '01'
},
{
id: 38,
name: '02'
},
{
id: 39,
name: '03'
},
{
id: 40,
name: '04'
},
],
// 选中的楼栋
selectedBuilding: uni.getStorageSync('buildingId'),
// 选中的单元
selectedUnit: uni.getStorageSync('unitId'),
// 选中的楼层
selectedFloor: uni.getStorageSync('floorId'),
// 选中的户号
selectedHouseNumber: uni.getStorageSync('houseNumberId'),
}
},
methods: {
selectBuilding(item) {
let modalContent = '确认选择此' + this.checkType + '吗?'
uni.showModal({
title: "确认选择",
content: modalContent,
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '选择中...',
mask: true
});
// 从缓存获取类型
const checkType = ref(uni.getStorageSync('checkType') || '')
const dataList = ref([])
// 添加防重复点击锁
const isNavigating = ref(false)
if (this.checkType == '楼栋') {
this.selectedBuilding = item.id
uni.setStorageSync('buildingId', item.id);
uni.setStorageSync('buildingName', item.name);
uni.removeStorageSync('unitId');
uni.removeStorageSync('unitName');
uni.removeStorageSync('floorId');
uni.removeStorageSync('floorName');
uni.removeStorageSync('houseNumberId');
uni.removeStorageSync('houseNumberName');
uni.setStorageSync('checkType', '单元');
} else if (this.checkType == '单元') {
this.selectedUnit = item.id
uni.setStorageSync('unitId', item.id);
uni.setStorageSync('unitName', item.name);
uni.removeStorageSync('floorId');
uni.removeStorageSync('floorName');
uni.removeStorageSync('houseNumberId');
uni.removeStorageSync('houseNumberName');
uni.setStorageSync('checkType', '楼层');
} else if (this.checkType == '楼层') {
this.selectedFloor = item.id
uni.setStorageSync('floorId', item.id);
uni.setStorageSync('floorName', item.name);
uni.removeStorageSync('houseNumberId');
uni.removeStorageSync('houseNumberName');
uni.setStorageSync('checkType', '户号');
} else if (this.checkType == '户号') {
this.selectedHouseNumber = item.id
uni.setStorageSync('houseNumberId', item.id);
uni.setStorageSync('houseNumberName', item.name);
// 选中项(从缓存读取)
const selectedBuilding = ref(uni.getStorageSync('buildingId') || '')
const selectedUnit = ref(uni.getStorageSync('unitId') || '')
const selectedFloor = ref(uni.getStorageSync('floorId') || '')
const selectedHouseNumber = ref(uni.getStorageSync('houseNumberId') || '')
}
setTimeout(() => {
uni.showToast({
title: '选择成功',
icon: 'success'
});
}, 500);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goFreash();
}, 2000);
// 页面挂载后赋值列表
onMounted(() => {
console.log(checkType);
if (checkType.value === '楼栋') {
getList('楼栋')
} else if (checkType.value === '单元') {
getList('单元')
} else if (checkType.value === '楼层') {
getList('楼层')
} else if (checkType.value === '户号') {
getList('户号')
}
})
// 选择项方法
const selectBuilding = (item) => {
// 防重复点击
if (isNavigating.value) return
isNavigating.value = true
}
},
});
},
goFreash() {
console.log(this.checkType, '刷新页面');
if (this.checkType != '户号') {
uni.redirectTo({
url: '/pageSubPack/my/selectBuilding',
success: res => {},
fail: () => {},
complete: () => {}
});
} else {
uni.redirectTo({
url: '/pageSubPack/my/addHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
}
},
goBackAdd() {
if (this.checkType == '楼栋') {
uni.removeStorageSync('buildingId');
uni.removeStorageSync('buildingName');
} else if (this.checkType == '单元') {
uni.removeStorageSync('unitId');
uni.removeStorageSync('unitName');
} else if (this.checkType == '楼层') {
uni.removeStorageSync('floorId');
uni.removeStorageSync('floorName');
} else if (this.checkType == '户号') {
uni.removeStorageSync('houseNumberId');
uni.removeStorageSync('houseNumberName');
}
uni.redirectTo({
url: '/pageSubPack/my/addHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
if (checkType.value === '楼栋') {
selectedBuilding.value = item.id
uni.setStorageSync('buildingId', item.id)
uni.setStorageSync('buildingName', item.name)
uni.removeStorageSync('unitId')
uni.removeStorageSync('unitName')
uni.removeStorageSync('floorId')
uni.removeStorageSync('floorName')
uni.removeStorageSync('houseNumberId')
uni.removeStorageSync('houseNumberName')
uni.setStorageSync('checkType', '单元')
} else if (checkType.value === '单元') {
selectedUnit.value = item.id
uni.setStorageSync('unitId', item.id)
uni.setStorageSync('unitName', item.name)
uni.removeStorageSync('floorId')
uni.removeStorageSync('floorName')
uni.removeStorageSync('houseNumberId')
uni.removeStorageSync('houseNumberName')
uni.setStorageSync('checkType', '楼层')
} else if (checkType.value === '楼层') {
selectedFloor.value = item.id
uni.setStorageSync('floorId', item.id)
uni.setStorageSync('floorName', item.name)
uni.removeStorageSync('houseNumberId')
uni.removeStorageSync('houseNumberName')
uni.setStorageSync('checkType', '户号')
} else if (checkType.value === '户号') {
selectedHouseNumber.value = item.id
uni.setStorageSync('houseNumberId', item.id)
uni.setStorageSync('houseNumberName', item.name)
}
uni.showToast({
title: '选择成功',
icon: 'success'
})
setTimeout(() => {
goFreash()
}, 1000)
}
// 刷新跳转
const goFreash = () => {
console.log(checkType.value, '刷新页面')
if (checkType.value !== '户号') {
// 楼栋/单元/楼层:替换当前页,链式选择不堆叠
uni.redirectTo({
url: '/pageSubPack/my/selectBuilding'
})
} else {
// 户号选择完成:返回已有的 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")
}
uni.navigateBack()
}
// 获取数据
const getList = async (type) => {
try {
if (type == '楼栋') {
const res = await getHouseBuildings({
villageId: uni.getStorageSync('communityId')
})
const newList = res.data.map(item => ({
id: item.id,
name: item.buildingName
}))
dataList.value = newList
} else if (type == '单元') {
const res = await getHouseUnits({
buildingId: selectedBuilding.value
})
const newList = res.data.map(item => ({
id: item.id,
name: item.name
}))
dataList.value = newList
} else if (type == '楼层') {
const res = await getHouseFloors({
buildingId: selectedBuilding.value,
unitNo: selectedUnit.value
})
const newList = res.data.map(item => ({
id: item,
name: item
}))
dataList.value = newList
} else if (type == '户号') {
const res = await getHouseHouses({
buildingId: selectedBuilding.value,
unitNo: selectedUnit.value,
floorNo: selectedFloor.value
})
const newList = res.data.map(item => ({
id: item.houseId,
name: item.houseNo
}))
dataList.value = newList
}
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
}
}
// 生命周期
onShow(() => {
})
onUnload(() => {})
</script>
<style lang="scss">
page {
@@ -400,6 +259,11 @@
gap: 30rpx;
}
/* 正在跳转时禁用点击 */
.building-grid.disabled {
pointer-events: none;
}
/* 空状态 */
.empty-state {
display: flex;
@@ -428,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;
}
/* 选中状态 */
@@ -445,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'
},
/* 设定状态栏默认高度 */
const statusBarHeight = ref(20)
const searchKey = ref('')
const carBrandList = ref()
const loadingMore = ref(false)
const noMoreData = ref(false)
created() {
const pageNum = ref(1)
const pageSize = ref(10)
// ==================== 获取列表数据 ====================
const getList = async () => {
// 防重复请求
},
onShow() {
if (loadingMore.value || noMoreData.value) return
},
onUnload() {
loadingMore.value = true
try {
const res = await getSystemDictData({
pageNum: pageNum.value,
pageSize: pageSize.value,
dictType: 'user_car_brand'
})
},
onLoad(option) {
const data = res
const newList = data.rows || []
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
searchKey: '',
carBrandList: [{
id: 1,
name: '宝马'
},
{
id: 2,
name: '奥迪'
},
]
// 第一页 → 覆盖数据
if (pageNum.value === 1) {
carBrandList.value = newList
} else {
// 后续页 → 追加数据
carBrandList.value = [...carBrandList.value, ...newList]
}
},
methods: {
// 搜索输入事件
onSearch() {
// 实时过滤computed 自动处理
},
// 选择小区
selectCarBrand(item) {
uni.setStorageSync('carBrandId', item.id);
uni.setStorageSync('carBrandName', item.name);
uni.redirectTo({
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: () => {}
});
},
},
// 判断是否没有更多数据
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())
)
})
// 搜索输入事件
const onSearch = () => {
// 实时过滤computed 自动处理
}
// 选择小区
const selectCarBrand = (item) => {
uni.setStorageSync('carBrandId', item.id);
uni.setStorageSync('carBrandName', item.name);
uni.navigateTo({
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,254 +1,174 @@
<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"
<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)}"
:class="{ active: (checkType == '停车场' && selectedParkingLot === item.id) || (checkType == '车位编号' && selectedParkingSpace === item.id) }"
v-for="(item, index) in dataList" :key="index" @click="selectedParking(item)">
<text class="parkingLot-text">{{ item.name }}</text>
</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'
},
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
computed: {
onMounted(() => {
checkType.value = uni.getStorageSync('checkType')
if (checkType.value == '停车场') {
loadAreaList()
} else if (checkType.value == '车位编号') {
loadParkingList()
}
})
},
created() {
},
onShow() {
},
onUnload() {},
mounted() {
if (this.checkType == '停车场') {
this.dataList = this.parkingLotList
} else if (this.checkType == '车位编号') {
this.dataList = this.parkingSpaceList
}
},
data() {
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'),
}
},
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
});
setTimeout(() => {
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);
}
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goFreash()
}, 2500);
}
},
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 {
id: item.id,
name: item.name || item.areaName || ''
}
})
}else {
uni.showToast({
title: `${res.msg}`,
icon: 'none'
});
},
goFreash() {
if (this.checkType != '车位编号') {
uni.redirectTo({
url: '/pageSubPack/my/selectParingLot',
success: res => {},
fail: () => {},
complete: () => {}
});
} else {
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
}
},
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 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'
})
navTimer = setTimeout(() => {
goFreash()
}, 1500)
} finally {
// 正常跳转后页面会销毁,防止异常时锁死
lockTimer = setTimeout(() => {
isSubmitting.value = false
}, 1500)
}
}
const goFreash = () => {
if (checkType.value != '车位编号') {
// 选完停车场后替换当前页进入选车位编号,避免压栈
uni.redirectTo({
url: '/pageSubPack/my/selectParingLot'
})
} else {
// 选完车位编号后返回原来的 addParkingSpace
uni.navigateBack()
}
}
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

@@ -22,7 +22,7 @@
</view>
<view class="community-item" v-for="(item, index) in filteredList" :key="index"
@click="selectCommunity(item)">
<text class="community-name">{{ item.name }}</text>
<text class="community-name">{{ item.villageName }}</text>
<text class="item-arrow"></text>
</view>
</view>
@@ -33,147 +33,118 @@
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
// 过滤后的列表(搜索功能)
filteredList() {
if (!this.searchKey.trim()) {
return this.communityList
}
return this.communityList.filter(item =>
item.name.includes(this.searchKey.trim())
)
},
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onReachBottom,
onPullDownRefresh,
onLoad
} from '@dcloudio/uni-app'
import {
getVillageList
} from '/pageSubPack/api/apiSub.js'
},
created() {
const sourcePage = ref('')
const searchKey = ref('')
const communityList = ref([])
// const refreshing = ref(false)
const filteredList = computed(() => {
if (!searchKey.value.trim()) {
return communityList.value
}
return communityList.value.filter(item =>
item.villageName.includes(searchKey.value.trim())
)
},
onShow() {
},
onUnload() {
},
onLoad(option) {
// addHouse
// addParkingSpace
this.sourcePage = option.sourcePage;
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
sourcePage: '',
searchKey: '',
communityList: [{
id: 1,
name: '北境碧桂园小区'
},
{
id: 2,
name: '东境碧桂园小区'
},
{
id: 3,
name: '南境碧桂园小区'
},
{
id: 4,
name: '西境碧桂园小区'
},
})
]
onLoad((option) => {
sourcePage.value = option.sourcePage;
getList()
})
// ==================== 获取列表数据 ====================
}
},
const getList = async () => {
methods: {
// 搜索输入事件
onSearch() {
// 实时过滤computed 自动处理
},
try {
const res = await getVillageList({
// 选择小区
selectCommunity(item) {
uni.setStorageSync('communityId', item.id);
uni.setStorageSync('communityName', item.name);
uni.setStorageSync('checkType', '楼栋');
uni.removeStorageSync('buildingId');
uni.removeStorageSync('buildingName');
uni.removeStorageSync('unitId');
uni.removeStorageSync('unitName');
uni.removeStorageSync('floorId');
uni.removeStorageSync('floorName');
uni.removeStorageSync('houseNumberId');
uni.removeStorageSync('houseNumberName');
})
console.log(res);
const data = res
communityList.value = data.data || []
uni.removeStorageSync('bindHouseId');
uni.removeStorageSync('bindHouseName');
uni.removeStorageSync('parkingLotId');
uni.removeStorageSync('parkingLotName');
uni.removeStorageSync('parkingSpaceId');
uni.removeStorageSync('parkingSpaceName');
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
if (this.sourcePage == 'addHouse') {
uni.redirectTo({
url: '/pageSubPack/my/selectBuilding',
success: res => {},
fail: () => {},
complete: () => {}
});
} else if (this.sourcePage == 'addParkingSpace') {
uni.redirectTo({
url: '/pageSubPack/my/bindHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
} else {
}
}
}
// 搜索输入事件
const onSearch = () => {
// 实时过滤computed 自动处理
}
},
goBackAdd() {
uni.removeStorageSync('communityId');
uni.removeStorageSync('communityName');
if (this.sourcePage == 'addHouse') {
uni.redirectTo({
url: '/pageSubPack/my/addHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
} else if (this.sourcePage == 'addParkingSpace') {
uni.redirectTo({
url: '/pageSubPack/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
} else {
// 选择小区
const selectCommunity = (item) => {
uni.setStorageSync('communityId', item.villageId);
uni.setStorageSync('communityName', item.villageName);
uni.setStorageSync('checkType', '楼栋');
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('bindHouseId');
uni.removeStorageSync('bindHouseName');
uni.removeStorageSync('parkingLotId');
uni.removeStorageSync('parkingLotName');
uni.removeStorageSync('parkingSpaceId');
uni.removeStorageSync('parkingSpaceName');
console.log(sourcePage.value);
if (sourcePage.value == 'addHouse') {
// 替换当前页进入楼栋选择,避免页面堆叠
uni.redirectTo({
url: '/pageSubPack/my/selectBuilding'
});
} else if (sourcePage.value == 'addParkingSpace') {
uni.setStorageSync('checkType', '停车场')
uni.navigateBack()
} else {
}
}
const goBackAdd = () => {
uni.removeStorageSync("communityId");
uni.removeStorageSync("communityName");
uni.navigateBack()
}
</script>
<style lang="scss">
page {
background: #f6f6f6;

View File

@@ -1,14 +1,9 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="设置" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<view class="contentStyle">
<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>
<!-- 不同类型的右侧内容 -->
<view class="item-right" v-if="item.type === 'text'">
<text :class="item.valueClass || 'item-value'">{{ item.value }}</text>
<view class="item-arrow"></view>
@@ -20,284 +15,447 @@
<view class="item-arrow" v-else></view>
</view>
</scroll-view>
<!-- 底部下一步按钮 -->
<view class="bottom-btn">
<button class="logout-btn" @click="logout">退出登录</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
authLogout, getUserProfile, getUserNotify, changeUserNotify, getAppVersion, checkAppUpdate, postUpdateLog
} from '/pageSubPack/api/apiSub.js'
//
const cacheSize = ref(0)
const currentVersion = ref('1.0.0')
const currentVersionCode = ref(1)
const settingList = ref([{
title: '更改手机号码',
type: 'text',
value: ''
},
computed: {
{
title: '修改密码',
type: 'arrow'
},
created() {
{
title: '消息通知',
type: 'switch',
checked: false
},
onShow() {
{
title: '清除缓存',
type: 'text',
value: ''
},
onLoad() {
{
title: '注销账号',
type: 'arrow'
},
mounted() {},
data() {
return {
message: '',
/* 设定状态栏默认高度 */
statusBarHeight: 20,
cacheSize: 0,
settingList: [{
title: '更改手机号码',
type: 'arrow'
},
{
title: '修改密码',
type: 'arrow'
},
{
title: '消息通知',
type: 'switch',
checked: false
},
{
title: '清除缓存',
type: 'text',
value: '9.2M'
},
{
title: '升级版本',
type: 'text',
value: '当前版本 2.9.8',
valueClass: 'item-version'
}
]
}
},
methods: {
onItemClick(item) {
// console.log('点击了:', item.title,item.title == '更改手机号码')
if (item.title == '更改手机号码') {
uni.redirectTo({
url: '/pageSubPack/my/changePhone',
success: res => {},
fail: () => {},
complete: () => {}
});
} else if (item.title == '修改密码') {
uni.redirectTo({
url: '/pageSubPack/my/changePwd',
success: res => {},
fail: () => {},
complete: () => {}
});
} else if (item.title == '清除缓存') {
uni.clearStorageSync();
this.message = '清除成功'
this.settingList[3].value = '0M'
uni.showLoading({
title: '清除中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '清除成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
}, 2500);
}
},
logout() {
uni.showModal({
title: '提示',
content: '确定要退出当前账号吗?',
success: (res) => {
if (res.confirm) {
this.message = '退出成功'
uni.showLoading({
title: '退出中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '退出成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 登出成功后跳转回登录页
uni.hideLoading();
uni.redirectTo({
url: '/pageSubPack/loginSub/pwd-login',
success: res => {},
fail: () => {},
complete: () => {}
});
}, 2500);
}
}
})
},
onSwitchChange(item, e) {
item.checked = e.detail.value
},
goBack() {
uni.switchTab({
url: '/pages/myIndex/myIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
{
title: '升级版本',
type: 'text',
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.navigateTo({
url: '/pageSubPack/my/changePhone'
});
} else if (item.title == '修改密码') {
uni.navigateTo({
url: '/pageSubPack/my/changePwd'
});
} else if (item.title == '清除缓存') {
// 保存登录相关数据,清完恢复
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
});
setTimeout(() => {
uni.showToast({
title: '清除成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
uni.hideLoading();
}, 2500);
} else if (item.title == '注销账号') {
uni.navigateTo({
url: '/pageSubPack/my/deleteAccount'
});
} else if (item.title == '升级版本') {
checkVersionUpdate()
}
}
const logout = () => {
uni.showModal({
title: '提示',
content: '确定要退出当前账号吗?',
success: (res) => {
if (!res.confirm) return
uni.showLoading({ title: '退出中...', mask: true })
const clearAndGo = () => {
const uid = uni.getStorageSync('userId') || ''
uni.removeStorageSync('token')
uni.removeStorageSync('userId')
uni.removeStorageSync('currentVillageId')
uni.removeStorageSync('home_cache_' + uid)
uni.removeStorageSync('home_cache')
// 清除小区相关缓存,防止切换账号后显示上一个账号的小区和公告
uni.removeStorageSync('communityName')
uni.removeStorageSync('communityId')
uni.removeStorageSync('needReloadUserInfo')
uni.hideLoading()
uni.hideToast()
uni.reLaunch({ url: '/pageSubPack/loginSub/login?mode=pwd' })
}
authLogout().then(res => {
if (res.code === 200) {
uni.hideLoading()
uni.showToast({ title: '退出成功', icon: 'success' })
setTimeout(clearAndGo, 1500)
} else {
clearAndGo()
}
}).catch(err => {
console.log(err)
clearAndGo()
})
}
})
}
const onSwitchChange = (item, e) => {
const checked = e.detail.value
const notifyEnabled = checked ? '1' : '0'
changeUserNotify({ notifyEnabled }).then(res => {
if (res.code === 200) {
item.checked = checked
} else {
uni.showToast({ title: res.msg || '更新失败', icon: 'none' })
item.checked = !checked
}
}).catch(() => {
uni.showToast({ title: '更新失败', icon: 'none' })
item.checked = !checked
})
}
onMounted(async () => {
initCurrentVersion()
settingList.value[3].value = await getCacheSizeData()
getUserProfile().then(res => {
if (res.code === 200 && res.data) {
const phone = res.data.phone || ''
settingList.value[0].value = phone
}
})
getUserNotify().then(res => {
if (res.code === 200 && res.data) {
settingList.value[2].checked = res.data.notifyEnabled === '1'
}
})
})
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
</style>
<style lang="scss" scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 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>
<style scoped>
/* 列表项 */
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #ffffff;
padding: 30rpx 30rpx;
background-color: #fff;
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
/* 标题文字 */
.item-title {
font-size: 32rpx;
color: #333333;
color: #333;
line-height: 1.4;
}
/* 右侧区域(文字+箭头) */
.item-right {
display: flex;
align-items: center;
gap: 10rpx;
}
/* 右侧数值/版本文字 */
.item-value {
font-size: 28rpx;
color: #333333;
color: #333;
}
.item-version {
font-size: 28rpx;
color: #999999;
color: #999;
}
/* 右侧箭头 */
.item-arrow {
display: inline-block;
width: 10rpx;
height: 10rpx;
border-top: 2rpx solid #999;
border-right: 2rpx solid #999;
transform: rotate(45deg);
margin-left: 10rpx;
margin-right: 10rpx;
}
/* 开关样式适配 */
/* switch {
transform: scale(0.8);
} */
/* 底部按钮 */
.logout-btn {
width: 100%;
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 {
background-color: #f5f5f5;
opacity: 0.8;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>
<style>
::v-deep .uni-switch-input:before {
background-color: #cccccd !important;
}
/* 隐藏 switch 关闭状态的 × 号 */
.no-cross-switch::before {
display: none !important;
}
.no-cross-switch {
@@ -308,4 +466,4 @@
.no-cross-switch>>>.uni-switch-input-checked {
background-color: #007aff !important;
}
</style>
</style>

View File

@@ -1,30 +1,7 @@
<template>
<view class="page">
<view class="status_bar"></view>
<!-- 固定导航栏 -->
<uni-nav-bar
left-icon="left"
title="通知公告"
background-color="#F9F9F9"
@clickLeft="narBack"
/>
<!-- 滚动区域能正常滚动的核心 -->
<scroll-view
scroll-y
class="scroll-box"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<view class="list-wrap">
<!-- 下拉刷新 -->
<view class="refresh" v-if="refreshing">
<uni-icons type="spinner-cycle" class="spin"/> 刷新中...
</view>
<!-- 列表 -->
<!-- 列表 -->
<view class="list-wrap">
<view
v-for="notice in noticeList"
:key="notice.noticeId"
@@ -34,9 +11,9 @@
<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">详情</text>
<text style="color:#2F77FD;font-size: 24rpx;">详情</text>
<uni-icons type="right" size="13" color="#007AFF"/>
</view>
</view>
@@ -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>
</view>
</scroll-view>
<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>
</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,14 +141,15 @@ page {
.title {
font-size: 32rpx;
font-weight: bold;
width: 500rpx;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dot {
width: 12rpx;
height: 12rpx;
width: 16rpx;
height: 16rpx;
background: red;
border-radius: 50%;
}
@@ -191,27 +159,51 @@ page {
margin: 10rpx 0;
}
.content {
color: #666;
font-size: 28rpx;
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;
}
.refresh, .load-more, .no-more {
.load-more, .no-more {
text-align: center;
padding: 20rpx;
color: #999;
font-size: 24rpx;
}
.empty {
text-align: center;
padding: 100rpx 0;
color: #999;
.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 {
@@ -234,7 +234,7 @@
justify-content: center;
// margin-bottom: 12px;
color: #666;
font-size: 16px;
font-size: 24rpx;
margin: 0 30rpx;
.notice-author {
@@ -271,18 +271,27 @@
padding: 20px;
color: #333333;
line-height: 1.8;
font-size: 16px;
font-size: 28rpx;
.articleimg{
width: 100%;
height: 249rpx;
height: 330rpx;
}
.paragraph {
margin-bottom: 20px;
text-align: justify;
text-indent: 2em;
white-space: pre-wrap;
word-break: break-word;
/* 富文本里的图片不超出屏幕 */
:deep(img),
:deep(image) {
max-width: 100% !important;
height: auto !important;
display: block;
margin: 10px 0;
}
}
.bullet-list {

View File

@@ -1,50 +1,46 @@
<template>
<!-- 页面主容器 -->
<view class="door-open-page">
<view class="status_bar"></view>
<!-- 顶部标题栏 -->
<view class="top-bar">
<uni-nav-bar left-icon="left" title="远程开门" backgroundColor ="none" :border="false" @clickLeft="narBack" />
</view>
<view class="top-img">
<image src="http://47.104.199.163:9090/images/openDoor/remoteOpen.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
<view class="page-content">
<view class="top-img">
<image src="https://wuye.ckdzkj.com/images/openDoor/remoteOpen.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
<!-- 提示文案 -->
<view class="tips-text">
远程开门
<text class="tips-desc">请选择要开的门并保持距离在10米以内单次仅开一门</text>
</view>
<!-- 提示文案 -->
<view class="tips-text">
远程开门
<text class="tips-desc">请选择要开的门并保持距离在10米以内单次仅开一门</text>
</view>
<!-- 开门成功提示默认隐藏 -->
<view v-if="showSuccess" class="success-toast">开门成功</view>
<!-- 开门成功提示默认隐藏 -->
<view v-if="showSuccess" class="success-toast">开门成功</view>
<!-- 开门授权区域 -->
<view class="auth-container">
<view class="auth-title">开门授权</view>
<!-- 网格布局2行2列 -->
<view class="grid-container">
<!-- 每个门选项 -->
<view
v-for="(item, index) in doorList"
:key="index"
:class="['door-item', { active: selectedDoor === item.id }]"
@click="selectDoor(item.id)"
>
<!-- 图标占位 -->
<view class="door-icon">
<image class="icon-text" :src="selectedDoor === item.id ? item.iconActive:item.iconText"></image>
<!-- 开门授权区域 -->
<view class="auth-container">
<view class="auth-title">开门授权</view>
<!-- 网格布局2行2列 -->
<view class="grid-container">
<!-- 每个门选项 -->
<view
v-for="(item, index) in doorList"
:key="index"
:class="['door-item', { active: selectedDoor === item.id }]"
@click="selectDoor(item.id)"
>
<!-- 图标占位 -->
<view class="door-icon">
<image class="icon-text" :src="selectedDoor === item.id ? item.iconActive:item.iconText"></image>
</view>
<!-- 门名称 -->
<text class="door-name" :class="{active :selectedDoor === item.id}">{{ item.deviceName }}</text>
</view>
<!-- 门名称 -->
<text class="door-name" :class="{active :selectedDoor === item.id}">{{ item.deviceName }}</text>
</view>
<!-- 空状态 -->
<view v-if="doorList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无可用门禁</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="doorList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">暂无可用门禁</text>
</view>
</view>
<!-- 确认开门按钮 -->
@@ -62,59 +58,40 @@ 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 () =>{
try{
const res = await getAccessDevices()
if(res.code === 200){
if(res.data){
doorList.value = res.data || []
}else{
doorList.value = []
}
}else{
doorList.value = []
uni.showToast({
title:'门禁查询失败',
icon:"error"
})
}
}catch{
doorList.value = []
uni.showToast({
title:res.msg,
icon:"error"
})
}
try{
const res = await getAccessDevices()
if(res.code === 200){
if(res.data){
doorList.value = res.data || []
}else{
doorList.value = []
}
}else{
doorList.value = []
uni.showToast({
title:'门禁查询失败',
icon:"none"
})
}
}catch(err){
doorList.value = []
uni.showToast({
title:err.msg || '查询失败',
icon:"none"
})
}
}
@@ -128,71 +105,70 @@ const openDoorClick = () => {
// })
// return
// }
getAccessOpenData()
uni.showToast({
title: '正在对接硬件中...',
icon: 'none'
})
// getAccessOpenData()
}
// 开门接口
const getAccessOpenData = async () =>{
try{
let params = {deviceId:selectedDoor.value}
const res = await getAccessOpen(params)
if(res.code === 200){
if(res.data){
uni.showToast({
title:'开门成功',
icon:"success"
})
}else{
uni.showToast({
title:'开门失败',
icon:"error"
})
}
}else{
uni.showToast({
title:'开门失败',
icon:"error"
})
}
}catch{
uni.showToast({
title: res.msg,
icon:"error"
})
}
uni.showLoading({
title: '开门中...',
mask: true
})
try{
let params = {deviceId:selectedDoor.value}
const res = await getAccessOpen(params)
uni.hideLoading()
if(res.code === 200){
uni.showToast({
title:'开门成功',
icon:'success'
})
}else{
uni.showToast({
title:'开门失败',
icon:"error"
})
}
}catch(err){
uni.hideLoading()
uni.showToast({
title: err.msg || '开门失败',
icon:"none"
})
}
}
onLoad(() =>{
getAccessControl()
getAccessControl()
})
</script>
<style scoped>
/* 页面全局样式 */
.door-open-page {
display: flex;
flex-direction: column;
min-height: 100%;
overflow: hidden;
background: linear-gradient(180deg, #E2F0FF, #F6FAFF);
min-height: 100vh;
padding: 30rpx 40rpx;
box-sizing: border-box;
}
.status_bar {
height: 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{
width: 460rpx;
height: 290rpx;
float: right;
width: 460rpx;
height: 290rpx;
float: right;
}
/* 提示文案 */
@@ -233,7 +209,7 @@ onLoad(() =>{
.auth-container {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx 20rpx;
padding: 40rpx 42rpx;
margin-bottom: 60rpx;
position: relative;
top: -86px;
@@ -242,9 +218,10 @@ onLoad(() =>{
.auth-title {
text-align: center;
font-size: 32rpx;
font-weight: 500;
color: #333333;
padding-bottom: 20rpx;
border-bottom: 1px solid #e5e5e5;
/* border-bottom: 1px solid #e5e5e5; */
margin-bottom: 30rpx;
}
@@ -275,14 +252,14 @@ onLoad(() =>{
/* 门图标 */
.door-icon {
width: 80rpx;
height: 80rpx;
width: 126rpx;
height: 114rpx;
/* background-color: #f5f5f5; */
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15rpx;
margin-bottom: 44rpx;
}
.icon-text {
/* font-size: 40rpx; */
@@ -307,7 +284,7 @@ onLoad(() =>{
}
.empty-text {
font-size: 32rpx;
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
@@ -315,16 +292,22 @@ 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;
color: #fff;
font-size: 32rpx;
font-size: 28rpx;
border-radius: 20rpx;
border: none;
z-index: 10;
}
.confirm-btn::after {
border: none; /* 去除uni-app默认按钮边框 */
}
</style>
</style>

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?.rows || res.data?.list || 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,20 +140,32 @@ const getCommunityList = async (pageNum = 1, keyword = '') => {
hasMoreData.value = displayedCommunities.value.length < totalCount.value
// 默认选中:优先匹配传入的 villageId否则选第一条仅第一页时
// 搜索场景下不覆盖用户已手动选择的项
if (pageNum === 1 && displayedCommunities.value.length > 0) {
const targetId = currentVillageIdFromHome.value || uni.getStorageSync('currentVillageId') || ''
if (targetId) {
const matched = displayedCommunities.value.find(
item => String(item.villageId) === String(targetId)
const userSelectedInCurrentList = selectedItem.value &&
displayedCommunities.value.some(
item => String(item.villageId) === String(selectedItem.value.villageId)
)
selectedItem.value = matched || displayedCommunities.value[0]
} else {
selectedItem.value = displayedCommunities.value[0]
// 如果用户已选中且该项仍在当前列表中,保留用户选择
if (!isSearching.value || !userSelectedInCurrentList) {
const targetId = currentVillageIdFromHome.value || uni.getStorageSync('currentVillageId') || ''
if (targetId) {
const matched = displayedCommunities.value.find(
item => String(item.villageId) === String(targetId)
)
selectedItem.value = matched || displayedCommunities.value[0]
} else {
selectedItem.value = displayedCommunities.value[0]
}
}
} else if (pageNum === 1) {
// 第一页返回空列表(搜索无结果等场景),清除选中项使确认按钮禁用
selectedItem.value = null
}
} else {
displayedCommunities.value = []
hasMoreData.value = false
selectedItem.value = null
uni.showToast({
title: res.msg || "查询失败",
icon: 'error'
@@ -164,18 +175,21 @@ const getCommunityList = async (pageNum = 1, keyword = '') => {
// console.error('获取小区列表失败:', err)
displayedCommunities.value = []
hasMoreData.value = false
selectedItem.value = null
uni.showToast({
title: "网络异常",
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,59 +250,68 @@ const handleConfirm = async () => {
})
return
}
// 缴费页为独立切换:不调用后端切换小区接口
const isPayment = sourcePage.value === 'payment'
try {
uni.showLoading({
title: '切换小区中...',
title: isPayment ? '加载中...' : '切换小区中...',
mask: true
})
const params = {
villageId: selectedItem.value.villageId,
}
const res = await getVillageSwitch(params)
uni.hideLoading()
if (res.code === 200) {
if (res.data.villageId) {
if (!isPayment) {
const res = await getVillageSwitch({
villageId: selectedItem.value.villageId,
})
uni.hideLoading()
if (res.code !== 200) {
uni.showToast({
title: res.msg || '切换小区失败',
icon: 'none',
duration: 2000
})
return
}
if (res.data?.villageId) {
selectedItem.value.villageId = res.data.villageId
}
uni.showToast({
title: '选择成功',
icon: 'success',
duration: 1500
} else {
uni.hideLoading()
}
uni.showToast({
title: '选择成功',
icon: 'success',
duration: 1500
})
if (isPayment) {
// 缴费页独立缓存 + 独立事件,通知缴费页刷新(按账号隔离)
const userId = uni.getStorageSync('userId') || ''
uni.setStorageSync('paymentVillageId_' + userId, selectedItem.value.villageId)
uni.setStorageSync('paymentVillageName_' + userId, selectedItem.value.villageName)
uni.$emit('confirmPayment', {
name: selectedItem.value.villageName,
villageId: selectedItem.value.villageId
})
} else {
// 更新缓存并通知首页
uni.setStorageSync('currentVillageId', selectedItem.value.villageId)
uni.$emit('confirm', {
name: selectedItem.value.villageName,
villageId: selectedItem.value.villageId
})
setTimeout(() => {
uni.navigateBack()
}, 1000)
} else {
uni.showToast({
title: res.msg || '切换小区失败',
icon: 'error',
duration: 2000
})
}
setTimeout(() => {
uni.navigateBack()
}, 1000)
} catch (err) {
uni.hideLoading()
uni.showToast({
title: '网络异常,请重试',
title: err.msg || '网络异常,请重试',
icon: 'error',
duration: 2000
})
}
}
// 监听搜索文本变化
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,16 +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 -->
<view class="scroll-container">
<view class="repair-type-tabs">
<view
class="type-tab"
@@ -18,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>
@@ -27,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>
@@ -66,22 +58,22 @@
<view class="repair-item" v-for="(item, index) in repairList" :key="index">
<!-- 标题和状态 -->
<view class="item-header">
<text class="item-title">{{ item.title || '报修' }}</text>
<text class="item-title">{{ item.item || '暂无标题' }}</text>
<text class="item-status" :class="item.statusClass">{{ item.status }}</text>
</view>
<!-- 标签 -->
<view class="item-tags" v-if="item.repairTypeName || item.category">
<text class="tag" v-if="item.repairTypeName">{{ item.repairTypeName }}</text>
<text class="tag" v-if="item.category">{{ item.category }}</text>
<view class="item-tags" v-if="item.projectTypeStr">
<text class="tag" v-if="item.projectTypeStr">{{item.projectTypeStr}}</text>
<text class="tag">{{ item.maintenanceItemName }}</text>
</view>
<!-- 内容 -->
<view class="item-content">
{{ item.content || item.description || '' }}
{{ item.problem || '' }}
</view>
<!-- 图片占位 -->
<!-- 图片 -->
<view class="item-imgs" v-if="item.imageList && item.imageList.length">
<view class="img-placeholder" v-for="(img, idx) in item.imageList" :key="idx">
<image :src="typeof img === 'string' ? img : img.img" style="width: 100%;height: 100%;"></image>
@@ -90,7 +82,7 @@
<!-- 时间和详情 -->
<view class="item-footer">
<text class="item-time">{{ item.time || item.createTime || '' }}</text>
<text class="item-time">{{ item.createTime || '' }}</text>
<text class="detail-btn" @click="goDetail(item)">报修详情</text>
</view>
</view>
@@ -100,12 +92,19 @@
<text class="empty-text">暂无数据</text>
</view>
</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 } 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:个人报修)
@@ -115,6 +114,12 @@ const activeStatus = ref('pending');
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 = {
@@ -123,33 +128,53 @@ const statusMap = {
completed: { text: '已完成', class: 'completed', value: '2' }
};
const getStatusInfo = (val) => {
if (val === '0' || val === 0) return statusMap.pending;
if (val === '1' || val === 1) return statusMap.assigned;
if (val === '2' || val === 2) return statusMap.completed;
return null;
};
// 获取报修列表
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 statusInfo = statusMap[activeStatus.value];
const params = {
villageId,
// 报修类型0-公共报修1-个人报修(按后端实际字段调整)
projectType: activeType.value === 'public' ? '0' : '1',
// 状态0-待派单1-已派单2-已完成(按后端实际字段调整)
problemStatus: statusInfo.value
// maintenanceItemId: maintenanceItemId.value,
pageSize: pageSize.value,
pageNum: pageNum.value,
problemStatus: statusMap[activeStatus.value].value
};
const res = await getQueryRepair(params);
// console.log('接口返回:', res);
if (res.code === 200) {
// 按后端实际返回结构调整
const list = res.data|| [];
repairList.value = list.map(item => ({
...item,
status: statusInfo.text,
statusClass: statusInfo.class
}));
const list = res.rows || [];
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).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' });
}
@@ -157,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(){
@@ -187,21 +230,30 @@ const goBack = () => {
uni.switchTab({
url:"/pages/serviceIndex/serviceIndex"
})
}else{
uni.navigateBack()
}
uni.removeStorageSync('sourcePage')
}
// 跳转报修详情
const goDetail = (item) => {
const itemStr = encodeURIComponent(JSON.stringify(item))
uni.navigateTo({
url: `/pageSubPack/online-repair/repairDetail?item = ${itemStr}`
url: `/pageSubPack/online-repair/repairDetail?id=${item.id}`
});
};
onLoad(() => {
fetchRepairList();
// maintenanceItemId.value = option.maintenanceProjectId || '' // 接收维修项目id
fetchRepairList(true)
});
onShow(() => {
// 从其他页面返回时刷新列表
fetchRepairList(true)
});
onUnload(() =>{
})
</script>
<style scoped>
@@ -212,8 +264,9 @@ onLoad(() => {
padding: 0 40rpx;
}
.status_bar {
height: 32px;
height: var(--status-bar-height);
width: 100%;
flex-shrink: 0;
}
/* 顶部导航栏 */
.nav-bar {
@@ -225,7 +278,7 @@ onLoad(() => {
position: sticky;
top: 44px;
z-index: 100;
margin-bottom: 20px;
margin-bottom: 60px;
/* box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.2); */
}
@@ -240,7 +293,7 @@ onLoad(() => {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 600;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -251,11 +304,16 @@ onLoad(() => {
.nav-right {
width: 60rpx;
}
/* 滚动区域 */
.scroll-container {
width: 100%;
box-sizing: border-box;
}
/* 报修类型Tab */
.repair-type-tabs {
display: flex;
/* background-color: #fff; */
/* padding: 10px; */
padding: 10px 0;
}
.type-tab {
flex: 1;
@@ -263,7 +321,7 @@ onLoad(() => {
flex-direction: column;
align-items: center;
justify-content: center;
height: 100px;
height: 90px;
border-radius: 8px;
color: #fff;
font-size: 14px;
@@ -291,13 +349,17 @@ onLoad(() => {
/* padding: 0 15px; */
}
.status-tab {
flex: 1;
text-align: left;
height: 40px;
line-height: 40px;
font-size: 15px;
color: #666;
position: relative;
/* flex: 1; */
display: flex;
justify-content: flex-start;
margin-right: 40rpx;
padding: 0 20rpx;
text-align: left;
height: 40px;
line-height: 40px;
font-size: 15px;
color: #666;
position: relative;
}
.status-tab.active {
color: #2F77FD;
@@ -307,7 +369,7 @@ onLoad(() => {
content: '';
position: absolute;
bottom: 0;
left: 20%;
left: 50%;
transform: translateX(-50%);
width: 45px;
height: 4px;
@@ -317,7 +379,7 @@ onLoad(() => {
/* 报修列表 */
.repair-list {
padding: 28rpx 0rpx;
padding: 28rpx 0rpx 0;
}
.repair-item {
background-color: #fff;
@@ -325,6 +387,9 @@ onLoad(() => {
padding: 15px;
margin-bottom: 10px;
}
.repair-item:last-child {
margin-bottom: 0;
}
/* 列表项头部 */
.item-header {
@@ -338,6 +403,11 @@ onLoad(() => {
font-weight: bold;
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;
@@ -371,12 +441,11 @@ onLoad(() => {
/* 标签 */
.item-tags {
/* display: flex; */
/* gap: 8px; */
display: flex;
gap: 2px;
margin-bottom: 10px;
}
.tag {
/* background-color: #e3f2fd; */
color: #999999;
font-size: 12px;
padding: 2px 8px;
@@ -391,24 +460,28 @@ onLoad(() => {
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;
@@ -419,12 +492,22 @@ onLoad(() => {
}
.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;
@@ -436,6 +519,6 @@ onLoad(() => {
color: #999;
}
.detail-btn {
color: #1E88E5;
color:#2F77FD;
}
</style>

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>
<text class="value time-value">{{ showTimeText }}</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">
@@ -55,19 +63,21 @@
<view class="upload-wrap">
<!-- 已上传图片 -->
<view class="upload-item" v-for="(img, index) in form.images" :key="index">
<image :src="img" mode="aspectFill" @click="previewImage(img)"></image>
<uni-icons type="close" size="16" @click="deleteImage(index)"></uni-icons>
<image :src="img" mode="aspectFill" class="upload-img" @click="previewImage(img)"></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">
<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">
@@ -78,7 +88,7 @@
<text class="popup-title">预约时间({{ tempTime.format('YYYY-MM-DD HH:mm') }})</text>
<text class="confirm-btn" @click="confirmTime">确认</text>
</view>
<!-- 时间选择列日期 | 小时 | 分钟 -->
<view class="time-selector">
<!-- 日期列 -->
@@ -142,350 +152,303 @@
</uni-popup>
</view>
</template>
<script setup>
import { ref,computed,onMounted} from 'vue'
import {
onLoad, onUnload
} from '@dcloudio/uni-app'
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: '',
desc: '',
phone: '',
datetime: '', // 默认时间
datetime: '',
images: []
})
// ========== 基础数据 ==========
const timePopupRef = ref(null)
// 初始时间()
const selectedTime = ref(moment().hour(9).minute(30).second(0));
// 选择的时间
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([
{ label: '(今天)', value: moment().format('YYYY-MM-DD') },
{ label: '(明天)', value: moment().add(1, 'day').format('YYYY-MM-DD') },
{ label: '(后天)', value: moment().add(2, 'day').format('YYYY-MM-DD') }
]);
// 小时列表()
{ label: '(后天)', value: moment().add(2, 'day').format('YYYY-MM-DD') }
])
const hourList = ref([
{time: moment().hours(8).format('HH')},
{time:moment().hours(9).format('HH')},
{time:moment().hours(10).format('HH')},
{time:moment().hours(11).format('HH')},
{time:moment().hours(13).format('HH')},
{time:moment().hours(14).format('HH')},
{time:moment().hours(15).format('HH')},
{time:moment().hours(16).format('HH')},
{time:moment().hours(17).format('HH')},
{time:moment().hours(18).format('HH')},
]);
// 分钟列表()
{ time: '08' }, { time: '09' }, { time: '10' }, { time: '11' },
{ time: '13' }, { time: '14' }, { time: '15' }, { time: '16' },
{ time: '17' }, { time: '18' }
])
const minuteList = ref([
{minute:moment().minute(0).format('mm')},
{minute:moment().minute(30).format('mm')}
]);
{ minute: '00' },
{ minute: '30' }
])
// ========== 选中状态 ==========
const selectedDate = ref(dateList.value[0].value);
const selectedHour = ref(hourList.value[1].time); // 默认选中8点
const selectedMinute = ref(minuteList.value[1].minute); // 默认选中00分
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
const dateScrollTop = ref(0 * itemHeight); // 初始选中明天
const hourScrollTop = ref(1 * itemHeight); // 初始选中9点
const minuteScrollTop = ref(1 * itemHeight); // 初始选中00分
// 每项高度 / 可视高度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)
// ========== 显示文本 ==========
const showTimeText = computed(() => {
return selectedTime.value.format('YYYY-MM-DD HH:mm');
});
if(!selectedTime.value) return ''
return selectedTime.value.format('YYYY-MM-DD HH:mm')
})
// ========== Popup 操作 ==========
// 打开时间弹窗
const openTimePopup = () => {
// 打开前同步临时时间为当前选中时间
tempTime.value = moment(selectedTime.value);
selectedDate.value = tempTime.value.format('YYYY-MM-DD');
selectedHour.value = tempTime.value.format('HH');
selectedMinute.value = tempTime.value.format('mm');
// 同步滚动位置
syncScrollPosition();
// 打开uni-popup
if(timePopupRef.value){
timePopupRef.value.open();
}
};
tempTime.value = moment(selectedTime.value)
selectedDate.value = tempTime.value.format('YYYY-MM-DD')
selectedHour.value = tempTime.value.format('HH')
selectedMinute.value = tempTime.value.format('mm')
syncScrollPosition()
timePopupRef.value?.open()
}
// 关闭时间弹窗
const closeTimePopup = () => {
timePopupRef.value.close();
};
timePopupRef.value?.close()
}
// 确认选择时间
const confirmTime = () => {
// 拼接选中的时间
const selectedDateTime = `${selectedDate.value} ${selectedHour.value}:${selectedMinute.value}`;
selectedTime.value = moment(selectedDateTime, 'YYYY-MM-DD HH:mm');
// 给表单赋值
form.value.datetime = selectedTime.value.format('YYYY-MM-DD HH:mm')
// 关闭弹窗
closeTimePopup();
// 提示
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()
uni.showToast({
title: `已选择 ${selectedTime.value.format('MM月DD日 HH:mm')}`,
icon: 'none'
});
};
})
}
// ========== 滚动/点击选择逻辑 ==========
// 同步滚动位置
// ========== 核心修复:只允许点击,滚动失效! ==========
const syncScrollPosition = () => {
// 日期滚动位置
const dateIndex = dateList.value.findIndex(item => item.value === selectedDate.value);
dateScrollTop.value = dateIndex * itemHeight;
// 小时滚动位置
const hourIndex = hourList.value.findIndex(item => item.time === selectedHour.value);
hourScrollTop.value = hourIndex * itemHeight;
// 分钟滚动位置
const minuteIndex = minuteList.value.findIndex(item => item.minute === selectedMinute.value);
minuteScrollTop.value = minuteIndex * itemHeight;
};
const dateIdx = dateList.value.findIndex(i => i.value === selectedDate.value)
const hourIdx = hourList.value.findIndex(i => i.time === selectedHour.value)
const minIdx = minuteList.value.findIndex(i => i.minute === selectedMinute.value)
// 选择日期(点击)
// 让选中项滚动到可视区域居中位置,并限制不小于 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
}
// 点击选择日期
const selectDate = (index) => {
selectedDate.value = dateList.value[index].value;
dateScrollTop.value = index * itemHeight;
updateTempTime();
};
selectedDate.value = dateList.value[index].value
syncScrollPosition()
updateTempTime()
}
// 选择小时(点击)
// 点击选择小时
const selectHour = (index) => {
selectedHour.value = hourList.value[index].time;
hourScrollTop.value = index * itemHeight;
updateTempTime();
};
selectedHour.value = hourList.value[index].time
syncScrollPosition() // 选中后立刻复位滚动
updateTempTime()
}
// 选择分钟(点击)
// 点击选择分钟
const selectMinute = (index) => {
selectedMinute.value = minuteList.value[index].minute;
minuteScrollTop.value = index * itemHeight;
updateTempTime();
};
selectedMinute.value = minuteList.value[index].minute
syncScrollPosition() // 选中后立刻复位滚动
updateTempTime()
}
// 滚动日期列(滑动
const onDateScroll = (e) => {
const index = Math.round(e.detail.scrollTop / itemHeight);
if (index >= 0 && index < dateList.value.length) {
selectedDate.value = dateList.value[index].value;
updateTempTime();
}
};
// 完全禁用滚动监听(去掉滚动干扰
const onDateScroll = () => {}
const onHourScroll = () => {}
const onMinuteScroll = () => {}
// 滚动小时列(滑动)
const onHourScroll = (e) => {
const index = Math.round(e.detail.scrollTop / itemHeight);
if (index >= 0 && index < hourList.value.length) {
selectedHour.value = hourList.value[index].time;
updateTempTime();
}
};
// 滚动分钟列(滑动)
const onMinuteScroll = (e) => {
const index = Math.round(e.detail.scrollTop / itemHeight);
minuteScrollTop.value = index * itemHeight;
if (index >= 0 && index < minuteList.value.length) {
selectedMinute.value = minuteList.value[index].minute;
updateTempTime();
}
};
// 更新临时时间(弹窗预览)
const updateTempTime = () => {
tempTime.value = moment(`${selectedDate.value} ${selectedHour.value}:${selectedMinute.value}`, 'YYYY-MM-DD HH:mm');
};
tempTime.value = moment(`${selectedDate.value} ${selectedHour.value}:${selectedMinute.value}`, 'YYYY-MM-DD HH:mm')
}
// ========== 初始化 ==========
onMounted(() => {
syncScrollPosition();
});
syncScrollPosition()
})
// 页面加载时监听事件
// ========== 页面通信 ==========
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
}
})
})
// 页面销毁移除监听
onUnload(() => {
uni.$off('selectHouse')
uni.$off('selectProject')
})
// 跳转到选择房屋页
// 跳转
const goToSelectHouse = () => {
uni.navigateTo({
url: `/pageSubPack/online-repair/selectHouse?houseId=${houseId.value}`
})
uni.navigateTo({ url: `/pageSubPack/online-repair/selectHouse?houseId=${houseId.value}` })
}
// 跳转到选择维修项目页
const goToSelectProject = () => {
uni.navigateTo({
url: '/pageSubPack/online-repair/personalSelectPoject'
})
uni.navigateTo({ url: '/pageSubPack/online-repair/personalSelectPoject' })
}
// 选择预约时间
const changeTime = () =>{
}
// const chooseDateTime = () => {
// uni.datePicker({
// type: 'datetime',
// value: form.value.datetime,
// success: (res) => {
// form.value.datetime = res.value
// }
// })
// }
// 选择图片
const chooseImage = () => {
// 计算还能选几张
const remainCount = 9 - form.value.images.length;
if (remainCount <= 0) {
uni.showToast({
title: '最多只能上传9张图片',
icon: 'none'
})
return;
}
uni.chooseImage({
count: 3,
count: remainCount, // 动态限制可选数量
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
form.value.images = [...form.value.images, ...res.tempFilePaths]
const tempFilePaths = res.tempFilePaths
form.value.images = [...form.value.images, ...tempFilePaths]
}
})
}
// 预览图片
const previewImage = (url) => {
uni.previewImage({
urls: form.value.images,
current: url
})
uni.previewImage({ urls: form.value.images, current: url })
}
// 删除图片
const deleteImage = (index) => {
form.value.images.splice(index, 1)
}
// 上传图片到服务器
// 上传图片
const uploadImages = async () => {
const uploadedUrls = []
const urls = []
for (const path of form.value.images) {
if (path.startsWith('http')) {
uploadedUrls.push(path)
urls.push(path)
continue
}
try {
const res = await uploadFile(path)
uploadedUrls.push(res.data || res.msg || '')
urls.push(res.url || '')
} catch (e) {
console.error('图片上传失败', e)
console.error('上传失败', e)
}
}
return uploadedUrls
return urls
}
// 表单提交
// ========== 表单提交 ==========
const submitForm = async () => {
// 校验
if (!houseName.value) {
uni.showToast({ title: '请选择报修房屋', icon: 'none' })
return
}
if (!projectName.value) {
uni.showToast({ title: '请选择维修项目', icon: 'none' })
return
}
if (!form.value.title) {
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
}
if (!form.value.datetime) {
uni.showToast({ title: '请选择预约时间', icon: 'none' })
return
}
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 (!/^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') {
uni.showToast({ title: '请先选择小区', icon: 'none' })
return
}
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: '提交中...' ,mask:true})
uni.showLoading({ title: '提交中...' })
try {
const imageUrls = await uploadImages()
const userId = uni.getStorageSync('userId')
const params = {
villageId,
houseId: houseId.value,
maintenanceItemId: projectId.value,
title: form.value.title,
problem: form.value.desc,
residentId: userId,
orderDate: form.value.datetime,
phone: form.value.phone,
problemImageUrl: imageUrls.join(','),
projectType: '1',
problemStatus: ''
villageId,
houseId: houseId.value,
maintenanceItemId: projectId.value,
item: form.value.title,
problem: form.value.desc,
residentId: residentId.value,
orderDate: form.value.datetime,
phone: form.value.phone,
problemImageUrl: imageUrls.join(','),
projectType: '1',
problemStatus: ''
}
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 = ''
uni.navigateBack({ delta: 1 })
projectId.value = ''
setTimeout(() => {
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
submitting.value = false
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '网络异常', icon: 'none' })
submitting.value = false
uni.showToast({ title: '提交失败,请检查网络或重试', icon: 'none' })
} finally {
uni.hideLoading()
}
@@ -505,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;
@@ -522,7 +489,8 @@ const submitForm = async () => {
}
.time-value {
color: #000;
white-space: nowrap;
font-size: 26rpx;
}
.input, .textarea {
flex: 1;
@@ -541,20 +509,28 @@ const submitForm = async () => {
/* border-bottom: 1px solid #eee; */
padding-bottom: 10px;
}
/* .label {
font-size: 14px;
color: #333;
margin-bottom: 8px;
display: block;
.textarea-wrap {
flex: 1;
position: relative;
}
.value {
font-size: 14px;
color: #666;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
} */
.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;
color: #007aff;
@@ -580,24 +556,25 @@ textarea {
width: 80px;
height: 80px;
}
.upload-item image {
.upload-item .upload-img{
width: 100%;
height: 100%;
border-radius: 4px;
}
.upload-item uni-icons {
.upload-item .upload-delete{
position: absolute;
top: -5px;
right: -5px;
background: #ff3b30;
color: #fff;
border-radius: 50%;
width: 32rpx;
height: 32rpx;
border-radius: 3rpx;
padding: 2px;
}
.upload-btn {
width: 80px;
height: 80px;
border: 1px dashed #ccc;
/* border: 1px dashed #ccc; */
background-color: #f3f3f3;
border-radius: 4px;
display: flex;
flex-direction: column;
@@ -606,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;
@@ -667,26 +654,16 @@ textarea {
.time-item {
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #666;
font-size: 26rpx;
color: #BBBCBE;
position: relative;
white-space: nowrap;
}
/* 选中项样式(高亮) */
.time-item.active {
color: #007aff;
color: #1D2129;
font-weight: 600;
background-color: #d0d0d0;
}
.time-item.active::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
/* width: 80%; */
/* height: 1px; */
background-color: #d0d0d0;
background-color: #F7F7F7;
}
</style>

View File

@@ -1,20 +1,20 @@
<template>
<view class="project-container">
<view class="select-container">
<!-- 搜索框 -->
<view class="search-bar">
<input type="text" placeholder="请输入关键字搜索" v-model="searchKey" />
<uni-icons type="search" size="18"></uni-icons>
</view>
<!-- 分类+项目 -->
<!-- 分类选择 -->
<view class="category-wrap">
<!-- 左侧分类 -->
<scroll-view scroll-y class="category-left">
<view
class="category-item"
<view
class="category-item"
:class="{ active: activeCategory === item.id }"
@click="selectCategory(item)"
v-for="item in categoryList"
v-for="item in categoryList"
:key="item.id"
>
{{ item.projectName }}
@@ -23,20 +23,16 @@
<!-- 右侧项目 -->
<scroll-view scroll-y class="category-right">
<view
<view
class="project-item"
:class="{ active: selectedProject.id === item.id }"
@click="selectProject(item)"
v-for="item in currentProjectList"
v-for="item in currentProjectList"
:key="item.id"
>
{{ item.projectName }}
</view>
</scroll-view>
</view>
<!-- 确认按钮 -->
<button class="confirm-btn" @click="confirmSelect">确认</button>
</view>
</template>
@@ -48,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 () => {
@@ -81,7 +75,10 @@ onLoad(() => {
fetchProjectList()
})
// 筛选当前分类的项目
// 搜索关键词
const searchKey = ref('')
// 筛选当前分类的项目(含搜索)
const currentProjectList = computed(() => {
let list = projectList.value.filter(item => item.categoryId === activeCategory.value)
if (searchKey.value) {
@@ -93,40 +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}`
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;
@@ -135,6 +126,7 @@ const confirmSelect = () => {
background: #f5f5f5;
border-radius: 6px;
margin-bottom: 10px;
flex-shrink: 0;
}
.search-bar input {
flex: 1;
@@ -143,48 +135,39 @@ const confirmSelect = () => {
.category-wrap {
display: flex;
flex: 1;
margin-bottom: 20px;
min-height: 0;
}
.category-left {
width: 80px;
height: 100%;
background: #f8f8f8;
width: 120px;
background: #fff;
border-radius: 6px 0 0 6px;
margin-right: 20rpx;
color: #666;
}
.category-item {
padding: 12px 10px;
padding: 15px 10px;
text-align: center;
font-size: 14px;
border-bottom: 1px solid #eee;
}
.category-item.active {
background: #fff;
color: #007aff;
background: #f9f9f9;
color: #222;
font-weight: bold;
}
.category-right {
flex: 1;
height: 100%;
background: #fff;
border-radius: 0 6px 6px 0;
padding: 10px;
text-align: center;
}
.project-item {
padding: 12px 10px;
font-size: 14px;
border-bottom: 1px solid #eee;
}
.project-item.active {
color: #007aff;
font-weight: bold;
.project-item:active {
background: #f5f5f5;
}
.confirm-btn {
width: 100%;
height: 44px;
line-height: 44px;
background: #007aff;
color: #fff;
border-radius: 8px;
font-size: 16px;
}
</style>
</style>

View File

@@ -2,7 +2,10 @@
<view class="form-container">
<!-- 维修项目展示 -->
<view class="form-item">
<text class="label">维修项目</text>
<!-- <text class="label">维修项目</text> -->
<view class="label">
<text class="required">*</text>维修项目
</view>
<view class="value" @click="goBackSelect">
<view>
<text v-if="!repairProject">请选择项目</text>
@@ -14,21 +17,26 @@
<!-- 标题输入 -->
<view class="form-item">
<text class="label">标题</text>
<!-- <input type="text" placeholder="请输入问题标题" v-model="form.title" /> -->
<uni-easyinput v-model="form.title" placeholder="请输入内容" :inputBorder="false"></uni-easyinput>
<view class="label">
<text class="required">*</text>标题
</view>
<uni-easyinput v-model="form.title" placeholder="请输入内容最多30个字符" :inputBorder="false" maxlength="30"></uni-easyinput>
</view>
<!-- 问题描述 -->
<view class="form-item-wenti">
<text class="label">问题描述</text>
<!-- <textarea placeholder="请输入问题内容" v-model="form.desc" rows="5" ></textarea> -->
<uni-easyinput v-model="form.desc" placeholder="请描述你的问题" type="textarea"></uni-easyinput>
<view class="textarea-wrap">
<textarea v-model="form.desc" placeholder="请描述你的问题或位置最多200字" maxlength="200" class="desc-textarea" />
<text class="char-count">{{ form.desc.length }}/200</text>
</view>
</view>
<!-- 手机号 -->
<view class="form-item">
<text class="label">手机号</text>
<view class="label">
<text class="required">*</text>手机号
</view>
<uni-easyinput type="number" v-model="form.phone" placeholder="请输入手机号" :inputBorder="false"></uni-easyinput>
</view>
@@ -38,31 +46,38 @@
<view class="upload-wrap">
<!-- 已上传图片 -->
<view class="upload-item" v-for="(img, index) in form.images" :key="index">
<image :src="img" mode="aspectFill"></image>
<uni-icons type="close" size="16" @click="deleteImage(index)"></uni-icons>
<image :src="img" mode="aspectFill" class="upload-img"></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">
<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: '',
@@ -71,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')
})
// 返回选择项目页
@@ -93,15 +128,22 @@ const goBackSelect = () => {
// 选择图片
const chooseImage = () => {
// 计算还能选几张
const remainCount = 9 - form.value.images.length;
if (remainCount <= 0) {
uni.showToast({
title: '最多只能上传9张图片',
icon: 'none'
})
return;
}
uni.chooseImage({
count: 3, // 最多选3张
count: remainCount, // 动态限制可选数量
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
// 临时文件路径
const tempFilePaths = res.tempFilePaths
form.value.images = [...form.value.images, ...tempFilePaths]
}
})
}
@@ -111,7 +153,7 @@ const deleteImage = (index) => {
form.value.images.splice(index, 1)
}
// 上传图片到服务器
// 上传图片
const uploadImages = async () => {
const uploadedUrls = []
for (const path of form.value.images) {
@@ -122,9 +164,12 @@ const uploadImages = async () => {
try {
const res = await uploadFile(path)
// 根据后端实际返回结构调整字段名
uploadedUrls.push(res.data || res.msg || '')
} catch (e) {
console.error('图片上传失败', e)
uploadedUrls.push(res.url || '')
} catch (err) {
uni.showToast({
title:e.msg || '图片上传失败',
icon:'error'
})
}
}
return uploadedUrls
@@ -141,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
@@ -156,7 +201,8 @@ const submitForm = async () => {
return
}
uni.showLoading({ title: '提交中...' })
submitting.value = true
uni.showLoading({ title: '提交中...' ,mask:true})
try {
// 先上传图片
@@ -166,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,
@@ -178,17 +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 })
uni.navigateTo({
url:'/pageSubPack/online-repair/onlineRepair'
})
setTimeout(() => {
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()
@@ -214,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;
@@ -238,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;
@@ -249,24 +323,25 @@ const submitForm = async () => {
width: 80px;
height: 80px;
}
.upload-item image {
.upload-item .upload-img{
width: 100%;
height: 100%;
border-radius: 4px;
}
.upload-item uni-icons {
.upload-item .upload-delete{
position: absolute;
top: -5px;
right: -5px;
background: #ff3b30;
color: #fff;
border-radius: 50%;
width: 32rpx;
height: 32rpx;
border-radius: 3rpx;
padding: 2px;
}
.upload-btn {
width: 80px;
height: 80px;
border: 1px dashed #ccc;
/* border: 1px dashed #ccc; */
background-color: #f3f3f3;
border-radius: 4px;
display: flex;
flex-direction: column;
@@ -275,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

@@ -13,9 +13,9 @@
</up-steps>
</uni-col>
<uni-col :span="12">
<view class="step-time">2026-03-24 09:30:38</view>
<view class="step-time">2026-03-26 09:30:38</view>
<view class="step-time-3">2026-03-28 09:30:38</view>
<view class="step-time" v-if="currentStep >= 0">{{timeData.createTime}}</view>
<view class="step-time" v-if="currentStep >= 1">{{timeData.dispatchTime}}</view>
<view class="step-time-3" v-if="currentStep >= 2">{{timeData.finishTime}}</view>
</uni-col>
</uni-row>
</view>
@@ -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>
@@ -49,11 +49,17 @@
</view>
<view class="info-item-desc">
<view class="info-label">问题照片</view>
<view class="photo-container">
<view class="photo-item" v-for="(item, index) in 2" :key="index">
<uni-icons type="plusempty" size="24" color="#ccc"></uni-icons>
</view>
<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-item">
</view>
</view> -->
</view>
<view class="info-item">
<view class="info-label">预约时间</view>
<view class="info-value">{{repairInfo.orderDate}}</view>
</view>
</view>
@@ -65,59 +71,110 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getRepairDetail } from '../api/api.js'
const nvueWidth = ref('300')
const nvueWidth = ref('300')
// 报修信息数据
const repairInfo = ref({
house: '桂花园(别墅) 2号楼2层',
project: '照明-路灯',
title: '1栋楼下道路灯不亮了',
description: '1栋楼下道路路灯不亮并且有的亮度不够请物业公司及时更换防止发生意外事故。',
phone: '15688888888',
time: '2023-08-01 15:30'
// 报修信息数据
const repairInfo = ref({
house: '',
project: '',
title: '',
description: '',
phone: '',
orderDate:'',
time: '',
images: [],
isPublic: false
})
// 步骤状态
const currentStep = ref(0)
// 三个步骤对应的时间
const timeData = ref({
createTime: '', // 待处理/提交时间
dispatchTime: '', // 已派单时间
finishTime: '' // 已完成时间
})
const setStep = (status) => {
if (status === '0' || status === 0) currentStep.value = 0
else if (status === '1' || status === 1) currentStep.value = 1
else if (status === '2' || status === 2) currentStep.value = 2
else currentStep.value = 0
}
const mapData = (data) => {
if (!data) return
const images = data.problemImageUrl ? data.problemImageUrl.split(',').filter(Boolean) : []
repairInfo.value = {
house: data.houseName || '',
project: data.maintenanceItemName || '',
title: data.item || '',
description: (data.problem || '').replace(/\\n/g, '\n'),
phone: data.phone || '',
orderDate: data.orderDate || '',
time: data.createTime || '',
images,
// 公共报修不展示报修房屋
isPublic: data.projectTypeStr === '公共报修'
}
setStep(data.problemStatus)
// 三个步骤时间赋值
timeData.value.createTime = data.updateTime || ''
timeData.value.dispatchTime = data.updateTime || ''
timeData.value.finishTime = data.updateTime || ''
}
// 获取详情接口
const fetchDetail = async (id) => {
if (!id) {
return
}
try {
const res = await getRepairDetail({ id })
if (res.code === 200) {
mapData(res.data || {})
} else {
uni.showToast({ title: res.msg || '获取详情失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '网络异常', icon: 'none' })
}
}
onLoad((option) => {
// console.log("页面打开,路由参数:", option)
// 传了 id 才调用接口
if (option?.id) {
fetchDetail(option.id) // 调用接口
} else {
uni.showToast({ title: '参数错误', icon: 'none' })
}
})
// 预览图片
const previewImage = (url) => {
uni.previewImage({ urls: repairInfo.value.images, current: url })
}
// 联系物业按钮点击事件
const contactProperty = () => {
uni.navigateTo({
url: '/pageSubPack/service-list/contactProperty'
})
// 步骤状态
const currentStep = ref(1)
onLoad((option) => {
const optionstr = decodeURIComponent(JSON.stringify(option))
// console.log('接收参数:', optionstr)
// ✅ 正确解析参数
const status = optionstr.statusClass
// ✅ 正确赋值
switch (status) {
case 'pending': //待处理
currentStep.value = 0
break
case 'assignde': //已派单
currentStep.value = 1
break
case 'completed': //已完成
currentStep.value = 2
break
default:
currentStep.value = 0
}
})
// 联系物业按钮点击事件
const contactProperty = () => {
uni.showToast({
title: '正在联系物业...',
icon: 'none'
})
}
}
</script>
<style scoped>
.repair-detail-container {
padding: 16px;
padding-bottom: 100px;
background-color: #f5f5f5;
min-height: 100vh;
}
@@ -203,6 +260,7 @@
.multi-line {
white-space: pre-wrap;
word-break: break-all;
line-height: 1.5;
border: 1px solid #f0f0f0;
border-radius: 8rpx;
@@ -212,22 +270,33 @@
.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 dashed #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 {
@@ -238,19 +307,19 @@
height: 48px;
font-size: 16px;
width: 100%;
line-height: 3;
}
.contact-btn::after {
border: none;
}
/* ========== 颜色的代码 ========== */
/* u-text__value u-text__value--main */
/* 激活的步骤标题 + 描述 强制蓝色 */
::v-deep .u-text__value .u-text__value--main {
:deep(.u-steps-item__content--current .u-text__value--main) {
color: #007aff !important;
}
::v-deep .up-steps-item--active .up-steps-item__desc {
:deep(.u-steps-item__content--current .u-text__value--tips) {
color: #007aff !important;
}
</style>

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>
@@ -108,6 +116,9 @@ const selectProject = (item) => {
padding: 10px;
height: 100vh;
box-sizing: border-box;
background-color: #f9f9f9;
display: flex;
flex-direction: column;
}
.search-bar {
display: flex;
@@ -116,6 +127,7 @@ const selectProject = (item) => {
background: #f5f5f5;
border-radius: 6px;
margin-bottom: 10px;
flex-shrink: 0;
}
.search-bar input {
flex: 1;
@@ -123,13 +135,15 @@ const selectProject = (item) => {
}
.category-wrap {
display: flex;
height: calc(100% - 50px);
flex: 1;
min-height: 0;
}
.category-left {
width: 120px;
height: 100%;
background: #f8f8f8;
background: #fff;
border-radius: 6px 0 0 6px;
margin-right: 20rpx;
color: #666;
}
.category-item {
padding: 15px 10px;
@@ -138,16 +152,16 @@ const selectProject = (item) => {
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;

View File

@@ -9,18 +9,28 @@
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.houseType }}</view>
<view class="house-detail">{{item.unitNo}}-{{item.floorNo}}-{{item.houseNo}}</view>
</view>
<uni-icons type="checkbox-filled" size="20" color="#007aff" v-if="selectedHouse.id === item.id"></uni-icons>
</view>
<!-- 空状态 -->
<view v-if="houseList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
暂无房屋数据
</text>
</view>
</view>
<!-- 确认按钮 -->
<button class="confirm-btn" @click="confirmSelect">确认</button>
<!-- 确认按钮固定底部不随内容滚动 -->
<view class="confirm-bar">
<button class="confirm-btn" @click="confirmSelect">确认</button>
</view>
</view>
</template>
@@ -47,14 +57,13 @@ const fetchHouseList = async () => {
try {
const res = await getHouseList({ pageNum: pageNum.value, pageSize: pageSize.value })
if (res.code === 200) {
const list = res.rows || []
const list = res.data || []
houseList.value = list
if (houseList.value.length > 0) {
if (preSelectedHouseId.value) {
const found = houseList.value.find(item => String(item.id) === String(preSelectedHouseId.value))
selectedHouse.value = found || houseList.value[0]
} else {
selectedHouse.value = houseList.value[0]
// 仅当从表单页带入了房屋ID时自动选中对应房屋否则不默认选择
if (preSelectedHouseId.value) {
const found = houseList.value.find(item => String(item.id) === String(preSelectedHouseId.value))
if (found) {
selectedHouse.value = found
}
}
}
@@ -77,7 +86,7 @@ const selectHouse = (item) => {
// 确认选择并返回
const confirmSelect = () => {
if (!selectedHouse.value) {
if (!selectedHouse.value.id) {
uni.showToast({ title: '请选择房屋', icon: 'none' })
return
}
@@ -85,7 +94,8 @@ const confirmSelect = () => {
// 把选中的房屋信息传给表单页通过uni.$emit
uni.$emit('selectHouse', {
id: selectedHouse.value.id,
name: `${selectedHouse.value.villageName} ${selectedHouse.value.houseType}`
name: `${selectedHouse.value.villageName} ${selectedHouse.value.unitNo}-${selectedHouse.value.floorNo}-${selectedHouse.value.houseNo}`,
residentId: selectedHouse.value.residentId
})
// 返回上一页
@@ -98,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;
@@ -135,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;
@@ -143,7 +163,21 @@ const confirmSelect = () => {
color: #fff;
border-radius: 8px;
font-size: 28rpx;
margin-top: 20px;
margin-bottom: 20rpx;
}
/* 空状态 */
.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

@@ -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({
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'
})
}
uni.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode'],
success: (res) => {
// uni.hideLoading()
console.log('扫码成功', res)
// 过滤模拟器/调试模式返回的 mock 数据
if (!res.result || res.result.toUpperCase() === 'TEST') {
uni.showToast({ title: '扫码识别失败,请手动输入设备号', icon: 'none' })
return
}
deviceNo.value = res.result
uni.showToast({ title: '扫码成功', icon: 'success' })
},
fail: (err) => {
// uni.hideLoading()
console.log('扫码失败', err)
uni.showToast({ title: '扫码取消/失败', icon: 'none' })
},
complete: () => {
// uni.hideLoading()
}
})
}
// 提交缴费
const goPay = () => {
const goPay = async () => {
if (!deviceNo.value) {
uni.showToast({
title: '请输入设备号',
@@ -143,55 +142,89 @@
})
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(() => {
setTimeout(() => {
goNext()
}, 1500)
} catch (err) {
uni.hideLoading()
goNext()
}, 2500)
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">
page {
@@ -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,8 +13,8 @@
<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'">
></image>
: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>
</view>
@@ -87,131 +87,108 @@
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted,
computed
},
computed: {
style() {
var statusBarHeight = this.statusBarHeight;
return statusBarHeight;
},
},
created() {
let statusBarObj = this.getPhoneInfo()
this.statusBarHeight = statusBarObj.statusBarHeight
},
onShow() {
},
mounted() {},
onReady() {
this.$nextTick(() => {
})
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
paymentType: uni.getStorageSync('addPaymentType'),
userNo: '',
isAgree: false,
showPopup: false, // 控制弹窗显示
}
},
methods: {
handleAgreeChange(e) {
// e.detail.value 是数组,有值 = 选中,空 = 未选中
this.isAgree = e.detail.value.length > 0
},
// 处理协议链接点击
handleProtocol() {
uni.navigateTo({
})
},
// 处理提交
handleSubmit() {
if (!this.userNo) {
uni.showToast({
title: '请输入户号',
icon: 'none'
})
return
}
if (!this.isAgree) {
uni.showToast({
title: '请先同意协议',
icon: 'none'
})
return
}
uni.showModal({
title: '确认提交',
content: '确认要提交吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '提交中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '提交成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goNext()
}, 2500);
}
}
})
},
goNext() {
uni.redirectTo({
url: '/pageSubPack/payment-list/addPaymentSuccess',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goToChange() {
// uni.redirectTo({
// url: '/pageSubPack/payment-list/selectPaymentEntity',
// success: res => {},
// fail: () => {},
// complete: () => {}
// });
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
// 响应式数据
const statusBarHeight = ref(20)
const paymentType = ref(uni.getStorageSync('addPaymentType'))
const userNo = ref('')
const isAgree = ref(false)
const showPopup = ref(false)
// 生命周期
onShow(() => {})
onMounted(() => {})
onReady(() => {
// nextTick 用法
// nextTick(() => {})
})
// 勾选协议
const handleAgreeChange = (e) => {
isAgree.value = e.detail.value.length > 0
}
// 协议链接点击
const handleProtocol = () => {
uni.navigateTo({})
}
// 提交
const handleSubmit = () => {
if (!userNo.value) {
uni.showToast({
title: '请输入户号',
icon: 'none'
})
return
}
if (!isAgree.value) {
uni.showToast({
title: '请先同意协议',
icon: 'none'
})
return
}
uni.showModal({
title: '确认提交',
content: '确认要提交吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '提交中...',
mask: true
})
setTimeout(() => {
uni.showToast({
title: '提交成功',
icon: 'success'
})
}, 1000)
setTimeout(() => {
uni.hideLoading()
goNext()
}, 2500)
}
}
})
}
// 跳转成功页
const goNext = () => {
uni.navigateTo({
url: '/pageSubPack/payment-list/addPaymentSuccess'
})
}
// 切换(注释保留)
const goToChange = () => {
// uni.navigateTo({
// url: '/pageSubPack/payment-list/selectPaymentEntity'
// })
}
// 返回
const goBack = () => {
uni.navigateBack();
}
</script>
<style lang="scss">
page {

View File

@@ -1,97 +1,102 @@
<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>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 成功图标 -->
<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>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted,
computed
},
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
computed: {
// 响应式数据
const statusBarHeight = ref(20)
const paymentType = ref(uni.getStorageSync('addPaymentType'))
const pageType = ref('bind') // 'bind' | 'pay'
},
created() {
},
onShow() {
},
mounted() {},
onReady() {
this.$nextTick(() => {
})
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
paymentType: uni.getStorageSync('addPaymentType'),
}
},
methods: {
goBack() {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
// 页面配置:根据类型返回对应文案和跳转
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.navigateBack();
}
const handlePrimaryClick = () => {
const url = pageConfig.value.backUrl
uni.navigateTo({ url });
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
@@ -103,19 +108,17 @@
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; */
}
</style>
<style scoped>
/* 全局容器 */
.container {
@@ -183,7 +186,9 @@
/* 按钮默认样式重置解决uni-app button默认样式问题 */
.btn-back::after {
border: none;
}.status-bar{
}
.status-bar {
width: 100vw;
height: 46px;
}

View File

@@ -1,8 +1,8 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="arrearsPayment" 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,148 +14,57 @@
<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="">
<view v-if="arrearsPayment=='物业费'">
<view class="info-item">
<text class="info-label">户名</text>
<text class="info-value">桂花园别墅2号楼2层</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">周大声</text>
</view>
<view class="info-item">
<text class="info-label">缴费单位</text>
<text class="info-value">CT物业</text>
</view>
<view class="info-item">
<text class="info-label">可用余额</text>
<text class="info-value">0.00</text>
</view>
<!-- <view class="info-payAmountItem">
<text class="info-payAmount">{{'¥'+payAmount}}</text>
</view> -->
</view>
<view v-else>
<view class="info-item" v-if="arrearsPayment=='暖气费'">
<text class="info-label">房屋</text>
<text class="info-value">桂花园别墅2号楼2层</text>
</view>
<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-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 class="arrears-row">
<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>
</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 v-if="showBankList" class="bank-list">
<view v-for="(item, index) in bankList" :key="index" class="bank-item"
@click="selectBank(item)">
<image class="bank-item-icon" :src="item.icon" mode="aspectFit"></image>
<text class="bank-item-name">{{ item.name }}</text>
<text v-if="selectedBank.id === item.id" class="check-icon">&#xe60c;</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 class="arrears-row" v-if="outstandingPayment==false">
<text class="arrears-text">暂未查询到欠费</text>
</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>
@@ -166,7 +75,7 @@
<!-- 提示文案 -->
<view class="desc">
成功缴纳物业费
成功缴纳{{ switchStatus(paymentItem.type) || '' }}费用 ¥{{ arrearsAmount || payAmount }}
</view>
@@ -180,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>
@@ -189,158 +98,139 @@
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
<script setup>
import {
ref,
reactive
} from 'vue'
import {
onShow,
onLoad
} from '@dcloudio/uni-app'
// 响应式数据
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')
// 页面加载时初始化数据
onLoad(() => {
initPaymentData()
})
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
}
}
},
created() {
// 返回
const goBack = () => {
uni.navigateBack();
}
},
onShow() {
// 立即缴费 — 通过微信拉起日照银行小程序进行支付
const handlePay = () => {
const amount = arrearsAmount.value
},
try {
uni.showLoading({ title: '加载中...' })
mounted() {},
// 设备类型映射电费→1(电表)水费→2(水表)
const deviceTypeMap = { '电费': '1', '水费': '2' }
const mappedDeviceType = deviceTypeMap[paymentItem.type] || paymentItem.deviceType || ''
onReady() {
this.$nextTick(() => {
// 直接从本地参数拼 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('&')
})
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
outstandingPayment: true, //是否欠费
arrearsPayment: uni.getStorageSync('arrearsPayment'),
currentStep: 1, // 1: 详情页 2: 密码弹窗 3: 成功页
payAmount: "1680.00", // 缴费金额
password: "", // 输入的密码
showBankList: false, // 是否显示银行卡列表
arrearsAmount: "21.21",
payNum: '',
// 银行卡列表
bankList: [{
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"
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
}
],
// 默认选中的银行卡
selectedBank: {
id: 1,
name: "建设银行储蓄卡(8888)",
icon: "/static/bank-logo.png"
}
if (!weixin.launchMiniProgram) {
uni.showToast({ title: '当前微信版本太低,不支持拉起小程序', icon: 'none' })
return
}
weixin.launchMiniProgram({
id: 'gh_a765aef0172c',
type: 1, // 0 正式版 / 1开发版 / 2体验版
path: 'pages/arrearsPayment/arrearsPayment?' + queryString,
}, (res) => {
console.log('回调---回调结果App', JSON.stringify(res))
}, (err) => {
console.error('err---拉起失败App', JSON.stringify(err))
uni.showToast({ title: '拉起支付小程序失败', icon: 'none' })
})
})
// #endif
} catch (err) {
uni.hideLoading()
console.error('拉起支付小程序异常', err)
uni.showToast({ title: '请求异常,请重试', icon: 'none' })
}
},
}
// 缴费成功推送通知
const sendPaySuccessPush = () => {
const userId = uni.getStorageSync('userId')
if (userId) {
getPushSingle({
userId: userId,
channel: 'resident',
title: '缴费成功',
content: `您已成功缴纳${switchStatus(paymentItem.type) || ''}费用 ¥${arrearsAmount.value || payAmount.value}`
}).catch(err => {
console.log('推送发送失败', err)
})
}
}
methods: {
handleAutoFill() {
this.payNum = this.arrearsAmount;
uni.vibrateShort({
type: "light"
});
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
// 点击立即缴费
handlePay() {
if (!this.payAmount || Number(this.payAmount) <= 0) {
uni.showToast({
title: "请输入正确的缴费金额",
icon: "none"
});
return;
}
this.currentStep = 2;
},
// 关闭密码弹窗
closePasswordModal() {
this.currentStep = 1;
this.password = "";
this.showBankList = false;
},
// 选择银行卡
selectBank(item) {
this.selectedBank = item;
this.showBankList = false;
},
// 输入密码
inputPassword(num) {
if (this.password.length >= 6) return;
this.password += num.toString();
// 密码满6位自动提交
if (this.password.length === 6) {
this.handleConfirmPay();
}
},
// 删除密码
deletePassword() {
this.password = this.password.slice(0, -1);
},
// 确认支付
handleConfirmPay() {
// 这里模拟支付请求,实际项目中调用后端接口
uni.showLoading({
title: "支付中...",
mask: true
});
setTimeout(() => {
uni.hideLoading();
// 支付成功,跳转到成功页
this.currentStep = 3;
this.password = "";
this.showBankList = false;
}, 1500);
},
// 返回生活缴费列表
handleBackToList() {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
}
},
// 返回缴费列表
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">
@@ -390,6 +280,10 @@
color: #E34D59;
}
.paymentClass {
color: #00aaff;
}
.info-section {
padding-top: 20rpx;
/* margin-bottom: 40rpx; */
@@ -699,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;
@@ -725,4 +678,4 @@
width: 100vw;
height: 46px;
}
</style>
</style>

View File

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

View File

@@ -5,211 +5,121 @@
:border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 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>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
reactive,
onMounted,
computed
},
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
computed: {
},
created() {
},
onShow() {
},
mounted() {},
onReady() {
this.$nextTick(() => {
})
},
data() {
return {
lastPayTime: '2022-11-29',
lastPayNum: '2022.00',
/* 设定状态栏默认高度 */
statusBarHeight: 20,
arrearsPayment: uni.getStorageSync('arrearsPayment'),
}
},
methods: {
goBack() {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
// 点击立即缴费
handlePay() {
if (!this.payAmount || Number(this.payAmount) <= 0) {
uni.showToast({
title: "请输入正确的缴费金额",
icon: "none"
});
return;
}
this.currentStep = 2;
},
// 关闭密码弹窗
closePasswordModal() {
this.currentStep = 1;
this.password = "";
this.showBankList = false;
},
// 选择银行卡
selectBank(item) {
this.selectedBank = item;
this.showBankList = false;
},
// 输入密码
inputPassword(num) {
if (this.password.length >= 6) return;
this.password += num.toString();
// 密码满6位自动提交
if (this.password.length === 6) {
this.handleConfirmPay();
}
},
// 删除密码
deletePassword() {
this.password = this.password.slice(0, -1);
},
// 确认支付
handleConfirmPay() {
// 这里模拟支付请求,实际项目中调用后端接口
uni.showLoading({
title: "支付中...",
mask: true
});
setTimeout(() => {
uni.hideLoading();
// 支付成功,跳转到成功页
this.currentStep = 3;
this.password = "";
this.showBankList = false;
}, 1500);
},
// 返回生活缴费列表
handleBackToList() {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
}
},
// 响应式数据
const statusBarHeight = ref(20)
const paymentItem = reactive(uni.getStorageSync('arrearsPayment') || {})
const arrearsPayment = computed(() => paymentItem.type || '缴费')
// 生命周期
onShow(() => {})
onMounted(() => {})
onReady(() => {})
// 方法
const goBack = () => {
uni.navigateBack();
}
// 以下为原代码保留的方法(未使用但保持功能完整)
const handlePay = () => {}
const closePasswordModal = () => {}
const selectBank = () => {}
const inputPassword = () => {}
const deletePassword = () => {}
const handleConfirmPay = () => {}
const handleBackToList = () => {}
</script>
<style lang="scss">
page {
background: #f9f9f9;
@@ -221,7 +131,6 @@
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
@@ -232,24 +141,31 @@
box-sizing: border-box;
}
/* 1. 物业费详情页 */
.pay-page {
width: 100%;
}
.amount-section {
padding: 60rpx 0;
text-align: center;
}
.no-arrears-section {
padding: 30rpx 0;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
color: #999999;
}
.amount-text {
font-size: 56rpx;
font-weight: 600;
color: #007aff;
}
.background-image-style {
width: 466rpx;
height: 250rpx;
margin-bottom: 140rpx;
@@ -268,11 +184,8 @@
font-weight: 600;
}
.info-section {}
.lineStyle {
margin: 0rpx 30rpx;
}
.info-item {
@@ -302,11 +215,10 @@
font-size: 50rpx;
color: #333;
font-weight: 600;
}
.input-section {
padding: 030rpx;
padding: 0 30rpx;
margin-bottom: 60rpx;
}
@@ -319,7 +231,7 @@
}
.btn-section {
padding: 030rpx;
padding: 0 30rpx;
}
.pay-btn {
@@ -334,7 +246,6 @@
outline: none;
}
/* 2. 支付密码弹窗 */
.modal-overlay {
position: fixed;
top: 0;
@@ -350,7 +261,7 @@
.password-modal {
width: 100%;
background-color: #fff;
border-radius: 24rpx24rpx 0 0;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: 40rpx;
}
@@ -359,7 +270,7 @@
align-items: center;
justify-content: space-between;
height: 100rpx;
padding: 030rpx;
padding: 0 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
@@ -382,7 +293,7 @@
}
.pay-info {
padding: 40rpx30rpx;
padding: 40rpx 30rpx;
text-align: center;
}
@@ -434,9 +345,8 @@
color: #999;
}
/* 银行卡列表 */
.bank-list {
padding: 030rpx;
padding: 0 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
@@ -468,17 +378,15 @@
color: #007aff;
}
/* 密码输入框 */
.password-input {
display: flex;
justify-content: space-between;
padding: 40rpx30rpx;
padding: 40rpx 30rpx;
}
.password-item {
width: 100rpx;
height: 100rpx;
/* border: 2rpx solid #dcdcdc; */
background-color: #efefef;
border-radius: 8rpx;
display: flex;
@@ -493,7 +401,6 @@
border-radius: 50%;
}
/* 数字键盘 */
.keyboard {
width: 100%;
}
@@ -527,7 +434,6 @@
border: none;
}
/* 3. 缴费成功页 */
.success-page {
width: 100%;
min-height: 100vh;
@@ -544,7 +450,6 @@
}
.success-icon {
/* margin-top: 120rpx; */
margin-bottom: 40rpx;
width: 100px;
height: 100px;
@@ -553,12 +458,8 @@
background-color: #007aff;
color: #fff;
border-radius: 50%;
}
.success-icon .iconfont {}
.success-text {
font-size: 40rpx;
font-weight: 600;

View File

@@ -1,25 +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>
<!-- 折线图 Canvas -->
<!-- <canvas canvas-id="chargeChart" class="chart-canvas"></canvas> -->
<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>
@@ -37,264 +32,224 @@
</view>
<!-- 缴费明细 -->
<view class="list-wrapper">
<!-- 按月份分组渲染 -->
<view v-if="monthGroups.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view>
<view class="month-group" v-for="(item, idx) in monthGroups" :key="idx">
<view class="month-title">
{{ item.month }} 缴费 ¥{{ item.total }}
<!-- 缴费明细分页滚动 -->
<scroll-view class="list-scroll" scroll-y="true" @scrolltolower="loadMore">
<view class="list-wrapper">
<!-- 按月份分组渲染 -->
<view v-if="monthGroups.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<view class="month-group" v-for="(item, idx) in monthGroups" :key="idx">
<view class="month-title">
{{ item.year }}{{ item.month }} 缴费 ¥{{ item.total.toFixed(2) }}
</view>
<view class="item-list">
<view class="bill-item" v-for="bill in item.list" :key="bill.id">
<view class="bill-icon">
<image class="iconStyle" :src="bill.icon"></image>
<view class="item-list">
<view class="bill-item" v-for="bill in item.list" :key="bill.id">
<view class="bill-icon">
<image class="iconStyle" :src="bill.icon"></image>
</view>
<view class="bill-info">
<view class="name">{{ bill.name }}</view>
<view class="account">{{ bill.account }}</view>
</view>
<view class="bill-price">-{{ bill.amount.toFixed(2) }}</view>
</view>
<view class="bill-info">
<view class="name">{{ bill.name }}</view>
<view class="account">{{ bill.account }}</view>
</view>
<view class="bill-price">-{{ bill.amount }}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted,
computed
},
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
computed: {
// 自动按月份分组
monthGroups() {
let map = {}
this.billList.forEach(bill => {
if (!map[bill.month]) {
map[bill.month] = {
month: bill.month,
total: 0,
list: []
}
}
map[bill.month].list.push(bill)
map[bill.month].total += bill.amount
})
return Object.values(map).sort((a, b) => b.month - a.month)
}
},
created() {
},
onShow() {
},
mounted() {},
onReady() {
this.$nextTick(() => {
this.drawLineChart();
})
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
// 完全对应你图片的数据
chartData: [{
month: '1月',
price: 250
},
{
month: '2月',
price: 130
},
{
month: '3月',
price: 220
},
{
month: '4月',
price: 250
},
{
month: '5月',
price: 100
},
{
month: '6月',
price: 130
}
],
maxPrice: 250, // 最大值(基准高度)
barMaxHeight: 300, // 柱子最大高度rpx
// canvas折线图数据
chart: {
months: ['1月', '2月', '3月', '4月', '5月', '6月'],
values: [100, 140, 230, 100, 130, 100]
},
billList: [
// 5月数据
{
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",
},
// 6月数据
{
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",
}
]
}
},
methods: {
// 计算每个柱子自适应高度(按金额比例换算)
barHeight(price) {
const height = (price / this.maxPrice) * this.barMaxHeight
return `${height}rpx`
},
drawLineChart() {
const ctx = uni.createCanvasContext('chargeChart', this)
const canvasPage = uni.getSystemInfoSync();
const w = canvasPage.windowWidth - 30;
const h = canvasPage.windowHeight * 0.3;
const padding = 40
const stepX = (w - 60) / (this.chart.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()
// 绘制X轴月份
this.chart.months.forEach((m, i) => {
let x = 40 + i * stepX
ctx.fillText(m, x, h - 20)
})
// 绘制折线
let points = []
this.chart.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(this.chart.values[i] + '元', p.x, p.y - 10)
})
ctx.draw()
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
import {
getTopupRecordWithStat
} from '/pageSubPack/api/apiSub.js'
// 响应式数据
const statusBarHeight = ref(20)
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 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
}
}
})
// 取最近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(() => {
const map = {}
billList.value.forEach(bill => {
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[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
})
})
// 生命周期
onShow(() => {})
onMounted(() => {})
onLoad(() => {
getRecords()
})
onReady(() => {})
// 方法
const barHeight = (price) => {
const height = (price / maxPrice.value) * barMaxHeight.value
return `${height}rpx`
}
const goBack = () => {
uni.navigateBack();
}
</script>
<style lang="scss">
page {
// background: #f2f1f6;
@@ -306,24 +261,27 @@
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.page {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
padding: 0px 40rpx;
box-sizing: border-box;
/* background-color: #fff; */
overflow: hidden;
}
.list-scroll {
flex: 1;
overflow-y: auto;
}
.total-box {
margin-bottom: 30rpx;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
@@ -399,7 +357,6 @@
.bill-icon {
width: 80rpx;
height: 80rpx;
/* background: #e6efff; */
border-radius: 50%;
display: flex;
align-items: center;
@@ -440,19 +397,17 @@
height: 46px;
}
</style>
<style scoped>
/* 图表外层容器 横向排列 */
.chart-box {
width: 100%;
height: 444rpx;
display: flex;
align-items: flex-end;
justify-content: space-around;
/* padding: 40rpx 20rpx; */
box-sizing: border-box;
}
/* 单个柱子单元:垂直排列 文字+柱子+月份 */
.chart-item {
display: flex;
flex-direction: column;
@@ -462,24 +417,20 @@
font-size: 24rpx;
}
/* 顶部金额标签 */
.chart-label {
font-size: 32rpx;
color: #333;
margin-bottom: 20rpx;
}
/* 蓝色圆角柱子 完全还原原图顶部圆角 */
.chart-bar {
width: 40rpx;
background: #3388ff;
border-radius: 40rpx 40rpx 0 0;
/* 只有顶部圆角!和原图一模一样 */
}
/* 底部月份文字 */
.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

@@ -4,7 +4,6 @@
<uni-nav-bar title="选择缴费单位" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<!-- 城市 + 搜索 -->
@@ -20,9 +19,6 @@
</uni-data-picker>
<!-- 搜索框 -->
<!-- <view class="search-input">
<input v-model="keyword" placeholder="请输入缴费单位名称" placeholder-class="ph" />
</view> -->
<view class="search-box">
<input class="search-input" v-model="searchKey" placeholder="请输入缴费单位名称"
placeholder-class="input-placeholder" @input="onSearch" />
@@ -32,11 +28,9 @@
<view v-if="filterList.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>
<!-- <scroll-view scroll-y class="list-scroll"> -->
<view v-for="item in filterList">
<view class="group-title">{{item.name}}</view>
<view class="item" v-for="itemChild in item.children" :key="itemChild.id"
@@ -44,258 +38,205 @@
{{ itemChild.name }}
</view>
</view>
<!-- </scroll-view> -->
</scroll-view>
</view>
</template>
<script>
// 引入外部城市数据
// import Address from '@/static/js/chinaRegions/picker-region.js'
export default {
onReady: function(e) {},
components: {
<script setup>
import {
ref,
onMounted,
computed
},
computed: {
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
filterList() {
// 没有关键词直接返回全部
if (!this.searchKey) return this.companyList;
const key = this.searchKey.trim().toLowerCase();
// 过滤第一层 + 第二层
return this.companyList.filter(group => {
// 过滤子项:只要子项包含关键词就保留
const hasChild = group.children.some(child =>
child.name.toLowerCase().includes(key)
);
// 同时支持 分组名 搜索(不需要可删除)
const groupMatch = group.name.toLowerCase().includes(key);
return hasChild || groupMatch;
}).map(group => {
// 返回过滤后的子项
return {
...group,
children: group.children.filter(child =>
child.name.toLowerCase().includes(key)
)
};
});
}
},
created() {
},
onShow() {
},
onLoad() {
},
mounted() {},
onReady() {
this.$nextTick(() => {
})
},
data() {
return {
selectedCity: '选择城市',
cityValue: '',
alladdress: [{
text: "北京市",
value: "110000000000",
children: [{
text: "市辖区",
value: "110100000000",
children: [{
text: "东城区",
value: "110101000000"
},
{
text: "西城区",
value: "110102000000"
},
{
text: "朝阳区",
value: "110105000000"
},
{
text: "丰台区",
value: "110106000000"
},
{
text: "石景山区",
value: "110107000000"
},
{
text: "海淀区",
value: "110108000000"
},
{
text: "门头沟区",
value: "110109000000"
},
{
text: "房山区",
value: "110111000000"
},
{
text: "通州区",
value: "110112000000"
},
{
text: "顺义区",
value: "110113000000"
},
{
text: "昌平区",
value: "110114000000"
},
{
text: "大兴区",
value: "110115000000"
},
{
text: "怀柔区",
value: "110116000000"
},
{
text: "平谷区",
value: "110117000000"
},
{
text: "密云区",
value: "110118000000"
},
{
text: "延庆区",
value: "110119000000"
}
]
}]
}, ],
searchKey: '',
/* 设定状态栏默认高度 */
statusBarHeight: 20,
showPicker: false,
keyword: '',
// 列表:一个大数组
companyList: [{
id: 7,
name: '官方机构',
children: [{
id: 1,
name: "XX市燃气集团有限公司"
},
{
id: 2,
name: "XX市供水有限公司"
},
{
id: 3,
name: "XX供电服务中心"
},
{
id: 4,
name: "XX新能源服务公司"
},
{
id: 5,
name: "XX公共事业缴费中心"
},
{
id: 6,
name: "XX综合能源服务站"
}
]
},
{
id: 8,
name: '非智享日照提供的服务单位',
children: [{
id: 9,
name: "XX市燃气集团有限公司"
},
{
id: 10,
name: "XX能源集团"
},
]
}
]
}
},
methods: {
selectItem(item) {
console.log(item);
uni.redirectTo({
url: '/pageSubPack/payment-list/addPayment',
success: res => {},
fail: () => {},
complete: () => {}
});
},
onchange(e) {
console.log('选中text数组:', e.detail.value)
let arr = e.detail.value
let selectedCity = ''
// 拼接省市区
// arr.forEach((item, index) => {
// if (index != arr.length - 1) {
// selectedCity = selectedCity + item.text + '/'
// } else {
// selectedCity = selectedCity + item.text
// }
// })
// 只显示城市判断
if (arr[1].text == '市辖区') {
selectedCity = arr[0].text
} else {
selectedCity = arr[1].text
// 响应式数据
const selectedCity = ref('选择城市')
const cityValue = ref('')
const alladdress = ref([{
text: "北京市",
value: "110000000000",
children: [{
text: "市辖区",
value: "110100000000",
children: [{
text: "东城区",
value: "110101000000"
},
{
text: "西城区",
value: "110102000000"
},
{
text: "朝阳区",
value: "110105000000"
},
{
text: "丰台区",
value: "110106000000"
},
{
text: "石景山区",
value: "110107000000"
},
{
text: "海淀区",
value: "110108000000"
},
{
text: "门头沟区",
value: "110109000000"
},
{
text: "房山区",
value: "110111000000"
},
{
text: "通州区",
value: "110112000000"
},
{
text: "顺义区",
value: "110113000000"
},
{
text: "昌平区",
value: "110114000000"
},
{
text: "大兴区",
value: "110115000000"
},
{
text: "怀柔区",
value: "110116000000"
},
{
text: "平谷区",
value: "110117000000"
},
{
text: "密云区",
value: "110118000000"
},
{
text: "延庆区",
value: "110119000000"
}
this.selectedCity = selectedCity
},
]
}]
}])
// 搜索输入事件
onSearch() {
// 实时过滤computed 自动处理
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/payment-list/paymentIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
const searchKey = ref('')
const statusBarHeight = ref(20)
const showPicker = ref(false)
const keyword = ref('')
const companyList = ref([{
id: 7,
name: '官方机构',
children: [{
id: 1,
name: "XX市燃气集团有限公司"
},
{
id: 2,
name: "XX市供水有限公司"
},
{
id: 3,
name: "XX供电服务中心"
},
{
id: 4,
name: "XX新能源服务公司"
},
{
id: 5,
name: "XX公共事业缴费中心"
},
{
id: 6,
name: "XX综合能源服务站"
}
]
},
{
id: 8,
name: '非智享日照提供的服务单位',
children: [{
id: 9,
name: "XX市燃气集团有限公司"
},
{
id: 10,
name: "XX能源集团"
}
]
}
])
// 计算属性:搜索过滤
const filterList = computed(() => {
if (!searchKey.value) return companyList.value
const key = searchKey.value.trim().toLowerCase()
return companyList.value.filter(group => {
const hasChild = group.children.some(child =>
child.name.toLowerCase().includes(key)
)
const groupMatch = group.name.toLowerCase().includes(key)
return hasChild || groupMatch
}).map(group => {
return {
...group,
children: group.children.filter(child =>
child.name.toLowerCase().includes(key)
)
}
})
})
// 生命周期
onShow(() => {})
onMounted(() => {})
onReady(() => {})
// 方法
const selectItem = (item) => {
console.log(item)
uni.navigateTo({
url: '/pageSubPack/payment-list/addPayment'
})
}
const onchange = (e) => {
console.log('选中text数组:', e.detail.value)
const arr = e.detail.value
let city = ''
if (arr[1].text === '市辖区') {
city = arr[0].text
} else {
city = arr[1].text
}
selectedCity.value = city
}
const onSearch = () => {}
const goBack = () => {
uni.navigateBack();
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
@@ -307,14 +248,12 @@
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
box-sizing: border-box;
background-color: #fff;
}
@@ -326,13 +265,11 @@
align-items: center;
}
/* 隐藏 uni-data-picker 原生样式,只保留点击功能 */
.city-picker {
display: block;
width: auto;
}
/* 自定义城市选择区域,完美还原「日照市 ▼」 */
.city-select {
display: flex;
align-items: center;
@@ -350,34 +287,28 @@
.city-text {
font-size: 28rpx;
/* 匹配截图字体大小 */
font-weight: 600;
color: #000000;
line-height: 1;
}
/* 自定义下拉箭头1:1 还原截图样式 */
.arrow-icon {
width: 0;
height: 0;
border-left: 10rpx solid transparent;
border-right: 10rpx solid transparent;
border-top: 16rpx solid #333333;
/* 纯黑实心三角,和截图一致 */
margin-top: 4rpx;
}
/* 搜索框 */
.search-box {
display: flex;
width: 100%;
align-items: center;
background-color: #f3f4f6;
border-radius: 20rpx;
/* margin: 0rpx 30rpx 20rpx 30rpx; */
padding: 0 30rpx;
margin: 10rpx 0px 0px10rpx;
margin: 10rpx 0px 0px 10rpx;
height: 70rpx;
}
@@ -399,9 +330,6 @@
color: #999;
}
/* 列表 */
.list-scroll {}
.group-title {
padding: 20rpx 40rpx;
font-size: 28rpx;
@@ -430,7 +358,6 @@
height: 46px;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;

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,13 +1,8 @@
<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">
<scroll-view class="page" scroll-y>
<!-- 表单主体 -->
<view class="form-content">
<text class="form-tip">请填写反馈信息</text>
@@ -19,9 +14,10 @@
<text class="label-text">投诉类型</text>
</view>
<view class="row-right">
<text class="value-text"
:class="{placeholderStyle:selectedType}">{{ selectedType || '请选择投诉类型' }}</text>
<text class="arrow-icon">></text>
<text class="value-text" :class="{placeholderStyle:selectedType}">
{{ selectedType || '请选择投诉类型' }}
</text>
<uni-icons type="arrowright" size="16"></uni-icons>
</view>
</view>
@@ -32,9 +28,11 @@
<text class="label-text">投诉物业</text>
</view>
<view class="row-right">
<text class="value-text"
:class="{placeholderStyle:selectedProperty}">{{ selectedProperty || '请选择' }}</text>
<text class="arrow-icon">></text>
<text class="value-text" :class="{placeholderStyle:selectedProperty}">
{{ selectedProperty || '请选择' }}
</text>
<uni-icons type="arrowright" size="16"></uni-icons>
<!-- <text class="arrow-icon"> > </text> -->
</view>
</view>
@@ -45,8 +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>
@@ -93,15 +93,13 @@
</view>
</view>
</view>
</scroll-view>
<!-- 底部下一步按钮 -->
<view class="bottom-btn-wrap">
<button class="next-btn" @click="submitForm">提交</button>
<button class="next-btn" :disabled="isSubmitting" @click="submitForm">提交</button>
</view>
<!-- ================== 自定义Popup弹窗纯view实现 ================== -->
<!-- 蒙层 -->
<view class="mask" v-if="showTypePopup || showPropertyPopup" @click="closeAllPopup"></view>
@@ -113,194 +111,190 @@
<text class="title">选择投诉类型</text>
<text class="close" @click="showTypePopup = false"></text>
</view>
<view class="list">
<view :class="{ active: selectedType === item }" class="item" v-for="(item, index) in typeOptions"
:key="index" @click="selectType(item)">
{{ item }}
<view :class="{ active: selectedType === item }" class="item" v-for="(item, index) in typeOptions" :key="index" @click="selectType(item)">
{{ item.name }}
</view>
</view>
</view>
<!-- 投诉物业弹窗 -->
<view v-if="showPropertyPopup" class="mask" @click="showPropertyPopup=false"></view>
<view v-if="showPropertyPopup" class="popup">
<view class="popup-header">
<text class="title">选择投诉类型</text>
<text class="title">选择投诉物业</text>
<text class="close" @click="showPropertyPopup = false"></text>
</view>
<view class="list">
<view :class="{ active: selectedProperty === item }" class="item"
v-for="(item, index) in propertyOptions" :key="index" @click="selectProperty(item)">
<view :class="{ active: selectedProperty === item }" class="item" v-for="(item, index) in propertyOptions" :key="index" @click="selectProperty(item)">
{{ item }}
</view>
</view>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getComplainType, addComplain } from '../api/apiSub.js'
import { uploadFile } from '../api/api.js'
},
// 弹窗控制
const showTypePopup = ref(false)
const showPropertyPopup = ref(false)
computed: {
// 下拉选项
const typeOptions = ref([])
const propertyOptions = ref(['XX物业A', 'XX物业B', 'XX物业C'])
// 表单数据
const selectedType = ref('')
const selectedTypeId = ref('')
const selectedProperty = ref('')
const description = ref('')
const name = ref('')
const phone = ref('')
const villageId = ref('')
// 多图上传
const tempFilePaths = ref([])
const maxCount = ref(9)
const isSubmitting = ref(false)
},
created() {
// 关闭所有弹窗
const closeAllPopup = () => {
showTypePopup.value = false
showPropertyPopup.value = false
}
},
onShow() {
onLoad(() => {
getSelectType()
villageId.value = uni.getStorageSync('currentVillageId') || ''
})
},
// 获取投诉类型
const getSelectType = async () => {
let params = {status:'0'}
try {
const res = await getComplainType(params)
if (res.code === 200) {
typeOptions.value = res.data || []
} else {
uni.showToast({ title: res.msg || '获取失败', icon: 'none' })
}
} catch (err) {
uni.showToast({ title: '网络异常', icon: 'none' })
}
}
mounted() {},
// 选择投诉类型
const selectType = (item) => {
selectedType.value = item.name
selectedTypeId.value = item.id || ''
showTypePopup.value = false
// 切换类型时清空物业选择
if (item.name !== '物业服务') {
selectedProperty.value = ''
}
}
onReady() {
this.$nextTick(() => {
// 选择物业
const selectProperty = (item) => {
selectedProperty.value = item
showPropertyPopup.value = false
}
})
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
// 弹窗控制
showTypePopup: false,
showPropertyPopup: false,
// 多图选择
const chooseImages = () => {
uni.chooseImage({
count: maxCount.value - tempFilePaths.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
tempFilePaths.value = [...tempFilePaths.value, ...res.tempFilePaths]
}
})
}
// 下拉选项
typeOptions: ['物业服务', '社区服务', '其他'],
propertyOptions: ['XX物业A', 'XX物业B', 'XX物业C'], // 替换为实际数据
// 上传单张图片
const uploadImageData = async (filePath) => {
try {
const res = await uploadFile(filePath)
return res.url || ''
} catch (err) {
console.error('单张图片上传失败', err)
return ''
}
}
// 表单数据
selectedType: '',
selectedProperty: '',
description: '',
name: '',
phone: '',
// 删除图片
const deletePhoto = (index) => {
tempFilePaths.value.splice(index, 1)
}
// 多图上传
tempFilePaths: [],
maxCount: 9 // 最大上传9张
// 表单提交
const submitForm = async () => {
// 校验
if (!selectedTypeId.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 })
try {
// 上传图片
let imageUrls = []
for (let path of tempFilePaths.value) {
let url = await uploadImageData(path)
if (url) imageUrls.push(url)
}
}
},
// 构造提交参数
const params = {
typeId: selectedTypeId.value,
problemDes: description.value.trim(),
complaintName: name.value || '',
phone: phone.value || '',
villageId: villageId.value || '',
problemImageUrl: imageUrls.join(',')
}
// 调用提交接口
const res = await addComplain(params)
methods: {
// 返回上一页
navigateBack() {
uni.navigateBack();
},
// 关闭所有弹窗
closeAllPopup() {
this.showTypePopup = false;
this.showPropertyPopup = false;
},
// 选择投诉类型
selectType(item) {
this.selectedType = item;
this.showTypePopup = false;
// 切换类型时清空物业选择
if (item !== '物业服务') {
this.selectedProperty = '';
}
},
// 选择物业
selectProperty(item) {
this.selectedProperty = item;
this.showPropertyPopup = false;
},
// 多图选择
chooseImages() {
uni.chooseImage({
count: this.maxCount - this.tempFilePaths.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.tempFilePaths = [...this.tempFilePaths, ...res.tempFilePaths];
}
});
},
// 删除图片
deletePhoto(index) {
this.tempFilePaths.splice(index, 1);
},
// 表单提交
submitForm() {
// 基础校验
if (!this.selectedType) {
return uni.showToast({
title: '请选择投诉类型',
icon: 'none'
});
}
if (this.selectedType === '物业服务' && !this.selectedProperty) {
return uni.showToast({
title: '请选择物业',
icon: 'none'
});
}
if (!this.description.trim()) {
return uni.showToast({
title: '请输入问题描述',
icon: 'none'
});
}
uni.showLoading({
title: '提交中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '提交成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/service-list/complaintsSuggestionsIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
isSubmitting.value = false
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (err) {
isSubmitting.value = false
uni.hideLoading()
uni.showToast({
title: err.msg || '提交失败,请重试',
icon: 'none'
})
}
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss">
@@ -314,8 +308,6 @@
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%); */
}
.page {
@@ -397,27 +389,41 @@
/* 问题描述行 */
.desc-row {
align-items: flex-start;
padding-top: 32rpx;
padding-bottom: 32rpx;
}
.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;
}
/* 照片上传区域 */
@@ -486,7 +492,6 @@
line-height: 1;
}
/* 底部按钮 */
.bottom-btn-wrap {
padding: 20rpx 30rpx 40rpx;
@@ -532,7 +537,6 @@
.popup-header {
padding: 30rpx;
text-align: center;
/* border-bottom: 1rpx solid #f0f0f0; */
position: relative;
}
@@ -571,9 +575,4 @@
font-weight: 600;
background-color: #f8f9fa;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -1,12 +1,11 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<view class="contentStyle">
<!-- <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="true">
<scroll-view class="page" scroll-y>
<view class="page-container">
<!-- 投诉状态 -->
<view class="status-section">
@@ -14,10 +13,12 @@
<text class="title-line"></text>
<text class="title-text">投诉状态</text>
</view>
<text class="status-value"
:class="status=='pending'?'pending':status=='processing'?'processing':status=='completed'?'completed':''">{{status=='pending'?'待处理':status=='processing'?'处理中':status=='completed'?'已完成':''}}</text>
<text class="status-value" :class="statusClass">{{ statusText }}</text>
</view>
<view class="completedDesc" v-if="status=='completed'">已完成处理已联系工作人员粘贴标语提醒并派人定时巡视</view>
<view class="completedDesc" v-if="detail.status === '2' && detail.handleContent">
{{ detail.handleContent }}
</view>
<!-- 投诉详情 -->
<view class="detail-section">
<view class="section-title">
@@ -25,122 +26,104 @@
<text class="title-text">投诉详情</text>
</view>
<!-- 投诉类型 -->
<view class="info-item">
<text class="info-label">投诉类型</text>
<text class="info-value">社区服务</text>
<text class="info-value">{{ detail.typeName}}</text>
</view>
<!-- 问题描述 -->
<view class="info-item desc-item">
<text class="info-label">问题描述</text>
<text class="info-value desc-value">
投诉内容投诉内容投诉内容投诉内容投诉内容投诉内容投诉内容投诉内容投诉内容投诉内容
</text>
<text class="info-value desc-value">{{ detail.problemDes }}</text>
</view>
<!-- 投诉人姓名 -->
<view class="info-item">
<text class="info-label">投诉人姓名</text>
<text class="info-value">匿名</text>
<text class="info-value">{{ detail.complaintName }}</text>
</view>
<!-- 手机号码 -->
<view class="info-item">
<text class="info-label">手机号码</text>
<text class="info-value"></text>
<text class="info-value">{{ detail.phone}}</text>
</view>
<!-- 问题照片 -->
<view class="photo-section">
<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"
class="photo-item"></image>
<image v-for="(img, index) in photoList" :key="index" :src="img" mode="aspectFit"
class="photo-item" @click="previewImage(index)"></image>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getComplainDetail } from '../api/apiSub.js'
},
const id = ref('')
computed: {
const detail = ref({})
const photoList = ref([])
// 状态映射
const statusMap = {
'0': { text: '待处理', class: 'pending' },
// '1': { text: '处理中', class: 'processing' },
'1': { text: '已完成', class: 'completed' }
}
const statusText = computed(() => {
return statusMap[detail.value.status]?.text || '待处理'
})
const statusClass = computed(() => {
return statusMap[detail.value.status]?.class || 'pending'
})
},
created() {
},
onShow() {
},
onLoad(option) {
this.id = option.id;
this.status = option.status;
},
mounted() {},
onReady() {
this.$nextTick(() => {
})
},
data() {
return {
id: '',
status: '',
/* 设定状态栏默认高度 */
statusBarHeight: 20,
// 问题照片列表,替换为你的实际图片路径
photoList: [
"http://47.104.199.163:9090/images/propertyImgs/community1.png",
"http://47.104.199.163:9090/images/propertyImgs/community2.png",
"http://47.104.199.163:9090/images/propertyImgs/community3.png"
]
onLoad((option) => {
id.value = option.id || ''
if (id.value) {
detailData()
}
})
// 获取详情
const detailData = async () => {
try {
const res = await getComplainDetail({ id: id.value })
if (res.code === 200 && res.data) {
detail.value = res.data
// 图片URL逗号分隔转数组
if (res.data.problemImageUrl) {
photoList.value = res.data.problemImageUrl.split(',').filter(url => url.trim())
} else {
photoList.value = []
}
},
methods: {
goBack() {
uni.redirectTo({
url: '/pageSubPack/service-list/complaintsSuggestionsIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
} else {
uni.showToast({ title: res.msg || '获取失败', icon: 'none' })
}
} catch (err) {
uni.showToast({ title: typeof err === 'string' ? err.msg : '网络异常', icon: 'none' })
}
}
// 预览图片
const previewImage = (index) => {
uni.previewImage({
current: photoList.value[index],
urls: photoList.value
})
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss">
page {
background: #fff;
@@ -152,8 +135,6 @@
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%); */
}
.page {
@@ -161,12 +142,10 @@
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
/* background-color: #fff; */
}
.page-container {}
/* 通用标题样式(带蓝色竖线) */
.section-title {
display: flex;
align-items: center;
@@ -187,7 +166,6 @@
color: #333333;
}
/* 投诉状态区域 */
.status-section {
display: flex;
justify-content: space-between;
@@ -210,7 +188,6 @@
font-weight: 500;
}
/* 详情信息项通用样式 */
.info-item {
display: flex;
padding: 24rpx 0;
@@ -233,7 +210,6 @@
line-height: 1.5;
}
/* 问题描述特殊样式(多行文本) */
.desc-item {
align-items: flex-start;
}
@@ -243,7 +219,6 @@
word-break: break-all;
}
/* 问题照片区域 */
.photo-section {
padding: 24rpx 0;
}
@@ -258,9 +233,7 @@
.photo-list {
display: flex;
flex-wrap: wrap;
/* 自动换行 */
gap: 20rpx;
/* 图片之间的间距(可改) */
margin: 10rpx 0px 20rpx 0px;
}
@@ -279,7 +252,6 @@
}
.completed {
color: #999999;
}
@@ -287,4 +259,4 @@
width: 100vw;
height: 46px;
}
</style>
</style>

View File

@@ -1,228 +1,152 @@
<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>
<!-- 1. 顶部分段控制器 -->
<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="contentStyle">
<!-- 顶部分段控制器 -->
<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="true">
<!-- 列表区域 -->
<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>
<text class="empty-text">
{{ '暂无数据'}}
</text>
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<!-- 2. 投诉卡片主体 -->
<view class="card-wrapper" v-for="item,index in dataList" :key="index">
<!-- 卡片头部标题 + 标签 -->
<!-- 投诉卡片 -->
<view class="card-wrapper" v-for="(item, index) in dataList" :key="index">
<view class="card-header">
<text class="card-title">{{item.title}}</text>
<text class="card-title">{{ item.title }}</text>
<text class="card-tag"
:class="item.status=='pending'?'pending':item.status=='processing'?'processing':item.status=='completed'?'completed':''">{{item.status=='pending'?'待处理':item.status=='processing'?'处理中':item.status=='completed'?'已完成':''}}</text>
:class="item.status === 'pending' ? 'pending' : 'completed'">
{{ item.status === 'pending' ? '待处理' : '已完成' }}
</text>
</view>
<!-- 卡片内容描述 + 横向图片 -->
<view class="card-body">
<text class="card-desc">
{{item.desc}}
</text>
<text class="card-desc">{{ item.desc }}</text>
<!-- 横向图片滚动区 -->
<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>
<!-- 时间 & 详情 -->
<view class="card-footer">
<text class="card-time">{{item.time}}</text>
<text class="card-time">{{ item.time }}</text>
<text class="card-detail" @click="turnToDetail(item)">投诉详情</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="bottom-btn-wrap">
<button class="next-btn" @click="addComplaintsSuggestions">投诉建议</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import { ref, onMounted } from 'vue'
import {onLoad, onShow, onPullDownRefresh} from '@dcloudio/uni-app'
import {getComplainList} from '../api/apiSub.js'
},
// 分段器
const tabs = ref(["待处理", "已完成"])
const currentTab = ref(0)
const dataList = ref([])
computed: {
},
created() {
},
onShow() {
},
mounted() {
this.getDataList()
},
onReady() {
this.$nextTick(() => {
})
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
// 分段器数据
tabs: ["待处理", "处理中", "已完成"],
currentTab: 0,
dataList: [],
pendingList: [{
id: 1,
title: '社区服务',
status: 'pending',
desc: '1栋楼下道路路灯不亮并且有的亮度不够请物业公司及时更换防止发生意外事故。',
time: '2026-08-01 10:05:46',
imgList: [
"http://47.104.199.163:9090/images/propertyImgs/community1.png",
"http://47.104.199.163:9090/images/propertyImgs/community2.png",
"http://47.104.199.163:9090/images/propertyImgs/community3.png"
]
},
{
id: 1,
title: '社区服务',
status: 'pending',
desc: '1栋楼下道路路灯不亮并且有的亮度不够请物业公司及时更换防止发生意外事故。',
time: '2026-08-01 10:05:46',
imgList: [
"http://47.104.199.163:9090/images/propertyImgs/community1.png",
"http://47.104.199.163:9090/images/propertyImgs/community2.png",
"http://47.104.199.163:9090/images/propertyImgs/community3.png"
]
},
{
id: 1,
title: '社区服务',
status: 'pending',
desc: '1栋楼下道路路灯不亮并且有的亮度不够请物业公司及时更换防止发生意外事故。',
time: '2026-08-01 10:05:46',
imgList: [
"http://47.104.199.163:9090/images/propertyImgs/community1.png",
"http://47.104.199.163:9090/images/propertyImgs/community2.png",
"http://47.104.199.163:9090/images/propertyImgs/community3.png"
]
},
{
id: 1,
title: '社区服务',
status: 'pending',
desc: '1栋楼下道路路灯不亮并且有的亮度不够请物业公司及时更换防止发生意外事故。',
time: '2026-08-01 10:05:46',
imgList: [
"http://47.104.199.163:9090/images/propertyImgs/community1.png",
"http://47.104.199.163:9090/images/propertyImgs/community2.png",
"http://47.104.199.163:9090/images/propertyImgs/community3.png"
]
}
],
processingList: [{
id: 2,
title: '社区服务',
status: 'processing',
desc: '1栋楼下道路路灯不亮并且有的亮度不够请物业公司及时更换防止发生意外事故。',
time: '2026-08-01 10:05:46',
imgList: []
}],
completedList: [{
id: 3,
title: '社区服务',
status: 'completed',
desc: '1栋楼下道路路灯不亮并且有的亮度不够请物业公司及时更换防止发生意外事故。',
time: '2026-08-01 10:05:46',
imgList: [
"http://47.104.199.163:9090/images/propertyImgs/community1.png",
"http://47.104.199.163:9090/images/propertyImgs/community2.png",
"http://47.104.199.163:9090/images/propertyImgs/community3.png"
]
}],
}
},
methods: {
turnToDetail(item) {
uni.redirectTo({
url: '/pageSubPack/service-list/complaintsDetail?id=' + item.id + '&status=' + item.status,
success: res => {},
fail: () => {},
complete: () => {}
});
},
getDataList() {
if (this.currentTab == 0) {
this.dataList = this.pendingList
} else if (this.currentTab == 1) {
this.dataList = this.processingList
} else if (this.currentTab == 2) {
this.dataList = this.completedList
}
},
addComplaintsSuggestions() {
uni.redirectTo({
url: '/pageSubPack/service-list/addComplaints',
success: res => {},
fail: () => {},
complete: () => {}
});
},
onTabClick(e) {
this.currentTab = e.currentIndex;
this.getDataList()
},
goBack() {
uni.switchTab({
url: '/pages/serviceIndex/serviceIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
// 模拟数据
const pendingList = ref([])
const processingList = ref([])
const completedList = ref([])
// 状态映射
const statusMap = {
"0":'pending',
// '1':'processing',
'1':'completed'
}
// 接口数据映射为模板所需格式
const mapItem = (item) => {
//列表默认只展示前三张
let imgList = []
if (item.problemImageUrl) {
imgList = item.problemImageUrl.split(',').filter(url => url.trim()).slice(0, 3)
}
return {
id: item.id,
title: item.typeName,
status: statusMap[item.status] || 'pending',
desc: item.problemDes || '',
time: item.createTime || '',
imgList
}
}
// 切换tab
const onTabClick = (e) => {
currentTab.value = e.currentIndex
getComplainListData()
}
// 获取列表数据
const getComplainListData = async () =>{
try{
let params ={
status:currentTab.value,
villageId:uni.getStorageSync('currentVillageId')
}
const res = await getComplainList(params)
if(res.code === 200){
dataList.value = res.data.map(mapItem)
}else{
dataList.value = []
uni.showToast({title:res.msg || '获取失败',icon:'error'})
}
}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) => {
uni.navigateTo({
url: `/pageSubPack/service-list/complaintsDetail?id=${item.id}&status=${item.status}`
})
}
// 新增投诉
const addComplaintsSuggestions = () => {
uni.navigateTo({
url: '/pageSubPack/service-list/addComplaints'
})
}
// 页面挂载后加载数据
// onMounted(() => {
// getDataList()
// })
</script>
<style lang="scss">
page {
background: #f2f1f6;
@@ -231,26 +155,19 @@
<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;
/* background-color: #fff; */
}
.page-container {
padding-top: 20rpx;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
@@ -261,36 +178,66 @@
}
.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;
}
.cardScroll {
padding: 0rpx 0rpx 0px 0rpx;
overflow-y: scroll;
.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 {
background: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
margin-bottom: 20rpx;
}
/* 卡片头部:标题 & 标签 */
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
@@ -322,46 +269,39 @@
color: #FFFFFF;
}
/* 卡片内容区 */
.card-body {
display: flex;
flex-direction: column;
}
/* 描述文本 */
.card-desc {
font-size: 28rpx;
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 {
width: calc(33.333% - 14rpx);
height: 52px;
}
/* 图片样式 */
.card-img {
width: 100%;
height: 52px;
height: 56px;
object-fit: cover;
}
/* 底部栏:时间 & 详情 */
.card-footer {
display: flex;
justify-content: space-between;
@@ -378,12 +318,16 @@
color: #2F77FD;
}
/* 底部按钮 */
.bottom-btn-wrap {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
/* background-color: #fff; */
background: #f9f9f9;
}
.next-btn {
@@ -392,13 +336,9 @@
line-height: 96rpx;
background-color: #1677ff;
color: #fff;
font-size: 36rpx;
font-size: 28rpx;
border-radius: 12rpx;
border: none;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -1,117 +1,100 @@
<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">
<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>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
onShow() {
},
mounted() {},
onReady() {
this.$nextTick(() => {
})
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
dataList: [{
src: 'http://47.104.199.163:9090/images/propertyImgs/community1.png',
propertyName: '北境碧桂园小区物业中心',
propertyAddress: 'A栋3单元1801',
phoneList: [{
phoneNum: '080-12312312312',
},
{
phoneNum: '080-12312312312',
}
],
}],
<script setup>
import { ref, onMounted } from 'vue';
import { getManagePhoneData } from '../api/apiSub.js'
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: cleanPhone,
fail: (err) => {
console.log('拨号失败或用户取消', err);
uni.showToast({ title: '拨号失败,请长按复制号码', icon: 'none' });
}
},
methods: {
// 电话点击拨号
callPhone(phone) {
uni.makePhoneCall({
phoneNumber: phone.replace(/-/g, "")
});
},
goBack() {
uni.switchTab({
url: '/pages/serviceIndex/serviceIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
});
}
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' });
}
});
}
onMounted(() => {
const villageId = uni.getStorageSync('currentVillageId')
if (!villageId) {
uni.showToast({ title: '请先选择小区', icon: 'none' })
return
}
uni.showLoading({ title: '加载中...' })
getManagePhoneData({ villageId }).then(res => {
uni.hideLoading()
if (res.code === 200 && res.data) {
const d = res.data
dataList.value = [{
src: d.villageImageUrl || 'http://wuye.ckdzkj.com/images/propertyImgs/community1.png',
propertyName: d.villageName || '',
propertyAddress: d.address || '',
phoneList: d.managePhone ? [{ phoneNum: d.managePhone }] : []
}]
} else {
uni.showToast({ title: res.msg || '获取失败', icon: 'none' })
}
}).catch(() => {
uni.hideLoading()
uni.showToast({ title: '获取失败', icon: 'none' })
})
})
</script>
<style lang="scss">
page {
background: #f9f9f9;
@@ -122,10 +105,16 @@
.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;
@@ -135,25 +124,17 @@
padding: 100rpx 0;
color: #ccc;
}
.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);
@@ -171,6 +152,7 @@
height: 60px;
border-radius: 8rpx;
margin-right: 24rpx;
margin-top: 20rpx;
flex-shrink: 0;
}
@@ -182,7 +164,6 @@
gap: 12rpx;
}
/* 物业名称 */
.property-name {
font-size: 32rpx;
font-weight: 600;
@@ -190,14 +171,13 @@
line-height: 35px;
}
/* 物业地址 */
.property-address {
font-size: 28rpx;
color: #666666;
line-height: 24rpx;
line-height: 35rpx;
}
/* 电话行 */
.phone-item {
display: flex;
align-items: center;
@@ -205,27 +185,19 @@
line-height: 60rpx;
}
/* 电话标签 */
.phone-label {
font-size: 28rpx;
color: #999999;
}
/* 电话号码 */
.phone-num {
font-size: 28rpx;
color: #666666;
}
/* 电话图标 */
.phone-icon {
font-size: 24rpx;
color: #409eff !important;
/* 蓝色,和原图一致 */
margin-left: 8rpx;
}
.status-bar{
width: 100vw;
height: 46px;
}
</style>
</style>

View File

@@ -1,239 +1,225 @@
<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 class="survey-title">关于用户体验反馈的调查</view>
<!-- 问卷说明 -->
<view class="survey-desc">
为了给您提供更好的服务希望您能抽出几分钟时间将进的感受和建议告诉我们我们非常重视每位用户的宝贵意见期待您的参与
<view class="survey-title">{{ detailItem.title || '' }}</view>
<view class="survey-desc" v-if="questionList.length > 0">
为了给您提供更好的服务希望您能抽出几分钟时间将最近的感受和建议告诉我们
我们十分重视每位用户的宝贵意见期待您的参与
</view>
<!-- 问题1社区大门数量 -->
<view class="question-item">
<view class="question-title">
<text class="required">*</text>1.您社区的大门数量
</view>
<view class="answer-text">1</view>
<view v-if="questionList.length === 0" class="empty-tip">
<text>暂无问卷内容</text>
</view>
<!-- 问题2消防演练符合项 -->
<view class="question-item">
<view v-for="(q, idx) in questionList" :key="q.cid" class="question-item">
<view class="question-title">
<text class="required">*</text>2.您所在的社区关于消防演练下列符合几项
<text v-if="q.required" class="required">*</text>
<text>{{ idx + 1 }}.{{ q.title || '未命名问题' }}</text>
</view>
<view class="answer-text">一周2~3</view>
</view>
<!-- 问题3健康医疗服务 -->
<view class="question-item">
<view class="question-title">
<text class="required">*</text>3.您所在的社区是否有健康医疗服务
</view>
<view class="answer-text"></view>
</view>
<!-- 问题4综合评分星级 -->
<view class="question-item">
<view class="question-title">
<text class="required">*</text>4.您对所在社区的综合评分
</view>
<view class="star-container">
<view v-for="(item, index) in 5" :key="index" class="star-item" :class="{ active: index < score }">
<text class="star-icon"></text>
<!-- radio / checkbox: 展示选项及选中状态 -->
<view v-if="q.type === 'radio' || q.type === 'checkbox'" class="option-list">
<view
v-for="(opt, oIdx) in q.options"
:key="oIdx"
class="option-item"
:class="{ active: isOptionSelected(q, opt.label) }"
>
<text class="option-icon">{{ q.type === 'radio' ? '●' : '■' }}</text>
<text class="option-label">{{ opt.label }}</text>
</view>
</view>
</view>
<!-- 问题5建议 -->
<view class="question-item">
<view class="question-title">
<text class="required">*</text>5.您还有哪些建议
<!-- input / textarea: 展示文本答案 -->
<view v-if="q.type === 'input' || q.type === 'textarea'" class="answer-text">
{{ getAnswerText(q.cid) || '未填写' }}
</view>
<view class="answer-text">没有其他建议</view>
<!-- 如需可编辑输入框替换为下方textarea -->
<!-- <textarea class="suggest-input" placeholder="请输入您的建议" v-model="suggestion" /> -->
</view>
</scroll-view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
onShow() {
},
mounted() {},
onReady() {
this.$nextTick(() => {
})
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
score: 3, // 默认3星对应原图
suggestion: ''
}
},
methods: {
goBack() {
uni.redirectTo({
url: '/pageSubPack/service-list/questionnaireSurveyIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getQuestionnaireDetail } from '../api/apiSub.js'
const currentId = ref('')
const detailItem = ref({})
const questionList = computed(() => {
const content = detailItem.value?.content
if (!content) return []
try {
const list = JSON.parse(content)
return Array.isArray(list) ? list : []
} catch (e) {
return []
}
})
const answerMap = computed(() => {
const answerContent = detailItem.value?.myAnswer
if (!answerContent) return {}
try {
const list = JSON.parse(answerContent)
if (!Array.isArray(list)) return {}
const map = {}
for (const item of list) {
map[item.cid] = item.value
}
return map
} catch (e) {
return {}
}
})
const getDetailData = async () => {
try {
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 || {}
} else {
detailItem.value = {}
uni.showToast({ title: res.msg || '获取失败', icon: 'none' })
}
} catch (err) {
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
}
}
onLoad((option) => {
if (option.id) {
currentId.value = option.id
getDetailData()
}
})
function isOptionSelected(question, label) {
const val = answerMap.value[question.cid]
if (val === undefined || val === null) return false
if (Array.isArray(val)) {
return val.includes(label)
}
return val === label
}
function getAnswerText(cid) {
return answerMap.value[cid]
}
function goBack() {
uni.navigateBack()
}
</script>
<style lang="scss">
page {
background: #fff;
}
page {
background: #fff;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%); */
}
.contentStyle {
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;
box-sizing: border-box;
/* background-color: #fff; */
}
.page {
flex: 1;
overflow-y: auto;
padding: 20px 40rpx;
box-sizing: border-box;
margin-bottom: 48rpx;
}
/* 滚动内容区 */
.content-scroll {
flex: 1;
padding: 0 30rpx;
}
.survey-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
text-align: center;
margin: 0px 0 20rpx 0;
}
/* 标题与描述 */
.survey-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
text-align: center;
margin: 0px 0 20rpx 0;
;
}
.survey-desc {
font-size: 24rpx;
color: #4A4A4A;
line-height: 1.6;
margin-bottom: 20rpx;
text-align: justify;
word-break: break-all;
word-wrap: break-word;
}
.survey-desc {
font-size: 24rpx;
color: #4A4A4A;
line-height: 1.6;
margin-bottom: 20rpx;
text-align: justify;
}
.empty-tip {
text-align: center;
padding: 100rpx 0;
font-size: 28rpx;
color: #999;
}
.question-item {
margin-bottom: 30rpx;
}
.question-item {
margin-bottom: 30rpx;
}
.question-title {
font-size: 32rpx;
color: #000;
margin-bottom: 10rpx;
font-weight: 600;
}
.question-title {
font-size: 32rpx;
color: #000;
margin-bottom: 10rpx;
font-weight: 600;
}
.required {
color: #ff0000;
font-size: 34rpx;
margin-right: 8rpx;
}
.required {
color: #ff0000;
font-size: 34rpx;
margin-right: 8rpx;
}
.answer-text {
font-size: 32rpx;
color: #333;
line-height: 1.5;
padding-left: 30rpx;
}
.answer-text {
font-size: 32rpx;
color: #333;
line-height: 1.5;
padding-left: 30rpx;
word-break: break-all;
word-wrap: break-word;
}
/* 星级评分样式 */
.star-container {
display: flex;
align-items: center;
gap: 20rpx;
padding-left: 30rpx;
}
.option-list {
padding-left: 30rpx;
}
.star-item {
font-size: 48rpx;
color: #dcdcdc;
cursor: pointer;
transition: color 0.2s;
}
.option-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx 0;
font-size: 32rpx;
color: #333;
}
.star-item.active {
color: #409eff;
/* 蓝色,和原图一致 */
}
.option-item.active {
color: #409eff;
}
.star-icon {
font-family: monospace;
}
.option-icon {
font-size: 24rpx;
color: #ccc;
}
/* 建议输入框样式(如需可编辑) */
.suggest-input {
width: 100%;
min-height: 200rpx;
padding: 20rpx;
border: 1rpx solid #e5e5e5;
border-radius: 8rpx;
background: #fff;
font-size: 32rpx;
color: #333;
box-sizing: border-box;
}
.option-item.active .option-icon {
color: #409eff;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>
.option-label {
font-size: 32rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -1,498 +1,475 @@
<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">
<!-- <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">
<scroll-view class="page" scroll-y>
<view class="content-wrap">
<!-- 问卷标题 -->
<view class="survey-title">关于用户体验反馈的调查</view>
<view class="survey-title">{{ detailItem.title || '' }}</view>
<!-- 问卷说明 -->
<view class="survey-desc">
为了给您提供更好的服务希望您能抽出几分钟时间将进的感受和建议告诉我们我们非常重视每位用户的宝贵意见期待您的参与
<!-- <view class="survey-desc">{{ detailItem.description || '' }}</view> -->
<view class="survey-desc" v-if="questionList.length >0">
为了给您提供更好的服务希望您能抽出几分钟时间将最近的感受和建议告诉我们
我们十分重视每位用户的宝贵意见期待您的参与
</view>
<!-- 问题1单选 -->
<view class="question-item">
<!-- 空状态 -->
<view v-if="questionList.length === 0" class="empty-tip">
<text>暂无数据</text>
</view>
<!-- 动态问题列表 -->
<view v-for="(q, idx) in questionList" :key="q.cid" class="question-item">
<view class="question-title">
<text class="required">*</text>1.您社区的大门数量
<text v-if="q.required" class="required">*</text>
<text>{{ idx + 1 }}.{{ q.title || '未命名问题' }}</text>
</view>
<view class="radio-group">
<view class="radio-item" v-for="(item, index) in q1Options" :key="index"
@click="selectRadio(1, index)">
<view class="radio-circle" :class="{ checked: q1Selected === index }">
<text v-if="q1Selected === index" class="check-icon"></text>
<!-- 单选 -->
<view v-if="q.type === 'radio'" class="radio-group">
<view
v-for="(opt, oIdx) in q.options"
:key="oIdx"
class="radio-item"
:class="{ checked: answers[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>
</view>
<text class="radio-label">{{ item }}</text>
<text class="radio-label">{{ opt.label }}</text>
</view>
</view>
</view>
<!-- 问题2 -->
<view class="question-item">
<view class="question-title">
<text class="required">*</text>2.您所在的社区关于消防演练下列符合几项
</view>
<view class="radio-group">
<view class="radio-item" v-for="(item, index) in q2Options" :key="index"
@click="selectRadio(2, index)">
<view class="radio-circle" :class="{ checked: q2Selected === index }">
<text v-if="q2Selected === index" class="check-icon"></text>
<!-- -->
<view v-if="q.type === 'checkbox'" class="checkbox-group">
<view
v-for="(opt, oIdx) in q.options"
:key="oIdx"
class="checkbox-item"
:class="{ checked: isChecked(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>
</view>
<text class="radio-label">{{ item }}</text>
<text class="checkbox-label">{{ opt.label }}</text>
</view>
</view>
</view>
<!-- 问题3自定义 Select (核心部分不使用 picker) -->
<view class="question-item">
<view class="question-title">
<text class="required">*</text>3.您所在的社区是否有健康医疗服务
</view>
<!-- 模拟输入框点击展开下拉 -->
<view class="select-wrapper" @click="toggleDropdown">
<text class="select-text" v-if="q3Selected">
{{ q3Selected }}
</text>
<text class="select-placeholder" v-else>
请选择是否有该项服务
</text>
<text class="select-arrow"></text>
<!-- 单行输入 -->
<view v-if="q.type === 'input'">
<input
class="text-input"
:class="{ 'input-disabled': isEndedAndUnjoined }"
v-model="answers[q.cid]"
placeholder="请输入"
:disabled="isEndedAndUnjoined"
/>
</view>
<!-- 下拉面板 (遮罩 + 列表) -->
<view class="dropdown-mask" v-if="showDropdown" @click="hideDropdown"></view>
<view class="dropdown-panel" v-if="showDropdown">
<view class="dropdown-item" v-for="(item, index) in q3Options" :key="index"
@click="selectQ3(index)" :class="{ active: index === q3Index }">
{{ item }}
</view>
<!-- 多行输入 -->
<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>
<!-- 问题4星级评分 -->
<view class="question-item">
<view class="question-title">
<text class="required">*</text>4.您对所在社区的综合评分
</view>
<view class="star-group">
<view class="star-item" v-for="(item, index) in 5" :key="index" @click="selectStar(index + 1)">
<text class="star-icon" :class="{ active: index < q4Score }"></text>
</view>
</view>
</view>
<!-- 问题5文本输入 -->
<view class="question-item">
<view class="question-title">
<text class="required">*</text>5.您还有哪些建议
</view>
<textarea class="textarea-input" v-model="q5Content" placeholder="请输入您还有哪些建议" maxlength="500" />
</view>
</view>
</scroll-view>
<!-- 固定底部提交按钮 -->
<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>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
onShow() {
},
mounted() {},
onReady() {
this.$nextTick(() => {
})
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
// 问题1
q1Options: ['1', '2', '3', '4'],
q1Selected: -1,
// 问题2
q2Options: ['一周2~3次', '一月2~3次', '一年2~3次', '基本不买'],
q2Selected: -1,
// 问题3 (自定义Select)
q3Options: ['有', '无'], // 选项列表
q3Selected: '', // 绑定显示值
q3Index: -1, // 绑定索引
showDropdown: false, // 控制下拉面板显示
// 问题4
q4Score: -1,
// 问题5
q5Content: ''
}
},
methods: {
selectRadio(type, index) {
if (type === 1) this.q1Selected = index
else this.q2Selected = index
},
// 问题3切换下拉显示
toggleDropdown() {
this.showDropdown = !this.showDropdown
},
// 问题3隐藏下拉
hideDropdown() {
this.showDropdown = false
},
// 问题3选择选项
selectQ3(index) {
this.q3Index = index
this.q3Selected = this.q3Options[index]
this.hideDropdown() // 选择后自动关闭
},
// 问题4
selectStar(score) {
this.q4Score = score
},
// 提交
handleSubmit() {
if (this.q1Selected === -1) {
uni.showToast({
title: '请选择社区大门数量',
icon: 'none'
})
return
}
if (this.q2Selected === -1) {
uni.showToast({
title: '请选择社区消防演练频率',
icon: 'none'
})
return
}
if (this.q3Index === -1) {
uni.showToast({
title: '请选择社区是否有健康医疗服务',
icon: 'none'
})
return
}
if (this.q4Score === -1) {
uni.showToast({
title: '请选择对社区的综合评分',
icon: 'none'
})
return
}
if (!this.q5Content.trim()) {
uni.showToast({
title: '请输入建议',
icon: 'none'
})
return
}
uni.showLoading({
title: '提交中...',
mask: true
});
setTimeout(() => {
uni.showToast({
title: '提交成功',
icon: 'success'
});
}, 1000);
setTimeout(() => {
// 提交成功后跳转列表页
uni.hideLoading();
this.goBack();
}, 2500);
},
goBack() {
uni.redirectTo({
url: '/pageSubPack/service-list/questionnaireSurveyIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
<script setup>
import { ref, computed } from 'vue'
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
if (!content) return []
try {
const list = JSON.parse(content)
return Array.isArray(list) ? list : []
} catch (e) {
return []
}
})
// 是否已结束且未参与
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 {
let params = {
villageId:uni.getStorageSync('currentVillageId')
}
const res = await getQuestionnaireDetail(id,params)
// console.log('接口返回:', JSON.stringify(res))
if (res.code === 200) {
const data = Array.isArray(res.data) ? res.data[0] : res.data
detailItem.value = data || {}
} else {
uni.showToast({ title: res.msg || '获取失败', icon: 'none' })
}
} catch (err) {
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
}
}
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]
if (!Array.isArray(arr)) return false
return arr.includes(label)
}
const toggleCheckbox = (cid, label) => {
if (!Array.isArray(answers.value[cid])) {
answers.value[cid] = []
}
const arr = answers.value[cid]
const pos = arr.indexOf(label)
if (pos > -1) {
arr.splice(pos, 1)
} else {
arr.push(label)
}
}
const handleSubmit = async () => {
for (const q of questionList.value) {
if (q.required) {
const ans = answers.value[q.cid]
if (ans === undefined || ans === null || ans === '' || (Array.isArray(ans) && ans.length === 0)) {
uni.showToast({ title: `请填写第${questionList.value.indexOf(q) + 1}`, icon: 'none' })
return
}
}
}
const answerList = []
for (const q of questionList.value) {
const val = answers.value[q.cid]
if (val !== undefined && val !== null && val !== '' && !(Array.isArray(val) && val.length === 0)) {
answerList.push({ cid: q.cid, value: val })
}
}
uni.showLoading({ title: "提交中...", mask: true })
try {
let params = {
answerList,
villageId: uni.getStorageSync("currentVillageId")
}
const res = await submitQuestionnair(detailItem.value.id,params)
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => { goBack() }, 1500)
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (err) {
console.log('errrr',err)
uni.hideLoading()
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
}
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss">
page {
background: #f2f1f6;
}
page {
background: #f2f1f6;
}
</style>
<style scoped lang="scss">
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.contentStyle {
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;
box-sizing: border-box;
/* background-color: #fff; */
}
.page {
flex: 1;
overflow-y: auto;
padding: 30rpx 40rpx 200rpx;
box-sizing: border-box;
}
/* 滚动内容区 */
.content-scroll {}
.content-wrap {
padding-bottom: 40rpx;
}
.content-wrap {
// padding-bottom: 40rpx;
}
.survey-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
text-align: center;
margin: 0px 0 20rpx 0;
white-space: pre-line;
}
/* 标题与描述 */
.survey-title {
.survey-desc {
font-size: 24rpx;
color: #4A4A4A;
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 {
text-align: center;
padding: 100rpx 0;
font-size: 28rpx;
color: #999;
}
.question-item {
margin-bottom: 30rpx;
.question-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
text-align: center;
margin: 0px 0 20rpx 0;
;
margin-bottom: 10rpx;
line-height: 1.4;
.required {
color: #ff3b30;
font-size: 36rpx;
margin-right: 8rpx;
}
.survey-desc {
font-size: 24rpx;
color: #4A4A4A;
line-height: 1.6;
margin-bottom: 20rpx;
text-align: justify;
}
}
/* 问题项 */
.question-item {
margin-bottom: 30rpx;
/* 单选 */
.radio-group {
display: flex;
flex-direction: column;
gap: 20rpx;
padding-left: 10rpx;
}
.question-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom:10rpx;
line-height: 1.4;
.radio-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 12rpx 0;
.required {
color: #ff3b30;
font-size: 36rpx;
margin-right: 8rpx;
}
}
}
/* 单选按钮 */
.radio-group {
display: flex;
flex-direction: column;
gap: 30rpx;
padding-left: 30rpx;
}
.radio-item {
.radio-circle {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
display: flex;
align-items: center;
gap: 20rpx;
.radio-circle {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.checked {
background-color: #007aff;
border-color: #007aff;
.check-icon {
color: #fff;
font-size: 24rpx;
font-weight: bold;
}
}
}
.radio-label {
font-size: 34rpx;
color: #333;
}
}
/* 核心:自定义 Select 样式 (模拟输入框) */
.select-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
padding: 0 20rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
background-color: #fff;
font-size: 32rpx;
color: #333;
margin-left: 30rpx;
}
.select-placeholder {
color: #999;
}
.select-arrow {
font-size: 24rpx;
color: #999;
margin-left: 10rpx;
}
/* 下拉遮罩层 */
.dropdown-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.1);
z-index: 99;
}
/* 下拉面板 */
.dropdown-panel {
position: absolute;
left: 0;
right: 0;
max-height: 400rpx;
background-color: #fff;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
z-index: 100;
margin-top: 10rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.dropdown-item {
height: 80rpx;
line-height: 80rpx;
padding: 0 20rpx;
font-size: 32rpx;
color: #333;
&:active {
background-color: #f5f5f5;
}
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
&.active {
color: #007aff;
font-weight: 500;
}
background-color: #007aff;
border-color: #007aff;
}
}
/* 星级评分 */
.star-group {
display: flex;
align-items: center;
gap: 16rpx;
margin-left: 30rpx;
.star-icon {
font-size: 56rpx;
color: #e5e5e5;
transition: all 0.2s ease;
&.active {
color: #007aff;
}
}
.check-icon {
color: #fff;
font-size: 22rpx;
font-weight: bold;
}
/* 文本输入框 */
.textarea-input {
width: calc(100% - 30rpx);
min-height: 240rpx;
padding: 20rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
background-color: #fff;
.radio-label {
font-size: 32rpx;
color: #333;
box-sizing: border-box;
margin-left: 30rpx;
}
}
/* 底部占位 & 提交按钮 */
.bottom-placeholder {
height: 120rpx; // 高度要大于等于按钮高度
}
/* 多选 */
.checkbox-group {
display: flex;
flex-direction: column;
gap: 20rpx;
padding-left: 10rpx;
}
.submit-btn-wrap {
padding: 20rpx 30rpx 40rpx;
.checkbox-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 12rpx 0;
.checkbox-box {
width: 40rpx;
height: 40rpx;
border-radius: 8rpx;
border: 2rpx solid #ccc;
display: flex;
gap: 20rpx;
// background-color: #fff;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
.submit-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
&.active {
background-color: #007aff;
color: #fff;
font-size: 36rpx;
font-weight: 500;
border-radius: 12rpx;
border-color: #007aff;
}
}
.check-icon {
color: #fff;
font-size: 22rpx;
font-weight: bold;
}
.checkbox-label {
font-size: 32rpx;
color: #333;
}
}
/* 文本输入 */
.text-input {
width: calc(100% - 30rpx);
height: 80rpx;
padding: 0 20rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
background-color: #fff;
font-size: 32rpx;
color: #333;
box-sizing: border-box;
margin-left: 30rpx;
}
.textarea-input {
width: calc(100% - 30rpx);
min-height: 240rpx;
padding: 20rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
background-color: #fff;
font-size: 32rpx;
color: #333;
box-sizing: border-box;
margin-left: 30rpx;
}
.input-disabled {
background-color: #f5f5f5;
color: #999;
}
/* 提交按钮 */
.submit-btn-wrap {
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%;
height: 96rpx;
line-height: 96rpx;
background-color: #007aff;
color: #fff;
font-size: 36rpx;
font-weight: 500;
border-radius: 12rpx;
border: none;
padding: 0;
margin: 0;
&::after {
border: none;
padding: 0;
margin: 0;
&::after {
border: none;
}
}
}
.status-bar {
width: 100vw;
height: 46px;
&.disabled-btn {
background-color: #ccc;
color: #999;
}
</style>
}
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -1,240 +1,147 @@
<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">
<view style="width: 40vw;">
<uni-segmented-control :current="currentTab" :values="tabs" @clickItem="switchTab" style-type="text"
active-color="#007AFF" inactive-color="#999" class="seg-control"></uni-segmented-control>
<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="true">
<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>
<text class="empty-text">
{{ '暂无数据'}}
</text>
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<view v-for="(item, index) in currentList" :key="index" class="list-item" :class="{
disabled: currentTab === 1,
selected: currentTab === 0 && item.status === '已参与'
}" @click="handleItemClick(item)">
<!-- 列表项点击高亮 -->
<view
v-for="(item, index) in currentList"
:key="index"
class="list-item"
:class="{
disabled: currentTab === 1,
selected: selectedIndex === index // 点击才高亮
}"
@click="handleItemClick(item, index)"
>
<view class="item-content">
<view class="item-title">{{ item.title }}</view>
<view class="item-info">
<text class="info-time">{{ item.time }}</text>
<text class="info-community">{{ item.community }}</text>
<text class="info-time">{{ item.createTime }}</text>
<text class="info-community">{{ item.villageName }}</text>
</view>
</view>
<view class="item-status"
:class="{joined: currentTab === 0 &&item.status === '已参与',unjoined: item.status === '未参与'||currentTab === 1, }">
{{ item.status }}
<view
class="item-status"
:class="{
joined: item.answerStatus === '1',
unjoined: item.answerStatus === '0'
}"
>
{{ item.statusName}}
</view>
</view>
</view>
<!-- 占位 -->
<view style="width: 100%;height:2rpx;"> </view>
</scroll-view>
<view style="width: 100%;height:2rpx;"></view>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
<script setup>
import { ref, computed } from 'vue'
import { onShow,onPullDownRefresh } from '@dcloudio/uni-app'
import {getQuestionnaireList} from '../api/apiSub.js'
},
// 状态栏高度
// 当前选中tab
const currentTab = ref(0)
// tab标题
const tabs = ["收集中", "已结束"]
computed: {
// 计算当前显示的列表
currentList() {
return this.currentTab === 0 ? this.collectingList : this.endedList;
},
},
created() {
},
onShow() {
},
mounted() {
this.getDataList()
},
onReady() {
this.$nextTick(() => {
})
},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
currentTab: 0, // 0=收集中1=已结束
tabs: ["收集中", "已结束"],
// 收集中列表数据
collectingList: [{
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2026-09-01 11:00:56',
community: '阳光海岸社区',
status: '已参与',
},
{
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2026-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
},
{
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2026-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
},
{
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2026-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
}, {
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2026-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
},
{
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2026-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
},
{
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2026-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
}, {
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2026-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
},
{
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2026-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
},
{
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2026-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
},
],
// 已结束列表数据
endedList: [{
title: '问卷调查名称问卷调查名称问卷调查...',
time: '2023-09-01 11:00:56',
community: '阳光海岸社区',
status: '已参与',
},
{
title: '问卷调查名称问卷调查名称调查...',
time: '2023-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
},
{
title: '问卷调查名称问卷调查名称调查...',
time: '2023-09-01 11:00:56',
community: '阳光海岸社区',
status: '未参与',
},
],
}
},
methods: {
// Tab切换
switchTab(e) {
this.currentTab = e.currentIndex;
this.getDataList()
},
// 列表项点击
handleItemClick(item) {
if (item.status == '已参与') {
this.turnToDetail(item)
} else {
if (this.currentTab === 0) {
this.turnToFillout(item)
} else if (this.currentTab === 1) {
// 未参与的已结束问卷
}
}
},
turnToFillout(item) {
// 跳转到问卷填写
uni.redirectTo({
url: `/pageSubPack/service-list/questionnaireFillout`,
success: res => {},
fail: () => {},
complete: () => {}
});
},
turnToDetail(item) {
// 跳转到问卷详情
uni.redirectTo({
url: `/pageSubPack/service-list/questionnaireDetail`,
success: res => {},
fail: () => {},
complete: () => {}
});
},
getDataList() {
if (this.currentTab == 0) {
this.dataList = this.collectingList
} else if (this.currentTab == 1) {
this.dataList = this.endedList
}
},
onTabClick(e) {
this.currentTab = e.currentIndex;
this.getDataList()
},
goBack() {
uni.switchTab({
url: '/pages/serviceIndex/serviceIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
// 收集中列表
const collectingList = ref([])
// 已结束列表
const endedList = ref([])
// 点击选中的索引(控制高亮)
const selectedIndex = ref(-1)
// 计算属性:当前显示列表
const currentList = computed(() => {
return currentTab.value === 0 ? collectingList.value : endedList.value
})
// Tab切换
const switchTab = (e) => {
currentTab.value = e.currentIndex
selectedIndex.value = -1 // 切换tab清空选中
getDataList()
}
// 获取列表数据
const getDataList = async () => {
try{
let params ={
status: currentTab.value === 0 ? 1 : 2
}
const res = await getQuestionnaireList(params)
if(res.code === 200){
const list = (res.data || []).map(item => {
return {
...item,
statusName: item.answerStatus == "0" ? '未参与' : '已参与'
}
})
if(currentTab.value === 0){
collectingList.value = list
}else{
endedList.value = list
}
}else{
uni.showToast({ title: res.msg || '获取失败', icon: 'none' })
}
}catch(err){
uni.showToast({ title: err.msg ||'网络异常', icon: 'none' })
}finally{
uni.stopPullDownRefresh();
}
}
// 列表项点击index 控制高亮)
const handleItemClick = (item, index) => {
selectedIndex.value = index //点击哪个,哪个高亮
if (item.answerStatus === '1') {
turnToDetail(item)
} else {
// if (currentTab.value === 0) {
turnToFillout(item)
// }
}
}
// 跳转到填写问卷
const turnToFillout = (item) => {
uni.navigateTo({
url: `/pageSubPack/service-list/questionnaireFillout?id=${item.id}`
})
}
// 跳转到详情
const turnToDetail = (item) => {
uni.navigateTo({
url: `/pageSubPack/service-list/questionnaireDetail?id=${item.id}`
})
}
onShow(() => {
getDataList()
})
// 下拉刷新
onPullDownRefresh(() => {
getDataList();
});
</script>
<style lang="scss">
page {
background: #f2f1f6;
@@ -243,10 +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%);
}
@@ -255,30 +162,12 @@
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
/* background-color: #fff; */
}
.page-container {
padding: 40rpx;
}
/* --- 分段控制器样式 --- */
.seg-control {
margin-bottom: 20rpx;
}
.cardScroll {
padding: 0rpx 0rpx 0px 0rpx;
overflow-y: scroll;
}
/* 列表区域 */
.list-scroll {
flex: 1;
}
.list-container {
padding-top: 20rpx;
}
@@ -289,35 +178,29 @@
display: flex;
align-items: center;
justify-content: space-between;
border:2rpx solid #E6E6E6;
border: 2rpx solid #E6E6E6;
background-color: #fff;
&.disabled {
opacity: 0.6;
.item-title,
.item-info {}
}
&.selected {
border:2rpx solid #2F77FD;
border: 2rpx solid #2F77FD;
background-color: #F5FAFF;
}
}
.item-content {
// flex: 1;
width: calc(100% - 100rpx);
// margin-right: 20rpx;
}
.item-title {
font-size: 32rpx;
font-weight: 600;
color: #222222;
color: #222;
line-height: 44rpx;
margin-bottom:10rpx;
/* 超长文本省略 */
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -334,7 +217,6 @@
font-size: 28rpx;
color: #666;
line-height: 32rpx;
/* 超长文本省略 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -344,8 +226,7 @@
.item-status {
font-size: 28rpx;
font-weight: 500;
padding: 0px 0px;
border-radius:8rpx;
border-radius: 8rpx;
&.joined {
color: #2F77FD;
@@ -356,11 +237,7 @@
}
}
.status-bar {
width: 100vw;
height: 46px;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
@@ -369,9 +246,9 @@
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;

View File

@@ -1,72 +1,115 @@
<template>
<view class="qrcode-page">
<view class="qrcode-wrap">
<!-- 显示后台返回的二维码图片 -->
<view class="qrcode-box">
<!-- 调用uqrcode组件 -->
<uqrcode
:value="qrcodeValue"
:size="400"
:logo="'http://47.104.199.163:9090/images/logo.png'"
:logoSize="0.2"
canvasId="visitor-qrcode"
@success="onQrcodeSuccess"
></uqrcode>
<!-- 二维码边框装饰 -->
<image :src="qrcodeUrl" mode="widthFix" class="qrcode-img" />
<!-- 四角装饰边框保留 -->
<view class="qrcode-border border-top"></view>
<view class="qrcode-border border-right"></view>
<view class="qrcode-border border-bottom"></view>
<view class="qrcode-border border-left"></view>
</view>
<text class="qrcode-desc">桂花园别墅东门通行二维码</text>
<text class="qrcode-desc">{{houseName}}通行二维码</text>
<text class="qrcode-tips">分享此二维码给访客可扫码开门单次有效</text>
</view>
<view class="btn-wrap">
<button class="send-btn" @click="sendToFriend">发送给好友</button>
<button class="send-btn" @click="saveToAlbum">保存图片到相册</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
// import { navigateBack, showToast, share } from '@dcloudio/uni-app';
// 引入本地uqrcode组件
import uqrcode from '@/components/uqrcode/uqrcode.vue';
import { onLoad } from '@dcloudio/uni-app';
// 二维码内容(替换为实际的通行链接)
const qrcodeValue = ref('https://your-domain.com/visitor/123456');
// 生成的二维码临时图片地址
// 直接接收图片URL
const qrcodeUrl = ref('');
const qrcodeImage = ref('');
const houseName = ref('')
// 二维码生成成功回调
const onQrcodeSuccess = (tempFilePath) => {
qrcodeImage.value = tempFilePath;
uni.showToast({ title: '二维码生成成功', icon: 'success' });
};
onLoad((option) => {
if (option.qrcodeUrl) {
// 接收后台返回的二维码图片链接
qrcodeUrl.value = decodeURIComponent(option.qrcodeUrl);
qrcodeImage.value = qrcodeUrl.value;
houseName.value = option.villageName
console.log('最终二维码图片:', qrcodeUrl.value);
}
});
// 分享给好友
const sendToFriend = () => {
if (!qrcodeImage.value) {
uni.showToast({ title: '二维码生成中...', icon: 'none' });
// 保存图片到相册
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>
.qrcode-page {
/* background-color: #f8f8f8; */
min-height: 100vh;
}
.qrcode-wrap {
@@ -80,7 +123,12 @@ const sendToFriend = () => {
width: 400rpx;
height: 400rpx;
}
/* 二维码边框样式 */
/* 图片铺满 */
.qrcode-img {
width: 100%;
height: 100%;
}
/* 边框装饰 */
.qrcode-border {
position: absolute;
width: 40rpx;
@@ -91,7 +139,7 @@ const sendToFriend = () => {
.border-right { top: 0; right: 0; border-left: none; border-bottom: none; }
.border-bottom { bottom: 0; right: 0; border-left: none; border-top: none; }
.border-left { bottom: 0; left: 0; border-right: none; border-top: none; }
/* 文字样式 */
.qrcode-desc {
font-size: 32rpx;
font-weight: 600;
@@ -103,9 +151,8 @@ const sendToFriend = () => {
color: #999;
margin-top: 10rpx;
}
/* 按钮样式 */
.btn-wrap {
margin: 80rpx 20rpx 20rpx;
margin: 80rpx 40rpx 20rpx;
}
.send-btn {
background-color: #007aff;
@@ -114,6 +161,5 @@ const sendToFriend = () => {
height: 88rpx;
font-size: 28rpx;
line-height: 88rpx;
}
</style>

File diff suppressed because it is too large Load Diff

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,8 +20,7 @@
<text class="label">联系方式</text>
<text class="value">{{ visitorInfo.phone }}</text>
</view>
<!-- 车牌号可选显示 -->
<view class="info-item" v-if="visitorInfo.carNumber">
<view class="info-item" v-if="visitorInfo.type !== '1'">
<text class="label">车牌号</text>
<text class="value">{{ visitorInfo.carNumber }}</text>
</view>
@@ -31,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>
@@ -47,40 +46,75 @@
<!-- 提示文字 -->
<view class="tips-text">
<view>预计来访时间盒预计离开时间间隔不能超过24小时</view>
<view>预计来访时间离开时间间隔不能超过24小时</view>
车辆停留请勿超出有效期需及时驶出小区
</view>
<!-- 分享二维码按钮仅等待来访状态显示 -->
<view class="btn-wrap" v-if="visitorInfo.status === 'waiting'">
<!-- <view class="btn-wrap" v-if="visitorInfo.status === 'waiting'">
<button class="share-btn" @click="goToQrcode">分享通行二维码</button>
</view>
</view> -->
</view>
</template>
<script setup>
import { ref } from 'vue';
import {onLoad} from '@dcloudio/uni-app'
import {getVisitorDetail} from '../api/api.js'
// 访客信息(实际项目中从接口获取)
// const visitorInfo = ref({})
const visitorInfo = ref({
name: '张三',
status: 'waiting', // waiting:等待来访visited:已到访
address: '桂花园别墅2号楼2层',
phone: '13866666666',
carNumber: '京A88888', // 可传空隐藏
name: '',
status: '', // waiting:等待来访visited:已到访
type: '',
address: '',
phone: '',
carNumber: '',
escortNum: 0,
validTime: '2023.06.30 10:00至2023.06.30 23:00',
inviteTime: '2023年06月30日 09:35',
visitTime: '2023年06月30日 14:32' // 已到访时显示
});
validTime: '',
inviteTime: '',
visitTime: ''
})
// 把后端返回的字段映射成详情页
const mapDetail = (item) => {
const addressParts = [
item.villageName,
item.floorNo ? item.floorNo + '栋' : '',
item.unitNo ? item.unitNo + '单元' : '',
item.houseNo
].filter(Boolean)
const status = item.status === '1' ? '已到访' : '等待来访'
return {
name: item.visitorName || '',
status,
type: item.type || '',
address: addressParts.join(''),
phone: item.visitorPhone || '',
carNumber: item.carNum || '',
escortNum: item.visitorNum || 0,
validTime: item.startTime && item.endTime ? `${item.startTime}${item.endTime}` : '',
inviteTime: item.createTime || '',
visitTime: item.visitTime || ''
}
}
onLoad((option) =>{
const infoStr = decodeURIComponent(JSON.stringify(option))
// console.log('infoStr',infoStr)
visitorInfo.value = infoStr.item
// 获取访客详情
const fetchVisitorDetail = async (id) => {
try {
const res = await getVisitorDetail(id)
if (res && res.data) {
visitorInfo.value = mapDetail(res.data)
}
} catch (e) {
console.error('获取访客详情失败:', e)
}
}
onLoad((option) => {
if (option && option.id) {
fetchVisitorDetail(option.id)
}
})
// 跳转到二维码页面
const goToQrcode = () => {
@@ -92,7 +126,7 @@ const goToQrcode = () => {
<style scoped>
.visitor-detail-page {
background-color: #f8f8f8;
background-color: #fff;
min-height: 100vh;
}
@@ -111,8 +145,7 @@ const goToQrcode = () => {
}
.title {
font-size: 32rpx;
font-weight: 600;
font-size: 28rpx;
}
.status {
@@ -120,10 +153,13 @@ const goToQrcode = () => {
padding: 6rpx 16rpx;
border-radius: 20rpx;
}
.status.active{
color: #2f77fd;
font-weight: 500;
}
.waiting {
color: #007aff;
background: #e8f4ff;
}
.visited {
@@ -142,20 +178,29 @@ const goToQrcode = () => {
}
.label {
color: #666;
color: #222;
width: 160rpx;
}
.value {
color: #333;
color: #222;
flex: 1;
text-align: right;
}
.value-valid{
color: #222;
flex: 1;
text-align: right;
white-space: nowrap;
}
.tips-text {
margin: 0 20rpx;
font-size: 24rpx;
color: #999;
color: #666;
line-height: 1.6;
text-align: center;
margin-top: 98rpx;
}
.btn-wrap {

View File

@@ -1,26 +1,16 @@
<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
class="tab-item"
<view
class="tab-item"
:class="{ active: activeTab === 'waiting' }"
@click="activeTab = 'waiting'"
>
等待来访
</view>
<view
class="tab-item"
<view
class="tab-item"
:class="{ active: activeTab === 'visited' }"
@click="activeTab = 'visited'"
>
@@ -29,23 +19,30 @@
</view>
<!-- 访客列表 -->
<scroll-view class="list-container" scroll-y>
<!-- 列表项根据标签切换显示对应数据 -->
<view
class="visitor-item"
v-for="(item, index) in filterdList"
<view class="list-container">
<view
class="visitor-item"
v-for="(item, index) in filterdList"
:key="index"
>
<!-- 访客基本信息 -->
<view class="item-header">
<text class="visitor-name" :class="item.status">来访者{{ item.name }}</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.timeRange }}</text>
<text class="label" :class="item.status">
{{ item.status === "waiting" ? "有效期" : "到访时间" }}
</text>
<text class="value" :class="item.status">
{{ item.startTime }}{{ item.endTime }}
</text>
</view>
<!-- 联系方式 -->
@@ -54,284 +51,287 @@
<text class="value" :class="item.status">{{ item.phone }}</text>
</view>
<!-- 车牌号 -->
<view class="item-row" v-if="item.carNumber">
<!-- 车牌号仅车辆来访显示 -->
<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">时间{{ 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>
</scroll-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" ? "暂无等待到访数据" : "暂无已到访数据" }}
</text>
</view>
<!-- 底部占位 -->
<view class="list-bottom-space"></view>
</view>
<!-- 新增邀请按钮 -->
<view class="add-btn" @click="addInvitation">
<!-- <uni-icons type="plus" size="18" color="#fff"></uni-icons> -->
<text class="add-text">新增邀请+</text>
<view class="add-btn-wrapper">
<view class="add-btn" @click="addInvitation">
<text class="add-text">新增邀请+</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
// import { uniNavigateTo, uniShowToast } from '@dcloudio/uni-app'
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([
{
name: '张三',
status: 'waiting', // waiting/visited
statusText: '等待来访',
timeRange: '2023.06.30 10:00至2023.06.30 23:00',
phone: '13866666666',
carNumber: '京A88888',
createTime: '2023年06月30日 09:35'
},
{
name: '张三',
status: 'visited',
statusText: '已到访',
timeRange: '2023.06.30 16:32:28',
phone: '13866666666',
carNumber: '京A88888',
createTime: '2023年06月30日 09:35'
},
{
name: '张三',
status: 'waiting',
statusText: '等待来访',
timeRange: '2023.06.30 10:00至2023.06.30 23:00',
phone: '13866666666',
carNumber: '',
createTime: '2023年06月30日 09:35'
}
])
const mapData = (list) => {
if (!Array.isArray(list)) return [];
return list.map((item) => {
const status =
item.status === 1 || item.status === "1" ? "visited" : "waiting";
return {
...item,
status,
statusText: status === "visited" ? "已到访" : "等待来访",
phone: item.visitorPhone,
carNumber: item.carNum,
createTime: item.createTime || "",
};
});
};
// 根据标签筛选列表
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"
})
const fetchVisitorList = async () => {
try {
const params = {
userId: uni.getStorageSync("userId"),
villageId: uni.getStorageSync('currentVillageId')
}
uni.removeStorageSync('sourcePage')
}
const res = await getVisitorList(params);
visitorList.value = mapData(res.data || []);
} catch (err) {
uni.showToast({ title: err.msg, icon: "none" });
} finally {
uni.stopPullDownRefresh();
}
};
onShow(() => {
fetchVisitorList();
});
onPullDownRefresh(() => {
fetchVisitorList();
});
const filterdList = computed(() => {
return visitorList.value.filter((item) => item.status === activeTab.value);
});
// 查看详情
const viewDetail = (item) => {
const itemStr = encodeURIComponent(JSON.stringify(item))
console.log('eeeee',itemStr)
uni.navigateTo({
url: `/pageSubPack/visitor/visitor-detail?item=${itemStr}` // 传参到详情页
})
}
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 - 150px); /* 预留导航、标签、按钮高度 */
}
/* 访客列表项 */
/* 访客卡片 */
.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;
}
.visitor-name {
font-size: 16px;
color: #333;
font-weight: 500;
}
.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; */
margin-bottom: 16rpx;
}
/* 列表项行数据 */
.visitor-name {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.visitor-name.visited {
color: #999;
}
.status-tag {
font-size: 26rpx;
}
.status-tag.waiting {
color: #2f77fd;
}
.status-tag.visited {
color: #999;
}
/* 信息行 */
.item-row {
display: flex;
margin-bottom: 8px;
font-size: 14px;
}
.label {
color: #222;
min-width: 80px;
}
.value {
color: #333;
flex: 1;
}
.label.visited{
color: #999;
}
.value.visited{
color: #999;
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;
}
/* 底部行 */
.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: 12px;
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;
}
/* 新增邀请按钮 */
.add-btn {
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;
/* 空状态 */
.empty-state {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.add-text {
margin-left: 5px;
.empty-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
</style>
/* 列表底部占位(为固定的按钮预留高度,保证最后一项能滚到按钮上方) */
.list-bottom-space {
height: 180rpx;
}
/* 新增邀请按钮 */
.add-btn-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #f9f9f9;
}
.add-btn {
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 {
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,31 +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",
@@ -39,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": "用户协议"
}
}
]
@@ -85,7 +88,7 @@
"path": "onlineRepair",
"style": {
"navigationBarTitleText": "在线报修",
"navigationStyle": "custom"
"enablePullDownRefresh": true
}
},
{
@@ -132,7 +135,7 @@
"path": "visitor-list",
"style": {
"navigationBarTitleText": "访客邀请",
"navigationStyle": "custom"
"enablePullDownRefresh": true
}
},
{
@@ -161,7 +164,7 @@
"path": "activityList",
"style": {
"navigationBarTitleText": "活动列表",
"navigationStyle": "custom"
"enablePullDownRefresh": true
}
},
{
@@ -186,7 +189,8 @@
"path": "noticeList",
"style": {
"navigationBarTitleText": "通知公告",
"navigationStyle": "custom"
"navigationBarBackgroundColor": "#F9F9F9",
"enablePullDownRefresh": true
}
},
{
@@ -198,14 +202,14 @@
{
"path": "oneClickOpen",
"style": {
"navigationBarTitleText": "一键开门",
"navigationStyle": "custom"
"navigationBarTitleText": "一键开门"
}
},
{
"path": "selectHome",
"style": {
"navigationBarTitleText": "选择小区"
"navigationBarTitleText": "选择小区",
"enablePullDownRefresh": true
}
}
]
@@ -213,38 +217,66 @@
{
"root": "pageSubPack/my",
"pages": [{
"path": "settingIndex",
"path": "settingIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "设置"
}
},
{
"path": "changePhone",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "更改手机号码"
}
},
{
"path": "changePwd",
"style": {
"navigationBarTitleText": "",
"navigationBarTitleText": "修改密码"
}
},
{
"path": "deleteAccount",
"style": {
"navigationBarTitleText": "注销账号"
}
},
{
"path": "myPointsIndex",
"style": {
"navigationBarTitleText": "我的积分",
"navigationStyle": "custom"
}
},
{
"path": "myPointsMall",
"style": {
"navigationBarTitleText": "积分商城"
}
},
{
"path": "myPointsRecord",
"style": {
"navigationBarTitleText": "积分明细"
}
},
{
"path": "myPointsOrder",
"style": {
"navigationBarTitleText": "积分订单"
}
},
{
"path": "myHouseIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "我的房屋",
"enablePullDownRefresh": true
// "navigationStyle": "custom"
}
},
{
"path": "houseDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "房屋详情"
}
}, {
"path": "addHouse",
@@ -267,15 +299,14 @@
}, {
"path": "myParkingSpaceIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "我的车位",
"enablePullDownRefresh": true
}
},
{
"path": "parkingSpaceDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "车位详情"
}
},
@@ -314,8 +345,8 @@
{
"path": "myCarIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "我的车辆",
"enablePullDownRefresh": true
}
@@ -332,12 +363,19 @@
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "carDetail",
},
{
"path": "selectOwner",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "carDetail",
"style": {
"navigationBarTitleText": "车辆详情"
}
}
]
}, {
@@ -370,8 +408,8 @@
{
"path": "paymentRecord",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "缴费记录"
// "navigationStyle": "custom"
}
},
{
@@ -384,8 +422,15 @@
{
"path": "paymentIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "生活缴费",
"navigationBarBackgroundColor": "#dfedfd",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,
"app-plus": {
"titleNView": {
"autoBackButton": true
}
}
}
}, {
"path": "addHydropower",
@@ -393,6 +438,25 @@
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "yinlianPayTn",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "propertyFeePayment",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "billPayment",
"style": {
"navigationBarTitleText": "缴费账单"
}
}
]
@@ -402,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": "收益详情"
}
}
]
@@ -488,4 +577,4 @@
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,9 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="我的" backgroundColor="transparent" :border="false">
</uni-nav-bar>
<view class="contentStyle">
<scroll-view class="page" scroll-y>
<view class="topBox">
<view class="name">
<image src="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 == '设置') {
} 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,54 +1,57 @@
<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"
:current="currentStep"
<swiper
class="guide-swiper"
:current="currentStep"
@change="handleSwiperChange"
:disable-scroll="false"
:indicator-dots="false"
:mode="mode"
:mode="mode"
>
<!-- 便捷服务 -->
<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>
</view>
<view style="height: 200rpx;width: 100%;"></view>
<view class="guide-title">便捷服务</view>
<view class="guide-subtitle">报修缴费预约</view>
<view class="guide-img">
<image src="https://wuye.ckdzkj.com/images/guide1.png" mode="aspectFit" class="img-content"></image>
</view>
<view class="guide-text">
<view class="guide-title">便捷服务</view>
<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>
</view>
<view style="height: 200rpx;width: 100%;"></view>
<view class="guide-title">高效管理</view>
<view class="guide-subtitle">自动化流程和智能设备</view>
<view class="guide-img">
<image src="https://wuye.ckdzkj.com/images/guide2.png" mode="aspectFit" class="img-content"></image>
</view>
<view class="guide-text">
<view class="guide-title">高效管理</view>
<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>
</view>
<view style="height: 200rpx;width: 100%;"></view>
<view class="guide-title">安全保障</view>
<view class="guide-subtitle">智能巡检和智慧访客</view>
<view class="guide-img">
<image src="https://wuye.ckdzkj.com/images/guide3.png" mode="aspectFit" class="img-content"></image>
</view>
<view class="guide-text">
<view class="guide-title">安全保障</view>
<view class="guide-subtitle">智能巡检和智慧访客</view>
</view>
</view>
</swiper-item>
</swiper>
<!-- 进度点 -->
<view class="progress-dots">
<view class="progress-dots">
<view class="dot" :class="{ active: currentStep === 0 }"></view>
<view class="dot" :class="{ active: currentStep === 1 }"></view>
<view class="dot" :class="{ active: currentStep === 2 }"></view>
@@ -56,8 +59,8 @@
<!-- 操作按钮 -->
<view class="guide-btn-wrap">
<button
class="guide-btn"
<button
class="guide-btn"
:class="{ primary: currentStep === 2 }"
@click="handleBtnClick"
>
@@ -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: 115px;
width: 100%;
}
/* 引导页淡入动画 */
@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,27 +204,32 @@ const handleBtnClick = () => {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 60rpx;
margin-bottom: 40rpx;
flex-shrink: 0;
}
/* 单个进度点 */
.dot {
width: 16rpx;
height: 16rpx;
height: 8rpx;
border-radius: 8rpx;
background-color: #ddd;
margin: 0 8rpx;
transition: all 0.3s ease;
}
/* 激活的进度点 */
.dot.active {
background-color: #007aff;
width: 40rpx;
width: 28rpx;
}
/* 按钮容器 */
.guide-btn-wrap {
width: 80%;
max-width: 600rpx;
margin-bottom: 80rpx;
flex-shrink: 0;
}
/* 引导按钮 */
@@ -216,4 +264,4 @@ const handleBtnClick = () => {
color: #fff;
border: none;
}
</style>
</style>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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