Compare commits

..

158 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
de6e6ebfc1 1 2026-04-30 08:29:24 +08:00
wangyuxin
2c528b7973 我的房屋流程对接接口,缴费页面功能修改一版 2026-04-30 08:24:22 +08:00
zhouyanli
d183cb00c0 对接在线报修接口 2026-04-29 17:39:55 +08:00
zhouyanli
46c9f6dacc 修改样式 2026-04-29 09:07:18 +08:00
zhouyanli
e3e74a1be0 恢复接口 2026-04-28 16:52:51 +08:00
zhouyanli
fdc53b09bb 生活缴费改成物业费 2026-04-28 14:52:47 +08:00
zhouyanli
4765cd8526 新增用户隐私 2026-04-28 08:53:53 +08:00
zhouyanli
d02af4a8b4 新增用户隐私协议 2026-04-28 08:50:00 +08:00
zhouyanli
5936da5547 去掉接口token 2026-04-27 15:27:32 +08:00
zhouyanli
855bb47ec7 注释掉token 2026-04-27 14:16:45 +08:00
zhouyanli
d4ebf2bff5 临时注释掉token 2026-04-27 14:07:58 +08:00
zhouyanli
d52cbbd51b 对接活动实况接口;注掉登录接口代码 2026-04-27 13:38:24 +08:00
zhouyanli
ae9465cc0e 对接活动、实况等接口 2026-04-25 17:23:57 +08:00
zhouyanli
06a391a668 对接活动列表接口 2026-04-25 07:57:49 +08:00
zhouyanli
d569bbc8e1 对接公告接口 2026-04-23 17:29:01 +08:00
zhouyanli
347917e40a 对接公告以及详情接口 2026-04-23 11:30:54 +08:00
zhouyanli
057758772e 对接切换小区接口 2026-04-22 18:00:48 +08:00
zhouyanli
c71102f256 修改在线报修样式 2026-04-21 18:33:35 +08:00
zhouyanli
0f6d6edcee Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side 2026-04-21 18:25:19 +08:00
zhouyanli
244048985a 修改页面样式 2026-04-21 18:25:00 +08:00
wangyuxin
fb08057489 分包 2026-04-21 17:24:52 +08:00
zhouyanli
b8fcaff48c Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side 2026-04-21 15:49:11 +08:00
zhouyanli
65e6e5bf30 替换图片并删掉本地图片,代码分包 2026-04-21 15:48:52 +08:00
wangyuxin
7341391c32 全部图片替换为后端云存储路径,缴费相关页面修改 2026-04-21 15:02:25 +08:00
zhouyanli
b0d90c27db 替换图片 2026-04-20 17:47:48 +08:00
zhouyanli
e6c6e2c291 修改禁用按钮边框 2026-04-20 17:20:22 +08:00
zhouyanli
c8940e5b02 修改跳转问题 2026-04-20 17:10:47 +08:00
wangyuxin
8af229f25b wyx 2026-04-20 16:21:51 +08:00
zhouyanli
1a70937fc0 Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side 2026-04-20 15:41:02 +08:00
zhouyanli
2a9dc159b1 修改禁用按钮样式 2026-04-20 15:40:43 +08:00
wangyuxin
cc1130e642 Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side 2026-04-20 15:23:25 +08:00
wangyuxin
85e802b568 4/20 水电表页面新增 2026-04-20 15:18:12 +08:00
zhouyanli
05307a5d58 修改引导页首页在线报修样式 2026-04-20 11:48:17 +08:00
zhouyanli
fc7ef36a93 修改服务页的跳转问题 2026-04-20 08:46:02 +08:00
zhouyanli
445e44b3d9 Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side 2026-04-18 17:35:15 +08:00
zhouyanli
71f422d254 首页 2026-04-18 17:35:00 +08:00
zhouyanli
6ab4904deb 注册用户 2026-04-18 17:34:49 +08:00
zhouyanli
a40a96b90b 密码登录 2026-04-18 17:34:36 +08:00
zhouyanli
88ecf15f9e 密码登录 2026-04-18 17:34:16 +08:00
zhouyanli
be154fcc1b 引导页 2026-04-18 17:34:01 +08:00
zhouyanli
08794aa023 选择小区 2026-04-18 17:33:50 +08:00
zhouyanli
97f1566132 远程开门 2026-04-18 17:33:38 +08:00
zhouyanli
2512da2324 公告详情 2026-04-18 17:33:26 +08:00
zhouyanli
be358d070b 公告 2026-04-18 17:33:15 +08:00
zhouyanli
d8a23063d9 在线报修 2026-04-18 17:32:42 +08:00
zhouyanli
dc9e722b3f 修改访客列表 2026-04-18 17:32:17 +08:00
zhouyanli
d97fdbe6a6 修改访客邀请 2026-04-18 17:31:46 +08:00
zhouyanli
821aeb227f 组件按需引入 2026-04-18 17:31:03 +08:00
zhouyanli
d18e5a3351 登录分包 2026-04-18 17:30:38 +08:00
wangyuxin
f5c58aed75 修改 wyx 2026-04-18 17:29:15 +08:00
wangyuxin
452bf0b84d 4/18 样式修改,缴费部分页面按照ui重做 2026-04-18 16:24:20 +08:00
zhouyanli
00e9de94f6 导航箭头 2026-04-18 16:15:04 +08:00
wangyuxin
72ec4017e8 Merge branch 'main' of http://gitea.bugtc.com/zhouyanli/property-resident-side 2026-04-18 10:53:11 +08:00
wangyuxin
523a98e80e 4/18 图片压缩提交 2026-04-18 10:52:06 +08:00
zhouyanli
a8d6c624a2 压缩图片 2026-04-18 10:48:27 +08:00
zhouyanli
889db1b991 login主包 2026-04-18 10:47:10 +08:00
zhouyanli
063b59d411 密码登录放在分包里 2026-04-18 10:45:24 +08:00
zhouyanli
de766a7e70 登录放在分包里 2026-04-18 10:44:56 +08:00
257 changed files with 28155 additions and 20035 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,14 +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>
@@ -31,4 +145,8 @@
/* line-height: 1.6 !important; */
padding: 0 !important;
}
/* button:after{
border-radius: 50px;
} */
</style>

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,23 +52,136 @@
"<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"
}
}
}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx4a49ad94e2b25927",
"appid" : "wx40625124315d2de1",
"setting" : {
"urlCheck" : false
},
"navigateToMiniProgramAppIdList" : [ "wxc0189436c58b59cd" ],
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ],
"permission" : {
"scope.camera" : {
"desc" : "用于扫码"
},
"scope.userLocation" : {
"desc" : "你的位置信息将用于定位选择"
}
},
"usingComponents" : true
},
"mp-alipay" : {
@@ -68,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

@@ -0,0 +1,734 @@
<template>
<view class="container">
<!-- 滚动区域从banner开始滚动 -->
<view class="scroll-container">
<!-- 顶部图片 -->
<view class="top-img">
<image class="banner-img" src="http://wuye.ckdzkj.com/images/home/banner.png" mode="aspectFill"></image>
</view>
<!-- 选项卡 -->
<view class="tabs-container">
<view class="tab-item" :class="{ 'active': activeTab === 'list' }" @tap="switchTab('list')">
活动列表
<view v-if="activeTab === 'list'" class="tab-indicator"></view>
</view>
<view class="tab-item" :class="{ 'active': activeTab === 'my' }" @tap="switchTab('my')">
我的活动
<view v-if="activeTab === 'my'" class="tab-indicator"></view>
</view>
</view>
<!-- 我的活动状态筛选 -->
<view v-if="activeTab === 'my'" class="filter-container">
<scroll-view class="filter-scroll" scroll-x>
<view class="filter-buttons">
<view v-for="filter in filters" :key="filter.value" class="filter-btn"
:class="{ 'active': myFilter === filter.value }" @tap="switchFilter(filter.value)">
{{ filter.label }}
</view>
</view>
</scroll-view>
</view>
<!-- 活动列表 -->
<view class="activity-list">
<!-- 空状态 -->
<view v-if="displayedActivities.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
{{ activeTab === 'list' ? '暂无活动' : '暂无参与的活动' }}
</text>
</view>
<!-- 活动卡片 -->
<view v-for="activity in displayedActivities" :key="activity.id" class="activity-card"
@tap="viewActivityDetail(activity)">
<view v-if="activity.applied && activeTab === 'list'" class="signed-badge">
<text>已报名</text>
</view>
<view class="activity-info">
<view class="u-demo-block__content">
<up-row justify="space-between" customStyle="margin-bottom: 10px">
<up-col span="4">
<view class="info-image-left">
<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">
<view class="demo-layout bg-purple">
<view class="activity-title">
{{ activity.title }}
</view>
<view class="activity-detail">
<text class="detail-text">时间{{ formatDate(activity.startTime) }}
{{ formatDate(activity.endTime) }}</text>
</view>
<view class="activity-detail">
<text class="detail-text-address">地点{{ activity.address }}</text>
</view>
<view class="activity-detail">
<text class="detail-text-num">
<text v-if="activity.quota !== 0">活动名额{{ activity.quota }},</text>
已报名{{ activity.appliedCount }}
</text>
</view>
</view>
</up-col>
</up-row>
</view>
<view class="action-buttons">
<button v-if="activeTab === 'list' && activity.applied === false && activity.statusText !== '已结束' && !isDeadlinePassed(activity) && !isQuotaFull(activity)" class="action-btn signup-btn"
@tap.stop="signupActivity(activity)">
立即报名
</button>
<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>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMoreData && displayedActivities.length > 0" class="load-more-container">
<view v-if="!isLoading" class="load-more-btn" @tap="loadMoreData">
<text>加载更多</text>
<uni-icons type="arrow-down" size="16" color="#007AFF"></uni-icons>
</view>
<view v-else class="loading-container">
<uni-icons type="spinner-cycle" size="20" color="#007AFF" class="loading-icon"></uni-icons>
<text>正在加载...</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch } from 'vue'
import { onLoad, onUnload, onShow, onPullDownRefresh, onReachBottom, onBackPress } from '@dcloudio/uni-app'
import moment from 'moment';
import { getActivityList,getMyActivityList} from '../api/api.js'
// ===================== 响应式数据 =====================
const activeTab = ref('list') // 'list' 或 'my'
const myFilter = ref('') // 我的活动筛选条件:''全部, '0'未开始, '1'进行中, '2'已结束
const isLoading = ref(false)
const hasMoreData = ref(true)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const displayedActivities = ref([]) // 当前显示的活动列表
// 筛选条件status ''全部, '0'未开始, '1'进行中, '2'已结束
const filters = ref([
{ label: '全部', value: '' },
{ label: '未开始', value: '0' },
{ label: '进行中', value: '1' },
{ label: '已结束', value: '2' }
])
// ===================== 核心方法 =====================
/**
* 重置并刷新第一页数据
*/
const resetAndLoad = () => {
currentPage.value = 1
displayedActivities.value = []
hasMoreData.value = true
getActivityListData(1)
}
// 页面返回拦截:根据来源页返回对应 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" })
return true
}
})
/**
* 切换选项卡(活动列表/我的活动)
* @param {string} tab - 选项卡类型
*/
const switchTab = (tab) => {
activeTab.value = tab
if (tab === 'list') {
resetAndLoad()
} else {
resetAndLoadMy()
}
}
/**
* 重置并刷新我的活动第一页数据
*/
const resetAndLoadMy = () => {
currentPage.value = 1
displayedActivities.value = []
hasMoreData.value = true
getMyActivityListData(1)
}
/**
* 切换我的活动筛选条件
* @param {string} filter - 筛选值
*/
const switchFilter = (filter) => {
myFilter.value = filter
resetAndLoadMy()
}
/**
* 获取活动列表数据
* @param {number} pageNum - 页码
*/
const getActivityListData = async (pageNum = 1) => {
// 防止重复加载
if (isLoading.value) return
isLoading.value = true
try {
// 构造请求参数(活动列表)
const params = {
pageNum: pageNum,
pageSize: pageSize.value,
villageId:uni.getStorageSync('currentVillageId')
}
const res = await getActivityList(params)
if (res.code === 200) {
const list = res.rows || []
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();
}
}
/**
* 获取我的活动列表数据
* @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();
}
}
/**
* 加载更多数据
*/
const loadMoreData = () => {
if (isLoading.value || !hasMoreData.value) return
if (activeTab.value === 'list') {
getActivityListData(currentPage.value + 1)
} else {
getMyActivityListData(currentPage.value + 1)
}
}
/**
* 格式化日期
* @param {string} date - 原始日期字符串
* @returns {string} 格式化后的日期
*/
const formatDate = (date) => {
if (!date) return ''
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}&statusText=${activity.statusText || ''}`
})
}
/**
* 取消报名(跳转详情页)
* @param {object} activity - 活动对象
*/
const cancelActivity = (activity) => {
uni.navigateTo({
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}&statusText=${activity.statusText || ''}`
})
}
/**
* 查看活动详情(跳转详情页)
* @param {object} activity - 活动对象
*/
const viewActivityDetail = (activity) => {
uni.navigateTo({
url: `/pageSubPack/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}&applied=${activity.applied}&statusText=${activity.statusText || ''}`
})
}
// ===================== 生命周期 & 监听 =====================
/**
* 页面显示时刷新数据
*/
onShow(() => {
if (activeTab.value === 'list') {
getActivityListData(1)
} else {
getMyActivityListData(1)
}
})
/**
* 页面加载时监听刷新事件(来自详情页)
*/
onLoad(() => {
// 监听详情页报名/取消报名后的刷新通知
uni.$on('refreshActivityList', (data) => {
getActivityListData(1)
})
})
/**
* 页面卸载时移除事件监听(避免内存泄漏)
*/
onUnload(() => {
uni.$off('refreshActivityList')
});
// 下拉刷新
onPullDownRefresh(() => {
if (activeTab.value === 'list') {
resetAndLoad();
} else {
resetAndLoadMy();
}
});
// 上拉加载更多
onReachBottom(() => {
loadMoreData();
});
/**
* 监听我的活动筛选条件变化
*/
watch(() => myFilter.value, () => {
if (activeTab.value === 'my') {
resetAndLoadMy()
}
})
</script>
<style scoped>
/* 页面容器 */
.container {
width: 100%;
min-height: 100vh;
background-color: #fff;
padding: 0;
}
/* 滚动区域 */
.scroll-container {
width: 100%;
padding: 0 30rpx;
box-sizing: border-box;
}
/* 顶部图片 */
.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;
}
.tab-item {
display: flex;
text-align: center;
padding: 20rpx 32rpx;
font-size: 32rpx;
color: #666;
position: relative;
}
.tab-item.active {
color: #2F77FD;
font-weight: 500;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 120rpx;
height: 8rpx;
background: linear-gradient(to bottom, #2F77FD, #CEDFFF);
/* background-color: #2F77FD; */
border-radius: 4rpx;
}
/* 筛选容器 */
.filter-container {
background-color: #fff;
padding: 20rpx 0;
border-bottom: 1rpx solid #eee;
}
.filter-scroll {
width: 100%;
white-space: nowrap;
}
.filter-buttons {
display: inline-flex;
padding: 0 10rpx;
}
.filter-btn {
display: inline-block;
padding: 12rpx 30rpx;
margin-right: 20rpx;
font-size: 28rpx;
color: #666;
background-color: #f8f8f8;
border-radius: 25rpx;
border: 1rpx solid #eee;
white-space: nowrap;
flex-shrink: 0;
}
.filter-btn.active {
color: #fff;
background-color: #2F77FD;
}
/* 活动列表 */
.activity-list {
width: 100%;
padding-bottom: 40rpx;
}
/* 活动卡片 */
.activity-card {
background-color: #fff;
border-radius: 16rpx;
padding: 12rpx;
margin-bottom: 30rpx;
/* box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); */
position: relative;
border: 1rpx solid #e1e1e1;
}
/* 活动状态标签 */
.status-badge {
position: absolute;
top: 0;
left: 0;
padding: 6rpx 20rpx;
font-size: 22rpx;
border-radius: 15rpx 0rpx 15rpx 0rpx;
color: #fff;
z-index: 1;
background: repeating-linear-gradient(to right, #FD7373, #FB3F3F);
}
/* 已报名标识 */
.signed-badge {
position: absolute;
top: 20rpx;
right: 8rpx;
display: flex;
align-items: center;
padding: 6rpx 14rpx;
color: #007AFF;
font-size: 22rpx;
border-radius: 20rpx;
z-index: 1;
background-color: rgba(0, 122, 255, 0.08);
}
.signed-badge text {
margin-left: 8rpx;
}
/* 活动信息 */
.activity-info {
padding-top: 24rpx;
}
/* 活动信息 */
.info-image-left {
width: 170rpx;
height: 220rpx;
position: relative;
}
.activity-title {
font-size: 30rpx;
font-weight: 500;
color: #222;
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: 16rpx;
color: #999;
font-size: 24rpx;
line-height: 1.4;
}
.activity-detail .uni-icons {
margin-right: 15rpx;
margin-top: 4rpx;
flex-shrink: 0;
}
.detail-text-num {
font-size: 28rpx;
color: #666666;
}
.detail-text {
flex: 1;
font-size: 20rpx;
}
.detail-text-address {
flex: 1;
font-size: 22rpx;
word-break: break-all;
overflow-wrap: break-word;
white-space: pre-wrap;
}
/* 操作按钮 */
.action-buttons {
margin-top: 20rpx;
display: flex;
justify-content: flex-end;
}
.action-btn {
padding: 0 26rpx;
height: 56rpx;
line-height: 56rpx;
font-size: 24rpx;
border-radius: 28rpx;
margin: 0;
min-width: 140rpx;
}
.signup-btn {
background-color: #007AFF;
color: #fff;
}
.signup-btn:active {
background-color: #0056cc;
}
.disabled-btn {
background-color: #f2f3f5;
color: #999;
text-align: center;
}
/* 加载更多 */
.load-more-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
color: #999;
}
.load-more-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background-color: #f8f8f8;
border-radius: 50rpx;
color: #007AFF;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.load-more-btn text {
margin-right: 10rpx;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background-color: #f8f8f8;
border-radius: 50rpx;
color: #007AFF;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.loading-icon {
margin-right: 10rpx;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 空状态 */
.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

@@ -1,5 +1,6 @@
<template>
<view class="container">
<view class="status_bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-left" @tap="goBack">
@@ -8,10 +9,10 @@
<view class="nav-title">{{navTitle}}</view>
<view class="nav-right"></view>
</view>
<view style="status_bar"></view>
<!-- 内容区域 -->
<scroll-view class="content" scroll-y>
<scroll-view class="scroll-content" scroll-y="true">
<!-- 选项卡 -->
<view class="tabs">
<view
@@ -36,68 +37,67 @@
<view class="info-section">
<view class="info-item">
<view class="info-label">报名截止时间</view>
<view class="info-value">2023-08-08 23:00</view>
<view class="info-value">{{formatTime(activityConent.applyDeadline)}}</view>
</view>
<view class="info-item">
<view class="info-label">活动时间</view>
<view class="info-value">
<text>2023-08-09 10:00 2023-08-20 16:00</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>2023-08-09 10:00 2023-08-10 16:00</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">山东省日照市高新区高新管委会大厦前高新公园</view>
<view class="info-value">{{activityConent.address}}</view>
</view>
<view class="info-item">
<view class="info-label">活动名额</view>
<view class="info-value">10</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">山海天社区</view>
<view class="info-value">{{activityConent.organizer}}</view>
</view>
<view class="info-item">
<view class="info-label">联系人</view>
<view class="info-value">赵璐</view>
<view class="info-value">{{activityConent.contactName}}</view>
</view>
<view class="info-item">
<view class="info-label">联系电话</view>
<view class="info-value">15799999999</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">100</view>
</view>
<view class="info-value">{{activityConent.pointsReward}}</view>
</view> -->
</view>
<!-- 招募要求 -->
<view class="section">
<view class="section-title">招募要求</view>
<view class="section-content">
要求就是要求就是要求就是
</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>1这就是活动详情zhejiushihuodongxiangqing这就是活动详情zhejiushihuodongxiangqing</view>
<view>2活动详情zhejiushuodongxiangqing</view>
<view class="section-content rich-content">
<view v-html="formatRichText(activityConent.content)"></view>
</view>
</view>
</view>
@@ -106,9 +106,9 @@
<view v-else class="status-content">
<view class="status-header">
<view class="status-title">
<image src="/static/活动/现场实况.png" style="width: 40rpx;height: 30rpx;"></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>
@@ -118,10 +118,11 @@
<view class="record-user">
<view class="user-avatar">
<uni-icons type="person" size="20" color="#999"></uni-icons>
<!-- <image :src="record.userAvatar" mode="aspectFill"></image> -->
</view>
<view class="user-info">
<view class="user-name">微信用户</view>
<view class="record-time">{{ record.time }}</view>
<view class="user-name">{{record.userNickname || '微信用户'}}</view>
<view class="record-time">{{ record.createTime }}</view>
<view class="record-content">{{ record.content }}</view>
<!-- 图片展示 -->
<view v-if="record.images && record.images.length > 0" class="record-images">
@@ -148,9 +149,18 @@
<!-- 底部操作栏 -->
<view class="bottom-bar">
<!-- 活动已结束显示不可点击的结束按钮 -->
<button
v-if="isActivityEnded && activeTab === 'detail'"
class="action-btn ended-btn"
disabled
>
报名已结束
</button>
<!-- 未报名状态显示报名按钮 -->
<button
v-if="!hasSigned && activeTab === 'detail'"
v-if="!isActivityEnded && !hasSigned && activeTab === 'detail'"
class="action-btn signup-btn"
@tap="handleSignup"
>
@@ -159,22 +169,18 @@
<!-- 已报名状态显示取消报名按钮 -->
<button
v-if="hasSigned && activeTab === 'detail'"
v-if="hasSigned && !isActivityEnded && activeTab === 'detail'"
class="action-btn cancel-btn"
@tap="handleCancelSignup"
>
取消报名
</button>
<button class="action-btn add-status-btn" v-if="activeTab === 'status'" @click="liveClick">添加实况</button>
<!-- 已报名且在活动实况页显示添加实况按钮 -->
<!-- <button
v-if="hasSigned && activeTab === 'status'"
class="action-btn add-status-btn"
@tap="handleAddStatus"
>
添加实况
</button> -->
<!-- 活动实况页显示添加实况按钮活动开始之后结束之前显示已报名显示 -->
<button class="action-btn add-status-btn"
v-if="isActivityStarted && !isActivityEndedByTime && activeTab === 'status'&& hasSigned" @click="liveClick">
添加实况
</button>
</view>
<!-- 报名确认模态框 -->
@@ -182,8 +188,9 @@
<view class="modal-content">
<view class="modal-header">报名确认</view>
<view class="modal-body">
<view>确定要报名参加"社区'小槐花课堂'开课了"</view>
<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>
@@ -197,8 +204,8 @@
<view class="modal-content">
<view class="modal-header">取消报名</view>
<view class="modal-body">
<view>确定要取消报名"社区'小槐花课堂'开课了"</view>
<view class="modal-tip">取消后如需参加重新报名</view>
<view>{{navTitle}}</view>
<view class="modal-tip">取消后如需参加请在规定时间内重新报名</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel" @tap="hideCancelModal">再想想</view>
@@ -215,48 +222,119 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import {onLoad} from '@dcloudio/uni-app'
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'
//
const activeTab = ref('detail') // 'detail' 'status'
const hasSigned = ref(false) //
const showSignupModal = ref(false)
const showCancelModal = ref(false)
// const showAddStatusModal = ref(false)
const showToast = ref(false)
const toastMessage = ref('')
const newStatusContent = ref('')
// const newStatusImages = 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: ['/static/logo.png', '/static/引导1.png']
}
])
//
onLoad((option) =>{
// console.log('dsdsds',option)
navTitle.value = option.title
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))
})
// =============
//
const formatTime = (timeStr) => {
if (!timeStr) return ''
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
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{
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()
}
}
//
@@ -276,12 +354,41 @@ const hideSignupModal = () => {
//
const confirmSignup = () => {
hasSigned.value = true
confirmRegist()
hideSignupModal()
showToastMessage('报名成功!')
}
//
// API
//
const confirmRegist = async () =>{
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)
}
}
//
@@ -296,26 +403,77 @@ const hideCancelModal = () => {
//
const confirmCancel = () => {
hasSigned.value = false
cancelActivityData()
hideCancelModal()
showToastMessage('已取消报名')
//
// API
}
//
const cancelActivityData = async () =>{
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{
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:'/pages/activity-list/live-add'
})
uni.navigateTo({
url:`/pageSubPack/activity-list/live-add?id=${curActivityId.value}`
})
}
//
//
const getCurrentTime = () => {
return moment(date).format('YYYY-MM-DD HH:mm:ss')
}
// ---------
onUnload(() => {
uni.$off('refreshLiveList')
})
//
const showToastMessage = (message) => {
@@ -327,29 +485,29 @@ const showToastMessage = (message) => {
}, 2000)
}
//
onMounted(() => {
//
//
setTimeout(() => {
hasSigned.value = false //
}, 100)
})
</script>
<style>
<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;
}
/* 顶部导航栏 */
.nav-bar {
height: 90rpx;
/* background: linear-gradient(135deg, #007AFF 0%, #0056cc 100%); */
display: flex;
align-items: center;
padding: 0 30rpx;
@@ -357,7 +515,7 @@ onMounted(() => {
position: sticky;
top: 44px;
z-index: 100;
/* box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.2); */
flex-shrink: 0;
}
.nav-left {
@@ -370,7 +528,7 @@ onMounted(() => {
.nav-title {
flex: 1;
text-align: left;
font-size: 36rpx;
font-size: 32rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
@@ -383,28 +541,21 @@ onMounted(() => {
width: 60rpx;
}
/* 内容区域 */
.content {
/* 滚动内容区域 */
.scroll-content {
flex: 1;
margin-bottom: 120rpx;
}
.status_bar {
height: 46px;
width: 100%;
width: 100%;
box-sizing: border-box;
overflow-y: auto;
}
/* 选项卡 */
.tabs {
display: flex;
/* background-color: #fff; */
border-bottom: 1rpx solid #e9ecef;
position: sticky;
top: 90rpx;
z-index: 90;
}
.tab-item {
/* flex: 1; */
text-align: center;
padding: 30rpx 35rpx;
font-size: 32rpx;
@@ -418,31 +569,15 @@ onMounted(() => {
font-weight: 600;
}
/* .tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 6rpx;
background: linear-gradient(90deg, #007AFF, #00aaff);
border-radius: 3rpx 3rpx 0 0;
} */
/* 活动详情内容 */
.detail-content {
/* padding: 30rpx; */
margin-top: 89rpx;
padding: 0;
}
/* 基本信息区域 */
.info-section {
/* background-color: #fff; */
border-radius: 16rpx;
padding: 30rpx;
/* margin-bottom: 30rpx; */
/* box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); */
padding: 30rpx 40rpx;
}
.info-item {
@@ -471,14 +606,15 @@ onMounted(() => {
color: #222;
font-weight: 500;
line-height: 1.4;
text-align: right;
white-space: pre-wrap;
word-break: break-all;
}
/* 区块样式 */
.section {
/* background-color: #fff; */
border-radius: 16rpx;
padding: 30rpx;
/* margin-bottom: 30rpx; */
padding: 30rpx 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
@@ -493,8 +629,11 @@ onMounted(() => {
.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 {
@@ -515,12 +654,9 @@ onMounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
/* background-color: #fff; */
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
margin-top: 80rpx;
/* box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); */
}
.status-title {
@@ -532,19 +668,16 @@ onMounted(() => {
.status-count {
padding: 8rpx 20rpx;
/* background-color: #007AFF; */
color: #666;
font-size: 24rpx;
font-size: 28rpx;
border-radius: 20rpx;
font-weight: 500;
}
/* 实况列表 */
.record-list {
/* background-color: #fff; */
border-radius: 16rpx;
overflow: hidden;
/* box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); */
}
.record-item {
@@ -552,10 +685,6 @@ onMounted(() => {
border-bottom: 1rpx solid #f0f0f0;
}
/* .record-item:last-child {
border-bottom: none;
} */
.record-user {
display: flex;
align-items: flex-start;
@@ -576,11 +705,11 @@ onMounted(() => {
}
.user-info {
/* flex: 1; */
display: flex;
flex-direction: column;
justify-content: flex-start;
flex: 1;
min-width: 0;
}
.user-name {
@@ -599,8 +728,10 @@ onMounted(() => {
font-size: 28rpx;
color: #333;
line-height: 1.5;
/* margin-bottom: 20rpx; */
margin: 25rpx 0rpx;
word-break: break-all;
overflow-wrap: break-word;
white-space: pre-wrap;
}
/* 实况图片 */
@@ -612,7 +743,7 @@ onMounted(() => {
.image-item {
width: 150rpx;
height: 170rpx;
height: 150rpx;
border-radius: 12rpx;
background-color: #f8f9fa;
background-size: cover;
@@ -623,14 +754,10 @@ onMounted(() => {
/* 底部操作栏 */
.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 {
@@ -680,6 +807,12 @@ onMounted(() => {
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;
@@ -741,6 +874,11 @@ onMounted(() => {
color: #999;
margin-top: 20rpx;
}
.modal-tip-second{
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.modal-footer {
display: flex;
@@ -774,97 +912,6 @@ onMounted(() => {
background-color: #f0f7ff;
}
/* 添加实况模态框 */
.add-status-modal {
width: 650rpx;
}
.status-input {
width: 100%;
min-height: 200rpx;
padding: 20rpx;
font-size: 28rpx;
color: #333;
background-color: #f8f9fa;
border-radius: 12rpx;
border: 1rpx solid #e9ecef;
margin-bottom: 20rpx;
}
.input-counter {
text-align: right;
font-size: 24rpx;
color: #999;
margin-bottom: 30rpx;
}
.image-upload-area {
margin-top: 30rpx;
}
.upload-tip {
font-size: 24rpx;
color: #999;
margin-bottom: 20rpx;
text-align: left;
}
.image-preview {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.preview-item {
position: relative;
width: 150rpx;
height: 150rpx;
border-radius: 12rpx;
overflow: hidden;
}
.preview-image {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-color: #f8f9fa;
}
.remove-btn {
position: absolute;
top: 0;
right: 0;
width: 40rpx;
height: 40rpx;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
border-radius: 0 0 0 12rpx;
}
.upload-btn {
width: 150rpx;
height: 150rpx;
border: 2rpx dashed #e9ecef;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
background-color: #f8f9fa;
}
.upload-text {
font-size: 24rpx;
margin-top: 10rpx;
}
/* 提示信息 */
.toast {
position: fixed;
@@ -901,3 +948,25 @@ onMounted(() => {
}
}
</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

@@ -0,0 +1,415 @@
<template>
<view class="add-status-page">
<!-- 主体内容 -->
<scroll-view class="content" scroll-y>
<!-- 实况描述输入框 -->
<view class="form-item">
<text class="label">实况描述</text>
<textarea
class="textarea"
placeholder="请输入活动实况内容,也可打卡签到"
v-model="statusDesc"
maxlength="500"
@input="handleTextInput"
></textarea>
<text class="count">{{ currentLength }}/500</text>
</view>
<!-- 活动照片上传 -->
<view class="form-item">
<text class="label">活动照片</text>
<view class="upload-area">
<!-- 已上传图片 -->
<view class="uploaded-img" v-for="(img, index) in imgList" :key="index">
<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">
<text class="plus-icon">+</text>
<text class="tips">最多3张图片</text>
</view>
</view>
</view>
<!-- 签到地点 -->
<view class="form-item">
<text class="label">签到地点</text>
<view class="location-area">
<text class="location-text">{{ location }}</text>
<view class="re-locate" @click="reLocate">
<uni-icons type="refreshempty" size="14" color="#2196f3" class="re-icon"></uni-icons>
重新定位
</view>
</view>
</view>
</scroll-view>
<!-- 底部提交按钮 -->
<view class="submit-btn-area">
<button class="submit-btn" :disabled="submitting" @click="submitStatus">{{ submitting ? '提交中...' : '提交' }}</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { addActivityLive, signinActivityLive, uploadFile } from '../api/api.js';
// 天地图 Web API Key用于逆地址解析
const TIANDITU_KEY = 'b0201708e26cb56e514d74d1686f6f91';
// 活动ID
const activityId = ref('');
// 实况描述内容
const statusDesc = ref('');
// 输入字符长度
const currentLength = ref(0);
// 提交状态锁(防重复点击)
const submitting = ref(false);
// 图片列表(本地临时路径)
const imgList = ref([]);
// 定位信息
const location = ref('');
// 经纬度
const longitude = ref('');
const latitude = ref('');
// 接收活动ID
onLoad((option) => {
activityId.value = option?.id || '';
// 进入页面自动定位,获取不到时可通过“重新定位”重试
reLocate();
});
// 监听输入框字数变化
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({
count: 3 - imgList.value.length,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
imgList.value = [...imgList.value, ...res.tempFilePaths];
},
fail: (err) => {
console.error('选择图片失败:', err);
}
});
};
// 删除图片
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.showLoading({ title: '正在定位...', mask: true });
uni.getLocation({
type: 'gcj02',
isHighAccuracy: true,
success: async (res) => {
longitude.value = res.longitude;
latitude.value = res.latitude;
// 逆地址解析
const address = await reverseGeocode(res.longitude, res.latitude);
location.value = address || `${res.longitude.toFixed(6)}, ${res.latitude.toFixed(6)}`;
uni.hideLoading();
},
fail: (err) => {
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: (res) => { if (res.confirm) openAppSettings(); }
});
} else {
uni.showToast({ title: `定位失败:${errMsgStr}`, icon: 'none', duration: 3000 });
}
}
});
};
// 上传所有图片
const uploadAllImages = async () => {
if (imgList.value.length === 0) return [];
const uploadPromises = imgList.value.map(img => uploadFile(img));
const results = await Promise.all(uploadPromises);
return results.map(res => res.url);
};
// 提交实况
const submitStatus = async () => {
if (submitting.value) return;
if (!statusDesc.value.trim()) {
uni.showToast({ title: '请输入实况描述', 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,
villageId:uni.getStorageSync('currentVillageId')
};
console.log('ssasss',uni.getStorageSync('currentVillageId'))
console.log("xxx",activityId.value)
await addActivityLive(activityId.value, liveData);
uni.hideLoading();
uni.showToast({ title: '添加成功', icon: 'success' });
uni.$emit('refreshLiveList');
setTimeout(() => {
uni.navigateBack();
}, 1500);
} catch (err) {
submitting.value = false;
uni.hideLoading();
uni.showToast({ title: err.msg || '提交失败', icon: 'none' });
}
};
</script>
<style scoped>
/* 页面整体样式 */
.add-status-page {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 主体内容 */
.content {
max-height: calc(100vh - 120px);
padding: 10px 0px;
}
/* 表单项通用样式 */
.form-item {
background-color: #fff;
padding: 20px 15px;
border-radius: 8px;
margin-bottom: 8px;
}
.label {
font-size: 16px;
font-weight: 500;
margin-bottom: 6px;
display: block;
}
/* 输入框样式 */
.textarea {
width: 100%;
height: 180px;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.count {
text-align: right;
font-size: 12px;
color: #999;
margin-top: 4px;
display: block;
}
/* 图片上传区域 */
.upload-area {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.uploaded-img {
width: 80px;
height: 80px;
position: relative;
}
.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;
right: -5px;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
background-color: #ff4444;
color: #fff;
border-radius: 50%;
font-size: 16px;
}
.upload-btn {
width: 80px;
height: 80px;
/* border: 1px dashed #ddd; */
background-color: #F3F3F3;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.plus-icon {
font-size: 24px;
color: #999;
margin-bottom: 5px;
}
.tips {
font-size: 12px;
color: #999;
}
/* 定位区域 */
.location-area {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
}
.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;
}
/* .re-locate::before {
content: '';
width: 16px;
height: 16px;
background: url(/static/logo.png) no-repeat center;
background-size: 100%;
margin-right: 5px;
} */
/* 提交按钮 */
.submit-btn-area {
padding: 15px;
}
.submit-btn {
width: 100%;
height: 48px;
line-height: 48px;
background-color: #007aff;
color: #fff;
border: none;
border-radius: 8px;
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,149 @@
import request,{get,post,upload} from "./request.js"
// =======账号密码登录========
export const getLoginPassword = (data) =>{
return post('/api/resident/auth/loginByPassword',data)
}
// 注册新用户获取验证码
export const getSendCode = (data) =>{
return post('/api/resident/auth/sendSmsCode',data)
}
// 注册新用户
export const getRegisterUser = (data) =>{
return post('/api/resident/auth/register',data)
}
// 短信验证码登录
export const getLoginBySms = (data) =>{
return post('/api/resident/auth/loginBySms',data)
}
// 忘记密码接口
export const getResetPassword = (data) =>{
return post('/api/resident/auth/resetPassword',data)
}
// =========首页=======
// 获取bannar图
export const getBannarList = (data) =>{
return get('/resident/banner',data)
}
// 首页最新公告
export const getNoticeLatest = (data) =>{
return get('/api/resident/notice/latest', data)
}
// 查询已认证的小区
export const getVillageList = (data) =>{
return get('/api/resident/village/myList', data)
}
// 获取当前小区
export const getCurrentVillage = () =>{
return get('/api/resident/village/current')
}
// 切换小区
export const getVillageSwitch = (data)=>{
return post('/api/resident/village/switch',data)
}
// 首页最新活动列表
export const getActivitylatest = (data) =>{
return get('/api/resident/activity/latest',data)
}
// 重新加载用户信息
export const getReloadUserInfo = () =>{
return get('/api/resident/auth/reloadUserInfo')
}
// ========公告列表======
// 获取公告列表
export const getNoticeList = (data) =>{
return get('/api/resident/notice/list',data)
}
// 获取公告详情
export const getNoticeDetails = (noticeId) =>{
return get(`/api/resident/notice/${noticeId}`)
}
// =========键开门===============
// 查询门禁列表
export const getAccessDevices = (data) =>{
return get("/api/resident/access/devices",data)
}
// 确认开门
export const getAccessOpen = (data) =>{
return post("/api/resident/access/open",data)
}
// ==========活动列表===========
// 获取活动列表
export const getActivityList = (data) =>{
return get("/api/resident/activity/list",data)
}
// 获取活动列表详情
export const getActivityDetails = (id,data) =>{
return get(`/api/resident/activity/${id}`,data)
}
// 确认报名
export const confirmResident = (id,data) =>{
return post(`/api/resident/activity/${id}/apply`,data)
}
// 取消报名
export const cancelActivity = (id,data) =>{
return post(`/api/resident/activity/${id}/cancel`,data)
}
// =========我的活动========
export const getMyActivityList = (data) =>{
return get("/api/resident/activity/my",data)
}
// ===========活动实况============
// 活动实况列表
export const getActivityLive = (id,data) =>{
return get(`/api/resident/activity/live/${id}`,data)
}
// 添加实况
export const addActivityLive = (id, data) =>{
return post(`/api/resident/activity/${id}/live`, data)
}
// 签到
export const signinActivityLive = (id, data) =>{
return post(`/api/resident/activity/${id}/signin`, data)
}
// ==============在线报修========
// 添加报修工单
export const addQueryRepair = (data) =>{
return post("/repairOrder/addRepair",data)
}
// 获取公共维修项目列表
export const getRepairProjectList = (data) =>{
return get("/repairOrder/list",data)
}
// 获取报修列表
export const getQueryRepair = (data) =>{
return get("/repairOrder/queryRepair",data)
}
// 获取房屋列表
export const getHouseList = (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 uploadFile = (filePath) => {
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

@@ -0,0 +1,104 @@
import { navigateToLogin } from '@/common/checkLogin.js'
let baseUrl = '';
if (process.env.NODE_ENV === 'development') {
baseUrl = 'http://192.168.1.6:8080'; // 开发环境
// baseUrl = 'http://192.168.1.222:8080';
} else {
baseUrl = 'https://wuyeapi.ckdzkj.com'; // 生产环境
}
export default function request(url, data = {}, method = "GET") {
return new Promise((resolve, reject) => {
// 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,
success: (res) => {
// uni.hideLoading();
// HTTP 失败
if (res.statusCode !== 200) {
reject({ msg: `请求异常 ${res.statusCode}`, code: res.statusCode });
return;
}
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);
}
// 业务失败
else {
reject({ msg: resData.msg || '请求失败', code: resData.code, data: resData });
}
},
fail: (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 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: uploadHeader,
success: (res) => {
try {
const data = JSON.parse(res.data);
resolve(data.data || ''); // 直接返回图片地址
} catch (e) {
resolve('');
}
},
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>
@@ -35,7 +38,7 @@
<script setup>
import { ref, computed } from 'vue';
import {onLoad,onShow} from '@dcloudio/uni-app'
import{getResetPassword} from '/pages/api/api.js'
import{getResetPassword} from '../api/api.js'
// ========== ==========
@@ -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)
@@ -91,29 +94,21 @@ const handleConfirm = () => {
if(res.code === 200){
//uni.setStorageSync('token', res.token); // token
uni.showToast({ title: '修改成功', icon: 'success' });
uni.navigateTo({ url: '/pages/login/pwd-login' });
setTimeout(() =>{
uni.navigateTo({ url: '/pageSubPack/loginSub/login?mode=pwd' });
},1500)
}else{
uni.showToast({ title: "修改失败", icon: 'error' });
uni.showToast({ title: "修改失败", icon: 'none' });
}
}).catch(err =>{
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
});
})
//
// uni.showLoading({
// title: '...'
// });
// setTimeout(() => {
// uni.hideLoading();
// uni.showToast({
// title: '',
// icon: 'success'
// });
// //
// uni.navigateTo({
// url:"/pages/login/pwd-login"
// });
// }, 1500);
};
</script>
@@ -121,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{
@@ -165,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%;
@@ -172,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;
}
/* 确定按钮禁用状态 */
@@ -182,4 +203,8 @@ const handleConfirm = () => {
background-color: #99c8ff;
color: #fff;
}
/* 禁用按钮边框 */
button:after{
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="/static/登录/手机号.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="/static/登录/验证码.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"
@@ -44,7 +37,10 @@
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import {getSendCode} from '/pages/api/api.js'
import {getSendCode} from '../api/api.js'
// text number -/. canGetVerify
const inputType = 'text'
// ==========
const phoneNumber = ref('')
@@ -74,23 +70,29 @@ const getVerifyCode = () => {
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'
icon: 'none'
});
}
}).catch(err =>{
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
});
})
// 60
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
};
//
@@ -101,14 +103,19 @@ const canRegister = computed(() => {
return phoneReg.test(phoneNumber.value)
&& verifyReg.test(verifyCode.value)
});
//
const handleNext = () =>{
//
uni.setStorageSync('tempChangePwdParams', {
phoneNumber:phoneNumber.value,
verifyCode: verifyCode.value
});
if(!canRegister.value){
uni.showToast({ title: '请输入',icon: 'error'});
return
}
uni.navigateTo({
url: '/pages/loginSub/change-pwd'
url: '/pageSubPack/loginSub/change-pwd'
});
}
//
@@ -120,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;
@@ -175,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;
}
@@ -199,6 +210,9 @@ const goBack = () => {
background-color: #ccc;
color: #fff;
}
.get-verify-btn::after{
border-radius: 50px;
}
/* 下一步按钮 */
.forget-btn{
width: 100%;
@@ -206,14 +220,19 @@ 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;
}
/* 禁用按钮边框 */
.forget-btn:after{
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,471 @@
<template>
<view class="login-page">
<view class="status_bar"></view>
<!-- 登录标题 -->
<view class="login-title">{{ loginMode === 'sms' ? '手机验证码登录' : '账号密码登录' }}</view>
<!-- ====== 验证码登录模式 ====== -->
<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>
<!-- 验证码输入框 + 获取验证码按钮 -->
<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>
</template>
<!-- ====== 账号密码登录模式 ====== -->
<template v-if="loginMode === 'pwd'">
<!-- 账号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="text" v-model="account"
placeholder="请输入帐号" :inputBorder="false" maxlength="20" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 密码输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/password.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="password" v-model="password"
placeholder="请输入密码" :inputBorder="false" maxlength="16" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
</template>
<!-- 协议勾选 + 忘记密码 -->
<view class="agreement-wrap">
<view class="agreement-item">
<view @click="toggleAgreement" class="checkbox-icon">
<uni-icons type="checkbox-filled" size="24" color="#007aff" v-if="agreeAgreement"></uni-icons>
<uni-icons type="checkbox" size="24" color="#999" v-else></uni-icons>
</view>
<text class="agreement-text">我已阅读并同意
<text class="agreePrivacy" @click.stop="goToUserAgreement">用户协议</text>
<text class="agreePrivacy" @click.stop="goToPrivacy">隐私政策</text>
</text>
</view>
<text class="forget-pwd" v-if="loginMode === 'pwd'" @click="goForgetPwd">忘记密码</text>
</view>
<!-- 登录按钮 -->
<button class="login-btn" @click="handleLogin">登录</button>
<!-- 底部链接 -->
<view class="bottom-link">
<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 { 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 account = ref('') // 密码登录:账号
const password = ref('') // 密码登录:密码
const agreeAgreement = ref(false)
const placeholderStyle = ref('font-size:24rpx')
// 验证码倒计时
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(() => {
if (loginMode.value !== 'sms') return false
const phoneReg = /^1[3-9]\d{9}$/
return phoneReg.test(phoneNumber.value)
})
// ========== 共享方法 ==========
const toggleAgreement = () => { agreeAgreement.value = !agreeAgreement.value }
const goToUserAgreement = () => {
uni.navigateTo({ url: '/pageSubPack/loginSub/user-agreement' })
}
const goToPrivacy = () => {
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 handleLogin = () => {
if (loginMode.value === 'sms') {
handleSmsLogin()
} else {
handlePwdLogin()
}
}
// ========== 验证码登录 ==========
const handleSmsLogin = () => {
if (!phoneNumber.value) { uni.showToast({ title: '请输入手机号', icon: 'none' }); return }
if (!/^1[3-9]\d{9}$/.test(phoneNumber.value)) { uni.showToast({ title: '请输入正确的手机号', icon: 'none' }); return }
if (!verifyCode.value) { uni.showToast({ title: '请输入验证码', icon: 'none' }); return }
if (!/^\d{6}$/.test(verifyCode.value)) { uni.showToast({ title: '验证码为 6 位数字', icon: 'none' }); return }
if (!agreeAgreement.value) { openPrivacyPopup('login'); return }
doLogin()
}
// ========== 密码登录 ==========
const handlePwdLogin = () => {
if (!account.value) { uni.showToast({ title: '请输入账号', icon: 'none' }); return }
if (account.value.length < 4 || account.value.length > 20) { uni.showToast({ title: '账号长度需为 4-20 位', icon: 'none' }); return }
if (!password.value) { uni.showToast({ title: '请输入密码', icon: 'none' }); return }
if (password.value.length < 6) { uni.showToast({ title: '密码至少输入 6 位', icon: 'none' }); return }
if (password.value.length > 16) { uni.showToast({ title: '密码不能超过 16 位', icon: 'none' }); return }
if (/\s/.test(password.value)) { uni.showToast({ title: '密码格式不对', icon: 'none' }); return }
if (!agreeAgreement.value) { openPrivacyPopup('login'); return }
doLogin()
}
// ========== 执行登录 ==========
const doLogin = () => {
uni.showLoading({ title: '登录中...', mask: true })
const push_cid = uni.getStorageSync('push_cid')
if (loginMode.value === 'sms') {
// 验证码登录
getLoginBySms({ phone: phoneNumber.value, smsCode: verifyCode.value, scene: 'login', cid: push_cid })
.then(onLoginSuccess).catch(onLoginError)
} else {
// 密码登录
getLoginPassword({ username: account.value, password: password.value, cid: push_cid })
.then(onLoginSuccess).catch(onLoginError)
}
}
const onLoginSuccess = (res) => {
if (res.code === 200 && res.data) {
// 换号登录时清除旧用户的所有缓存,防止新用户看到旧用户的小区数据和公告
const oldUserId = uni.getStorageSync('userId')
if (res.data.userId && String(oldUserId) !== String(res.data.userId)) {
uni.removeStorageSync('home_cache')
uni.removeStorageSync('home_cache_' + oldUserId)
uni.removeStorageSync('communityName')
uni.removeStorageSync('communityId')
uni.removeStorageSync('needReloadUserInfo')
}
uni.setStorageSync('token', res.data.token)
if (res.data.userId) { uni.setStorageSync('userId', res.data.userId) }
const currentVillageId = res.data.currentVillageId || 0
uni.setStorageSync('currentVillageId', currentVillageId)
uni.hideLoading()
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => { uni.switchTab({ url: '/pages/index/index' }) }, 1500)
} else {
uni.hideLoading()
uni.showToast({ title: res.msg || '登录失败', icon: 'none' })
}
}
const onLoginError = (err) => {
uni.hideLoading()
uni.showToast({ title: err.msg || '网络异常,请重试', icon: 'none' })
}
// ========== 生命周期 ==========
onUnmounted(() => {
if (timer) { clearInterval(timer) }
})
</script>
<style scoped>
/* 页面容器 */
.login-page {
padding: 80rpx 40rpx 40rpx;
height: 100vh;
overflow: hidden;
box-sizing: border-box;
background: repeating-linear-gradient(to bottom, #E2F0FF, #F6FAFF );
}
.status_bar {
height: 160px;
width: 100%;
}
/* 登录标题 */
.login-title {
font-size: 52rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 142rpx;
}
/* 输入框项 */
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
/* 验证码输入项(特殊布局) */
.verify-item {
position: relative;
padding-right: 220rpx;
}
/* 输入框图标 */
.input-icon {
width: 50rpx;
height: 50rpx;
margin-right: 20rpx;
}
/* 输入框内容 */
.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;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #007aff;
color: #fff;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
margin: 0;
}
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
border-radius: 50px;
}
.get-verify-btn::after {
border-radius: 50px;
}
/* 协议和忘记密码行 */
.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: 24rpx;
color: #666;
margin-left: 10rpx;
line-height: 1.5;
}
.agreePrivacy {
color: #2F77FD;
}
.forget-pwd {
font-size: 24rpx;
color: #666;
flex-shrink: 0;
margin-left: 16rpx;
line-height: 1.5;
}
/* 登录按钮 */
.login-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
margin-bottom: 60rpx;
}
.login-btn:disabled {
background-color: #99c8ff;
color: #fff;
}
button:after {
border-radius: 8px;
}
/* 底部链接 */
.bottom-link {
display: flex;
justify-content: space-between;
padding: 0 20rpx;
}
.link-item {
font-size: 28rpx;
color: #007aff;
text-decoration: none;
}
</style>

View File

@@ -0,0 +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-text">
欢迎使用我们的产品/服务以下统称"服务"我们高度重视用户隐私保护
致力于为您提供安全可靠的服务体验本隐私政策旨在向您说明我们如何收集使用存储保护您的个人信息
以及您享有的相关权利请您在使用我们的服务前仔细阅读并理解本政策全部内容
一旦您使用我们的服务即视为您同意本政策的所有条款
</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>
</template>
<script setup>
const goBack = () => {
uni.navigateBack({ delta: 1 })
}
</script>
<style scoped>
.privacy-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #fff;
}
.privacy-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;
}
.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;
}
</style>

View File

@@ -0,0 +1,547 @@
<template>
<view class="register-page">
<view class="status_bar"></view>
<!-- 注册标题 -->
<view class="register-title">注册用户</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="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>
<!-- 账号输入框 -->
<view class="input-item-wrap">
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/account.png" mode=""
style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="text" v-model="account" @blur="validateAccount" @focus="clearAccountError" placeholder="请输入账号4-20位" :inputBorder="false"
maxlength="20" :placeholderStyle="placeholderStyle" />
</view>
<text class="input-error" v-if="accountError">{{ accountError }}</text>
</view>
<!-- 密码输入框 -->
<view class="input-item-wrap">
<view class="input-item">
<view class="input-icon">
<image src="https://wuye.ckdzkj.com/images/login/password.png" mode=""
style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="password" v-model="password" @blur="validatePassword" @focus="clearPasswordError" placeholder="请输入密码6-16位"
:inputBorder="false" maxlength="16" :placeholderStyle="placeholderStyle" />
</view>
<text class="input-error" v-if="passwordError">{{ passwordError }}</text>
</view>
<!-- 协议勾选 -->
<view class="agreement-item">
<view @click="toggleAgreement">
<uni-icons type="checkbox-filled" size="24" color="#007aff" v-if="agreeAgreement"></uni-icons>
<uni-icons type="checkbox" size="24" color="#999" v-else></uni-icons>
</view>
<text class="agreement-text" >我已阅读并同意
<text class="agreePrivacy" @click.stop="goToUserAgreement">用户协议</text>
<text class="agreePrivacy" @click.stop="goToPrivacy">隐私政策</text>
</text>
</view>
<!-- 立即注册按钮 -->
<button class="register-btn" @click="handleRegister">
立即注册
</button>
<!-- 跳转登录链接 -->
<view class="login-link">
<navigator class="link-item" url="/pageSubPack/loginSub/login">已有账号去登录</navigator>
</view>
<!-- 隐私协议弹窗 -->
<privacy-popup v-model:show="showPrivacyPopup" @agree="agreePrivacy" @disagree="disagreePrivacy" @close="showPrivacyPopup = false" />
</view>
</template>
<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'
// 输入框类型:统一用 text避免 number 在真机上把 -/. 过滤掉导致手机号被误判合法;非法手机号由 canGetVerify 禁用获取验证码按钮
const inputType = 'text'
// ========== 响应式数据 ==========
const phoneNumber = ref('');
const verifyCode = ref('');
const account = ref('');
const accountError = ref('');
const password = ref('');
const passwordError = ref('');
const placeholderStyle = ref('font-size:24rpx')
const agreeAgreement = ref(false);
const showPrivacyPopup = ref(false)
const pendingAction = ref('')
const countDown = ref(0);
let timer = null;
// ========== 计算属性 ==========
const canGetVerify = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
const canRegister = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
const verifyReg = /^\d{6}$/;
const accountReg = /^.{4,20}$/;
const pwdReg = /^.{6,16}$/;
return phoneReg.test(phoneNumber.value) &&
verifyReg.test(verifyCode.value) &&
accountReg.test(account.value) &&
pwdReg.test(password.value) &&
agreeAgreement.value;
});
// 实时校验密码格式(禁止颜文字/emoji/特殊符号,只允许中文、字母、数字)
watch(password, (val) => {
if (!val) { passwordError.value = ''; return }
if (val.length < 6 || val.length > 16) {
passwordError.value = '密码需 6-16 位'
} else if (/[^一-龥a-zA-Z0-9]/.test(val)) {
passwordError.value = '密码不能包含特殊符号或表情'
} else {
passwordError.value = ''
}
})
// ========== 方法 ==========
const goBack = () => {
uni.navigateBack();
};
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 账号失焦校验(禁止颜文字/emoji/特殊符号,只允许中文、字母、数字)
const validateAccount = () => {
const val = account.value
if (!val) { accountError.value = ''; return }
if (/\s/.test(val)) {
accountError.value = '账号不能包含空格'
} else if (val.length < 4 || val.length > 20) {
accountError.value = '账号需 4-20 位'
} else if (/[^一-龥a-zA-Z0-9]/.test(val)) {
accountError.value = '账号不能包含特殊符号或表情'
} else {
accountError.value = ''
}
};
// 账号聚焦清除错误提示
const clearAccountError = () => {
accountError.value = ''
};
// 密码失焦校验(禁止颜文字/emoji/特殊符号,只允许中文、字母、数字)
const validatePassword = () => {
const val = password.value
if (!val) { passwordError.value = ''; return }
if (val.length < 6 || val.length > 16) {
passwordError.value = '密码需 6-16 位'
} else if (/[^一-龥a-zA-Z0-9]/.test(val)) {
passwordError.value = '密码不能包含特殊符号或表情'
} else {
passwordError.value = ''
}
};
// 密码聚焦清除错误提示
const clearPasswordError = () => {
passwordError.value = ''
};
const goToUserAgreement = () => {
uni.navigateTo({ url: '/pageSubPack/loginSub/user-agreement' })
}
const goToPrivacy = () => {
uni.navigateTo({ url: '/pageSubPack/loginSub/privacy' })
}
const checkForm = () => {};
const openPrivacyPopup = (action) => {
pendingAction.value = action || ''
showPrivacyPopup.value = true
}
const agreePrivacy = () => {
showPrivacyPopup.value = false
agreeAgreement.value = true
if (pendingAction.value === 'register') {
doRegister()
}
pendingAction.value = ''
}
const disagreePrivacy = () => {
showPrivacyPopup.value = false
pendingAction.value = ''
}
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone: phoneNumber.value,
scene: 'register'
}
getSendCode(params).then(res => {
if (res.code === 200) {
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
} else {
uni.showToast({
title: '验证码发送失败',
icon: 'none'
});
}
}).catch(err => {
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
});
})
};
// 处理注册逻辑
const handleRegister = () => {
if (!phoneNumber.value) {
uni.showToast({ title: '请输入手机号', icon: 'none' });
return;
}
if (!/^1[3-9]\d{9}$/.test(phoneNumber.value)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
if (!verifyCode.value) {
uni.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
if (!/^\d{6}$/.test(verifyCode.value)) {
uni.showToast({ title: '验证码为 6 位数字', icon: 'none' });
return;
}
if (!account.value) {
uni.showToast({ title: '请输入账号', icon: 'none' });
return;
}
if (account.value.length < 4 || account.value.length > 20) {
uni.showToast({ title: '账号长度需为 4-20 位', icon: 'none' });
return;
}
if (/\s/.test(account.value)) {
uni.showToast({ title: '账号不能包含空格', icon: 'none' });
return;
}
if (/[^一-龥a-zA-Z0-9]/.test(account.value)) {
uni.showToast({ title: '账号不能包含特殊符号或表情', icon: 'none' });
return;
}
if (!password.value) {
uni.showToast({ title: '请输入密码', icon: 'none' });
return;
}
if (password.value.length < 6) {
uni.showToast({ title: '密码至少输入 6 位', icon: 'none' });
return;
}
if (password.value.length > 16) {
uni.showToast({ title: '密码不能超过 16 位', icon: 'none' });
return;
}
if (/[^一-龥a-zA-Z0-9]/.test(password.value)) {
uni.showToast({ title: '密码不能包含特殊符号或表情', icon: 'none' });
return;
}
if (!agreeAgreement.value) {
openPrivacyPopup('register')
return;
}
doRegister()
};
const doRegister = () => {
uni.showLoading({
title: '注册中...',
mask: true
});
let params = {
phone: phoneNumber.value,
smsCode: verifyCode.value,
username: account.value,
password: password.value
}
getRegisterUser(params).then(res => {
if (res.code === 200) {
uni.hideLoading();
uni.showToast({
title: '注册成功',
icon: 'success'
});
setTimeout(() => {
uni.navigateTo({
url: '/pageSubPack/loginSub/login?mode=pwd'
});
}, 1500);
} else {
uni.showToast({
title: '注册失败',
icon: 'none'
});
}
}).catch((err) => {
uni.hideLoading();
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
});
})
};
// ========== 生命周期 ==========
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style scoped>
/* 页面容器 */
.register-page {
padding: 20rpx 40rpx 40rpx;
height: 100vh;
overflow: hidden;
box-sizing: border-box;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF);
}
.status_bar {
height: 46px;
width: 100%;
}
.nav-bar {
display: flex;
align-items: center;
margin-bottom: 70rpx;
}
.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

@@ -0,0 +1,495 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar :title="type + '车辆'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y="true">
<view class="form-container">
<view class="form-item">
<view class="item-label">车牌号</view>
<input class="item-input" v-model="formData.carNum" placeholder="请输入"
placeholder-class="input-placeholder" maxlength="8" @input="onCarNumInput" />
</view>
<view class="form-item">
<text class="item-label">车辆品牌</text>
<input class="item-input" v-model="formData.carBrand" placeholder="请输入"
placeholder-class="input-placeholder" maxlength="32"/>
</view>
<!-- <view class="form-item" @click="selectItem('车辆品牌')">
<text class="item-label">车辆品牌</text>
<text class="item-placeholder"
:class="{selected: formData.carBrand}">{{ formData.carBrand || '请选择' }}</text>
<text class="item-arrow"></text>
</view> -->
<view class="form-item">
<view class="item-label">车辆型号</view>
<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.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="!carImageUpload.state.previewUrl" class="upload-tip">
<text class="plus-icon">+</text>
<text class="upload-text">上传照片</text>
</view>
<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" :disabled="isSubmitting" @click="saveCar">保存</button>
</view>
</view>
</template>
<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)
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'
})
// 车牌号输入限制:省份简称(中文)+ 城市代码(字母)+ 序号(字母数字)
const onCarNumInput = (e) => {
let value = e.detail.value || ''
// 过滤非法字符,只保留中文、大写字母、数字
value = value.replace(/[^一-龥A-Z0-9]/g, '')
// 首字符必须是中文省份简称
if (value.length > 0 && !/^[一-龥]/.test(value)) {
value = value.replace(/[^一-龥]/g, '')
}
// 第二位必须是字母
if (value.length > 1) {
const first = value.charAt(0)
const second = value.charAt(1)
if (!/^[A-Z]$/.test(second)) {
value = first + second.replace(/[^A-Za-z]/g, '').toUpperCase()
}
// 剩余位只允许字母和数字
if (value.length > 2) {
const rest = value.substring(2)
value = value.substring(0, 2) + rest.replace(/[^A-Za-z0-9]/g, '').toUpperCase()
}
}
// 统一转大写
value = value.toUpperCase()
formData.value.carNum = value
}
const inputChange = (typeStr) => {
if (typeStr == '车牌号') {
uni.setStorageSync('carNum', formData.value.carNum)
} 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)
}
}
const chooseImage = async () => {
try {
await carImageUpload.chooseImage()
} catch (err) {
// 用户取消选择
}
}
// 车牌号正则:省份简称 + 城市代码字母 + 序号
// 蓝牌5位字母数字新能源绿牌6位其中第3位或末位必须是 D/F
const CAR_NUM_REGEX = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤川青藏琼宁][A-Z](?:[A-Z0-9]{5}|[DF][A-Z0-9]{5}|[A-Z0-9]{5}[DF])$/
const saveCar = async () => {
if (isSubmitting.value) return
if (!formData.value.carNum) {
return uni.showToast({
title: "请输入车牌号",
icon: "none"
})
}
if (!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'
})
}
}
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'
})
}
}).catch((err) => {
isSubmitting.value = false
uni.hideLoading()
uni.showToast({
title: err.msg || '提交失败',
icon: 'none'
})
})
} else {
postCar(params).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({
title: '提交成功',
icon: 'success'
})
setTimeout(() => {
uni.removeStorageSync("carNum")
uni.removeStorageSync("carBrand")
uni.removeStorageSync("carModel")
uni.removeStorageSync("carColor")
uni.removeStorageSync("carImageUrl")
uni.removeStorageSync("carBrandId")
uni.removeStorageSync("checkType")
uni.navigateBack()
}, 1500)
} else {
isSubmitting.value = false
uni.showToast({
title: res.msg || '提交失败',
icon: 'none'
})
}
}).catch((err) => {
isSubmitting.value = false
uni.hideLoading()
uni.showToast({
title: err.msg || '提交失败',
icon: 'none'
})
})
}
}
const selectItem = (typeStr) => {
if (typeStr === '车辆品牌') {
uni.navigateTo({
url: '/pageSubPack/my/selectCarBrand'
})
}
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.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;
}
.form-container {
padding: 0 0px;
}
.form-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.item-label {
font-size: 28rpx;
color: #000;
width: 90px;
font-weight: 600;
}
.item-placeholder {
flex: 1;
font-size: 28rpx;
color: #999;
}
.item-placeholder.selected {
flex: 1;
font-size: 28rpx;
color: #333;
}
.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;
}
.bottom-btn-wrap {
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
background-color: #fff;
}
.next-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background-color: #1677ff;
color: #fff;
font-size: 36rpx;
border-radius: 12rpx;
border: none;
}
.upload-item {
align-items: flex-start;
padding-top: 40rpx;
padding-bottom: 40rpx;
border-bottom: none;
}
.upload-box {
width: 280rpx;
height: 280rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f9f9f9;
}
.upload-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.plus-icon {
font-size: 80rpx;
color: #999;
line-height: 1;
margin-bottom: 10rpx;
}
.upload-text {
font-size: 28rpx;
color: #999;
}
.car-image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.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;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,501 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar :title="type + '车位'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y="true">
<view class="form-container">
<view class="form-item" :class="{ 'form-item-disabled': type === '修改' }" @click="selectItem('选择小区')">
<text class="item-label">选择小区</text>
<text class="item-placeholder"
:class="{selected: formData.community}">{{ formData.community || '请选择' }}</text>
<text class="item-arrow" v-if="type !== '修改'"></text>
</view>
<!-- <view class="form-item" @click="selectItem('绑定房屋')">
<text class="item-label">绑定房屋</text>
<text class="item-placeholder" :class="{selected: formData.bindHouse}">{{ formData.bindHouse || '请选择' }}</text>
<text class="item-arrow"></text>
</view> -->
<view class="form-item" :class="{ 'form-item-disabled': type === '修改' }" @click="selectItem('停车场')">
<text class="item-label">停车场</text>
<text class="item-placeholder"
:class="{selected: formData.parkingLot}">{{ formData.parkingLot || '请选择' }}</text>
<text class="item-arrow" v-if="type !== '修改'"></text>
</view>
<view class="form-item" :class="{ 'form-item-disabled': type === '修改' }" @click="selectItem('车位编号')">
<text class="item-label">车位编号</text>
<text class="item-placeholder"
:class="{selected: formData.parkingSpaceNum}">{{ formData.parkingSpaceNum || '请选择' }}</text>
<text class="item-arrow" v-if="type !== '修改'"></text>
</view>
<view class="form-item">
<text class="label">车位状态</text>
<view class="parkingStatus-group">
<view class="parkingStatus-item" :class="{ active: formData.parkingStatus == '0' }"
@click="changeParkingStatus('0')">
<view class="radio"></view>
<text>闲置</text>
</view>
<view class="parkingStatus-item" :class="{ active: formData.parkingStatus == '1' }"
@click="changeParkingStatus('1')">
<view class="radio"></view>
<text>自用</text>
</view>
<view class="parkingStatus-item" :class="{ active: formData.parkingStatus == '2' }"
@click="changeParkingStatus('2')">
<view class="radio"></view>
<text>租赁</text>
</view>
</view>
</view>
<view class="form-item" :class="{ 'form-item-disabled': checkStatus === '1' }" @click="selectItem('选择业主')">
<text class="item-label">选择业主</text>
<text class="item-placeholder"
:class="{selected: formData.owner}">{{ formData.owner || '请选择' }}</text>
<text class="item-arrow" v-if="checkStatus !== '1'"></text>
</view>
<view class="form-item" @click="bindCarClick" v-if="formData.certificationStatus === '1'">
<text class="item-label">绑定车辆</text>
<text class="item-placeholder"
:class="{selected: formData.bindCar}">{{ formData.bindCar || '请选择' }}</text>
<text class="item-arrow"></text>
</view>
</view>
</scroll-view>
<view class="bottom-btn-wrap">
<button class="next-btn" @click="goSubmit">提交审核</button>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
onReachBottom,
onPullDownRefresh,
onLoad,
onShow
} from '@dcloudio/uni-app'
import {
postCarBind,
getParkingDetail
} from '@/pageSubPack/api/apiSub.js'
const statusBarHeight = ref(20)
const goBack = () => {
uni.navigateBack()
}
const id = ref(undefined)
const type = ref('')
const checkStatus = ref('')
const formData = ref({
community: uni.getStorageSync('communityName'),
communityId: uni.getStorageSync('communityId'),
// bindHouse: uni.getStorageSync('bindHouseName'),
// bindHouseId: uni.getStorageSync('bindHouseId'),
parkingLot: uni.getStorageSync('parkingLotName'),
parkingLotId: uni.getStorageSync('parkingLotId'),
parkingSpaceNum: uni.getStorageSync('parkingSpaceName'),
parkingSpaceId: uni.getStorageSync('parkingSpaceId'),
parkingStatus: uni.getStorageSync('parkingStatus') || '0',
bindCar: uni.getStorageSync('bindCarName'),
bindCarId: uni.getStorageSync('bindCarId'),
owner: uni.getStorageSync('ownerName'),
ownerId: uni.getStorageSync('ownerId'),
certificationStatus:''
})
onShow(() => {
console.log(uni.getStorageSync('bindCarName'));
formData.value.community = uni.getStorageSync('communityName')
formData.value.communityId = uni.getStorageSync('communityId')
// formData.value.bindHouse = uni.getStorageSync('bindHouseName')
// formData.value.bindHouseId = uni.getStorageSync('bindHouseId')
formData.value.parkingLot = uni.getStorageSync('parkingLotName')
formData.value.parkingLotId = uni.getStorageSync('parkingLotId')
formData.value.parkingSpaceNum = uni.getStorageSync('parkingSpaceName')
formData.value.parkingSpaceId = uni.getStorageSync('parkingSpaceId')
formData.value.parkingStatus = uni.getStorageSync('parkingStatus') || '0',
formData.value.bindCar = uni.getStorageSync('bindCarName')
formData.value.bindCarId = uni.getStorageSync('bindCarId')
formData.value.owner = uni.getStorageSync('ownerName')
formData.value.ownerId = uni.getStorageSync('ownerId')
})
onLoad((option) => {
checkStatus.value = option.checkStatus || ''
if (uni.getStorageSync('operationType') == 'update') {
if (option.id) {
id.value = option.id
uni.setStorageSync('parkingSpaceId', option.id)
loadParkingSpaceDetail()
} else {
id.value = uni.getStorageSync('parkingSpaceId')
}
type.value = '修改'
} else if (uni.getStorageSync('operationType') == 'add') {
id.value = undefined
type.value = '添加'
// 添加模式下清空上次残留的缓存,保证每次进入都是干净的
uni.removeStorageSync('communityName')
uni.removeStorageSync('communityId')
uni.removeStorageSync('parkingLotName')
uni.removeStorageSync('parkingLotId')
uni.removeStorageSync('parkingSpaceName')
uni.removeStorageSync('parkingSpaceId')
uni.removeStorageSync('parkingStatus')
uni.removeStorageSync('bindCarName')
uni.removeStorageSync('bindCarId')
uni.removeStorageSync('ownerName')
uni.removeStorageSync('ownerId')
}
})
onMounted(() => {
})
const changeParkingStatus = (type) => {
formData.value.parkingStatus = type
uni.setStorageSync('parkingStatus', type)
}
const goSubmit = () => {
if (!formData.value.community) return uni.showToast({
title: '请选择小区',
icon: 'none'
})
// if (!formData.value.bindHouse) return uni.showToast({
// title: '请绑定房屋',
// icon: 'none'
// })
if (!formData.value.parkingLot) return uni.showToast({
title: '请选择停车场',
icon: 'none'
})
if (!formData.value.parkingSpaceNum) return uni.showToast({
title: '请选择车位编号',
icon: 'none'
})
// if (!formData.value.bindCar) return uni.showToast({
// title: '请选择绑定车辆',
// icon: 'none'
// })
console.log(formData);
uni.showModal({
title: '确认提交',
content: '提交后进入审核',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '提交中...',
mask: true
})
postCarBind({
// villageId: formData.value.communityId,
// villageName: formData.value.community,
// houseId: formData.value.bindHouseId,
parkingId: formData.value.parkingSpaceId,
parkingStatus: formData.value.parkingStatus,
carId: formData.value.bindCarId,
// residentId: uni.getStorageSync('userId')
residentId: formData.value.ownerId
}).then(res => {
uni.hideLoading()
console.log(res);
if (res.code === 200) {
uni.showToast({
title: '提交成功',
icon: 'success'
})
setTimeout(() => {
uni.removeStorageSync("communityId")
uni.removeStorageSync("communityName")
uni.removeStorageSync("checkType")
uni.removeStorageSync("parkingLotId")
uni.removeStorageSync("parkingLotName")
uni.removeStorageSync("parkingSpaceId")
uni.removeStorageSync("parkingStatus")
uni.removeStorageSync("parkingSpaceName")
uni.removeStorageSync("bindCarId")
uni.removeStorageSync("bindCarName")
uni.removeStorageSync("ownerId")
uni.removeStorageSync("ownerName")
uni.navigateBack({ delta: type.value === '修改' ? 2 : 1 })
}, 1500)
} else {
uni.showToast({
title: res.msg || '提交失败',
icon: 'none'
})
}
}).catch(err => {
uni.hideLoading()
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
})
})
}
}
})
}
const loadParkingSpaceDetail = async () => {
// 防重复请求
try {
const res = await getParkingDetail({
id: id.value
})
const data = res.data
// formData.value = data
formData.value.community = data.villageName
uni.setStorageSync('communityName', data.villageName)
formData.value.communityId = data.villageId
uni.setStorageSync('communityId', data.villageId)
formData.value.parkingLot = data.areaName
uni.setStorageSync('parkingLotName', data.areaName)
formData.value.parkingLotId = data.areaId
uni.setStorageSync('parkingLotId', data.areaId)
formData.value.parkingSpaceNum = data.parkingCode
uni.setStorageSync('parkingSpaceName', data.parkingCode)
formData.value.parkingSpaceId = data.id
uni.setStorageSync('parkingSpaceId', data.id)
formData.value.parkingStatus = data.parkingStatus,
uni.setStorageSync('parkingStatus', data.parkingStatus)
formData.value.bindCar = data.carNum
uni.setStorageSync('bindCarName', data.carNum)
formData.value.bindCarId = data.carId
uni.setStorageSync('bindCarId', data.carId)
formData.value.owner = data.residentName
uni.setStorageSync('ownerName', data.residentName)
formData.value.ownerId = data.residentId
uni.setStorageSync('ownerId', data.residentId)
formData.value.certificationStatus = data.certificationStatus
} catch (err) {
uni.showToast({
title: err.msg,
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
}
}
const bindCarClick = () => {
uni.navigateTo({
url: '/pageSubPack/my/bindCar'
})
}
const selectItem = (typeStr) => {
// 修改模式下,小区、停车场、车位编号禁止修改
if (type.value === '修改' && (typeStr === '选择小区' || typeStr === '停车场' || typeStr === '车位编号')) {
return
}
if (typeStr === '选择小区') {
uni.navigateTo({
url: '/pageSubPack/my/selectResidentialCommunity?sourcePage=addParkingSpace'
})
}
// if (typeStr === '绑定房屋') {
// if (!formData.value.community) {
// return uni.showToast({ title: '请先选择上级', icon: 'none' })
// }
// uni.navigateTo({ url: '/pageSubPack/my/bindHouse' })
// }
if (typeStr === '停车场') {
if (!formData.value.community) {
return uni.showToast({
title: '请先选择上级',
icon: 'none'
})
}
uni.setStorageSync('checkType', '停车场')
uni.navigateTo({
url: '/pageSubPack/my/selectParingLot'
})
}
if (typeStr === '车位编号') {
if (!formData.value.community || !formData.value.parkingLot) {
return uni.showToast({
title: '请先选择上级',
icon: 'none'
})
}
uni.setStorageSync('checkType', '车位编号')
uni.navigateTo({
url: '/pageSubPack/my/selectParingLot'
})
}
if (typeStr === '选择业主') {
// 审核通过后选择业主不可编辑
if (checkStatus.value === '1') return
if (!formData.value.community) {
return uni.showToast({
title: '请先选择小区',
icon: 'none'
})
}
uni.navigateTo({
url: '/pageSubPack/my/selectOwner?ownerId=' + (formData.value.ownerId || '')
})
}
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.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;
}
.form-container {
padding: 0 0px;
}
.form-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.form-item-disabled {
opacity: 0.5;
pointer-events: none;
}
.item-label {
font-size: 28rpx;
color: #000;
width: 90px;
font-weight: 600;
}
.item-placeholder {
flex: 1;
font-size: 28rpx;
color: #999;
}
.item-placeholder.selected {
flex: 1;
font-size: 28rpx;
color: #333;
}
.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;
}
.label {
font-size: 28rpx;
color: #000;
width: 90px;
font-weight: 600;
}
.parkingStatus-group {
flex: 1;
display: flex;
justify-content: flex-start;
gap: 40rpx;
}
.parkingStatus-item {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
color: #666;
}
.parkingStatus-item.active {
color: #1677ff;
}
.radio {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #ddd;
position: relative;
}
.parkingStatus-item.active .radio::after {
content: '';
width: 18rpx;
height: 18rpx;
background: #1677ff;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.bottom-btn-wrap {
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
background-color: #fff;
}
.next-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background-color: #1677ff;
color: #fff;
font-size: 36rpx;
border-radius: 12rpx;
border: none;
}
</style>

View File

@@ -0,0 +1,336 @@
<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 v-if="carList.length === 0" class="empty-state">
<uni-icons type="info" size="60" 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)"
:class="{ checked: selectedIndex === index }">
<image :src="item.carImageUrl" mode="aspectFill" class="car-avatar"
@error="item.carImageUrl = defaultCarImg" />
<view class="car-info">
<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 setup>
import {
ref,
onMounted,
} from 'vue'
import {
onReachBottom,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
getMyCarList,
postCarBind
} from '@/pageSubPack/api/apiSub.js'
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 () => {
// 防重复请求
if (loadingMore.value || noMoreData.value) return
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()
})
const handleItemClick = (item, index) => {
selectedIndex.value = index
selectedId.value = item.id
selectedName.value = item.carNum
}
const confirmBind = () => {
if (selectedIndex.value === -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'
// })
// }
// })
}
}
})
}
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;
}
.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;
}
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
.car-item {
display: flex;
align-items: center;
padding: 20rpx;
margin-bottom: 20rpx;
border: 1rpx solid #e6e9ff;
border-radius: 16rpx;
background-color: #fff;
position: relative;
}
.car-item.checked {
background: #f4f7ff;
border-color: #1677ff;
}
.car-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background-color: #f0f4ff;
margin-right: 24rpx;
flex-shrink: 0;
}
.status-tag {
font-size: 24rpx;
padding: 6rpx 14rpx;
border-radius: 6rpx;
font-weight: 500;
flex-shrink: 0;
position: absolute;
right: 30rpx;
top: 30rpx;
}
.status-tag.unbound {
color: #999;
}
.status-tag.bounded {
color: #2F77FD;
}
.status-tag.underReview {
color: #FF8F40;
}
.status-tag.unpass {
color: red;
}
.car-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.car-plate {
font-size: 32rpx;
font-weight: bold;
color: #000;
line-height: 1.4;
margin-bottom: 8rpx;
}
.car-model {
font-size: 28rpx;
color: #666;
line-height: 1.4;
}
.status-tag {
font-size: 24rpx;
padding: 6rpx 14rpx;
border-radius: 6rpx;
font-weight: 500;
flex-shrink: 0;
position: absolute;
right: 30rpx;
top: 30rpx;
}
.status-tag.unbound {
color: #999999;
}
.status-tag.bound {
color: #1677ff;
}
.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

@@ -18,12 +18,18 @@
<!-- 房屋列表 -->
<scroll-view class="list-container" scroll>
<view v-if="filteredList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view>
<view class="house-card" v-for="(item, index) in filteredList" :key="index"
@click="selectHouse(item,index)" :class="{ checked: selectedIndex === index }">
<!-- 小区名称 -->
<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>
@@ -60,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();
}, 3000);
}
},
});
},
goNext() {
uni.redirectTo({
url: '/pages/my/selectParingLot',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goBack() {
uni.redirectTo({
url: '/pages/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>
@@ -224,24 +227,20 @@
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background-color: #fff; */
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
padding: 0px 40rpx;
box-sizing: border-box;
}
/* 底部按钮 */
.bottom-btn {
padding: 10px 15px 20px;
padding: 20rpx 30rpx 40rpx;
}
/* 通用输入框样式 */
.input-item {
font-size: 30rpx;
@@ -266,62 +265,13 @@
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;
align-items: center;
background-color: #F3F3F3;
border-radius: 10px;
margin: 0px 0px 10px 0rpx;
border-radius: 20rpx;
margin: 0px 0px 20rpx 0rpx;
padding: 0 30rpx;
height: 70rpx;
}
@@ -344,15 +294,32 @@
color: #999;
}
/* ====================== 列表容器 ====================== */
/* 列表容器 */
.list-container {}
/* ====================== 房屋卡片 ====================== */
/* 空状态 */
.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;
}
/* 房屋卡片 */
.house-card {
border: 1rpx solid #e5e5e5;
border-radius: 12rpx;
padding: 10px;
margin-bottom: 15px;
padding: 20rpx;
margin-bottom: 30rpx;
position: relative;
background-color: #fff;
}
@@ -363,7 +330,7 @@
}
.community-name {
font-size: 16px;
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
@@ -382,7 +349,7 @@
}
.title-text {
font-size: 16px;
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
@@ -395,32 +362,29 @@
.info-label {
width: 160rpx;
font-size: 14px;
/* font-weight: 600; */
font-size: 28rpx;
color: #000;
}
.info-value {
font-size: 14px;
font-size: 28rpx;
color: #333;
/* font-weight: 600; */
}
.status-text {
font-weight: 500;
}
/* ====================== 选中对勾 ====================== */
/* 选中对勾 */
.check-icon {
position: absolute;
right: 10px;
top: 23px;
right: 20rpx;
top: 46rpx;
transform: translateY(-50%);
width: 26px;
height: 26px;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
/* background-color: #ccc; */
}
.check-icon::after {
@@ -451,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

@@ -0,0 +1,285 @@
<template>
<view class="contentStyle">
<scroll-view class="page" scroll-y="true">
<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.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 class="photo-row">
<text class="info-label" style="width: 200rpx;">车辆图片</text>
<view class="photo-list">
<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>
</template>
<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;
}
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f7fa;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
.info-card {
background-color: #fff;
padding: 20rpx;
}
.card-title {
display: flex;
align-items: center;
font-size: 32rpx;
font-weight: 600;
color: #000;
margin-bottom: 30rpx;
}
.title-line {
width: 8rpx;
height: 28rpx;
background-color: #1677ff;
margin-right: 10rpx;
border-radius: 4rpx;
}
.info-row {
display: flex;
margin-bottom: 24rpx;
align-items: center;
}
.info-label {
width: 180rpx;
font-size: 28rpx;
color: #000;
}
.info-value {
font-size: 28rpx;
color: #333;
flex: 1;
}
.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%;
background-color: #f0f4ff;
border-radius: 8rpx;
}
.bottom-btn-wrap {
padding: 20rpx 30rpx 40rpx;
display: flex;
}
.btn-modify {
flex: 1;
height: 88rpx;
line-height: 88rpx;
background-color: #1677ff;
color: #fff;
font-size: 30rpx;
border-radius: 8rpx;
border: none;
margin-right: 20rpx;
}
.btn-delete {
flex: 1;
height: 88rpx;
line-height: 88rpx;
background-color: #fff;
color: #f53f3f;
border: 1rpx solid #f53f3f;
font-size: 30rpx;
border-radius: 8rpx;
margin-right: 0rpx;
}
</style>

View File

@@ -0,0 +1,242 @@
<template>
<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"
/>
</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"
>
{{ countdown > 0 ? `${countdown}s后重发` : '获取验证码' }}
</button>
</view>
</view>
</scroll-view>
<view class="bottom-btn">
<button class="submit-btn" @click="onSubmit">确定</button>
</view>
</view>
</template>
<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;
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;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 表单容器 */
.form-container {}
/* 输入框组(手机号+验证码行) */
.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;
}
.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-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;
}
/* 通用输入框样式 */
.input-item {
font-size: 30rpx;
color: #333;
background-color: #fff;
padding-left: 20rpx;
}
.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;
}
</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();
}, 3000);
},
goBack() {
uni.redirectTo({
url: '/pages/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;
}
@@ -145,14 +97,14 @@
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
padding: 0px 40rpx;
box-sizing: border-box;
/* background-color: #fff; */
}
/* 底部按钮 */
.bottom-btn {
padding: 10px 15px 20px;
padding: 20rpx 30rpx 40rpx;
}
/* 表单容器 */
@@ -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: '/pages/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();
}, 3000);
}
},
});
},
goBack() {
uni.redirectTo({
url: '/pages/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,25 +262,21 @@
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f5f7fa;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
padding: 0px 40rpx;
box-sizing: border-box;
}
/* 顶部状态区 */
.status-header {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0px 10px;
padding: 20rpx 0px 20rpx;
}
.status-icon {
@@ -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,13 +385,12 @@
background-color: #1677ff;
}
/* 信息卡片 */
.info-card {
padding: 20rpx;
border: 1rpx solid #f0f0f0;
border-radius: 8rpx;
background-color: #fff;
margin-bottom: 10px;
margin-bottom: 20rpx;
}
.card-title {
@@ -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: 10px 15px 20px;
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

@@ -0,0 +1,285 @@
<template>
<view class="contentStyle" style="">
<view class="page">
<view v-if="carList.length === 0" class="empty-state">
<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.carImageUrl" mode="aspectFill" class="car-avatar"
@error="item.carImageUrl = defaultCarImg" />
<view class="car-info">
<view>
<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>
<!-- certificationStatus -->
<!-- <view class="status-tag"
:class="{ underReview: item.certificationStatus === '审核中', unbound: item.certificationStatus === '未绑定', bounded: item.certificationStatus === ''}">
{{ item.certificationStatus }}
</view> -->
</view>
</view>
<view class="bottom-btn">
<button class="confirm-btn" @click="addCar">添加车辆</button>
</view>
</view>
</template>
<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)
// ==================== 下拉刷新 ====================
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')
})
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: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
loadingMore.value = false
uni.stopPullDownRefresh()
}
}
// ==================== 上拉加载更多 ====================
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;
}
</style>
<style scoped>
.contentStyle {
min-height: 100vh;
background-color: #f5f7fa;
}
.page {
padding: 0px 40rpx 140rpx;
box-sizing: border-box;
}
.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;
}
.bottom-btn {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
flex-shrink: 0;
background-color: #f5f7fa;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.car-item {
display: flex;
align-items: center;
padding: 20rpx;
margin-bottom: 20rpx;
border: 1rpx solid #e6e9ff;
border-radius: 16rpx;
background-color: #fff;
position: relative;
width: calc(100% - 44rpx);
}
.car-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background-color: #f0f4ff;
margin-right: 24rpx;
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;
}
.car-bindParkingSpace {
font-size: 28rpx;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 90%;
margin-top: 10rpx;
}
.status-tag {
font-size: 24rpx;
padding: 6rpx 14rpx;
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;
}
.confirm-btn {
width: 100%;
height: 88rpx;
background-color: #1677ff;
color: #fff;
border-radius: 12rpx;
font-size: 34rpx;
border: none;
line-height: 88rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,429 @@
<template>
<view class="contentStyle" style="">
<view class="page">
<!-- 空状态 -->
<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>
<!-- 房屋列表 v-for 渲染 -->
<view class="house-list">
<view class="house-card" v-for="(item, index) in houseList" :key="index" @click="tapHouse(item)">
<!-- 标题 + 状态 -->
<view class="card-header">
<view class="title-box">
<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.certificationStatus=='0'?'pending':item.certificationStatus=='1'?'success':item.certificationStatus=='2'?'fail':''">{{ item.certificationStatus=='0'?'认证中':item.certificationStatus=='1'?'认证成功':item.certificationStatus=='2'?'认证失败':'' }}</text>
</view>
<!-- 房屋信息 -->
<view class="card-body">
<view class="row">
<text class="label">房间号</text>
<view class="value-box">
<text class="value">{{ item.houseNo }}</text>
</view>
</view>
<view class="row">
<text class="label">业主</text>
<text class="value">{{ item.name }}</text>
</view>
<view class="row">
<text class="label">房屋状态</text>
<text
class="value">{{ item.houseStatus==0?'自住':item.houseStatus==1?'闲置':item.houseStatus==2?'租赁':item.houseStatus==3?'其他':''}}</text>
</view>
<view class="defaultBox" v-if="item.certificationStatus=='1'">
<text class="set-default">设为默认</text>
<switch class="no-cross-switch" :checked="item.isDefault==1" color="#1677ff"
@change="onSwitchChange($event,item)" @click.stop />
</view>
</view>
</view>
</view>
</view>
<!-- 底部添加按钮 -->
<view class="bottom-btn">
<button class="add-btn" @click="addHouse">添加房屋</button>
</view>
</view>
</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)
// ==================== 下拉刷新 ====================
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()
})
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'
});
} else {
uni.showToast({
title: res.msg,
icon: 'none'
});
}
} catch (err) {
// uni.hideLoading();
uni.showToast({
title: err.msg,
icon: 'none'
})
}
}
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;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
</style>
<style scoped>
.contentStyle {
min-height: 100vh;
background-color: #f5f7fa;
}
.page {
padding: 0px 40rpx 120rpx;
box-sizing: border-box;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 500rpx;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
/* 列表 */
.house-list {
}
.house-card {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
/* 头部 */
.card-header {
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.title-box {
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;
border-radius: 30rpx;
min-width: 48rpx;
}
/* 状态标签 */
.status-tag {
font-size: 28rpx;
padding: 6rpx 14rpx;
font-weight: 600;
border-radius: 6rpx;
min-width: 120rpx;
text-align: center;
}
.success {
/* background: #e6f7ee; */
color: #2F77FD;
}
.pending {
/* background: #fff4e4; */
color: #09C2BD;
}
.fail {
/* background: #ffe7e7; */
color: #E34D59;
}
/* 内容行 */
.card-body {
padding: 30rpx;
}
.row {
display: flex;
margin-bottom: 24rpx;
}
.row:last-child {
margin-bottom: 0;
}
.label {
width: 100px;
font-size: 28rpx;
/* color: #666; */
}
.value-box {
flex: 1;
display: flex;
justify-content: space-between;
}
.value {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.defaultBox {
margin-top: 10rpx;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 99 !important;
}
.set-default {
color: #222222;
/* font-weight: 600; */
font-size: 32rpx;
}
.bottom-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 30rpx 40rpx;
background-color: #f5f7fa;
}
.add-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #007aff;
color: #fff;
border-radius: 12rpx;
font-size: 32rpx;
border: none;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>
<style>
/* 去掉 switch 内部的 × 和 √ 图标 */
uni-switch::before,
uni-switch::after {
display: none !important;
}
/* 微信小程序专属去除 */
wx-switch::before,
wx-switch::after {
display: none !important;
}
/* 支付宝/百度/抖音小程序兼容 */
my-switch::before,
swan-switch::before,
tt-switch::before {
display: none !important;
}
</style>
<style>
::v-deep .uni-switch-input:before {
background-color: #cccccd !important;
}
/* 隐藏 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>

View File

@@ -0,0 +1,347 @@
<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> -->
<view class="page">
<view v-if="parkingSpaceList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<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.villageName }}</text>
</view>
<text class="status-tag"
:class="item.checkStatus=='0'?'pending':item.checkStatus=='1'?'success':item.checkStatus=='2'?'fail':''">{{ item.checkStatus=='0'?'认证中':item.checkStatus=='1'?'认证成功':item.checkStatus=='2'?'认证失败':'' }}</text>
</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.areaName }}</text>
</view>
</view>
</view>
<view class="line">
<view class="row">
<text class="label">车位编号</text>
<text class="value">{{ item.parkingCode }}</text>
</view>
</view>
<view class="line">
<view class="row">
<text class="label">车位状态</text>
<text class="value">{{ getParkingStatus(item.parkingStatus) }}</text>
</view>
</view>
<view class="line">
<view class="row">
<text class="label">绑定车辆</text>
<text class="value">{{ item.carNum }}</text>
</view>
</view>
</view>
<view class="right">
<image :src="item.carImageUrl?item.carImageUrl:'http://47.104.199.163:9090/images/propertyImgs/myParkingSpace.png'"
class="imgItem" mode="aspectFill"></image>
</view>
</view>
</view>
</view>
</view>
<view class="bottom-btn">
<button class="add-btn" @click="addParkingSpace">添加车位</button>
</view>
</view>
</template>
<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([])
// ==================== 下拉刷新 ====================
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()
})
const loadingMore = ref(false)
const noMoreData = ref(false)
const pageNum = ref(1)
const pageSize = ref(10)
// 车位状态映射
const getParkingStatus =(status) =>{
const statusMap = {
'0':'闲置',
'1':'自用',
'2':'租赁',
'3':'借出',
'4':'其他'
}
return statusMap[status]
}
// ==================== 获取列表数据 ====================
const getList = async () => {
// 防重复请求
if (loadingMore.value || noMoreData.value) return
loadingMore.value = true
try {
const res = await getCarList({
pageNum: pageNum.value,
pageSize: pageSize.value,
// residentId: uni.getStorageSync('userId')
})
const data = res
const newList = data.rows || []
// 第一页 → 覆盖数据
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()
})
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;
}
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.contentStyle {
min-height: 100vh;
background-color: #f5f7fa;
}
.page {
padding: 0px 40rpx 140rpx;
box-sizing: border-box;
}
.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;
}
.bottom-btn {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f5f7fa;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.parkingSpace-card {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
.card-header {
padding: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.title-box {
display: flex;
align-items: center;
gap: 16rpx;
}
.parkingSpace-name {
font-size: 32rpx;
font-weight: 600;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.status-tag {
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 30rpx;
margin-left: 20rpx;
white-space: nowrap;
}
.success {
background: #2F77FD;
color: #fff;
}
.pending {
background: #09C2BD;
color: #fff;
}
.fail {
background: #E34D59;
color: #fff;
}
.card-body {
padding: 0rpx 30rpx 30rpx 30rpx;
display: flex;
justify-content: space-between;
}
.line {
display: flex;
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;
}
.add-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #007aff;
color: #fff;
border-radius: 12rpx;
font-size: 32rpx;
border: none;
}
.left {
flex: 1;
min-width: 0;
overflow: hidden;
}
.right {
width: 42%;
height: 180rpx;
border-radius: 8rpx;
overflow: hidden;
flex-shrink: 0;
}
.imgItem {
width: 100%;
height: 100%;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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: '/pages/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();
}, 3000);
}
},
});
},
goBack() {
uni.redirectTo({
url: '/pages/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,24 +272,25 @@
</style>
<style scoped>
.status-bar {
width: 100vw;
height: 46px;
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
padding: 0px 40rpx;
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,14 +392,36 @@
background-color: #1677ff;
}
/* 信息卡片 */
.info-card {
/* margin: 0 30rpx 30rpx; */
padding: 20rpx;
border: 1rpx solid #f0f0f0;
background-color: #fff;
border-radius: 8rpx;
margin-bottom: 10px;
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 {
@@ -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: 10px 15px 20px;
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

@@ -0,0 +1,367 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="'选择'+checkType" left-icon="left" @clickLeft="goBackAdd" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y>
<!-- 楼栋网格列表 -->
<view 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>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
} from 'vue'
import {
onUnload,
onShow
} from '@dcloudio/uni-app'
import {
getHouseBuildings,
getHouseUnits,
getHouseFloors,
getHouseHouses
} from '/pageSubPack/api/apiSub.js'
// 状态栏高度
const statusBarHeight = ref(20)
// 从缓存获取类型
const checkType = ref(uni.getStorageSync('checkType') || '')
const dataList = ref([])
// 添加防重复点击锁
const isNavigating = ref(false)
// 选中项(从缓存读取)
const selectedBuilding = ref(uni.getStorageSync('buildingId') || '')
const selectedUnit = ref(uni.getStorageSync('unitId') || '')
const selectedFloor = ref(uni.getStorageSync('floorId') || '')
const selectedHouseNumber = ref(uni.getStorageSync('houseNumberId') || '')
// 页面挂载后赋值列表
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
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 {
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
overflow: hidden;
height: 100vh;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background-color: #fff; */
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 网格布局3列 */
.building-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30rpx;
}
/* 正在跳转时禁用点击 */
.building-grid.disabled {
pointer-events: none;
}
/* 空状态 */
.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;
}
/* 楼栋按钮 */
.building-item {
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f9f9f9;
/* border: 1rpx solid #e5e5e5; */
border-radius: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
overflow: hidden;
min-width: 0;
}
/* 选中状态 */
.building-item.active {
background-color: #2F77FD;
/* border-color: #1677ff; */
}
.building-item.active .building-text {
color: #fff;
}
/* 楼栋文字 */
.building-text {
font-size: 32rpx;
color: #333;
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
/* 成功提示弹窗 */
.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

@@ -0,0 +1,278 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="车辆品牌" left-icon="left" @clickLeft="goBackAdd" 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 class="carBrand-list">
<view v-if="filteredList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view>
<view class="carBrand-item" v-for="(item, index) in filteredList" :key="index"
@click="selectCarBrand(item)">
<text class="carBrand-name">{{ item.name }}</text>
<text class="item-arrow"></text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
} 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)
const pageNum = ref(1)
const pageSize = ref(10)
// ==================== 获取列表数据 ====================
const getList = async () => {
// 防重复请求
if (loadingMore.value || noMoreData.value) return
loadingMore.value = true
try {
const res = await getSystemDictData({
pageNum: pageNum.value,
pageSize: pageSize.value,
dictType: 'user_car_brand'
})
const data = res
const newList = data.rows || []
// 第一页 → 覆盖数据
if (pageNum.value === 1) {
carBrandList.value = newList
} else {
// 后续页 → 追加数据
carBrandList.value = [...carBrandList.value, ...newList]
}
// 判断是否没有更多数据
if (newList.length < pageSize.value) {
noMoreData.value = true
} else {
pageNum.value++
}
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
loadingMore.value = false
uni.stopPullDownRefresh() // 关闭下拉刷新
}
}
// ==================== 上拉加载更多(页面生命周期) ====================
onReachBottom(() => {
getList()
})
// ==================== 下拉刷新 ====================
const refresh = () => {
// 重置所有状态
pageNum = 1
pageSize = 10
noMoreData = false
carBrandList = []
loadingMore = false
getList()
}
onPullDownRefresh(() => {
refresh()
})
// 页面加载时请求第一页
onMounted(() => {
getList()
})
// 过滤后的列表(搜索功能)
const filteredList = computed(() => {
if (!searchKey.value.trim()) {
return carBrandList.value
}
return carBrandList.value.filter(item =>
item.name.includes(searchKey.value.trim())
)
})
// 搜索输入事件
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 {
background: #fff;
overflow: hidden;
height: 100vh;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background-color: #fff; */
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
/* 底部按钮 */
.bottom-btn {
padding: 20rpx 30rpx 40rpx;
}
/* 搜索框 */
.search-box {
display: flex;
align-items: center;
background-color: #F3F3F3;
border-radius: 20rpx;
margin: 0rpx 0px 20rpx 0px;
padding: 0 30rpx;
height: 70rpx;
}
/* 空状态 */
.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;
}
.search-input {
flex: 1;
height: 100%;
font-size: 30rpx;
border: none;
background: transparent;
}
.input-placeholder {
color: #999;
font-size: 30rpx;
}
.search-icon {
font-size: 36rpx;
color: #999;
}
/* 小区列表 */
.carBrand-list {
padding: 0 30rpx;
}
.carBrand-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.carBrand-name {
font-size: 32rpx;
color: #000;
font-weight: 500;
}
.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;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

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

@@ -0,0 +1,258 @@
<template>
<view class="contentStyle">
<view class="status-bar"></view>
<uni-nav-bar :title="'选择' + checkType" left-icon="left" @clickLeft="goBackAdd" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y>
<view 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="parkingLot-grid">
<view class="parkingLot-item"
:class="{ active: (checkType == '停车场' && selectedParkingLot === item.id) || (checkType == '车位编号' && selectedParkingSpace === item.id) }"
v-for="(item, index) in dataList" :key="index" @click="selectedParking(item)">
<text class="parkingLot-text">{{ item.name }}</text>
</view>
</view>
</scroll-view>
</view>
</template>
<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
onMounted(() => {
checkType.value = uni.getStorageSync('checkType')
if (checkType.value == '停车场') {
loadAreaList()
} else if (checkType.value == '车位编号') {
loadParkingList()
}
})
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'
});
}
})
}
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%);
overflow: hidden;
height: 100vh;
}
</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;
}
.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;
}
.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;
align-items: center;
justify-content: center;
background-color: #fff;
border: 1rpx solid #e5e5e5;
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;
}
.parkingLot-item.active .parkingLot-text {
color: #fff;
}
.parkingLot-text {
font-size: 32rpx;
color: #333;
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,259 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar title="选择小区" left-icon="left" @clickLeft="goBackAdd" 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 class="community-list">
<view v-if="filteredList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view>
<view class="community-item" v-for="(item, index) in filteredList" :key="index"
@click="selectCommunity(item)">
<text class="community-name">{{ item.villageName }}</text>
<text class="item-arrow"></text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onReachBottom,
onPullDownRefresh,
onLoad
} from '@dcloudio/uni-app'
import {
getVillageList
} from '/pageSubPack/api/apiSub.js'
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())
)
})
onLoad((option) => {
sourcePage.value = option.sourcePage;
getList()
})
// ==================== 获取列表数据 ====================
const getList = async () => {
try {
const res = await getVillageList({
})
console.log(res);
const data = res
communityList.value = data.data || []
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.error('列表接口报错:', err)
} finally {
}
}
// 搜索输入事件
const onSearch = () => {
// 实时过滤computed 自动处理
}
// 选择小区
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;
overflow: hidden;
height: 100vh;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #fff;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
/* 底部按钮 */
/* .bottom-btn {
padding: 20rpx 30rpx 40rpx;
} */
/* 空状态 */
.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;
}
/* 搜索框 */
.search-box {
display: flex;
align-items: center;
background-color: #F3F3F3;
border-radius: 20rpx;
margin: 0rpx 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;
}
.search-icon {
font-size: 36rpx;
color: #999;
}
/* 小区列表 */
.community-list {
/* padding: 0 30rpx; */
}
.community-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.community-name {
font-size: 32rpx;
color: #000;
font-weight: 500;
}
.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;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,469 @@
<template>
<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>
</view>
<switch class="no-cross-switch" v-else-if="item.type === 'switch'" :checked="item.checked"
@change="onSwitchChange(item, $event)" />
<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 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: ''
},
{
title: '修改密码',
type: 'arrow'
},
{
title: '消息通知',
type: 'switch',
checked: false
},
{
title: '清除缓存',
type: 'text',
value: ''
},
{
title: '注销账号',
type: 'arrow'
},
{
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: 0 40rpx 140rpx;
box-sizing: border-box;
background-color: #fff;
}
.bottom-btn {
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: #fff;
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.item-title {
font-size: 32rpx;
color: #333;
line-height: 1.4;
}
.item-right {
display: flex;
align-items: center;
gap: 10rpx;
}
.item-value {
font-size: 28rpx;
color: #333;
}
.item-version {
font-size: 28rpx;
color: #999;
}
.item-arrow {
width: 10rpx;
height: 10rpx;
border-top: 2rpx solid #999;
border-right: 2rpx solid #999;
transform: rotate(45deg);
margin-left: 10rpx;
}
.logout-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
text-align: center;
font-size: 32rpx;
border-radius: 12rpx;
color: #666;
background-color: #F3F3F3;
/* box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); */
}
.logout-btn:active {
background-color: #f5f5f5;
opacity: 0.8;
}
</style>
<style>
::v-deep .uni-switch-input:before {
background-color: #cccccd !important;
}
.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>

View File

@@ -0,0 +1,215 @@
<template>
<view class="page">
<!-- 列表 -->
<view class="list-wrap">
<view
v-for="notice in noticeList"
:key="notice.noticeId"
class="item">
<view class="title-box">
<view class="title">{{ notice.noticeTitle }}</view>
<view class="dot" v-if="notice.readStatus === '0'"></view>
</view>
<view class="info">{{ notice.author }} · {{ notice.publishTime }}</view>
<view class="content" v-html="notice.noticeContent"></view>
<view class="detail" @click="goToDetail(notice)">
<text style="color:#2F77FD;font-size: 24rpx;">详情</text>
<uni-icons type="right" size="13" color="#007AFF"/>
</view>
</view>
<!-- 加载中 / 无更多 / 空布局 -->
<view class="load-more" v-if="loadingMore">加载中...</view>
<view class="no-more" v-if="noMoreData && noticeList.length">没有更多通知了</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, onBackPress, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getNoticeList } from '../api/api.js'
const noticeList = 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 goToDetail = (notice) => {
uni.navigateTo({
url: `/pageSubPack/notice-list/noticeListDetails?id=${notice.noticeId}`
})
}
const loadNotices = async (isRefresh = false) => {
if (loading.value) return
loading.value = true
if (isRefresh) {
pageNum.value = 1
noMoreData.value = false
}
try {
const res = await getNoticeList({ pageNum: pageNum.value, pageSize })
if (res.code === 200) {
const list = res.data.rows || []
noticeList.value = isRefresh ? list : [...noticeList.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 = noticeList.value.find(n => String(n.noticeId) === noticeId)
if (item) {
item.readStatus = readStatus
}
}
onShow(() =>{
if(isFirstShow.value) return
loadNotices(true)
})
onPullDownRefresh(() => {
loadNotices(true)
})
onReachBottom(() => {
loadMore()
})
const loadMore = () => {
if (noMoreData.value || loadingMore.value) return
loadingMore.value = true
pageNum.value++
loadNotices()
}
onBackPress(() => {
const page = uni.getStorageSync('sourcePage')
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
uni.$on('noticeRead',updateReadStatus)
loadNotices(true)
})
onUnload(()=>{
uni.$off('noticeRead',updateReadStatus)
})
</script>
<style lang="scss">
page {
background: linear-gradient(to bottom left, #E2F0FF 0%, #f9f9f9 50%);
}
.list-wrap {
padding: 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;
}
.refresh-btn {
background: #007AFF;
color: #fff;
border-radius: 50rpx;
padding: 10rpx 30rpx;
margin-top: 20rpx;
}
</style>

View File

@@ -5,29 +5,24 @@
<!-- 文章标题 -->
<view class="article-header">
<view class="article-title">
以实干担当不断开创中国特色大国外交新局面
{{noticeDetail.noticeTitle}}
</view>
</view>
<!-- 作者和日期 -->
<view class="notice-meta">
<view class="notice-author">
<text class="author-name">张三</text>
<text class="separator">·</text>
<text class="publish-time">2026.3.6</text>
<text class="author-name">{{noticeDetail.author}}</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">
学习贯彻习近平新时代中国特色社会主义思想主题教育开展以来外交外事战线认真学习贯彻习近平总书记重要讲话精神和党中央部署要求
全面准确把握目标要求紧密联系实际精心组织周密部署紧密结合外交中心工作开展主题教育
坚持学思用贯通知信行统一把习近平新时代中国特色社会主义思想转化为坚定理想锤炼党性和指导实践
推动工作的强大力量以实干担当不断开创中国特色大国外交新局面
</view>
<view class="paragraph" v-html="noticeDetail.noticeContent"></view>
<view class="articleimg">
<image src="/static/bannar1.png" mode="" style="width: 100%;height: 100%;border-radius: 20rpx;"></image>
<image src="/static/bannar1.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>
@@ -75,9 +70,48 @@
<script setup>
import { ref, onMounted } from 'vue'
// import { onLoad, onShow } from '@dcloudio/uni-app'
import { onLoad, onShow } from '@dcloudio/uni-app'
import {getNoticeDetails} from '../api/api.js'
const isLoading = ref(false)
const noticeDetail = ref({})
const currentNoticeId = ref('') // id
// =========
onLoad((options) =>{
currentNoticeId.value = options?.id
// console.log('currentNoticeId',currentNoticeId.value)
if(currentNoticeId.value){
getDetailsData(currentNoticeId.value)
}
// console.log('currentNoticeId',currentNoticeId.value)
})
//
const getDetailsData = async (noticeId) =>{
try{
const res = await getNoticeDetails(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"
})
}
}
//
const downloadFile = (fileName) => {
uni.showModal({
@@ -139,6 +173,7 @@
.container {
min-height: 100vh;
background-color: #ffffff;
padding: 20px;
// font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
@@ -155,12 +190,14 @@
// border-bottom: 1px solid #f0f0f0;
.article-title {
font-size: 18px;
font-size: 32rpx;
font-weight: bold;
color: #333333;
line-height: 1.4;
margin-bottom: 16px;
text-align: center;
word-break: break-all;
white-space: pre-wrap;
}
.article-meta {
@@ -197,7 +234,7 @@
justify-content: center;
// margin-bottom: 12px;
color: #666;
font-size: 16px;
font-size: 24rpx;
margin: 0 30rpx;
.notice-author {
@@ -234,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

@@ -0,0 +1,313 @@
<template>
<!-- 页面主容器 -->
<view class="door-open-page">
<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 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>
<!-- 门名称 -->
<text class="door-name" :class="{active :selectedDoor === item.id}">{{ item.deviceName }}</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="doorList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无可用门禁</text>
</view>
</view>
</view>
<!-- 确认开门按钮 -->
<button class="confirm-btn" @click="openDoorClick" :disabled="selectedDoor === -1">确认开门</button>
</view>
</template>
<script setup>
import { ref } from 'vue'
import {onLoad} from '@dcloudio/uni-app'
import {getAccessDevices,getAccessOpen} from '../api/api.js'
// 响应式数据:选中的门索引、开门成功提示
const selectedDoor = ref(-1) // -1表示未选中
const showSuccess = ref(false)
// 门列表数据
const doorList = ref([])
// 选择门的方法
const selectDoor = (id) => {
selectedDoor.value = id
showSuccess.value = false // 选择新门时隐藏成功提示
}
// 查询门禁列表
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:"none"
})
}
}catch(err){
doorList.value = []
uni.showToast({
title:err.msg || '查询失败',
icon:"none"
})
}
}
// 确认开门方法
const openDoorClick = () => {
// 校验:未选择门时提示
// if (selectedDoor.value === -1) {
// uni.showToast({
// title: '请先选择要开的门',
// icon: 'none'
// })
// return
// }
uni.showToast({
title: '正在对接硬件中...',
icon: 'none'
})
// getAccessOpenData()
}
// 开门接口
const getAccessOpenData = async () =>{
uni.showLoading({
title: '开门中...',
mask: true
})
try{
let params = {deviceId:selectedDoor.value}
const res = await getAccessOpen(params)
uni.hideLoading()
if(res.code === 200){
uni.showToast({
title:'开门成功',
icon:'success'
})
}else{
uni.showToast({
title:'开门失败',
icon:"error"
})
}
}catch(err){
uni.hideLoading()
uni.showToast({
title: err.msg || '开门失败',
icon:"none"
})
}
}
onLoad(() =>{
getAccessControl()
})
</script>
<style scoped>
/* 页面全局样式 */
.door-open-page {
display: flex;
flex-direction: column;
min-height: 100%;
overflow: hidden;
background: linear-gradient(180deg, #E2F0FF, #F6FAFF);
box-sizing: border-box;
}
.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;
}
/* 提示文案 */
.tips-text {
color: #000;
margin-bottom: 30rpx;
font-size: 68rpx;
line-height: 1.5;
font-weight: 700;
font-style: italic;
clear: both;
position: relative;
top: -86px;
left: 0;
}
.tips-desc {
display: block;
font-size: 24rpx;
opacity: 0.9;
color: #666666;
}
/* 开门成功提示 */
.success-toast {
background: rgba(0, 0, 0, 0.7);
color: #ffffff;
padding: 10rpx 20rpx;
border-radius: 30rpx;
font-size: 24rpx;
position: absolute;
top: 200rpx;
left: 50%;
transform: translateX(-50%);
z-index: 99;
}
/* 开门授权容器 */
.auth-container {
background: #ffffff;
border-radius: 20rpx;
padding: 40rpx 42rpx;
margin-bottom: 60rpx;
position: relative;
top: -86px;
left: 0;
}
.auth-title {
text-align: center;
font-size: 32rpx;
font-weight: 500;
color: #333333;
padding-bottom: 20rpx;
/* border-bottom: 1px solid #e5e5e5; */
margin-bottom: 30rpx;
}
/* 网格布局 */
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30rpx;
}
/* 门选项样式 */
.door-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx 20rpx;
border-radius: 10rpx;
border: 2px solid #2F77FD;
background-color: #F6F9FE;
transition: all 0.3s;
}
/* 选中状态 */
.door-item.active {
/* border-color: #2c6bc7; */
background-color: #2F77FD;
color: #ffffff;
}
/* 门图标 */
.door-icon {
width: 126rpx;
height: 114rpx;
/* background-color: #f5f5f5; */
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 44rpx;
}
.icon-text {
/* font-size: 40rpx; */
width: 100%;
height: 100%;
}
/* 门名称 */
.door-name {
font-size: 26rpx;
color: #333333;
}
.door-name.active {
color: #ffffff;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
/* 确认按钮 */
.confirm-btn {
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: 28rpx;
border-radius: 20rpx;
border: none;
z-index: 10;
}
.confirm-btn::after {
border: none; /* 去除uni-app默认按钮边框 */
}
</style>

View File

@@ -0,0 +1,539 @@
<template>
<view class="container">
<!-- 搜索区域 -->
<view class="search-container">
<view class="search-input-wrapper">
<uni-icons class="search-icon" type="search" size="20" color="#999"></uni-icons>
<input
class="search-input"
placeholder="请输入关键字进行搜索"
placeholder-class="placeholder"
: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> -->
</view>
<!-- 小区列表 -->
<view class="community-list">
<view
v-for="item in displayedCommunities"
:key="item.villageId"
class="community-item"
:class="{ 'active-item': selectedItem && selectedItem.villageId === item.villageId }"
:id="'item-' + item.villageId"
@tap="selectCommunity(item)"
>
<view class="community-item-left">
<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>
</view>
<view v-if="selectedItem && selectedItem.villageId === item.villageId" class="selected-icon">
<uni-icons type="checkbox-filled" size="24" color="#007AFF"></uni-icons>
</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMoreData" class="load-more-container">
<view v-if="!isLoading" class="load-more-btn" @tap="loadMoreData">
<text>加载更多</text>
<uni-icons type="arrow-down" size="16" color="#007AFF"></uni-icons>
</view>
<view v-else class="loading-container">
<uni-icons type="spinner-cycle" size="20" color="#007AFF" class="loading-icon"></uni-icons>
<text>正在加载...</text>
</view>
</view>
<!-- 没有更多数据 -->
<view v-if="!hasMoreData && displayedCommunities.length > 0" class="no-more-data">
<text>没有更多数据了</text>
</view>
<!-- 空状态 -->
<view v-if="displayedCommunities.length === 0 && !isSearching && !isLoading" class="empty-state">
<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="30" color="#ccc"></uni-icons>
<text class="empty-text">未找到相关小区</text>
<text class="empty-tip">请尝试其他关键词</text>
</view>
</view>
<!-- 确认按钮 -->
<view class="confirm-area">
<button
class="confirm-btn"
:disabled="!selectedItem"
:class="{ 'disabled-btn': !selectedItem }"
@tap="handleConfirm"
>
确认选择
</button>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import { getVillageList, getVillageSwitch } from "../api/api.js"
// 响应式数据
const searchText = ref('')
const selectedItem = ref(null)
const scrollIntoViewId = ref('')
const currentVillageIdFromHome = ref('') // 首页传入的当前选中小区ID
const sourcePage = ref('') // 来源页面payment 为缴费页独立切换,其他走首页切换
// 分页相关
const currentPage = ref(1)
const pageSize = 10
const totalCount = ref(0)
const displayedCommunities = ref([])
const hasMoreData = ref(true)
const isLoading = ref(false)
// 搜索状态
const isSearching = ref(false)
let searchTimeout = null
// =====方法======
// 查询已认证小区列表
const getCommunityList = async (pageNum = 1, keyword = '') => {
isLoading.value = true
try {
// keyword 来自用户输入时是普通文本,无需 decodeURIComponent
// 仅当 keyword 来自 URL 参数时才需要解码(已在外部处理)
const params = {
pageNum,
pageSize,
keyword: keyword.trim()
}
const res = await getVillageList(params)
if (res.code === 200) {
// 兼容两种返回格式:数组 或 { 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
} else {
displayedCommunities.value = [...displayedCommunities.value, ...listData]
}
totalCount.value = total
hasMoreData.value = displayedCommunities.value.length < totalCount.value
// 默认选中:优先匹配传入的 villageId否则选第一条仅第一页时
// 搜索场景下不覆盖用户已手动选择的项
if (pageNum === 1 && displayedCommunities.value.length > 0) {
const userSelectedInCurrentList = selectedItem.value &&
displayedCommunities.value.some(
item => String(item.villageId) === String(selectedItem.value.villageId)
)
// 如果用户已选中且该项仍在当前列表中,保留用户选择
if (!isSearching.value || !userSelectedInCurrentList) {
const targetId = currentVillageIdFromHome.value || uni.getStorageSync('currentVillageId') || ''
if (targetId) {
const matched = displayedCommunities.value.find(
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'
})
}
} catch (err) {
// console.error('获取小区列表失败:', err)
displayedCommunities.value = []
hasMoreData.value = false
selectedItem.value = null
uni.showToast({
title: err.msg || "网络异常",
icon: 'error'
})
} finally {
isLoading.value = false
uni.stopPullDownRefresh()
}
}
// 页面加载时初始化
onLoad((options) => {
// 接收来源页面及选中小区id
sourcePage.value = options?.source || ''
currentVillageIdFromHome.value = options?.villageId || ''
getCommunityList(1)
})
// 加载更多数据
const loadMoreData = () => {
if (isLoading.value || !hasMoreData.value) return
getCommunityList(currentPage.value + 1, searchText.value)
currentPage.value += 1
}
// 下拉刷新
onPullDownRefresh(() => {
currentPage.value = 1
getCommunityList(1, searchText.value)
})
// 上拉加载更多
onReachBottom(() => {
loadMoreData()
})
// 处理搜索输入(防抖)
const handleSearchInput = (e) => {
let val = e.detail.value || ''
searchText.value = val
if (searchTimeout) {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(() => {
currentPage.value = 1
isSearching.value = !!val.trim()
getCommunityList(1, val.trim())
}, 300)
}
// 清空搜索
const clearSearch = () => {
searchText.value = ''
isSearching.value = false
currentPage.value = 1
getCommunityList(1)
}
// 选择小区
const selectCommunity = (item) => {
selectedItem.value = item
}
// 确认选择
const handleConfirm = async () => {
if (!selectedItem.value) {
uni.showToast({
title: '请先选择小区',
icon: 'none',
duration: 2000
})
return
}
// 缴费页为独立切换:不调用后端切换小区接口
const isPayment = sourcePage.value === 'payment'
try {
uni.showLoading({
title: isPayment ? '加载中...' : '切换小区中...',
mask: true
})
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
}
} 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)
} catch (err) {
uni.hideLoading()
uni.showToast({
title: err.msg || '网络异常,请重试',
icon: 'error',
duration: 2000
})
}
}
// 初始化
onMounted(() => {
if (displayedCommunities.value.length === 0 && !isLoading.value) {
getCommunityList(1)
}
})
</script>
<style scoped>
.container {
min-height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
padding: 40rpx;
}
.search-container {
padding: 20rpx 30rpx;
background-color: #ffffff;
display: flex;
align-items: center;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 10;
position: sticky;
top: 0;
}
.search-input-wrapper {
flex: 1;
background-color: #f8f8f8;
border-radius: 10rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
height: 80rpx;
margin-right: 20rpx;
}
.search-icon {
margin-right: 15rpx;
}
.search-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #333;
}
.placeholder {
color: #999;
font-size: 28rpx;
}
.clear-icon {
padding: 10rpx;
display: flex;
align-items: center;
justify-content: center;
}
.search-btn {
background-color: #007AFF;
color: #fff;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 10rpx;
padding: 0 30rpx;
margin: 0;
white-space: nowrap;
}
.community-list {
margin-top: 20rpx;
margin-bottom: 180rpx;
}
.community-item {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.2s;
}
.community-item:active,
.community-item.active-item {
/* background-color: #e6f2ff; */
}
.community-item-left {
display: flex;
align-items: center;
flex: 1;
}
.community-info {
display: flex;
flex-direction: column;
flex: 1;
margin-left: 20rpx;
}
.community-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
.selected-icon {
margin-left: 20rpx;
}
.load-more-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
color: #999;
}
.load-more-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background-color: #f8f8f8;
border-radius: 50rpx;
color: #007AFF;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.load-more-btn text {
margin-right: 10rpx;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background-color: #f8f8f8;
border-radius: 50rpx;
color: #007AFF;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.loading-icon {
margin-right: 10rpx;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.no-more-data {
text-align: center;
padding: 40rpx 0;
color: #999;
font-size: 28rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
.empty-tip {
font-size: 24rpx;
color: #ccc;
}
.confirm-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 30rpx 40rpx;
background-color: #fff;
box-shadow: 0 -2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.confirm-btn {
background-color: #007AFF;
color: #fff;
height: 100rpx;
line-height: 100rpx;
border-radius: 12rpx;
font-size: 34rpx;
width: 100%;
margin: 0;
}
.disabled-btn {
background-color: #ccc;
color: #fff;
}
@media (min-width: 768px) {
.container {
max-width: 500px;
margin: 0 auto;
}
}
</style>

View File

@@ -0,0 +1,524 @@
<template>
<view class="repair-page">
<!-- 报修类型Tab -->
<view class="scroll-container">
<view class="repair-type-tabs">
<view
class="type-tab"
:class="{ active: activeType === 'public' }"
@click="publicClick"
style="margin-right: 20rpx;"
>
<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>
<view
class="type-tab"
:class="{ active: activeType === 'personal' }"
@click="personalClick"
>
<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>
</view>
<view style="margin: 14px 0px;">
<text class="repair-title">报修记录</text>
</view>
<!-- 报修记录状态Tab -->
<view class="record-status-tabs">
<text
class="status-tab"
:class="{ active: activeStatus === 'pending' }"
@click="activeStatus = 'pending'" >
待派单
</text>
<text
class="status-tab"
:class="{ active: activeStatus === 'assigned' }"
@click="activeStatus = 'assigned'"
>
已派单
</text>
<text
class="status-tab"
:class="{ active: activeStatus === 'completed' }"
@click="activeStatus = 'completed'"
>
已完成
</text>
</view>
<!-- 报修列表 -->
<view class="repair-list">
<view class="repair-item" v-for="(item, index) in repairList" :key="index">
<!-- 标题和状态 -->
<view class="item-header">
<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.projectTypeStr">
<text class="tag" v-if="item.projectTypeStr">{{item.projectTypeStr}}</text>
<text class="tag">{{ item.maintenanceItemName }}</text>
</view>
<!-- 内容 -->
<view class="item-content">
{{ 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>
</view>
</view>
<!-- 时间和详情 -->
<view class="item-footer">
<text class="item-time">{{ item.createTime || '' }}</text>
<text class="detail-btn" @click="goDetail(item)">报修详情</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="repairList.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="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, computed } from 'vue';
import { onLoad, onShow, onUnload, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app';
import { getQueryRepair } from '../api/api.js';
// 报修类型public:公共报修personal:个人报修)
const activeType = ref('public');
// 报修状态pending:待派单assigned:已派单completed:已完成)
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 = {
pending: { text: '待派单', class: 'pending', value: '0' },
assigned: { text: '已派单', class: 'assigned', value: '1' },
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;
};
// 获取报修列表
// reset = true 时重置到第一页;否则按当前 pageNum 追加加载
const fetchRepairList = async (reset = false) => {
const villageId = uni.getStorageSync('currentVillageId');
if (!villageId || villageId === '0') {
uni.showToast({ title: '请先选择小区', icon: 'none' });
return;
}
// 防止重复请求
if (loading.value) return;
loading.value = true;
if (reset) {
pageNum.value = 1;
repairList.value = [];
total.value = 0;
}
try {
const params = {
villageId,
// 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.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' });
}
} catch (e) {
uni.showToast({ title: '网络异常', icon: 'none' });
} finally {
loading.value = false;
uni.stopPullDownRefresh();
}
};
// 加载更多(滚动到底部触发)
const loadMore = () => {
if (loading.value || !hasMoreData.value) return;
pageNum.value++;
fetchRepairList();
};
// 下拉刷新
onPullDownRefresh(() => {
fetchRepairList(true);
});
// 上拉加载更多
onReachBottom(() => {
loadMore();
});
// 监听状态变化自动刷新(切换 tab 时重置到第一页)
watch(activeStatus, () => {
fetchRepairList(true);
});
function publicClick(){
activeType.value = 'public'
uni.navigateTo({
url:"/pageSubPack/online-repair/publicRepair"
})
};
function personalClick(){
activeType.value = 'personal'
uni.navigateTo({
url:"/pageSubPack/online-repair/personalRepair"
})
}
// 返回上一页
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"
})
}else{
uni.navigateBack()
}
uni.removeStorageSync('sourcePage')
}
// 跳转报修详情
const goDetail = (item) => {
uni.navigateTo({
url: `/pageSubPack/online-repair/repairDetail?id=${item.id}`
});
};
onLoad(() => {
// maintenanceItemId.value = option.maintenanceProjectId || '' // 接收维修项目id
fetchRepairList(true)
});
onShow(() => {
// 从其他页面返回时刷新列表
fetchRepairList(true)
});
onUnload(() =>{
})
</script>
<style scoped>
/* 页面整体样式 */
.repair-page {
background: linear-gradient(to bottom left, #E2F0FF 0%, #f9f9f9 50%);
min-height: 100vh;
padding: 0 40rpx;
}
.status_bar {
height: var(--status-bar-height);
width: 100%;
flex-shrink: 0;
}
/* 顶部导航栏 */
.nav-bar {
height: 90rpx;
display: flex;
align-items: center;
/* padding: 0 30rpx; */
color: #fff;
position: sticky;
top: 44px;
z-index: 100;
margin-bottom: 60px;
/* box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.2); */
}
.nav-left {
width: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0 20rpx;
color: #000;
}
.nav-right {
width: 60rpx;
}
/* 滚动区域 */
.scroll-container {
width: 100%;
box-sizing: border-box;
}
/* 报修类型Tab */
.repair-type-tabs {
display: flex;
/* background-color: #fff; */
padding: 10px 0;
}
.type-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 90px;
border-radius: 8px;
color: #fff;
font-size: 14px;
}
.type-tab.active {
background: linear-gradient(135deg, #1E88E5 0%, #64B5F6 100%);
}
.type-tab:not(.active) {
background: linear-gradient(135deg, #4DB6AC 0%, #80CBC4 100%);
}
.type-text {
margin-top: 5px;
}
.repair-title{
color: #222222;
font-size: 16px;
font-weight: 600;
}
/* 报修状态Tab */
.record-status-tabs {
display: flex;
/* background-color: #fff; */
/* margin-top: 10px; */
/* padding: 0 15px; */
}
.status-tab {
/* 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;
font-weight: bold;
}
.status-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 45px;
height: 4px;
border-radius: 4rpx;
background-color: #1E88E5;
}
/* 报修列表 */
.repair-list {
padding: 28rpx 0rpx 0;
}
.repair-item {
background-color: #fff;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
}
.repair-item:last-child {
margin-bottom: 0;
}
/* 列表项头部 */
.item-header {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 10px;
}
.item-title {
font-size: 16px;
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;
}
.item-status.pending {
/* 待派单-红色 */
color: #ffffff;
background-color: #E34D59;
border-radius: 20rpx;
font-size: 12px;
line-height: 22px;
padding: 0px 10px;
}
.item-status.assigned {
/* 已派单-绿色 */
color: #ffffff;
background-color: #09C2BD;
border-radius: 20rpx;
font-size: 12px;
line-height: 22px;
padding: 0px 10px;
}
.item-status.completed{
color: #ffffff;
background-color: #2F77FD;
border-radius: 20rpx;
font-size: 12px;
line-height: 22px;
padding: 0px 10px;
}
/* 标签 */
.item-tags {
display: flex;
gap: 2px;
margin-bottom: 10px;
}
.tag {
color: #999999;
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid #999999;
margin-right: 8px;
}
/* 内容 */
.item-content {
font-size: 14px;
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;
flex-wrap: wrap;
margin: -6rpx;
margin-bottom: 16px;
}
.img-placeholder {
width: 95px;
height: 80px;
background-color: #f5f5f5;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin: 6rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-text {
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;
justify-content: space-between;
align-items: center;
font-size: 28rpx;
}
.item-time {
color: #999;
}
.detail-btn {
color:#2F77FD;
}
</style>

View File

@@ -0,0 +1,669 @@
<template>
<view class="form-container">
<!-- 报修房屋 -->
<view class="form-item">
<view class="label">
<text class="required">*</text>报修房屋
</view>
<view class="value" @click="goToSelectHouse">
{{ houseName || '请选择房屋' }}
<!-- <text class="switch" v-if="houseName">切换</text> -->
<uni-icons type="arrowright" size="16"></uni-icons>
</view>
</view>
<!-- 维修项目 -->
<view class="form-item">
<view class="label">
<text class="required">*</text>维修项目
</view>
<view class="value" @click="goToSelectProject">
{{ projectName || '请选择维修项目' }}
<uni-icons type="arrowright" size="16"></uni-icons>
</view>
</view>
<!-- 标题 -->
<view class="form-item">
<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>
<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">
<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">
<view class="label">
<text class="required">*</text>预约时间
</view>
<text class="value time-value">{{ showTimeText || '请选择预约时间' }}</text>
</view>
<!-- 问题照片 -->
<view class="form-item-wenti">
<text class="label">问题照片</text>
<view class="upload-wrap">
<!-- 已上传图片 -->
<view class="upload-item" v-for="(img, index) in form.images" :key="index">
<image :src="img" mode="aspectFill" class="upload-img" @click="previewImage(img)"></image>
<image src="https://wuye.ckdzkj.com/images/imgs/delete.png" class="upload-delete" @click="deleteImage(index)"></image>
</view>
<!-- 上传按钮 -->
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 9">
<uni-icons type="plusempty" size="24"></uni-icons>
<!-- <text>上传照片</text> -->
</view>
</view>
</view>
<!-- 提交按钮 -->
<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">
<view class="time-popup-container">
<!-- 弹窗头部取消 + 标题 + 确认 -->
<view class="time-popup-header">
<text class="cancel-btn" @click="closeTimePopup">取消</text>
<text class="popup-title">预约时间({{ tempTime.format('YYYY-MM-DD HH:mm') }})</text>
<text class="confirm-btn" @click="confirmTime">确认</text>
</view>
<!-- 时间选择列日期 | 小时 | 分钟 -->
<view class="time-selector">
<!-- 日期列 -->
<scroll-view
class="time-column"
scroll-y
:scroll-top="dateScrollTop"
@scroll="onDateScroll"
scroll-with-animation
>
<view
class="time-item"
:class="{ active: dateItem.value === selectedDate }"
v-for="(dateItem, index) in dateList"
:key="index"
@click="selectDate(index)"
>
{{ dateItem.value }}{{dateItem.label}}
</view>
</scroll-view>
<!-- 小时列 -->
<scroll-view
class="time-column"
scroll-y
:scroll-top="hourScrollTop"
@scroll="onHourScroll"
scroll-with-animation
>
<view
class="time-item"
:class="{ active: hourItem.time === selectedHour }"
v-for="(hourItem, index) in hourList"
:key="index"
@click="selectHour(index)"
>
{{ hourItem.time }}
</view>
</scroll-view>
<!-- 分钟列 -->
<scroll-view
class="time-column"
scroll-y
:scroll-top="minuteScrollTop"
@scroll="onMinuteScroll"
scroll-with-animation
>
<view
class="time-item"
:class="{ active: minuteItem.minute === selectedMinute }"
v-for="(minuteItem, index) in minuteList"
:key="index"
@click="selectMinute(index)"
>
{{ minuteItem.minute }}
</view>
</scroll-view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
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: '',
images: []
})
// ========== 基础数据 ==========
const timePopupRef = ref(null)
// 默认预约时间:今天(向上取整到下一个整点/半点)
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') }
])
const hourList = ref([
{ time: '08' }, { time: '09' }, { time: '10' }, { time: '11' },
{ time: '13' }, { time: '14' }, { time: '15' }, { time: '16' },
{ time: '17' }, { time: '18' }
])
const minuteList = ref([
{ minute: '00' },
{ minute: '30' }
])
// ========== 选中状态 ==========
const selectedDate = ref(selectedTime.value.format('YYYY-MM-DD'))
const selectedHour = ref(selectedTime.value.format('HH'))
const selectedMinute = ref(selectedTime.value.format('mm'))
// ========== 滚动配置 ==========
// 每项高度 / 可视高度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(() => {
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()
timePopupRef.value?.open()
}
const closeTimePopup = () => {
timePopupRef.value?.close()
}
const confirmTime = () => {
const selectedDateTime = `${selectedDate.value} ${selectedHour.value}:${selectedMinute.value}`
const now = moment()
if (moment(selectedDateTime, 'YYYY-MM-DD HH:mm').isBefore(now)) {
uni.showToast({ title: '预约时间不能早于当前时间', icon: 'none' })
return
}
selectedTime.value = moment(selectedDateTime, 'YYYY-MM-DD HH:mm')
form.value.datetime = selectedTime.value.format('YYYY-MM-DD HH:mm')
closeTimePopup()
uni.showToast({
title: `已选择 ${selectedTime.value.format('MM月DD日 HH:mm')}`,
icon: 'none'
})
}
// ========== 核心修复:只允许点击,滚动失效! ==========
const syncScrollPosition = () => {
const dateIdx = dateList.value.findIndex(i => i.value === selectedDate.value)
const hourIdx = hourList.value.findIndex(i => i.time === selectedHour.value)
const minIdx = minuteList.value.findIndex(i => i.minute === selectedMinute.value)
// 让选中项滚动到可视区域居中位置,并限制不小于 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
syncScrollPosition()
updateTempTime()
}
// 点击选择小时
const selectHour = (index) => {
selectedHour.value = hourList.value[index].time
syncScrollPosition() // 选中后立刻复位滚动
updateTempTime()
}
// 点击选择分钟
const selectMinute = (index) => {
selectedMinute.value = minuteList.value[index].minute
syncScrollPosition() // 选中后立刻复位滚动
updateTempTime()
}
// 完全禁用滚动监听(去掉滚动干扰)
const onDateScroll = () => {}
const onHourScroll = () => {}
const onMinuteScroll = () => {}
const updateTempTime = () => {
tempTime.value = moment(`${selectedDate.value} ${selectedHour.value}:${selectedMinute.value}`, 'YYYY-MM-DD HH:mm')
}
onMounted(() => {
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}` })
}
const goToSelectProject = () => {
uni.navigateTo({ url: '/pageSubPack/online-repair/personalSelectPoject' })
}
// 选择图片
const chooseImage = () => {
// 计算还能选几张
const remainCount = 9 - form.value.images.length;
if (remainCount <= 0) {
uni.showToast({
title: '最多只能上传9张图片',
icon: 'none'
})
return;
}
uni.chooseImage({
count: remainCount, // 动态限制可选数量
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths
form.value.images = [...form.value.images, ...tempFilePaths]
}
})
}
// 预览图片
const previewImage = (url) => {
uni.previewImage({ urls: form.value.images, current: url })
}
const deleteImage = (index) => {
form.value.images.splice(index, 1)
}
// 上传图片
const uploadImages = async () => {
const urls = []
for (const path of form.value.images) {
if (path.startsWith('http')) {
urls.push(path)
continue
}
try {
const res = await uploadFile(path)
urls.push(res.url || '')
} catch (e) {
console.error('上传失败', e)
}
}
return urls
}
// ========== 表单提交 ==========
const submitForm = async () => {
if (!houseName.value) return uni.showToast({ title: '请选择报修房屋', icon: 'none' })
if (!projectName.value) return uni.showToast({ title: '请选择维修项目', icon: 'none' })
if (!form.value.title) return uni.showToast({ title: '请输入标题', icon: 'none' })
// if (!form.value.desc) return uni.showToast({ title: '请输入问题描述', icon: 'none' })
if (!/^1[3-9]\d{9}$/.test(form.value.phone)) return uni.showToast({ title: '手机号不正确', icon: 'none' })
if (!form.value.datetime) return uni.showToast({ title: '请选择预约时间', icon: 'none' })
if (moment(form.value.datetime, 'YYYY-MM-DD HH:mm').isBefore(moment())) return uni.showToast({ title: '预约时间不能早于当前时间', icon: 'none' })
const villageId = uni.getStorageSync('currentVillageId')
if (!villageId || villageId === '0') return uni.showToast({ title: '请先选择小区', icon: 'none' })
const userId = uni.getStorageSync('userId')
if (!userId) return uni.showToast({ title: '用户信息失效', icon: 'none' })
submitting.value = true
uni.showLoading({ title: '提交中...' ,mask:true})
try {
const imageUrls = await uploadImages()
const params = {
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 = ''
projectId.value = ''
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()
}
}
</script>
<style scoped>
.form-container {
padding: 15px;
background: #fff;
min-height: 100vh;
}
.form-item {
display: flex;
margin-bottom: 30rpx;
align-items: flex-start;
border-bottom: 1px solid #eee;
}
.label {
width: 180rpx;
font-size: 28rpx;
color: #333;
line-height: 80rpx;
}
.required {
color: red;
margin-right: 4rpx;
}
.value {
flex: 1;
font-size: 28rpx;
color: #666;
line-height: 80rpx;
text-align: right;
display: flex;
justify-content: space-between;
}
.time-value {
white-space: nowrap;
font-size: 26rpx;
}
.input, .textarea {
flex: 1;
padding: 20rpx;
font-size: 28rpx;
border: 1px solid #eee;
border-radius: 8rpx;
}
.textarea {
min-height: 160rpx;
line-height: 40rpx;
}
.form-item-wenti {
margin-bottom: 20px;
/* border-bottom: 1px solid #eee; */
padding-bottom: 10px;
}
.textarea-wrap {
flex: 1;
position: relative;
}
.desc-textarea {
width: 100%;
height: 160rpx;
font-size: 28rpx;
padding: 10rpx 0;
box-sizing: border-box;
border: 1px solid #eee;
border-radius: 4px;
padding: 5px;
}
.char-count {
position: absolute;
right: 10rpx;
bottom: 10rpx;
font-size: 24rpx;
color: #999;
}
.switch {
font-size: 12px;
color: #007aff;
margin-right: 5px;
}
input, textarea {
font-size: 14px;
width: 100%;
padding: 8px 0;
box-sizing: border-box;
}
textarea {
resize: none;
}
.upload-wrap {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 8px;
}
.upload-item {
position: relative;
width: 80px;
height: 80px;
}
.upload-item .upload-img{
width: 100%;
height: 100%;
border-radius: 4px;
}
.upload-item .upload-delete{
position: absolute;
top: -5px;
right: -5px;
width: 32rpx;
height: 32rpx;
border-radius: 3rpx;
padding: 2px;
}
.upload-btn {
width: 80px;
height: 80px;
/* border: 1px dashed #ccc; */
background-color: #f3f3f3;
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
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;
color: #fff;
border-radius: 8px;
height: 44px;
line-height: 44px;
font-size: 16px;
margin-top: 20px;
}
/* ========== uni-popup 时间选择器样式 ========== */
.time-popup-container {
background-color: #fff;
border-top-left-radius: 16rpx;
border-top-right-radius: 16rpx;
}
/* 弹窗头部 */
.time-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1px solid #eee;
}
.cancel-btn {
font-size: 28rpx;
color: #666;
}
.popup-title {
font-size: 28rpx;
color: #333;
}
.confirm-btn {
font-size: 28rpx;
color: #007aff;
font-weight: 600;
}
/* 时间选择器容器 */
.time-selector {
display: flex;
height: 400rpx;
padding: 20rpx 0;
}
/* 单个时间列 */
.time-column {
flex: 1;
height: 100%;
text-align: center;
}
/* 时间项 */
.time-item {
height: 80rpx;
line-height: 80rpx;
font-size: 26rpx;
color: #BBBCBE;
position: relative;
white-space: nowrap;
}
/* 选中项样式(高亮) */
.time-item.active {
color: #1D2129;
font-weight: 600;
background-color: #F7F7F7;
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<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"
:class="{ active: activeCategory === item.id }"
@click="selectCategory(item)"
v-for="item in categoryList"
:key="item.id"
>
{{ item.projectName }}
</view>
</scroll-view>
<!-- 右侧项目 -->
<scroll-view scroll-y class="category-right">
<view
class="project-item"
@click="selectProject(item)"
v-for="item in currentProjectList"
:key="item.id"
>
{{ item.projectName }}
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getRepairProjectList } from '../api/api.js'
// 分类列表
const categoryList = ref([])
// 所有项目数据
const projectList = ref([])
// 选中的分类
const activeCategory = ref(1)
// 从接口获取个人报修项目列表
const fetchProjectList = async () => {
try {
const res = await getRepairProjectList({ projectType: '1' })
if (res.code === 200) {
const data = res.data || []
categoryList.value = data.map(item => ({ id: item.id, projectName: item.projectName }))
projectList.value = data.flatMap(item =>
(item.children || []).map(child => ({
id: child.id,
categoryId: item.id,
projectName: child.projectName
}))
)
activeCategory.value = categoryList.value[0]?.id || 1
}
} catch (e) {
uni.showToast({ title: '获取项目列表失败', icon: 'none' })
}
}
onLoad(() => {
fetchProjectList()
})
// 搜索关键词
const searchKey = ref('')
// 筛选当前分类的项目(含搜索)
const currentProjectList = computed(() => {
let list = projectList.value.filter(item => item.categoryId === activeCategory.value)
if (searchKey.value) {
list = list.filter(item => item.projectName.includes(searchKey.value))
}
return list
})
// 选择分类
const selectCategory = (item) => {
activeCategory.value = item.id
}
// 选择项目并返回
const selectProject = (item) => {
// 获取分类名称
const categoryName = categoryList.value.find(c => c.id === item.categoryId)?.projectName || ''
// 用全局事件发送参数
uni.$emit('selectProject', {
name: `${categoryName}-${item.projectName}`,
id: item.id
})
// 返回上一页
uni.navigateBack({
delta: 1
})
}
</script>
<style scoped>
.select-container {
padding: 10px;
height: 100vh;
box-sizing: border-box;
background-color: #f9f9f9;
display: flex;
flex-direction: column;
}
.search-bar {
display: flex;
align-items: center;
padding: 8px 10px;
background: #f5f5f5;
border-radius: 6px;
margin-bottom: 10px;
flex-shrink: 0;
}
.search-bar input {
flex: 1;
font-size: 14px;
}
.category-wrap {
display: flex;
flex: 1;
min-height: 0;
}
.category-left {
width: 120px;
background: #fff;
border-radius: 6px 0 0 6px;
margin-right: 20rpx;
color: #666;
}
.category-item {
padding: 15px 10px;
text-align: center;
font-size: 14px;
border-bottom: 1px solid #eee;
}
.category-item.active {
background: #f9f9f9;
color: #222;
font-weight: bold;
}
.category-right {
flex: 1;
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 {
background: #f5f5f5;
}
</style>

View File

@@ -0,0 +1,372 @@
<template>
<view class="form-container">
<!-- 维修项目展示 -->
<view class="form-item">
<!-- <text class="label">维修项目</text> -->
<view class="label">
<text class="required">*</text>维修项目
</view>
<view class="value" @click="goBackSelect">
<view>
<text v-if="!repairProject">请选择项目</text>
{{ repairProject }}
</view>
<uni-icons type="arrowright" size="16"></uni-icons>
</view>
</view>
<!-- 标题输入 -->
<view class="form-item">
<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>
<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">
<view class="label">
<text class="required">*</text>手机号
</view>
<uni-easyinput type="number" v-model="form.phone" placeholder="请输入手机号" :inputBorder="false"></uni-easyinput>
</view>
<!-- 图片上传 -->
<view class="form-item-wenti">
<text class="label">问题照片</text>
<view class="upload-wrap">
<!-- 已上传图片 -->
<view class="upload-item" v-for="(img, index) in form.images" :key="index">
<image :src="img" mode="aspectFill" class="upload-img"></image>
<image src="https://wuye.ckdzkj.com/images/imgs/delete.png" class="upload-delete" @click="deleteImage(index)"></image>
<!-- <uni-icons type="close" size="16" @click="deleteImage(index)"></uni-icons> -->
</view>
<!-- 上传按钮 -->
<view class="upload-btn" @click="chooseImage" v-if="form.images.length < 9">
<uni-icons type="plusempty" size="24"></uni-icons>
<!-- <text>上传照片</text> -->
</view>
</view>
</view>
<!-- 提交按钮 -->
<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,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: '',
desc: '',
phone: '',
images: [] // 图片列表
})
// 页面加载
onLoad((options) => {
// 第一次进入页面接收参数
if (options.category && options.project) {
repairProject.value = `${options.category}-${options.project}`
projectId.value = options.projectId
form.value.title = `${options.category}-${options.project}`
}
// ✅ 监听返回选择的项目
uni.$on('selectRepairProject', (data) => {
repairProject.value = `${data.category}-${data.project}`
projectId.value = data.projectId
form.value.title = `${data.category}-${data.project}`
})
// 自动获取当前登录用户手机号
getUserProfile().then(res => {
if (res.code === 200 && res.data && res.data.phone) {
form.value.phone = res.data.phone
}
})
})
// ✅ 页面卸载时移除监听(必须加)
onUnload(() => {
uni.$off('selectRepairProject')
})
// 返回选择项目页
const goBackSelect = () => {
uni.navigateTo({
url:"/pageSubPack/online-repair/repairSelect"
})
// uni.navigateBack({
// delta: 1
// })
}
// 选择图片
const chooseImage = () => {
// 计算还能选几张
const remainCount = 9 - form.value.images.length;
if (remainCount <= 0) {
uni.showToast({
title: '最多只能上传9张图片',
icon: 'none'
})
return;
}
uni.chooseImage({
count: remainCount, // 动态限制可选数量
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths
form.value.images = [...form.value.images, ...tempFilePaths]
}
})
}
// 删除图片
const deleteImage = (index) => {
form.value.images.splice(index, 1)
}
// 上传图片
const uploadImages = async () => {
const uploadedUrls = []
for (const path of form.value.images) {
if (path.startsWith('http')) {
uploadedUrls.push(path)
continue
}
try {
const res = await uploadFile(path)
// 根据后端实际返回结构调整字段名
uploadedUrls.push(res.url || '')
} catch (err) {
uni.showToast({
title:e.msg || '图片上传失败',
icon:'error'
})
}
}
return uploadedUrls
}
// 表单提交
const submitForm = async () => {
// 表单校验
if (!repairProject.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
}
const villageId = uni.getStorageSync('currentVillageId')
if (!villageId || villageId === '0') {
uni.showToast({ title: '请先选择小区', icon: 'none' })
return
}
submitting.value = true
uni.showLoading({ title: '提交中...' ,mask:true})
try {
// 先上传图片
const imageUrls = await uploadImages()
const userId = uni.getStorageSync('userId')
const params = {
villageId,
maintenanceItemId:projectId.value,
item: form.value.title,
problem: form.value.desc,
residentId:userId,//人员id
// repairTypeName: repairProject.value,
phone: form.value.phone,
problemImageUrl: imageUrls.join(','),
projectType: '0',
problemStatus:''
}
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 = ''
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()
}
}
</script>
<style scoped>
.form-container {
padding: 15px;
background: #fff;
min-height: 100vh;
}
.form-item-wenti {
margin-bottom: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.form-item {
display: flex;
margin-bottom: 30rpx;
align-items: flex-start;
border-bottom: 1px solid #eee;
}
.label {
width: 180rpx;
font-size: 28rpx;
color: #333;
line-height: 80rpx;
}
.required {
color: red;
margin-right: 4rpx;
}
.value {
flex: 1;
font-size: 28rpx;
color: #666;
line-height: 80rpx;
text-align: right;
display: flex;
justify-content: space-between;
}
.input, .textarea {
font-size: 14px;
width: 100%;
padding: 8px 0;
box-sizing: border-box;
}
.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;
gap: 10px;
margin-top: 8px;
}
.upload-item {
position: relative;
width: 80px;
height: 80px;
}
.upload-item .upload-img{
width: 100%;
height: 100%;
border-radius: 4px;
}
.upload-item .upload-delete{
position: absolute;
top: -5px;
right: -5px;
width: 32rpx;
height: 32rpx;
border-radius: 3rpx;
padding: 2px;
}
.upload-btn {
width: 80px;
height: 80px;
/* border: 1px dashed #ccc; */
background-color: #f3f3f3;
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
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;
color: #fff;
border-radius: 6px;
height: 44px;
line-height: 44px;
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,325 @@
<template>
<view class="repair-detail-container">
<!-- 维修流程模块 -->
<view class="info-section">
<view class="section-title">维修流程</view>
<uni-row class="demo-uni-row" :width="nvueWidth">
<uni-col :span="12">
<up-steps :current="currentStep" direction="column" space="30" active-color="#007aff">
<up-steps-item title="待处理" desc="提交报修成功"></up-steps-item>
<up-steps-item title="已派单" desc="维修师傅已接单"></up-steps-item>
<up-steps-item title="已完成" desc="维修完成"></up-steps-item>
</up-steps>
</uni-col>
<uni-col :span="12">
<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>
<!-- 报修信息模块 -->
<view class="info-section">
<view class="section-title">报修信息</view>
<view class="info-item" v-if="!repairInfo.isPublic">
<view class="info-label">报修房屋</view>
<view class="info-value">{{repairInfo.house}}</view>
</view>
<view class="info-item">
<view class="info-label">维修项目</view>
<view class="info-value">{{repairInfo.project}}</view>
</view>
<view class="info-item">
<view class="info-label">标题</view>
<view class="info-value">{{repairInfo.title}}</view>
</view>
<view class="info-item-desc">
<view class="info-label">问题描述</view>
<view class="info-value multi-line">{{repairInfo.description}}</view>
</view>
<view class="info-item">
<view class="info-label">手机号</view>
<view class="info-value">{{repairInfo.phone}}</view>
</view>
<view class="info-item">
<view class="info-label">报修时间</view>
<view class="info-value">{{repairInfo.time}}</view>
</view>
<view class="info-item-desc">
<view class="info-label">问题照片</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>
<!-- 联系物业按钮 -->
<view class="btn-container">
<button class="contact-btn" @click="contactProperty">联系物业</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getRepairDetail } from '../api/api.js'
const nvueWidth = ref('300')
// 报修信息数据
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'
})
}
</script>
<style scoped>
.repair-detail-container {
padding: 16px;
padding-bottom: 100px;
background-color: #f5f5f5;
min-height: 100vh;
}
.process-section,
.info-section {
background-color: #fff;
border-radius: 8px;
padding: 13px;
margin-bottom: 16px;
}
.repair-steps-container {
padding: 20rpx 30rpx;
background-color: #fff;
}
.title {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 30rpx;
color: #333;
}
.step-time {
height: 54px;
line-height: 54px;
font-size: 24rpx;
color: #999;
}
.step-time-3{
height: 38px;
line-height: 38px;
font-size: 24rpx;
color: #999;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 12px;
color: #333;
}
.slot-title {
position: absolute;
top: -10px;
font-size: 12px;
font-weight: 400;
}
.slot-desc {
font-size: 10px;
}
.info-item {
display: flex;
padding: 12px 0;
}
.info-item-desc {
padding: 12px 0;
}
.info-section.active {
color: #007AFF;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
width: 80px;
color: #222;
font-size: 14px;
}
.info-value {
flex: 1;
color: #666;
font-size: 14px;
}
.multi-line {
white-space: pre-wrap;
word-break: break-all;
line-height: 1.5;
border: 1px solid #f0f0f0;
border-radius: 8rpx;
padding: 30rpx;
margin: 30rpx 0;
}
.photo-container {
display: flex;
flex-wrap: wrap;
margin-top: 4px;
margin-right: -12px;
margin-bottom: -12px;
}
.photo-item {
width: 95px;
height: 80px;
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 {
background-color: #007AFF;
color: #fff;
border: none;
border-radius: 8px;
height: 48px;
font-size: 16px;
width: 100%;
line-height: 3;
}
.contact-btn::after {
border: none;
}
/* ========== 颜色的代码 ========== */
/* 激活的步骤标题 + 描述 强制蓝色 */
:deep(.u-steps-item__content--current .u-text__value--main) {
color: #007aff !important;
}
:deep(.u-steps-item__content--current .u-text__value--tips) {
color: #007aff !important;
}
</style>

View File

@@ -17,7 +17,7 @@
v-for="item in categoryList"
:key="item.id"
>
{{ item.name }}
{{ item.projectName }}
</view>
</scroll-view>
@@ -29,7 +29,7 @@
v-for="item in currentProjectList"
:key="item.id"
>
{{ item.name }}
{{ item.projectName }}
</view>
</scroll-view>
</view>
@@ -38,24 +38,43 @@
<script setup>
import { ref, computed } from 'vue'
// import { uniNavigateTo } from '@dcloudio/uni-app'
import { onLoad } from '@dcloudio/uni-app'
import { getRepairProjectList } from '../api/api.js'
//
const categoryList = ref([
{ id: 1, name: '基础设施' },
{ id: 2, name: '电子设施' }
])
const categoryList = ref([])
//
const projectList = ref([
{ id: 101, categoryId: 1, name: '路灯' },
{ id: 102, categoryId: 1, name: '楼层指示灯' },
{ id: 103, categoryId: 2, name: '应急出口灯' },
{ id: 104, categoryId: 1, name: '草坪灯' }
])
const projectList = ref([])
//
const activeCategory = ref(1)
//
const fetchProjectList = async () => {
try {
const res = await getRepairProjectList({ projectType: '0' })
if (res.code === 200) {
const data = res.data || []
// [{id, name, children: [{id, name}]}]
categoryList.value = data.map(item => ({ id: item.id, projectName: item.projectName }))
projectList.value = data.flatMap(item =>
(item.children || []).map(child => ({
id: child.id,
categoryId: item.id,
projectName: child.projectName
}))
)
activeCategory.value = categoryList.value[0]?.id || 1
}
} catch (e) {
uni.showToast({ title: '获取项目列表失败', icon: 'none' })
}
}
onLoad(() => {
fetchProjectList()
})
//
const searchKey = ref('')
@@ -63,7 +82,7 @@ const searchKey = ref('')
const currentProjectList = computed(() => {
let list = projectList.value.filter(item => item.categoryId === activeCategory.value)
if (searchKey.value) {
list = list.filter(item => item.name.includes(searchKey.value))
list = list.filter(item => item.projectName.includes(searchKey.value))
}
return list
})
@@ -72,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)?.name
//
uni.navigateTo({
url: `/pages/online-repair/publicRepair?category=${categoryName}&project=${item.name}`
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>
@@ -89,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;
@@ -97,6 +127,7 @@ const selectProject = (item) => {
background: #f5f5f5;
border-radius: 6px;
margin-bottom: 10px;
flex-shrink: 0;
}
.search-bar input {
flex: 1;
@@ -104,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;
@@ -119,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

@@ -0,0 +1,183 @@
<template>
<view class="house-container">
<!-- 房屋列表 -->
<view class="house-list">
<view
class="house-item"
:class="{ active: selectedHouse.id === item.id }"
@click="selectHouse(item)"
v-for="item in houseList"
:key="item.id"
>
<image src="https://wuye.ckdzkj.com/images/repair/house.png" style="width: 48rpx;height: 48rpx;"></image>
<view class="house-name">
{{ item.villageName }}
<view class="house-detail">{{item.unitNo}}-{{item.floorNo}}-{{item.houseNo}}</view>
</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>
<!-- 确认按钮固定底部不随内容滚动 -->
<view class="confirm-bar">
<button class="confirm-btn" @click="confirmSelect">确认</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getHouseList } from '../api/api.js'
// 房屋列表数据
const houseList = ref([])
// 选中的房屋
const selectedHouse = ref({})
// 分页参数
const pageNum = ref(1)
const pageSize = ref(10)
// 预传入的房屋ID从表单页带过来
const preSelectedHouseId = ref('')
// 获取房屋列表
const fetchHouseList = async () => {
try {
const res = await getHouseList({ pageNum: pageNum.value, pageSize: pageSize.value })
if (res.code === 200) {
const list = res.data || []
houseList.value = list
// 仅当从表单页带入了房屋ID时自动选中对应房屋否则不默认选择
if (preSelectedHouseId.value) {
const found = houseList.value.find(item => String(item.id) === String(preSelectedHouseId.value))
if (found) {
selectedHouse.value = found
}
}
}
} catch (e) {
uni.showToast({ title: '获取房屋列表失败', icon: 'none' })
}
}
onLoad((options) => {
if (options.houseId) {
preSelectedHouseId.value = options.houseId
}
fetchHouseList()
})
// 选择房屋
const selectHouse = (item) => {
selectedHouse.value = item
}
// 确认选择并返回
const confirmSelect = () => {
if (!selectedHouse.value.id) {
uni.showToast({ title: '请选择房屋', icon: 'none' })
return
}
// 把选中的房屋信息传给表单页通过uni.$emit
uni.$emit('selectHouse', {
id: selectedHouse.value.id,
name: `${selectedHouse.value.villageName} ${selectedHouse.value.unitNo}-${selectedHouse.value.floorNo}-${selectedHouse.value.houseNo}`,
residentId: selectedHouse.value.residentId
})
// 返回上一页
uni.navigateBack({ delta: 1 })
}
</script>
<style scoped>
.house-container {
padding: 15px;
height: 100vh;
box-sizing: border-box;
background: #f9f9f9;
}
.house-list {
height: 100%;
overflow-y: auto;
padding-bottom: 120rpx;
}
.house-item {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 15px 10px;
background-color: #fff;
border-radius: 8px;
margin-bottom: 10px;
}
.house-item.active {
border-color: #007aff;
background: #f0f8ff;
border: 1px solid #2F77FD;
}
.house-name {
font-size: 16px;
color: #333;
font-weight: 500;
display: flex;
flex-direction: column;
align-items: flex-start;
margin-left: 30rpx;
width: 80%;
}
.house-detail {
font-size: 12px;
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;
line-height: 44px;
background: #007aff;
color: #fff;
border-radius: 8px;
font-size: 28rpx;
}
/* 空状态 */
.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

@@ -0,0 +1,555 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="'新增'+paymentType+'缴费'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="card-box">
<!-- 头部物业费A标题+水滴图标 -->
<view class="card-header">
<image class="iconStyle"
:src="paymentType=='水表'?'https://wuye.ckdzkj.com/images/propertyImgs/water.png':'https://wuye.ckdzkj.com/images/propertyImgs/electric.png'">
</image>
<text class="card-title">{{paymentType}}</text>
</view>
<!-- 分割线 -->
<view class="line"></view>
<view class="input-item">
<view class="input-label">缴费单位</view>
<text class="company-name">花开物业</text>
</view>
<!-- 分割线 -->
<view class="line"></view>
<!-- 设备号输入项 -->
<view class="input-item">
<view class="input-label">缴费设备号</view>
<view class="input-wrap">
<!-- 输入框 -->
<input class="input" placeholder="请输入设备号" placeholder-class="input-placeholder"
v-model="deviceNo" />
<!-- 右边扫码图标 text标签直接用 双端不乱码 -->
<image class="scanTheCodeIconStyle"
src="https://wuye.ckdzkj.com/images/propertyImgs/scanTheCodeIcon.png" @click="scanCode">
</image>
</view>
</view>
<!-- 底部分割线 -->
<view class="line"></view>
</view>
<!-- 底部协议和提交按钮 -->
<view class="bottom-area">
<!-- <view class="agreement">
<checkbox-group @change="handleAgreeChange">
<checkbox :checked="isAgree" />
</checkbox-group>
<text class="agreement-text">我已阅并同意</text>
<text class="agreement-link" @click="handleProtocol">协议链接</text>
</view> -->
<button class="submit-btn" @click="goPay">
提交
</button>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
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)
// 生命周期
onMounted(() => {})
// 扫码
const scanCode = () => {
// uni.showLoading({
// title: '扫码中...',
// mask: true
// })
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 = async () => {
if (!deviceNo.value) {
uni.showToast({
title: '请输入设备号',
icon: 'none'
})
return
}
uni.showModal({
title: '确认提交',
content: '确认要提交吗?',
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '提交中...',
mask: true
})
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'
})
setTimeout(() => {
goNext()
}, 1500)
} catch (err) {
uni.hideLoading()
uni.showToast({
title: err.msg || '网络异常',
icon: 'none'
})
console.error('绑定设备失败:', err)
}
}
}
})
}
// 跳转成功页
const goNext = () => {
uni.navigateTo({
url: '/pageSubPack/payment-list/addPaymentSuccess?type=bind'
})
}
// 返回
const goBack = () => {
uni.navigateBack();
}
</script>
<style lang="scss">
page {
background: #f2f1f6;
}
</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;
}
/* 白色卡片容器 */
.card-box {
background: #ffffff;
border-radius: 24rpx;
padding: 32rpx;
}
/* 头部物业费A标题 */
.card-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 32rpx;
}
.iconStyle {
width: 88rpx;
height: 88rpx;
}
.scanTheCodeIconStyle {
width: 48rpx;
height: 48rpx;
padding-left: 20rpx;
}
.water-icon {
font-size: 32rpx;
}
.card-title {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
/* 分割线 */
.line {
width: 100%;
height: 1rpx;
background: #eeeeee;
}
/* 输入项整体 */
.input-item {
padding: 32rpx 0;
}
.input-label {
font-size: 28rpx;
color: #333;
margin-bottom: 24rpx;
}
/* 输入框+扫码图标横向布局 */
.input-wrap {
display: flex;
align-items: center;
justify-content: space-between;
}
.input {
flex: 1;
font-size: 32rpx;
color: #333;
}
/* 占位符样式 */
.input-placeholder {
color: #cccccc;
}
/* 底部协议+按钮区域 */
.bottom-area {
margin-top: 60rpx;
padding: 0 10rpx;
}
/* 协议勾选 */
.agree-item {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 40rpx;
}
.radio-box {
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.radio-icon {
font-size: 32rpx;
color: #999;
}
.agree-text {
font-size: 28rpx;
color: #666;
}
.agree-link {
font-size: 28rpx;
color: #007aff;
}
/* 立即缴费按钮 */
.pay-btn {
width: 100%;
height: 96rpx;
background: #007aff;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.pay-text {
font-size: 36rpx;
color: #ffffff;
}
/* 白色卡片容器 */
.card-box {
background: #ffffff;
border-radius: 24rpx;
padding: 32rpx;
}
/* 头部物业费A标题 */
.card-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 32rpx;
}
.water-icon {
font-size: 48rpx;
}
.card-title {
font-size: 36rpx;
color: #333;
font-weight: 500;
}
/* 分割线 */
.line {
width: 100%;
height: 1rpx;
background: #eeeeee;
}
/* 输入项整体 */
.input-item {
padding: 32rpx 0;
}
.input-label {
font-size: 32rpx;
color: #333;
margin-bottom: 24rpx;
}
/* 输入框+扫码图标横向布局 */
.input-wrap {
display: flex;
align-items: center;
justify-content: space-between;
}
.input {
flex: 1;
font-size: 32rpx;
color: #333;
}
/* 占位符样式 */
.input-placeholder {
color: #cccccc;
}
/* 右边扫码图标 text标签直接用 双端不乱码 */
.scan-icon {
font-size: 40rpx;
color: #999;
padding-left: 20rpx;
}
/* 底部协议+按钮区域 */
.bottom-area {
margin-top: 60rpx;
padding: 0 10rpx;
}
/* 协议勾选 */
.agree-item {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 40rpx;
}
.radio-box {
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.radio-icon {
font-size: 32rpx;
color: #999;
}
.agree-text {
font-size: 28rpx;
color: #666;
}
.agree-link {
font-size: 28rpx;
color: #007aff;
}
/* 立即缴费按钮 */
.pay-btn {
width: 100%;
height: 96rpx;
background: #007aff;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.pay-text {
font-size: 36rpx;
color: #ffffff;
}
.status-bar {
width: 100vw;
height: 46px;
}
/* 底部区域 */
.bottom-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 30rpx;
box-sizing: border-box;
}
.agreement {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.agreement checkbox {
transform: scale(0.8);
margin-right: 10rpx;
}
.agreement-text {
font-size: 24rpx;
color: #333;
}
.agreement-link {
font-size: 28rpx;
color: #4080ff;
margin-left: 10rpx;
}
.submit-btn {
width: 100%;
height: 90rpx;
line-height: 90rpx;
background-color: #4080ff;
color: #fff;
border-radius: 10rpx;
font-size: 36rpx;
font-weight: 500;
border: none;
outline: none;
}
.submit-btn[disabled] {
background-color: #a0c4ff;
color: #fff;
opacity: 0.7;
}
</style>

View File

@@ -4,25 +4,26 @@
<uni-nav-bar :title="'新增'+paymentType+'缴费'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<Loading mask="true" click="true" ref="loading"></Loading>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 顶部蓝色标题栏 -->
<view class="header-card">
<view class="header-icon">
<text class="icon-fire">🔥</text>
<image class="iconStyle"
:src="paymentType=='燃气'?'https://wuye.ckdzkj.com/images/propertyImgs/gas.png':'https://wuye.ckdzkj.com/images/propertyImgs/heating.png'">
></image>
</view>
<text class="header-title">燃气费</text>
<text class="header-title">{{paymentType+'费'}}</text>
</view>
<!-- 缴费信息卡片 -->
<view class="info-card">
<view class="form-item">
<view class="form-item" @click="goToChange()">
<text class="label">缴费单位</text>
<text class="company-name">XXXXXXXX燃气有限公司</text>
<text class="company-name">XXXXXXXX有限公司</text>
</view>
<view class="form-item">
@@ -34,21 +35,7 @@
</view>
</view>
<!-- 底部协议和提交按钮 -->
<view class="bottom-area">
<view class="agreement">
<checkbox-group @change="handleAgreeChange">
<checkbox :checked="isAgree" />
</checkbox-group>
<text class="agreement-text">已阅并同意</text>
<text class="agreement-link" @click="handleProtocol">协议链接</text>
</view>
<button class="submit-btn" @click="handleSubmit">
提交
</button>
</view>
</view>
@@ -81,133 +68,131 @@
</scroll-view>
<!-- 底部协议和立即缴费按钮 -->
<view class="bottom-area">
<view class="agreement">
<checkbox-group @change="handleAgreeChange">
<checkbox :checked="isAgree" />
</checkbox-group>
<text class="agreement-text">我已阅并同意</text>
<text class="agreement-link" @click="handleProtocol">协议链接</text>
</view>
<button class="submit-btn" @click="handleSubmit">
立即缴费
</button>
</view>
</view>
</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()
}, 3000);
}
}
})
},
goNext() {
uni.redirectTo({
url: '/pages/payment-list/addPaymentSuccess',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goBack() {
uni.redirectTo({
url: '/pages/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 {
background: #f2f1f6;
background: #f9f9f9;
}
</style>
@@ -223,37 +208,39 @@
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
padding: 0px 40rpx;
box-sizing: border-box;
background-color: #fff;
}
.container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
box-sizing: border-box;
border-radius: 10rpx;
}
/* 顶部标题栏 */
.header-card {
background: linear-gradient(135deg, #66b3ff 0%, #4080ff 100%);
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx 40rpx;
background: #fff;
padding: 30rpx;
display: flex;
align-items: center;
color: #fff;
border-bottom: 2rpx solid #ededed;
}
.header-icon {
width: 60rpx;
height: 60rpx;
width: 56rpx;
height: 56rpx;
background-color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.iconStyle {
width: 100%;
height: 100%;
}
.icon-fire {
@@ -262,20 +249,22 @@
}
.header-title {
font-size: 16px;
font-size: 32rpx;
font-weight: bold;
}
/* 信息卡片 */
.info-card {
background-color: #fff;
border-radius: 0 0 20rpx 20rpx;
padding: 40rpx;
border-radius: 10rpx;
/* padding: 30rpx; */
margin-bottom: 40rpx;
}
.form-item {
margin-bottom: 40rpx;
/* margin-bottom: 40rpx; */
border-bottom: 2rpx solid #ededed;
padding: 30rpx;
}
.form-item:last-child {
@@ -284,14 +273,14 @@
.label {
display: block;
font-size: 14px;
color: #565656;
font-size: 28rpx;
color: #222;
margin-bottom: 20rpx;
}
.company-name {
display: block;
font-size: 16px;
font-size: 32rpx;
color: #333;
font-weight: 600;
}
@@ -304,7 +293,7 @@
.input-field {
flex: 1;
font-size: 16px;
font-size: 32rpx;
color: #333;
padding: 10rpx 0;
border: none;
@@ -316,7 +305,7 @@
}
.help-link {
font-size: 14px;
font-size: 28rpx;
color: #4080ff;
white-space: nowrap;
margin-left: 20rpx;
@@ -328,15 +317,14 @@
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 15px;
padding: 30rpx;
box-sizing: border-box;
}
.agreement {
display: flex;
align-items: center;
margin-bottom: 30rpx;
margin-bottom: 20rpx;
}
.agreement checkbox {
@@ -345,12 +333,12 @@
}
.agreement-text {
font-size: 14px;
font-size: 24rpx;
color: #333;
}
.agreement-link {
font-size: 14px;
font-size: 28rpx;
color: #4080ff;
margin-left: 10rpx;
}
@@ -406,15 +394,15 @@
}
.popup-title {
font-size: 16px;
line-height: 30px;
font-size: 32rpx;
line-height: 60rpx;
font-weight: bold;
color: #333;
width: 100%;
display: flex;
/* align-items: center; */
justify-content: space-evenly;
border-bottom: 1px solid #ebebeb;
border-bottom: 2rpx solid #ebebeb;
}
.popup-close {
@@ -460,20 +448,20 @@
}
.item-title {
font-size: 16px;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.item-desc {
font-size: 14px;
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
/* 弹窗动画 */
@keyframes slideUp {
/* @keyframes slideUp {
from {
transform: translateY(100%);
}
@@ -481,7 +469,7 @@
to {
transform: translateY(0);
}
}
} */
/* 成功提示弹窗 */
.toast-mask {

View File

@@ -0,0 +1,195 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<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">{{ pageTitle }}</view>
<!-- 提示文案 -->
<view class="desc" v-html="descText"></view>
<!-- 操作按钮 -->
<view class="btn-wrap">
<button class="btn-back" @click="handlePrimaryClick">{{ btnText }}</button>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
// 响应式数据
const statusBarHeight = ref(20)
const paymentType = ref(uni.getStorageSync('addPaymentType'))
const pageType = ref('bind') // 'bind' | 'pay'
// 页面配置:根据类型返回对应文案和跳转
const pageConfig = computed(() => {
const configs = {
bind: {
navTitle: '新增' + paymentType.value + '缴费',
pageTitle: '绑定成功',
descText: '恭喜您绑定成功费用将在24小时内更新<br>请您按时查看,避免造成费用欠费',
btnText: '返回生活缴费',
backUrl: '/pageSubPack/payment-list/paymentIndex'
},
pay: {
navTitle: '缴费支付',
pageTitle: '支付成功',
descText: '恭喜您支付成功,感谢您的使用',
btnText: '返回生活缴费',
backUrl: '/pageSubPack/payment-list/paymentIndex' // TODO: 如有缴费记录页请修改
}
}
return configs[pageType.value] || configs.bind
})
const navTitle = computed(() => pageConfig.value.navTitle)
const pageTitle = computed(() => pageConfig.value.pageTitle)
const descText = computed(() => pageConfig.value.descText)
const btnText = computed(() => pageConfig.value.btnText)
// 生命周期
onLoad((options) => {
if (options?.type) {
pageType.value = options.type
}
})
onShow(() => {})
onMounted(() => {})
onReady(() => {})
// 方法
const goBack = () => {
uni.navigateBack();
}
const handlePrimaryClick = () => {
const url = pageConfig.value.backUrl
uni.navigateTo({ url });
}
</script>
<style lang="scss">
page {
background: #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;
}
</style>
<style scoped>
/* 全局容器 */
.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: 30rpx;
}
.icon-check {
color: #fff;
font-size: 130rpx;
font-weight: bold;
line-height: 1;
}
/* 主标题 */
.title {
font-size: 44rpx;
font-weight: bold;
color: #000;
margin-bottom: 40rpx;
}
/* 提示文案 */
.desc {
font-size: 32rpx;
color: #666;
line-height: 1.6;
text-align: center;
margin-bottom: 150rpx;
}
/* 按钮容器 */
.btn-wrap {
width: 100%;
}
/* 返回按钮 */
.btn-back {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background-color: #1677ff;
color: #fff;
font-size: 36rpx;
border-radius: 8rpx;
border: none;
padding: 0;
margin: 0;
}
/* 按钮默认样式重置解决uni-app button默认样式问题 */
.btn-back::after {
border: none;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,681 @@
<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.deviceCode || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">{{ paymentItem.name || '-' }}</text>
</view>
<view class="info-item">
<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 class="input-section" style="display: flex;">
<text class="amount-input" style="width: auto;margin: 0rpx 20rpx;">¥</text>
<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" v-if="outstandingPayment==true">
<text class="arrears-text">当前欠费金额¥{{ arrearsAmount }}</text>
</view>
<!-- <view class="arrears-row" v-if="outstandingPayment==false">
<text class="arrears-text">暂未查询到欠费</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) || '' }}费用 ¥{{ arrearsAmount || 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'
// 响应式数据
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
}
}
// 返回
const goBack = () => {
uni.navigateBack();
}
// 立即缴费 — 通过微信拉起日照银行小程序进行支付
const handlePay = () => {
const amount = arrearsAmount.value
try {
uni.showLoading({ title: '加载中...' })
// 设备类型映射电费→1(电表)水费→2(水表)
const deviceTypeMap = { '电费': '1', '水费': '2' }
const mappedDeviceType = deviceTypeMap[paymentItem.type] || paymentItem.deviceType || ''
// 直接从本地参数拼 path 查询字符串传给支付小程序,不走接口
const token = uni.getStorageSync('token') || ''
const params = {
acct: uni.getStorageSync('userId'),
orderType: '20',
deviceCode: paymentItem.deviceCode || '',
deviceType: mappedDeviceType,
billId: paymentItem.billId || '',
residentUserId: uni.getStorageSync('userId'),
// goodName: paymentItem.name || switchStatus(paymentItem.type) || '',
// remark: '服务费',
token: token
}
const queryString = Object.keys(params).map(k =>
encodeURIComponent(k) + '=' + encodeURIComponent(params[k])
).join('&')
uni.hideLoading()
console.log('传入中间小程序的path参数:', queryString)
// #ifdef APP-PLUS
// 原生App通过微信SDK拉起小程序
plus.share.getServices((services) => {
const weixin = services.find(s => s.id === 'weixin')
if (!weixin) {
uni.showToast({ title: '未检测到微信客户端', icon: 'none' })
return
}
if (!weixin.launchMiniProgram) {
uni.showToast({ title: '当前微信版本太低,不支持拉起小程序', icon: 'none' })
return
}
weixin.launchMiniProgram({
id: 'gh_a765aef0172c',
type: 1, // 0 正式版 / 1开发版 / 2体验版
path: 'pages/arrearsPayment/arrearsPayment?' + queryString,
}, (res) => {
console.log('回调---回调结果App', JSON.stringify(res))
}, (err) => {
console.error('err---拉起失败App', JSON.stringify(err))
uni.showToast({ title: '拉起支付小程序失败', icon: 'none' })
})
})
// #endif
} catch (err) {
uni.hideLoading()
console.error('拉起支付小程序异常', err)
uni.showToast({ title: '请求异常,请重试', icon: 'none' })
}
}
// 缴费成功推送通知
const sendPaySuccessPush = () => {
const userId = uni.getStorageSync('userId')
if (userId) {
getPushSingle({
userId: userId,
channel: 'resident',
title: '缴费成功',
content: `您已成功缴纳${switchStatus(paymentItem.type) || ''}费用 ¥${arrearsAmount.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;
}
.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;
}
.info-value {
font-size: 30rpx;
color: #333;
font-weight: 600;
}
.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

@@ -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

@@ -0,0 +1,480 @@
<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>
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view class="container">
<!-- 1. 物业费详情页 -->
<view class="pay-page">
<!-- 未欠费 -->
<!-- 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>{{'最近充值:'+paymentItem.LastRechargeTime || '-'}}</text>
<text class="lineStyle">{{'|'}}</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">{{paymentItem.villageName+ paymentItem.fullAddress || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">{{ paymentItem.name || '-' }}</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">{{ Math.abs(paymentItem.balance || 0).toFixed(2) }}</text>
</view>
</view>
<view v-else>
<view class="info-item">
<text class="info-label">房屋</text>
<text class="info-value">{{ paymentItem.fullAddress || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户号</text>
<text class="info-value">{{ paymentItem.deviceCode || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">户主</text>
<text class="info-value">{{ paymentItem.name || '-' }}</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">{{ Math.abs(paymentItem.balance || 0).toFixed(2) }}</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
// 响应式数据
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;
}
</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;
}
.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;
}
.noArrearsText {
padding-bottom: 50rpx;
width: 100%;
border-bottom: 2rpx solid #ededed;
}
.recordText {
font-size: 24rpx;
color: #222;
margin-top: 20rpx;
font-weight: 600;
}
.lineStyle {
margin: 0rpx 30rpx;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
height: 80rpx;
}
.info-label {
font-size: 30rpx;
color: #666;
}
.info-value {
font-size: 30rpx;
color: #333;
}
.info-payAmountItem {
height: 100rpx;
line-height: 100rpx;
margin-bottom: 60rpx;
}
.info-payAmount {
font-size: 50rpx;
color: #333;
font-weight: 600;
}
.input-section {
padding: 0 30rpx;
margin-bottom: 60rpx;
}
.amount-input {
width: 100%;
height: 100rpx;
font-size: 48rpx;
border: none;
outline: none;
}
.btn-section {
padding: 0 30rpx;
}
.pay-btn {
width: 100%;
height: 100rpx;
line-height: 100rpx;
background-color: #007aff;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
border: none;
outline: none;
}
.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: 0 30rpx;
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;
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;
}
.success-page {
width: 100%;
min-height: 100vh;
background-color: #fff;
text-align: center;
}
.icon-box {
display: flex;
justify-content: center;
width: 100vw;
height: 1100rpx;
padding: 60rpx 0px;
}
.success-icon {
margin-bottom: 40rpx;
width: 100px;
height: 100px;
font-size: 80px;
color: #007aff;
background-color: #007aff;
color: #fff;
border-radius: 50%;
}
.success-text {
font-size: 40rpx;
font-weight: 600;
color: #000;
margin-bottom: 20rpx;
}
.success-desc {
font-size: 30rpx;
color: #999;
margin-bottom: 100rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

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

View File

@@ -0,0 +1,437 @@
<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="page">
<!-- 累计缴费 -->
<view class="total-box">
<view class="top-box">
<view class="total-title">累计缴费</view>
<view class="total-price">¥{{ totalAmount }}</view>
</view>
<view class="chart-box">
<view v-if="chartData.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view>
<!-- 循环6个月柱状图 -->
<view class="chart-item" v-for="(item,index) in chartData" :key="index">
<!-- 柱子顶部金额文字 -->
<view class="chart-label">{{ item.price }}</view>
<!-- 柱状图柱子 -->
<view class="chart-bar" :style="{ height: barHeight(item.price) }"></view>
<!-- 底部月份文字 -->
<view class="chart-month">{{ item.month }}</view>
</view>
</view>
</view>
<!-- 缴费明细分页滚动 -->
<scroll-view class="list-scroll" scroll-y="true" @scrolltolower="loadMore">
<view class="list-wrapper">
<!-- 按月份分组渲染 -->
<view v-if="monthGroups.length === 0" class="empty-state">
<uni-icons type="info" size="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>
<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>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
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;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.page {
flex: 1;
display: flex;
flex-direction: column;
padding: 0px 40rpx;
box-sizing: border-box;
overflow: hidden;
}
.list-scroll {
flex: 1;
overflow-y: auto;
}
.total-box {
margin-bottom: 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;
}
.top-box {
display: flex;
justify-content: space-between;
align-items: center;
}
.total-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.total-price {
font-size: 48rpx;
font-weight: bold;
margin-top: 10rpx;
margin-bottom: 20rpx;
}
.chart-canvas {
width: calc(100vw - 60rpx);
height: 30vh;
}
.list-wrapper {
padding: 10rpx 0rpx;
}
.month-group {
border-bottom: 2rpx solid #f2f2f2;
padding: 30rpx;
background-color: #fff;
margin-bottom: 30rpx;
border-radius: 10rpx;
}
.month-title {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 30rpx;
}
.bill-item {
display: flex;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 30rpx;
border-bottom: 1px solid #ebebeb;
}
.bill-item:last-child {
border-bottom: none;
margin-bottom: 0rpx;
padding-bottom: 0rpx;
}
.bill-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #4080FF;
margin-right: 24rpx;
font-size: 36rpx;
}
.iconStyle {
width: 100%;
height: 100%;
}
.bill-info {
flex: 1;
}
.name {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
.account {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.bill-price {
font-size: 32rpx;
color: #333;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>
<style scoped>
.chart-box {
width: 100%;
height: 444rpx;
display: flex;
align-items: flex-end;
justify-content: space-around;
box-sizing: border-box;
}
.chart-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
width: 120rpx;
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: 26rpx;
color: #666;
margin-top: 24rpx;
}
</style>

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

@@ -0,0 +1,376 @@
<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="search-section">
<!-- 城市选择器 -->
<uni-data-picker placeholder="选择省市" popup-title="选择省市" :localdata="alladdress"
field="code as value, value as text" orderby="value asc" :step-searh="true" self-field="code"
parent-field="code" @change="onchange" v-model="cityValue" class="city-picker">
<view class="city-select">
<text class="city-text ellipsis">{{ selectedCity }}</text>
<text class="arrow-icon"></text>
</view>
</uni-data-picker>
<!-- 搜索框 -->
<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>
<view v-if="filterList.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">{{ '暂无数据' }}</text>
</view>
<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"
@click="selectItem(itemChild)">
{{ itemChild.name }}
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
// 响应式数据
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"
}
]
}]
}])
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;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
box-sizing: border-box;
background-color: #fff;
}
.search-section {
background-color: #fff;
padding: 20rpx;
display: flex;
align-items: center;
}
.city-picker {
display: block;
width: auto;
}
.city-select {
display: flex;
align-items: center;
padding: 8rpx 0;
cursor: pointer;
width: 80px;
height: 70rpx;
}
.ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.city-text {
font-size: 28rpx;
font-weight: 600;
color: #000000;
line-height: 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;
padding: 0 30rpx;
margin: 10rpx 0px 0px 10rpx;
height: 70rpx;
}
.search-input {
flex: 1;
height: 100%;
font-size: 30rpx;
border: none;
background: transparent;
}
.input-placeholder {
color: #999;
font-size: 30rpx;
}
.search-icon {
font-size: 36rpx;
color: #999;
}
.group-title {
padding: 20rpx 40rpx;
font-size: 28rpx;
color: #666;
background-color: #f3f3f3;
}
.item {
background: #fff;
padding: 20rpx 40rpx;
font-size: 28rpx;
border-bottom: 2rpx solid #f2f2f2;
}
.item:active {
background: #f2f2f2;
}
::v-deep .selected-item {
margin-left: 10rpx !important;
margin-right: 10rpx !important;
}
.status-bar {
width: 100vw;
height: 46px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
</style>

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();
}, 3000);
},
goBack() {
uni.redirectTo({
url: '/pages/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,34 +308,32 @@
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 20px;
padding: 0px 40rpx;
box-sizing: border-box;
background-color: #fff;
}
/* 表单主体 */
.form-content {
}
.form-content {}
.form-tip {
display: block;
font-size: 16px;
font-size: 32rpx;
color: #1D1D1D;
margin-bottom: 10px;
margin-bottom: 10rpx;
font-weight: 600;
}
/* 表单项:一行布局,不换行 */
.form-row {
display: flex;
align-items: center;
padding: 16px 0;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
@@ -354,12 +346,12 @@
.required-star {
color: #ff3b30;
font-size: 15px;
margin-right: 4px;
font-size: 30rpx;
margin-right: 8rpx;
}
.label-text {
font-size: 14px;
font-size: 28rpx;
color: #333;
}
@@ -371,7 +363,7 @@
}
.value-text {
font-size: 15px;
font-size: 30rpx;
color: grey;
}
@@ -387,7 +379,7 @@
/* 输入框样式 */
.text-input {
width: 100%;
font-size: 15px;
font-size: 30rpx;
color: #333;
border: none;
outline: none;
@@ -397,34 +389,48 @@
/* 问题描述行 */
.desc-row {
align-items: flex-start;
padding-top: 16px;
padding-bottom: 16px;
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 - 42px);
width: 100%;
min-height: 80px;
margin-top: 10px;
padding: 12px;
border: 1rpx solid #f0f0f0;
border-radius: 8rpx;
font-size: 15px;
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;
}
/* 照片上传区域 */
.photo-row {
align-items: flex-start;
padding-top: 16px;
padding-bottom: 16px;
padding-top: 32rpx;
padding-bottom: 32rpx;
border-bottom: none;
}
@@ -435,7 +441,7 @@
.photo-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
gap: 24rpx;
}
.photo-item {
@@ -486,10 +492,9 @@
line-height: 1;
}
/* 底部按钮 */
.bottom-btn-wrap {
padding: 10px 15px 20px;
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
background-color: #fff;
@@ -530,9 +535,8 @@
}
.popup-header {
padding: 15px;
padding: 30rpx;
text-align: center;
/* border-bottom: 1rpx solid #f0f0f0; */
position: relative;
}
@@ -543,7 +547,7 @@
.close {
position: absolute;
right: 15px;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
font-size: 36rpx;
@@ -552,13 +556,13 @@
/* 选项列表 */
.list {
padding: 0 15px;
padding: 0 30rpx;
}
.item {
padding: 15px 0;
padding: 30rpx 0;
text-align: center;
font-size: 14px;
font-size: 28rpx;
border-bottom: 1rpx solid #f5f5f5;
}
@@ -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,125 +26,107 @@
<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: [
"/static/propertyImgs/community1.png",
"/static/propertyImgs/community2.png",
"/static/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: '/pages/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: #f2f1f6;
background: #fff;
}
</style>
@@ -152,22 +135,17 @@
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 20px;
padding: 0px 40rpx;
box-sizing: border-box;
/* background-color: #fff; */
}
.page-container {
}
.page-container {}
/* 通用标题样式(带蓝色竖线) */
.section-title {
display: flex;
align-items: center;
@@ -183,12 +161,11 @@
}
.title-text {
font-size: 16px;
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
/* 投诉状态区域 */
.status-section {
display: flex;
justify-content: space-between;
@@ -200,8 +177,8 @@
.completedDesc {
color: #2F77FD;
font-size: 14px;
margin-bottom: 15px;
font-size: 28rpx;
margin-bottom: 30rpx;
}
@@ -211,7 +188,6 @@
font-weight: 500;
}
/* 详情信息项通用样式 */
.info-item {
display: flex;
padding: 24rpx 0;
@@ -221,7 +197,7 @@
.info-label {
width: 220rpx;
font-size: 14px;
font-size: 28rpx;
color: #333333;
flex-shrink: 0;
line-height: 1.5;
@@ -229,12 +205,11 @@
.info-value {
flex: 1;
font-size: 14px;
font-size: 28rpx;
color: #000;
line-height: 1.5;
}
/* 问题描述特殊样式(多行文本) */
.desc-item {
align-items: flex-start;
}
@@ -244,7 +219,6 @@
word-break: break-all;
}
/* 问题照片区域 */
.photo-section {
padding: 24rpx 0;
}
@@ -259,14 +233,12 @@
.photo-list {
display: flex;
flex-wrap: wrap;
/* 自动换行 */
gap: 10px;
/* 图片之间的间距(可改) */
margin: 5px 0px 10px 0px;
gap: 20rpx;
margin: 10rpx 0px 20rpx 0px;
}
.photo-item {
width: calc(33.333% - 7px);
width: calc(33.333% - 14rpx);
height: 66px;
object-fit: cover;
}
@@ -280,9 +252,10 @@
}
.completed {
color: #999999;
}.status-bar{
}
.status-bar {
width: 100vw;
height: 46px;
}

View File

@@ -0,0 +1,344 @@
<template>
<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>
<!-- 列表区域 -->
<view class="page">
<view class="page-container">
<view v-if="dataList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<!-- 投诉卡片 -->
<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-tag"
:class="item.status === 'pending' ? 'pending' : 'completed'">
{{ item.status === 'pending' ? '待处理' : '已完成' }}
</text>
</view>
<view class="card-body">
<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="aspectFit" class="card-img"></image>
</view>
</view>
<view class="card-footer">
<text class="card-time">{{ item.time }}</text>
<text class="card-detail" @click="turnToDetail(item)">投诉详情</text>
</view>
</view>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-btn-wrap">
<button class="next-btn" @click="addComplaintsSuggestions">投诉建议</button>
</view>
</view>
</template>
<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([])
// 模拟数据
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;
}
</style>
<style scoped>
.contentStyle {
min-height: 100vh;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.page {
padding: 0px 40rpx 200rpx;
box-sizing: border-box;
}
.page-container {
padding-top: 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;
}
.tab-wrap {
position: sticky;
top: 0;
z-index: 10;
display: flex;
justify-content: flex-start;
align-items: center;
padding: 10rpx 40rpx 20rpx;
}
.tab-item {
position: relative;
padding: 10rpx 0;
margin-right: 80rpx;
}
.tab-item:last-child {
margin-right: 0;
}
.tab-text {
font-size: 30rpx;
color: #999;
}
.tab-item.active .tab-text {
color: #007AFF;
font-weight: 600;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 96rpx;
height: 4rpx;
background: #2F77FD;
border-radius: 32rpx;
}
.card-wrapper {
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;
}
.card-title {
font-size: 32rpx;
font-weight: 400;
color: #222222;
margin-right: 20rpx;
}
.card-tag {
font-size: 24rpx;
padding: 2rpx 16rpx;
border-radius: 40rpx;
}
.pending {
background: #E34D59;
color: #FFFFFF;
}
.processing {
background: #09C2BD;
color: #FFFFFF;
}
.completed {
background: #2F77FD;
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 40rpx 0px;
}
.img-item {
width: calc(33.333% - 14rpx);
height: 52px;
}
.card-img {
width: 100%;
height: 56px;
object-fit: cover;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-time {
font-size: 24rpx;
color: #999999;
}
.card-detail {
font-size: 26rpx;
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: #f9f9f9;
}
.next-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background-color: #1677ff;
color: #fff;
font-size: 28rpx;
border-radius: 12rpx;
border: none;
}
</style>

View File

@@ -0,0 +1,203 @@
<template>
<view class="contentStyle">
<!-- 表单区域 -->
<scroll-view class="page" scroll-y="true">
<view v-if="dataList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
暂无数据
</text>
</view>
<view class="property-card" v-for="item,index in dataList" :key="index">
<view class="topBox">
<image class="avatar" :src="item.src" mode="aspectFill"></image>
<view class="info-content">
<view class="property-name">{{item.propertyName}}</view>
<view class="property-address">{{item.propertyAddress}}</view>
</view>
</view>
<view class="phone-item" v-for="phoneItems,phoneIndex in item.phoneList" :key="phoneIndex" @click="callPhone(phoneItems.phoneNum)" @longpress="copyPhone(phoneItems.phoneNum)">
<text class="phone-label">值班电话</text>
<text class="phone-num">{{phoneItems.phoneNum}}</text>
<uni-icons type="phone" size="16" color="#2F77FD"></uni-icons>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getManagePhoneData } from '../api/apiSub.js'
const 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' });
}
});
}
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;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100%;
}
.page {
flex: 1;
overflow-y: auto;
padding: 20px 40rpx;
box-sizing: border-box;
}
/* 空状态 */
.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;
}
/* 卡片容器 */
.property-card {
padding: 30rpx;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.topBox {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
}
/* 头像 */
.avatar {
width: 60px;
height: 60px;
border-radius: 8rpx;
margin-right: 24rpx;
margin-top: 20rpx;
flex-shrink: 0;
}
/* 信息内容区 */
.info-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.property-name {
font-size: 32rpx;
font-weight: 600;
color: #333333;
line-height: 35px;
}
.property-address {
font-size: 28rpx;
color: #666666;
line-height: 35rpx;
}
.phone-item {
display: flex;
align-items: center;
gap: 8rpx;
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;
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<view class="contentStyle">
<scroll-view class="page" scroll-y="true">
<view class="survey-title">{{ detailItem.title || '' }}</view>
<view class="survey-desc" v-if="questionList.length > 0">
为了给您提供更好的服务希望您能抽出几分钟时间将最近的感受和建议告诉我们
我们十分重视每位用户的宝贵意见期待您的参与
</view>
<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 v-if="q.required" class="required">*</text>
<text>{{ idx + 1 }}.{{ q.title || '未命名问题' }}</text>
</view>
<!-- 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>
<!-- input / textarea: 展示文本答案 -->
<view v-if="q.type === 'input' || q.type === 'textarea'" class="answer-text">
{{ getAnswerText(q.cid) || '未填写' }}
</view>
</view>
</scroll-view>
</view>
</template>
<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;
}
</style>
<style scoped>
.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: 20px 40rpx;
box-sizing: border-box;
margin-bottom: 48rpx;
}
.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;
}
.empty-tip {
text-align: center;
padding: 100rpx 0;
font-size: 28rpx;
color: #999;
}
.question-item {
margin-bottom: 30rpx;
}
.question-title {
font-size: 32rpx;
color: #000;
margin-bottom: 10rpx;
font-weight: 600;
}
.required {
color: #ff0000;
font-size: 34rpx;
margin-right: 8rpx;
}
.answer-text {
font-size: 32rpx;
color: #333;
line-height: 1.5;
padding-left: 30rpx;
word-break: break-all;
word-wrap: break-word;
}
.option-list {
padding-left: 30rpx;
}
.option-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx 0;
font-size: 32rpx;
color: #333;
}
.option-item.active {
color: #409eff;
}
.option-icon {
font-size: 24rpx;
color: #ccc;
}
.option-item.active .option-icon {
color: #409eff;
}
.option-label {
font-size: 32rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,475 @@
<template>
<view class="contentStyle">
<!-- <view class="status-bar"></view>
<uni-nav-bar title="问卷详情" backgroundColor="transparent" :border="false" left-icon="left" @clickLeft="goBack">
</uni-nav-bar> -->
<!-- 表单区域 -->
<scroll-view class="page" scroll-y>
<view class="content-wrap">
<!-- 问卷标题 -->
<view class="survey-title">{{ detailItem.title || '' }}</view>
<!-- 问卷说明 -->
<!-- <view class="survey-desc">{{ detailItem.description || '' }}</view> -->
<view class="survey-desc" v-if="questionList.length >0">
为了给您提供更好的服务希望您能抽出几分钟时间将最近的感受和建议告诉我们
我们十分重视每位用户的宝贵意见期待您的参与
</view>
<!-- 空状态 -->
<view v-if="questionList.length === 0" class="empty-tip">
<text>暂无数据</text>
</view>
<!-- 动态问题列表 -->
<view v-for="(q, idx) in questionList" :key="q.cid" class="question-item">
<view class="question-title">
<text v-if="q.required" class="required">*</text>
<text>{{ idx + 1 }}.{{ q.title || '未命名问题' }}</text>
</view>
<!-- 单选 -->
<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">{{ opt.label }}</text>
</view>
</view>
<!-- 多选 -->
<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="checkbox-label">{{ opt.label }}</text>
</view>
</view>
<!-- 单行输入 -->
<view v-if="q.type === 'input'">
<input
class="text-input"
:class="{ 'input-disabled': isEndedAndUnjoined }"
v-model="answers[q.cid]"
placeholder="请输入"
:disabled="isEndedAndUnjoined"
/>
</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>
</view>
</scroll-view>
<!-- 固定底部提交按钮 -->
<view class="submit-btn-wrap">
<button
v-if="isEndedAndUnjoined"
class="submit-btn disabled-btn"
disabled
>已结束</button>
<button v-else class="submit-btn" @click="handleSubmit">提交</button>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad, 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;
}
</style>
<style scoped lang="scss">
.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: 30rpx 40rpx 200rpx;
box-sizing: border-box;
}
.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-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;
margin-bottom: 10rpx;
line-height: 1.4;
.required {
color: #ff3b30;
font-size: 36rpx;
margin-right: 8rpx;
}
}
}
/* 单选 */
.radio-group {
display: flex;
flex-direction: column;
gap: 20rpx;
padding-left: 10rpx;
}
.radio-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 12rpx 0;
.radio-circle {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
&.active {
background-color: #007aff;
border-color: #007aff;
}
}
.check-icon {
color: #fff;
font-size: 22rpx;
font-weight: bold;
}
.radio-label {
font-size: 32rpx;
color: #333;
}
}
/* 多选 */
.checkbox-group {
display: flex;
flex-direction: column;
gap: 20rpx;
padding-left: 10rpx;
}
.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;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
&.active {
background-color: #007aff;
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;
}
&.disabled-btn {
background-color: #ccc;
color: #999;
}
}
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -0,0 +1,256 @@
<template>
<view class="contentStyle">
<view style="width: 40vw;">
<uni-segmented-control :current="currentTab" :values="tabs" @click-item="switchTab" style-type="text"
active-color="#007AFF" inactive-color="#999" class="seg-control" />
</view>
<!-- 表单区域 -->
<view class="page">
<view class="list-container">
<view v-if="currentList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<!-- 列表项点击高亮 -->
<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.createTime }}</text>
<text class="info-community">{{ item.villageName }}</text>
</view>
</view>
<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>
</view>
</view>
</template>
<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 = ["收集中", "已结束"]
// 收集中列表
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;
}
</style>
<style lang="scss" scoped>
.contentStyle {
// display: flex;
// flex-direction: column;
min-height: 100%;
// height: 100%;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 40rpx;
box-sizing: border-box;
}
.seg-control {
margin-bottom: 20rpx;
}
.list-container {
padding-top: 20rpx;
}
.list-item {
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
border: 2rpx solid #E6E6E6;
background-color: #fff;
&.disabled {
opacity: 0.6;
}
&.selected {
border: 2rpx solid #2F77FD;
background-color: #F5FAFF;
}
}
.item-content {
width: calc(100% - 100rpx);
}
.item-title {
font-size: 32rpx;
font-weight: 600;
color: #222;
line-height: 44rpx;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.item-info {
display: flex;
align-items: center;
gap: 20rpx;
.info-time,
.info-community {
font-size: 28rpx;
color: #666;
line-height: 32rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.item-status {
font-size: 28rpx;
font-weight: 500;
border-radius: 8rpx;
&.joined {
color: #2F77FD;
}
&.unjoined {
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;
}
</style>

View File

@@ -0,0 +1,165 @@
<template>
<view class="qrcode-page">
<view class="qrcode-wrap">
<!-- 显示后台返回的二维码图片 -->
<view class="qrcode-box">
<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">{{houseName}}通行二维码</text>
<text class="qrcode-tips">分享此二维码给访客可扫码开门单次有效</text>
</view>
<view class="btn-wrap">
<button class="send-btn" @click="saveToAlbum">保存图片到相册</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
// 直接接收图片URL
const qrcodeUrl = ref('');
const qrcodeImage = ref('');
const houseName = ref('')
onLoad((option) => {
if (option.qrcodeUrl) {
// 接收后台返回的二维码图片链接
qrcodeUrl.value = decodeURIComponent(option.qrcodeUrl);
qrcodeImage.value = qrcodeUrl.value;
houseName.value = option.villageName
console.log('最终二维码图片:', qrcodeUrl.value);
}
});
// 保存图片到相册
const saveToAlbum = () => {
if (!qrcodeUrl.value) {
uni.showToast({ title: '二维码加载中...', icon: 'none' });
return;
}
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 {
min-height: 100vh;
}
.qrcode-wrap {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 60rpx;
}
.qrcode-box {
position: relative;
width: 400rpx;
height: 400rpx;
}
/* 图片铺满 */
.qrcode-img {
width: 100%;
height: 100%;
}
/* 边框装饰 */
.qrcode-border {
position: absolute;
width: 40rpx;
height: 40rpx;
border: 4rpx solid #007aff;
}
.border-top { top: 0; left: 0; border-right: none; border-bottom: none; }
.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;
margin-top: 30rpx;
color: #333;
}
.qrcode-tips {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.btn-wrap {
margin: 80rpx 40rpx 20rpx;
}
.send-btn {
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
height: 88rpx;
font-size: 28rpx;
line-height: 88rpx;
}
</style>

View File

@@ -0,0 +1,705 @@
<template>
<view class="visitor-invite" :class="{ 'no-scroll': pageFixed }">
<!-- 标签切换车辆来访/人员来访 -->
<view class="tab-wrap">
<view class="tab-item" :class="{ active: activeTab === 'car' }" @click="switchTab('car')">
车辆来访
</view>
<view class="tab-item" :class="{ active: activeTab === 'person' }" @click="switchTab('person')">
人员来访
</view>
</view>
<!-- 表单区域 -->
<view class="form-wrap">
<!-- 房屋信息 -->
<view class="section-title">房屋信息</view>
<view class="form-section">
<view class="form-item">
<view class="label">
<text class="required">*</text>来访地址
</view>
<view class="value" @click="openAddressModal">
{{ form.address }}
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- 访客信息 -->
<view class="section-title">访客信息</view>
<view class="form-section">
<view class="form-item">
<view class="label">
<text class="required">*</text>访客姓名
</view>
<input class="input" placeholder="请输入访客姓名" v-model="form.name" maxlength="20" @input="onNameInput" />
</view>
<view class="form-item">
<view class="label">
<text class="required">*</text>访客性别
</view>
<view class="value" @click="openGenderModal">
{{ form.gender }}
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
<view class="form-item">
<view class="label">
<text class="required">*</text>访客联系方式
</view>
<input class="input" placeholder="请输入联系方式" v-model="form.phone" type="number" maxlength="11" @input="onPhoneInput" />
</view>
<!-- 车牌号仅车辆来访显示 -->
<view class="form-item" v-if="activeTab === 'car'">
<view class="label">
<text class="required">*</text>车牌号
</view>
<view class="car-number-wrap">
<input class="car-input" v-for="(item, index) in 8" :key="index"
:value="form.carNumber[index]" @input="(e) => onCarInput(index, e)"
:focus="carFocusIndex === index"
@focus="carFocusIndex = index"
type="text" />
</view>
</view>
<view class="form-item">
<view class="label">随行人数</view>
<input class="input" placeholder="请输入随行人数" v-model="form.peopleNum" type="number" maxlength="2" @input="onPeopleNumInput" />
</view>
</view>
<!-- 有效期 -->
<view class="section-title">有效期</view>
<view class="form-section">
<view class="form-item" @click="onPickerOpen">
<view class="label">
<text class="required">*</text>来访时间
</view>
<view class="value">
<uni-datetime-picker type="datetime" v-model="form.visitTime" @change="handleTimeChange" @maskClick="pickerVisible = false"
:border="false" :hide-second="true"/>
</view>
</view>
<view class="form-item" @click="onPickerOpen">
<view class="label">
<text class="required">*</text>离开时间
</view>
<view class="value">
<uni-datetime-picker type="datetime" v-model="form.leaveTime" @change="handleTimeChange" @maskClick="pickerVisible = false"
:border="false" :hide-second="true"/>
</view>
</view>
<view class="tips">预计来访时间和预计离开时间间隔不能超过24小时</view>
<view class="tips-2">车辆停留请勿超出有效期需及时驶出小区</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="btn-wrap">
<button class="submit-btn" @click="submitForm">
{{ activeTab === 'car' ? '确认邀请' : '生成通行二维码' }}
</button>
</view>
<!-- 地址选择弹窗 -->
<uni-popup ref="addressModalRef" type="bottom" :mask-click="true" @change="onAddressModalChange">
<view class="modal-wrap">
<view class="modal-title">选择来访地址</view>
<view class="address-list">
<view class="address-item" :class="{ active: item.label === form.address }" v-for="item in addressList"
:key="item.id" @click="selectAddress(item)">
{{ item.label }}
</view>
</view>
<uni-icons type="closeempty" size="20" class="modal-close" @click="closeAddressModal"></uni-icons>
</view>
</uni-popup>
<!-- 性别选择弹窗 -->
<uni-popup ref="genderModalRef" type="bottom" :mask-click="true" @change="onGenderModalChange">
<view class="modal-wrap">
<view class="gender-list">
<view class="gender-item" :class="{ active: item === form.gender }" v-for="item in genderList"
:key="item" @click="selectGender(item)">
{{ item }}
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import {
ref,
reactive
} from 'vue';
import {
onLoad,
onBackPress
} from "@dcloudio/uni-app"
import moment from 'moment';
import{getHouseList,addVisitorData} from '../api/api.js'
// 标签激活状态
const activeTab = ref('car');
// 表单数据
const form = reactive({
address: '',
villageId: '',
houseId: '',
name: '',
gender: '',
phone: '',
carNumber:['', '', '', '', '', '', '', ''],
peopleNum: '',
visitTime: '',
leaveTime: ''
});
// 地址列表
const addressList = ref([]);
// 性别列表
const genderList = ref(['男', '女']);
// 弹窗实例
const addressModalRef = ref(null);
const genderModalRef = ref(null);
// 页面固定标志
const pageFixed = ref(false);
// 车牌号输入框聚焦索引
const carFocusIndex = ref(-1);
// 日期选择器是否打开
const pickerVisible = ref(false);
// 拦截返回事件:若日期选择器打开则关闭选择器而非返回上一页
onBackPress(() => {
if (pickerVisible.value) {
pickerVisible.value = false;
return true;
}
});
// =========方法===========
// 获取房屋列表
const fetchHouseList = async () => {
try {
const res = await getHouseList({ pageNum: 1, pageSize: 10 })
if (res.code === 200) {
addressList.value = (res.data || []).map(item => ({
...item,
label: `${item.buildingName || ''}${item.unitNo}-${item.floorNo || ''}-${item.houseNo || ''}`.trim()
}))
}
} catch (e) {
uni.showToast({
title:e.msg || '获取房屋列表失败',
icon:'none'
})
// console.error('获取房屋列表失败', e)
}
}
onLoad(() => {
fetchHouseList()
});
// 切换tab
const switchTab = (tab) => {
activeTab.value = tab;
resetForm();
};
// 打开地址弹窗
const openAddressModal = () => {
addressModalRef.value.open();
};
// 关闭地址弹窗
const closeAddressModal = () => {
addressModalRef.value.close();
};
// 地址弹窗状态变化
const onAddressModalChange = (e) => {
pageFixed.value = e.show;
};
// 选择地址
const selectAddress = (item) => {
form.address = item.label;
form.villageId = item.villageId || '';
form.houseId = item.houseId || '';
closeAddressModal();
};
// 打开性别弹窗
const openGenderModal = () => {
genderModalRef.value.open();
};
// 选择性别
const selectGender = (item) => {
form.gender = item;
genderModalRef.value.close();
};
// 性别弹窗状态变化
const onGenderModalChange = (e) => {
pageFixed.value = e.show;
};
// 车牌号单格输入
const onCarInput = (index, e) => {
const val = (e.detail && e.detail.value !== undefined) ? e.detail.value : (e.target ? e.target.value : '');
const upperVal = (val || '').toUpperCase().slice(0, 1);
form.carNumber[index] = upperVal;
// 输入一位后自动聚焦到下一个
if (upperVal && index < 7) {
carFocusIndex.value = index + 1;
}
};
// 姓名输入过滤特殊字符
const onNameInput = (e) => {
const val = e.detail.value;
// 只允许中文、英文字母、数字和中间点(·)
const filtered = val.replace(/[^一-龥a-zA-Z0-9·]/g, '')
if (filtered !== val) {
form.name = filtered;
}
};
// 手机号输入过滤非数字字符
const onPhoneInput = (e) => {
const val = e.detail.value;
const filtered = val.replace(/\D/g, '');
if (filtered !== val) {
form.phone = filtered;
}
};
// 随行人数输入过滤小数
const onPeopleNumInput = (e) => {
const val = e.detail.value;
// 只保留整数(去除小数点及之后的内容)
const filtered = val.replace(/[^\d]/g, '');
if (filtered !== val) {
form.peopleNum = filtered;
}
};
// 打开选择器时标记状态,供 onBackPress 拦截用
const onPickerOpen = () => {
pickerVisible.value = true;
};
// 时间校验
const handleTimeChange = () => {
pickerVisible.value = false;
if (!form.visitTime || !form.leaveTime) return true;
const visitMoment = moment(form.visitTime);
const leaveMoment = moment(form.leaveTime);
const now = moment();
// 来访时间不能是过去时间
if (visitMoment.isBefore(now)) {
uni.showToast({
title: '来访时间不能早于当前时间',
icon: 'none'
});
return false;
}
// 离开时间不能早于来访时间
if (leaveMoment.isBefore(visitMoment)) {
uni.showToast({
title: '离开时间不能早于来访时间',
icon: 'none'
});
return false;
}
// 离开时间与来访时间间隔不能少于30分钟
const diffMinutes = leaveMoment.diff(visitMoment, 'minutes');
if (diffMinutes < 30) {
uni.showToast({
title: '离开时间与来访时间间隔不能少于30分钟',
icon: 'none'
});
return false;
}
// 不能超过24小时
const diffHours = leaveMoment.diff(visitMoment, 'hours', true);
if (diffHours > 24) {
uni.showToast({
title: '时间间隔不能超过24小时',
icon: 'none'
});
return false;
}
return true;
};
// 随行人数输入过滤小数
// 提交表单
const submitForm = async () => {
// 1. 必填项校验
if (!form.address) {
uni.showToast({
title: '请选择来访地址',
icon: 'none'
});
return;
}
if (!form.name) {
uni.showToast({
title: '请输入访客姓名',
icon: 'none'
});
return;
}
if (!form.gender) {
uni.showToast({
title: '请选择性别',
icon: 'none'
});
return;
}
if (!form.phone) {
uni.showToast({
title: '请输入访客联系方式',
icon: 'none'
});
return;
}
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
});
return;
}
if (activeTab.value === 'car' && form.carNumber.filter(Boolean).join('').length < 7) {
uni.showToast({
title: '请填写完整车牌号',
icon: 'none'
});
return;
}
// if (form.peopleNum === '' || form.peopleNum === null || form.peopleNum === undefined) {
// uni.showToast({
// title: '请输入随行人数',
// icon: 'none'
// });
// return;
// }
if (!form.visitTime) {
uni.showToast({
title: '请选择来访时间',
icon: 'none'
});
return;
}
if (!form.leaveTime) {
uni.showToast({
title: '请选择离开时间',
icon: 'none'
});
return;
}
// 2. 时间规则校验
if (!handleTimeChange()) return;
// 3. 构造参数并提交
const villageId = uni.getStorageSync('currentVillageId')
const carNumStr = form.carNumber.filter(Boolean).join('')
const params = {
villageId: villageId,
houseId: form.houseId,
visitorName: form.name,
visitorGender: form.gender === '男' ? '0' : (form.gender === '女' ? '1' : ''),
visitorPhone: form.phone,
carNum: activeTab.value === 'car' ? carNumStr : '',
visitorNum: String(form.peopleNum),
startTime: form.visitTime,
endTime: form.leaveTime,
type: activeTab.value === 'car' ? '0' : '1',
status: '0'
}
uni.showLoading({ title: '提交中...' });
try {
const res = await addVisitorData(params)
uni.hideLoading();
if (res.code === 200) {
uni.showToast({
title: activeTab.value === 'car' ? '邀请成功' : '二维码生成成功',
icon: 'success'
});
setTimeout(() => {
uni.navigateTo({ url: `/pageSubPack/visitor/qrcode-page?qrcodeUrl=${encodeURIComponent(res.data.url)}&villageName=${res.data.villageName}` })
}, 1500);
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' });
}
} catch (e) {
uni.hideLoading();
uni.showToast({ title: e.msg || '网络异常,请重试', icon: 'none' });
}
};
// 重置表单
const resetForm = () => {
form.address = '';
form.villageId = '';
form.houseId = '';
form.name = '';
form.gender = '';
form.phone = '';
form.carNumber = ['', '', '', '', '', '', '', ''];
form.peopleNum = '';
form.visitTime = '';
form.leaveTime = '';
};
</script>
<style scoped>
.visitor-invite {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
box-sizing: border-box;
background-color: #f5f5f5;
}
.no-scroll {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #fff;
}
.nav-title {
font-size: 36rpx;
font-weight: 500;
margin-left: 20rpx;
}
/* 标签切换 */
.tab-wrap {
display: flex;
flex-shrink: 0;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.tab-item {
/* flex: 1; */
display: flex;
padding: 30rpx;
text-align: center;
font-size: 32rpx;
color: #333;
}
.tab-item.active {
color: #007aff;
/* border-bottom: 4rpx solid #007aff; */
}
/* 表单区域 */
.form-wrap {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 10rpx 10rpx 180rpx;
}
.form-section {
background-color: #fff;
margin-bottom: 10rpx;
padding: 20rpx 40rpx;
}
.section-title {
font-size: 28rpx;
font-weight: 500;
/* margin-bottom: 20rpx; */
margin: 30rpx 40rpx 10rpx;
}
.form-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.form-item:last-child {
border-bottom: none;
}
.label {
width: 225rpx;
font-size: 32rpx;
color: #333;
}
.required {
color: red;
margin-right: 4rpx;
}
.value {
flex: 1;
font-size: 30rpx;
color: #666;
display: flex;
justify-content: flex-end;
align-items: center;
}
.input {
flex: 1;
font-size: 30rpx;
color: #666;
text-align: right;
}
/* 车牌号输入 */
.car-number-wrap {
flex: 1;
display: flex;
justify-content: space-between;
}
.car-input {
width: 52rpx;
height: 60rpx;
border: 1rpx solid #eee;
text-align: center;
font-size: 30rpx;
}
/* 提示文字 */
.tips {
font-size: 24rpx;
color: #999;
margin-top: 44rpx;
text-align: center;
}
.tips-2 {
font-size: 24rpx;
color: #999;
margin: 20rpx;
text-align: center;
}
/* 底部按钮 */
.btn-wrap {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
padding: 20rpx 40rpx 50rpx;
padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
background-color: #f5f5f5;
}
.submit-btn {
background-color: #007aff;
color: #fff;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 8rpx;
}
/* 弹窗样式 */
.modal-wrap {
background-color: #fff;
padding: 30rpx;
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
position: relative;
}
.modal-title {
font-size: 32rpx;
text-align: center;
margin-bottom: 30rpx;
}
.modal-close {
position: absolute;
top: 20rpx;
right: 20rpx;
color: #999;
}
.address-list,
.gender-list {
display: flex;
flex-direction: column;
max-height: 60vh;
overflow-y: auto;
}
.address-item,
.gender-item {
padding: 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
text-align: center;
font-size: 30rpx;
margin-bottom: 20rpx;
}
.address-item:last-child,
.gender-item:last-child {
margin-bottom: 0;
}
.address-item.active,
.gender-item.active {
background-color: #007aff;
color: #fff;
}
</style>
<style>
/* 页面容器撑满并禁止整体滚动,让表单区域单独滚动 */
page {
height: 100%;
overflow: hidden;
}
</style>

View File

@@ -4,8 +4,8 @@
<view class="info-card">
<view class="info-header">
<text class="title">来访者{{ visitorInfo.name }}</text>
<text class="status {{ visitorInfo.status === 'visited' ? 'visited' : 'waiting' }}">
{{ visitorInfo.status === 'visited' ? '已到访' : '等待来访' }}
<text class="status" :class="{ active: visitorInfo.status === '等待来访' }">
{{ visitorInfo.status}}
</text>
</view>
@@ -20,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,52 +46,87 @@
<!-- 提示文字 -->
<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 = () => {
uni.navigateTo({
url: '/pages/visitor/qrcode-page'
url: '/pageSubPack/visitor/qrcode-page'
});
};
</script>
<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 {
@@ -167,6 +212,7 @@ const goToQrcode = () => {
color: #fff;
border-radius: 8rpx;
height: 88rpx;
font-size: 32rpx;
font-size: 28rpx;
line-height: 88rpx
}
</style>

View File

@@ -0,0 +1,337 @@
<template>
<view class="visitor-page">
<!-- 标签切换栏 -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: activeTab === 'waiting' }"
@click="activeTab = 'waiting'"
>
等待来访
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'visited' }"
@click="activeTab = 'visited'"
>
已到访
</view>
</view>
<!-- 访客列表 -->
<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.visitorName }}
</text>
<text class="status-tag" :class="item.status">
{{ item.statusText }}
</text>
</view>
<!-- 有效期/到访时间 -->
<view class="item-row">
<text class="label" :class="item.status">
{{ item.status === "waiting" ? "有效期" : "到访时间" }}
</text>
<text class="value" :class="item.status">
{{ item.startTime }}{{ item.endTime }}
</text>
</view>
<!-- 联系方式 -->
<view class="item-row">
<text class="label" :class="item.status">联系方式</text>
<text class="value" :class="item.status">{{ item.phone }}</text>
</view>
<!-- 车牌号仅车辆来访显示 -->
<view class="item-row" v-if="item.type !== '1'">
<text class="label" :class="item.status">车牌号</text>
<text class="value" :class="item.status">{{ item.carNumber }}</text>
</view>
<!-- 时间+查看详情 -->
<view class="item-footer">
<text class="time-text" :class="item.status">
时间{{ item.createTime }}
</text>
<view class="detail-btn" @click="viewDetail(item)">
<text>查看详情</text>
<uni-icons type="arrowright" size="14"></uni-icons>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="filterdList.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
{{ activeTab === "waiting" ? "暂无等待到访数据" : "暂无已到访数据" }}
</text>
</view>
<!-- 底部占位 -->
<view class="list-bottom-space"></view>
</view>
<!-- 新增邀请按钮 -->
<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 { onShow, onPullDownRefresh } from "@dcloudio/uni-app";
import { getVisitorList } from "../api/api.js";
const activeTab = ref("waiting");
const visitorList = ref([]);
const mapData = (list) => {
if (!Array.isArray(list)) return [];
return list.map((item) => {
const status =
item.status === 1 || item.status === "1" ? "visited" : "waiting";
return {
...item,
status,
statusText: status === "visited" ? "已到访" : "等待来访",
phone: item.visitorPhone,
carNumber: item.carNum,
createTime: item.createTime || "",
};
});
};
const fetchVisitorList = async () => {
try {
const params = {
userId: uni.getStorageSync("userId"),
villageId: uni.getStorageSync('currentVillageId')
}
const res = await getVisitorList(params);
visitorList.value = mapData(res.data || []);
} catch (err) {
uni.showToast({ title: err.msg, icon: "none" });
} finally {
uni.stopPullDownRefresh();
}
};
onShow(() => {
fetchVisitorList();
});
onPullDownRefresh(() => {
fetchVisitorList();
});
const filterdList = computed(() => {
return visitorList.value.filter((item) => item.status === activeTab.value);
});
const viewDetail = (item) => {
uni.navigateTo({
url: `/pageSubPack/visitor/visitor-detail?id=${item.id}`,
});
};
const addInvitation = () => {
uni.navigateTo({ url: "/pageSubPack/visitor/visitor-add" });
};
</script>
<style scoped>
/* 页面根容器 */
.visitor-page {
min-height: 100vh;
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 {
position: relative;
padding: 24rpx 0;
margin-right: 48rpx;
font-size: 30rpx;
color: #666;
}
.tab-item.active {
color: #2f77fd;
font-weight: 500;
}
.tab-item.active::after {
content: "";
position: absolute;
bottom: 8rpx;
left: 50%;
transform: translateX(-50%);
width: 126rpx;
height: 8rpx;
border-radius: 32rpx;
background: linear-gradient(to bottom, #2F77FD, #CEDFFF);
}
/* 列表区域 */
.list-container {
}
/* 访客卡片 */
.visitor-item {
margin-top: 24rpx;
padding: 24rpx;
background-color: #fff;
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: 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: 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: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f5f5f5;
}
.time-text {
font-size: 24rpx;
color: #999;
}
.time-text.visited {
color: #bbb;
}
.detail-btn {
display: flex;
align-items: center;
font-size: 26rpx;
color: #666;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.empty-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
/* 列表底部占位(为固定的按钮预留高度,保证最后一项能滚到按钮上方) */
.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,88 +1,94 @@
{
"pages": [
// {
// "path": "pages/navigation/navigation",
// "style": {
// "navigationBarTitleText": "引导页",
// "navigationBarTextStyle": "white",
// "enablePullDownRefresh": false,
// "navigationStyle": "custom"
// }
// },
// {
// "path": "pages/login/pwd-login",
// "style": {
// "navigationBarTitleText": "密码登录",
// "navigationBarTextStyle": "white",
// "enablePullDownRefresh": false,
// "navigationStyle": "custom"
// }
// },
// {
// "path": "pages/login/login",
// "style": {
// "navigationBarTitleText": "验证码登录",
// "navigationBarTextStyle": "white",
// "enablePullDownRefresh": false,
// "navigationStyle": "custom"
// }
// },
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationBarTitleText": "",
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#F8F8F8"
"enablePullDownRefresh": true
// "app-plus": {
// "pullToRefresh": {
// "support": true,
// "style": "circle",
// "offset": "70px"
// }
// }
}
},
{
"path": "pages/serviceIndex/serviceIndex",
"style": {
"navigationBarTitleText": "服务",
"navigationStyle": "custom"
"navigationBarTitleText": "服务"
}
},
{
"path": "pages/myIndex/myIndex",
"style": {
"navigationBarTitleText": "我的",
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/navigation/navigation",
"style": {
"navigationBarTitleText": "引导页",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
}
],
"subPackages": [{
"root": "pages/loginSub",
"root": "pageSubPack/loginSub",
"pages": [{
"path": "register-new-user",
"style": {
"navigationBarTitleText": "注册新用户",
"navigationStyle": "custom"
"disableScroll": true
}
},
{
"path": "login",
"style": {
"navigationBarTitleText": "验证码登录",
"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": "隐私政策"
}
},
{
"path": "user-agreement",
"style": {
"navigationBarTitleText": "用户协议"
}
}
]
},
{
"root": "pages/online-repair",
"root": "pageSubPack/online-repair",
"pages": [{
"path": "onlineRepair",
"style": {
"navigationBarTitleText": "在线报修"
"navigationBarTitleText": "在线报修",
"enablePullDownRefresh": true
}
},
{
@@ -124,11 +130,12 @@
]
},
{
"root": "pages/visitor",
"root": "pageSubPack/visitor",
"pages": [{
"path": "visitor-list",
"style": {
"navigationBarTitleText": "访客邀请"
"navigationBarTitleText": "访客邀请",
"enablePullDownRefresh": true
}
},
{
@@ -152,11 +159,12 @@
]
},
{
"root": "pages/activity-list",
"root": "pageSubPack/activity-list",
"pages": [{
"path": "activityList",
"style": {
"navigationBarTitleText": "活动列表"
"navigationBarTitleText": "活动列表",
"enablePullDownRefresh": true
}
},
{
@@ -176,11 +184,13 @@
]
},
{
"root": "pages/notice-list",
"root": "pageSubPack/notice-list",
"pages": [{
"path": "noticeList",
"style": {
"navigationBarTitleText": "通知公告"
"navigationBarTitleText": "通知公告",
"navigationBarBackgroundColor": "#F9F9F9",
"enablePullDownRefresh": true
}
},
{
@@ -192,53 +202,81 @@
{
"path": "oneClickOpen",
"style": {
"navigationBarTitleText": "一键开门",
"navigationStyle": "custom"
"navigationBarTitleText": "一键开门"
}
},
{
"path": "selectHome",
"style": {
"navigationBarTitleText": "选择小区"
"navigationBarTitleText": "选择小区",
"enablePullDownRefresh": true
}
}
]
},
{
"root": "pages/my",
"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",
@@ -261,15 +299,14 @@
}, {
"path": "myParkingSpaceIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "我的车位",
"enablePullDownRefresh": true
}
},
{
"path": "parkingSpaceDetail",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "车位详情"
}
},
@@ -308,8 +345,8 @@
{
"path": "myCarIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "我的车辆",
"enablePullDownRefresh": true
}
@@ -326,16 +363,23 @@
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "carDetail",
},
{
"path": "selectOwner",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "carDetail",
"style": {
"navigationBarTitleText": "车辆详情"
}
}
]
}, {
"root": "pages/payment-list",
"root": "pageSubPack/payment-list",
"pages": [{
"path": "addPayment",
"style": {
@@ -364,8 +408,8 @@
{
"path": "paymentRecord",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "缴费记录"
// "navigationStyle": "custom"
}
},
{
@@ -378,57 +422,115 @@
{
"path": "paymentIndex",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": "生活缴费",
"navigationBarBackgroundColor": "#dfedfd",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,
"app-plus": {
"titleNView": {
"autoBackButton": true
}
}
}
}
]
},
{
"root": "pages/service-list",
"pages": [{
"path": "complaintsSuggestionsIndex",
}, {
"path": "addHydropower",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}, {
"path": "yinlianPayTn",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "propertyFeePayment",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "billPayment",
"style": {
"navigationBarTitleText": "缴费账单"
}
}
]
},
{
"root": "pageSubPack/service-list",
"pages": [{
"path": "complaintsSuggestionsIndex",
"style": {
"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": "收益详情"
}
}
]
@@ -450,20 +552,20 @@
"selectedColor": "#666666",
"list": [{
"pagePath": "pages/index/index",
"iconPath": "/static/tabbar/首页.png",
"selectedIconPath": "/static/tabbar/首页蓝.png",
"iconPath": "/static/tabbar/home.png",
"selectedIconPath": "/static/tabbar/homeBlue.png",
"text": "首页"
},
{
"pagePath": "pages/serviceIndex/serviceIndex",
"iconPath": "/static/tabbar/服务.png",
"selectedIconPath": "/static/tabbar/服务蓝.png",
"iconPath": "/static/tabbar/service.png",
"selectedIconPath": "/static/tabbar/serviceBlue.png",
"text": "服务"
},
{
"pagePath": "pages/myIndex/myIndex",
"iconPath": "/static/tabbar/我的.png",
"selectedIconPath": "/static/tabbar/我的蓝.png",
"iconPath": "/static/tabbar/mine.png",
"selectedIconPath": "/static/tabbar/mineBlue.png",
"text": "我的"
}
]

View File

@@ -1,845 +0,0 @@
<template>
<view class="container">
<!-- 顶部图片 -->
<view class="topImg">
<image src="/static/首页/banner.png" style="width: 100%;height:100%;"></image>
</view>
<!-- 选项卡 -->
<view class="tabs-container">
<view class="tab-item" :class="{ 'active': activeTab === 'list' }" @tap="switchTab('list')">
活动列表
<view v-if="activeTab === 'list'" class="tab-indicator"></view>
</view>
<view class="tab-item" :class="{ 'active': activeTab === 'my' }" @tap="switchTab('my')">
我的活动
<view v-if="activeTab === 'my'" class="tab-indicator"></view>
</view>
</view>
<!-- 我的活动状态筛选 -->
<view v-if="activeTab === 'my'" class="filter-container">
<scroll-view class="filter-scroll" scroll-x>
<view class="filter-buttons">
<view v-for="filter in filters" :key="filter.value" class="filter-btn"
:class="{ 'active': myFilter === filter.value }" @tap="switchFilter(filter.value)">
{{ filter.label }}
</view>
</view>
</scroll-view>
</view>
<!-- 活动列表 -->
<scroll-view class="activity-list" scroll-y @scrolltolower="loadMoreData" :lower-threshold="100">
<!-- 空状态 -->
<view v-if="displayedActivities.length === 0" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">
{{ activeTab === 'list' ? '暂无活动' : '暂无参与的活动' }}
</text>
</view>
<!-- 活动卡片 -->
<view v-for="activity in displayedActivities" :key="activity.id" class="activity-card"
@tap="viewActivityDetail(activity)">
<!-- 活动状态标签 -->
<!-- <view class="status-badge" :class="getStatusClass(activity.status)">
{{ getStatusText(activity.status) }}
</view> -->
<view class="status-badge">
进行中
</view>
<!-- 已报名标识 -->
<view v-if="activity.hasSigned && activeTab === 'list'" class="signed-badge">
<uni-icons type="checkbox-filled" size="14" color="#fff"></uni-icons>
<text>已报名</text>
</view>
<!-- 活动信息 -->
<view class="activity-info">
<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="/static/logo.png" style="width: 100%;height: 100%;"></image>
</view>
</up-col>
<up-col span="8">
<view class="demo-layout bg-purple">
<view class="activity-title">
{{ activity.title }}
</view>
<view class="activity-detail">
<!-- <uni-icons type="calendar" size="16" color="#999"></uni-icons> -->
<text class="detail-text">时间{{ formatDate(activity.startTime) }}
{{ formatDate(activity.endTime) }}</text>
</view>
<view class="activity-detail">
<!-- <uni-icons type="location-filled" size="16" color="#999"></uni-icons> -->
<text class="detail-text">地点{{ activity.location }}</text>
</view>
<view class="activity-detail">
<!-- <uni-icons type="person" size="16" color="#999"></uni-icons> -->
<text class="detail-text-num">活动名额{{ activity.totalQuota }}已报名{{ activity.signedCount }}</text>
</view>
</view>
</up-col>
</up-row>
</view>
<!-- 我的活动额外信息 -->
<!-- <view v-if="activeTab === 'my'" class="my-activity-info">
<view class="my-status" :class="getMyStatusClass(activity.myStatus)">
{{ getMyStatusText(activity.myStatus) }}
</view>
<view v-if="activity.myStatus === 'reviewing'" class="review-tips">
<text>审核中请耐心等待</text>
</view>
<view v-if="activity.myStatus === 'rejected'" class="review-tips">
<text>未通过审核如有疑问请联系管理员</text>
</view>
</view> -->
<!-- 操作按钮 -->
<view class="action-buttons">
<button v-if="activeTab === 'list' && !activity.hasSigned" class="action-btn signup-btn"
@tap.stop="signupActivity(activity)">
立即报名
</button>
<!-- <button v-if="activeTab === 'list' && activity.hasSigned" class="action-btn signed-btn"
disabled>
已报名
</button> -->
<!-- <button
v-if="activeTab === 'my' && activity.myStatus === 'passed' && activity.status === 'ongoing'"
class="action-btn signup-btn" @tap.stop="viewActivityDetail(activity)">
查看详情
</button> -->
<!-- <button
v-if="activeTab === 'my' && activity.myStatus === 'passed' && activity.status === 'ended'"
class="action-btn ended-btn" disabled>
已结束
</button> -->
<!-- <button v-if="activeTab === 'my' && activity.myStatus === 'rejected'"
class="action-btn reject-btn" @tap.stop="contactAdmin(activity)">
联系管理员
</button> -->
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMoreData && displayedActivities.length > 0" class="load-more-container">
<view v-if="!isLoading" class="load-more-btn" @tap="loadMoreData">
<text>加载更多</text>
<uni-icons type="arrow-down" size="16" color="#007AFF"></uni-icons>
</view>
<view v-else class="loading-container">
<uni-icons type="spinner-cycle" size="20" color="#007AFF" class="loading-icon"></uni-icons>
<text>正在加载...</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
computed,
onMounted,
watch
} from 'vue'
import moment from 'moment';
// 响应式数据
const activeTab = ref('list') // 'list' 或 'my'
const myFilter = ref('reviewing') // 我的活动筛选条件
const isLoading = ref(false)
const hasMoreData = ref(true)
const currentPage = ref(1)
const pageSize = 5
// 筛选条件
const filters = ref([{
label: '审核中',
value: 'reviewing'
},
{
label: '已通过',
value: 'passed'
},
{
label: '未通过',
value: 'rejected'
},
{
label: '已结束',
value: 'ended'
}
])
// 模拟活动数据
const allActivities = ref([{
id: 1,
title: '阳光海岸社区生活节活动',
startTime: '2023-06-19 08:00',
endTime: '2023-06-30 17:00',
location: '山东省日照市东港区莱顿二期花语墅西沿街',
totalQuota: 100,
signedCount: 68,
status: 'ongoing', // ongoing, upcoming, ended
hasSigned: true,
myStatus: 'passed', // reviewing, passed, rejected
coverImage: '/static/activity1.jpg',
description: '阳光海岸社区生活节是一个为期两周的社区文化活动,包括文艺表演、手工艺品展示、美食节等多种活动。'
},
{
id: 2,
title: '社区环保志愿者招募活动',
startTime: '2023-07-01 09:00',
endTime: '2023-07-31 18:00',
location: '社区公园及周边街道',
totalQuota: 50,
signedCount: 32,
status: 'upcoming',
hasSigned: false,
myStatus: 'reviewing',
coverImage: '/static/activity2.jpg',
description: '招募社区环保志愿者,参与垃圾分类宣传、社区清洁等环保活动。'
},
{
id: 3,
title: '端午节包粽子亲子活动',
startTime: '2023-06-10 14:00',
endTime: '2023-06-10 17:00',
location: '社区活动中心',
totalQuota: 30,
signedCount: 30,
status: 'ended',
hasSigned: true,
myStatus: 'passed',
coverImage: '/static/activity3.jpg',
description: '端午节亲子活动,教孩子们包粽子,了解传统文化。'
},
{
id: 4,
title: '老年人智能手机使用培训',
startTime: '2023-07-05 14:00',
endTime: '2023-07-12 16:00',
location: '社区图书馆二楼',
totalQuota: 20,
signedCount: 15,
status: 'upcoming',
hasSigned: false,
myStatus: null,
coverImage: '/static/activity4.jpg',
description: '为社区老年人提供智能手机使用培训包括微信、支付宝等常用APP的使用。'
},
{
id: 5,
title: '社区篮球友谊赛',
startTime: '2023-06-25 15:00',
endTime: '2023-06-25 18:00',
location: '社区体育馆',
totalQuota: 40,
signedCount: 25,
status: 'ongoing',
hasSigned: false,
myStatus: 'rejected',
coverImage: '/static/activity5.jpg',
description: '社区篮球友谊赛,欢迎篮球爱好者报名参加。'
},
{
id: 6,
title: '儿童暑期绘画班',
startTime: '2023-07-10 09:00',
endTime: '2023-08-20 11:00',
location: '社区艺术工作室',
totalQuota: 25,
signedCount: 18,
status: 'upcoming',
hasSigned: true,
myStatus: 'reviewing',
coverImage: '/static/activity6.jpg',
description: '暑期儿童绘画班,培养孩子的艺术兴趣和创造力。'
},
{
id: 7,
title: '社区消防安全演练',
startTime: '2023-05-15 10:00',
endTime: '2023-05-15 12:00',
location: '社区广场',
totalQuota: 100,
signedCount: 85,
status: 'ended',
hasSigned: true,
myStatus: 'passed',
coverImage: '/static/activity7.jpg',
description: '社区消防安全知识讲座和应急演练。'
},
{
id: 8,
title: '邻里美食分享会',
startTime: '2023-07-08 16:00',
endTime: '2023-07-08 19:00',
location: '社区花园',
totalQuota: 40,
signedCount: 22,
status: 'upcoming',
hasSigned: false,
myStatus: null,
coverImage: '/static/activity8.jpg',
description: '邻里美食分享会,每家带一道拿手菜,共享美食。'
}
])
// 当前显示的活动列表
const displayedActivities = ref([])
// 计算属性:根据当前标签和筛选条件过滤活动
const filteredActivities = computed(() => {
if (activeTab.value === 'list') {
// 活动列表页:显示所有活动
return allActivities.value
} else {
// 我的活动页:根据筛选条件过滤
if (myFilter.value === 'all') {
return allActivities.value.filter(activity => activity.hasSigned)
} else {
return allActivities.value.filter(activity =>
activity.hasSigned && activity.myStatus === myFilter.value
)
}
}
})
// 切换选项卡
const switchTab = (tab) => {
activeTab.value = tab
currentPage.value = 1
displayedActivities.value = []
loadPageData(1)
}
// 切换筛选条件
const switchFilter = (filter) => {
myFilter.value = filter
currentPage.value = 1
displayedActivities.value = []
loadPageData(1)
}
// 加载分页数据
const loadPageData = (pageNum) => {
isLoading.value = true
// 模拟网络请求延迟
setTimeout(() => {
const startIndex = (pageNum - 1) * pageSize
const endIndex = startIndex + pageSize
const pageData = filteredActivities.value.slice(startIndex, endIndex)
if (pageNum === 1) {
displayedActivities.value = pageData
} else {
displayedActivities.value = [...displayedActivities.value, ...pageData]
}
currentPage.value = pageNum
hasMoreData.value = displayedActivities.value.length < filteredActivities.value.length
isLoading.value = false
}, 500)
}
// 加载更多数据
const loadMoreData = () => {
if (isLoading.value || !hasMoreData.value) return
loadPageData(currentPage.value + 1)
}
// 获取活动状态文本
// const getStatusText = (status) => {
// const statusMap = {
// 'ongoing': '进行中',
// 'upcoming': '即将开始',
// 'ended': '已结束'
// }
// return statusMap[status] || '未知'
// }
// 获取活动状态类名
const getStatusClass = (status) => {
const classMap = {
'ongoing': 'status-ongoing',
// 'upcoming': 'status-upcoming',
// 'ended': 'status-ended'
}
return classMap[status] || ''
}
// 获取我的活动状态文本
const getMyStatusText = (status) => {
const statusMap = {
'reviewing': '审核中',
'passed': '已通过',
'rejected': '未通过',
'ended': '已结束'
}
return statusMap[status] || '未知'
}
// 获取我的活动状态类名
const getMyStatusClass = (status) => {
const classMap = {
'reviewing': 'my-status-reviewing',
'passed': 'my-status-passed',
'rejected': 'my-status-rejected',
'ended': 'my-status-ended'
}
return classMap[status] || ''
}
// 格式化日期
function formatDate(date){
return moment(date).format('YYYY-MM-DD HH:mm:ss')
}
// 报名活动
const signupActivity = (activity) => {
// console.log('fffff', activity)
uni.navigateTo({
url: `/pages/activity-list/activityListDetails?id=${activity.id}&title=${activity.title}`
})
}
// const signupActivity = (activity) => {
// uni.showModal({
// title: '报名确认',
// content: `确定要报名参加"${activity.title}"吗?`,
// success: (res) => {
// if (res.confirm) {
// // 模拟报名成功
// activity.hasSigned = true
// activity.myStatus = 'reviewing'
// activity.signedCount += 1
// uni.showToast({
// title: '报名成功,等待审核',
// icon: 'success',
// duration: 2000
// })
// // 如果是我的活动页,刷新列表
// if (activeTab.value === 'my') {
// currentPage.value = 1
// displayedActivities.value = []
// loadPageData(1)
// }
// }
// }
// })
// }
// 查看活动详情
const viewActivityDetail = (activity) => {
uni.navigateTo({
url: `/pages/activityDetail/activityDetail?id=${activity.id}`,
fail: () => {
// 如果页面不存在,显示活动信息
uni.showModal({
title: activity.title,
content: `${activity.description}\n\n时间${formatDate(activity.startTime)}${formatDate(activity.endTime)}\n地点${activity.location}\n名额${activity.totalQuota}人,已报名:${activity.signedCount}`,
showCancel: false,
confirmText: '知道了'
})
}
})
}
// 联系管理员
const contactAdmin = (activity) => {
uni.showModal({
title: '联系管理员',
content: `您报名"${activity.title}"未通过审核,是否联系管理员了解原因?`,
success: (res) => {
if (res.confirm) {
uni.makePhoneCall({
phoneNumber: '400-123-4567'
})
}
}
})
}
// 监听筛选条件变化
watch(() => myFilter.value, () => {
if (activeTab.value === 'my') {
currentPage.value = 1
displayedActivities.value = []
loadPageData(1)
}
})
// 初始化
onMounted(() => {
loadPageData(1)
})
</script>
<style>
/* 页面容器 */
.container {
min-height: 100vh;
background-color: #f9f9f9;
display: flex;
flex-direction: column;
padding: 20rpx;
}
.topImg {
width: 100%;
height: 280rpx;
margin-bottom: 20rpx;
}
/* 顶部选项卡 */
.tabs-container {
display: flex;
/* background-color: #fff; */
/* border-bottom: 1px solid #eee; */
/* position: sticky; */
top: 0;
z-index: 10;
margin-bottom: 20rpx
}
.tab-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 32rpx;
color: #666;
position: relative;
}
.tab-item.active {
color: #007AFF;
font-weight: 500;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 120rpx;
height: 6rpx;
background-color: #007AFF;
border-radius: 4rpx;
}
/* 筛选容器 */
.filter-container {
background-color: #fff;
padding: 20rpx 0;
border-bottom: 1px solid #eee;
}
.filter-scroll {
width: 100%;
white-space: nowrap;
}
.filter-buttons {
display: inline-flex;
padding: 0 30rpx;
}
.filter-btn {
display: inline-block;
padding: 12rpx 30rpx;
margin-right: 20rpx;
font-size: 28rpx;
color: #666;
background-color: #f8f8f8;
border-radius: 25rpx;
border: 1px solid #eee;
}
.filter-btn.active {
color: #fff;
background-color: #2F77FD;
/* border-color: #007AFF; */
}
/* 活动列表 */
.activity-list {
flex: 1;
/* padding: 30rpx; */
margin-bottom: 20rpx;
}
/* 活动卡片 */
.activity-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
position: relative;
}
/* 活动状态标签 */
.status-badge {
position: absolute;
top: 10rpx;
left: 20rpx;
padding: 6rpx 20rpx;
font-size: 22rpx;
border-radius: 15rpx 0rpx 15rpx 0rpx;
color: #fff;
z-index: 1;
background: repeating-linear-gradient(to right, #FD7373 , #FB3F3F);
}
/* .status-ongoing {
background: repeating-linear-gradient(to right, #FD7373 , #FB3F3F);
} */
/* .status-upcoming {
background-color: #ff9d00;
}
.status-ended {
background-color: #999;
} */
/* 已报名标识 */
.signed-badge {
position: absolute;
top: 20rpx;
right: 8rpx;
display: flex;
align-items: center;
padding: 6rpx 14rpx;
background-color: #007AFF;
color: #fff;
font-size: 22rpx;
border-radius: 20rpx;
z-index: 1;
}
.signed-badge text {
margin-left: 8rpx;
}
/* 活动信息 */
.activity-info {
margin-top: 10rpx;
}
.info-image-left {
width: 170rpx;
height: 220rpx;
}
.activity-title {
font-size: 30rpx;
font-weight: 500;
color: #222;
margin-bottom: 30rpx;
line-height: 1.4;
padding-right: 100rpx;
}
.activity-detail {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
color: #999;
font-size: 24rpx;
line-height: 1.4;
}
.activity-detail .uni-icons {
margin-right: 15rpx;
margin-top: 4rpx;
flex-shrink: 0;
}
.detail-text-num{
font-size: 28rpx;
color: #666666;
}
.detail-text {
flex: 1;
}
/* 我的活动信息 */
.my-activity-info {
margin-top: 20rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
}
.my-status {
display: inline-block;
padding: 8rpx 24rpx;
font-size: 24rpx;
border-radius: 20rpx;
margin-bottom: 10rpx;
}
.my-status-reviewing {
background-color: #e6f2ff;
color: #007AFF;
}
.my-status-passed {
background-color: #e6f7f0;
color: #07c160;
}
.my-status-rejected {
background-color: #fdeeee;
color: #f5222d;
}
.my-status-ended {
background-color: #f5f5f5;
color: #999;
}
.review-tips {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
/* 操作按钮 */
.action-buttons {
margin-top: 30rpx;
display: flex;
justify-content: flex-end;
}
.action-btn {
padding: 0 40rpx;
height: 60rpx;
line-height: 60rpx;
font-size: 24rpx;
border-radius: 35rpx;
margin: 0;
}
.signup-btn {
background-color: #007AFF;
color: #fff;
}
.signup-btn:active {
background-color: #0056cc;
}
.signed-btn {
background-color: #f0f0f0;
color: #999;
}
.reject-btn {
background-color: #fff;
color: #f5222d;
border: 1px solid #f5222d;
}
.ended-btn {
background-color: #f5f5f5;
color: #999;
}
/* 加载更多 */
.load-more-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
color: #999;
}
.load-more-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background-color: #f8f8f8;
border-radius: 50rpx;
color: #007AFF;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.load-more-btn text {
margin-right: 10rpx;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background-color: #f8f8f8;
border-radius: 50rpx;
color: #007AFF;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.loading-icon {
margin-right: 10rpx;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 空状态 */
.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;
}
/* 适配不同屏幕 */
@media (min-width: 768px) {
.container {
max-width: 500px;
margin: 0 auto;
}
}
</style>

View File

@@ -1,285 +0,0 @@
<template>
<view class="add-status-page">
<!-- 主体内容 -->
<scroll-view class="content" scroll-y>
<!-- 实况描述输入框 -->
<view class="form-item">
<text class="label">实况描述</text>
<textarea
class="textarea"
placeholder="请输入活动实况内容,也可打卡签到"
v-model="statusDesc"
maxlength="500"
@input="handleTextInput"
></textarea>
<text class="count">{{ currentLength }}/500</text>
</view>
<!-- 活动照片上传 -->
<view class="form-item">
<text class="label">活动照片</text>
<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>
</view>
<!-- 上传按钮最多3张 -->
<view class="upload-btn" @click="chooseImg" v-if="imgList.length < 3">
<text class="plus-icon">+</text>
<text class="tips">最多3张图片</text>
</view>
</view>
</view>
<!-- 签到地点 -->
<view class="form-item">
<text class="label">签到地点</text>
<view class="location-area">
<text class="location-text">{{ location }}</text>
<view class="re-locate" @click="reLocate">
<uni-icons type="refreshempty" size="14" color="#2196f3" class="re-icon"></uni-icons>
重新定位
</view>
</view>
</view>
</scroll-view>
<!-- 底部提交按钮 -->
<view class="submit-btn-area">
<button class="submit-btn" @click="submitStatus">提交</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
// import { chooseImage, uploadFile, getLocation } from '@dcloudio/uni-app';
// 实况描述内容
const statusDesc = ref('');
// 输入字符长度
const currentLength = ref(0);
// 图片列表
const imgList = ref([]);
// 定位信息
const location = ref('山东省日照市东港区学院路');
// 监听输入框字数变化
const handleTextInput = (e) => {
currentLength.value = e.detail.value.length;
};
// 选择图片
const chooseImg = () => {
uni.chooseImage({
count: 3 - imgList.value.length, // 最多可选择的数量
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
// 临时文件路径转存到图片列表
imgList.value = [...imgList.value, ...res.tempFilePaths];
},
fail: (err) => {
uni.showToast({ title: '选择图片失败', icon: 'none' });
console.error('选择图片失败:', err);
}
});
};
// 删除图片
const deleteImg = (index) => {
imgList.value.splice(index, 1);
};
// 重新定位
const reLocate = () => {
uni.showLoading({ title: '定位中...' });
uni.getLocation({
type: 'gcj02',
success: (res) => {
uni.hideLoading();
// 实际项目中可调用逆地理编码接口转换为具体地址,这里模拟
location.value = '山东省日照市东港区学院路(新定位)';
uni.showToast({ title: '定位成功', icon: 'success' });
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: '定位失败,请检查权限', icon: 'none' });
console.error('定位失败:', err);
}
});
};
// 提交实况
const submitStatus = () => {
// 简单校验
if (!statusDesc.value.trim()) {
uni.showToast({ title: '请输入实况描述', icon: 'none' });
return;
}
// 模拟提交逻辑(实际项目中可上传图片+提交表单)
uni.showLoading({ title: '提交中...' });
// 如需上传图片可循环调用uploadFile
// 示例:
// const uploadPromises = imgList.value.map(img => {
// return uploadFile({
// url: '你的上传接口',
// filePath: img,
// name: 'file'
// });
// });
// Promise.all(uploadPromises).then(res => { /* 处理上传结果 */ });
setTimeout(() => {
uni.hideLoading();
uni.showToast({ title: '添加成功', icon: 'success' });
// 提交成功后可返回上一页
uni.navigateBack();
}, 1000);
};
</script>
<style scoped>
/* 页面整体样式 */
.add-status-page {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 主体内容 */
.content {
flex: 1;
padding: 15px 0px;
}
/* 表单项通用样式 */
.form-item {
background-color: #fff;
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
}
.label {
font-size: 16px;
font-weight: 500;
margin-bottom: 10px;
display: block;
}
/* 输入框样式 */
.textarea {
width: 100%;
min-height: 120px;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.count {
text-align: right;
font-size: 12px;
color: #999;
margin-top: 5px;
display: block;
}
/* 图片上传区域 */
.upload-area {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.uploaded-img {
width: 80px;
height: 80px;
position: relative;
}
.uploaded-img image {
width: 100%;
height: 100%;
border-radius: 4px;
}
.del-img {
position: absolute;
top: -5px;
right: -5px;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
background-color: #ff4444;
color: #fff;
border-radius: 50%;
font-size: 16px;
}
.upload-btn {
width: 80px;
height: 80px;
border: 1px dashed #ddd;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.plus-icon {
font-size: 24px;
color: #999;
margin-bottom: 5px;
}
.tips {
font-size: 12px;
color: #999;
}
/* 定位区域 */
.location-area {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
}
.location-text {
font-size: 14px;
color: #333;
}
.re-locate {
font-size: 14px;
color: #007aff;
display: flex;
align-items: center;
}
.re-icon{
color: #007aff;
}
/* .re-locate::before {
content: '';
width: 16px;
height: 16px;
background: url(/static/logo.png) no-repeat center;
background-size: 100%;
margin-right: 5px;
} */
/* 提交按钮 */
.submit-btn-area {
padding: 15px;
}
.submit-btn {
width: 100%;
height: 48px;
line-height: 48px;
background-color: #007aff;
color: #fff;
border: none;
border-radius: 8px;
font-size: 16px;
}
</style>

View File

@@ -1,23 +0,0 @@
import request,{get,post} from "/pages/api/request.js"
// =======账号密码登录========
export const getLoginPassword = (data) =>{
return post('/api/resident/auth/loginByPassword',data)
}
// 注册新用户获取验证码
export const getSendCode = (data) =>{
return post('/api/resident/auth/sendSmsCode',data)
}
// 注册新用户
export const getRegisterUser = (data) =>{
return post('/api/resident/auth/register',data)
}
// 短信验证码登录
export const getLoginBySms = (data) =>{
return post('/api/resident/auth/register',data)
}
// 忘记密码接口
export const getResetPassword = (data) =>{
return post('/api/resident/auth/resetPassword',data)
}

View File

@@ -1,75 +0,0 @@
let baseUrl = '';
if (process.env.NODE_ENV === 'development') {
baseUrl = 'http://192.168.0.224:8080'; // 开发环境
} else {
baseUrl = 'http://192.168.1.224:8080'; // 生产环境
}
export default function request(url, data = {}, method = "GET") {
return new Promise((resolve, reject) => {
// 加载中提示(提升用户体验)
uni.showLoading({ title: '请求中...', mask: true });
uni.request({
url: baseUrl + url,
data: data,
method: method,
header: {
"token": uni.getStorageSync("token") || '',
'content-type': 'application/json'
},
success: (res) => {
uni.hideLoading(); // 关闭加载提示
// 处理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: '/pages/login/login' });
}, 1500);
reject('token 过期');
return;
}
// 业务成功code=1
if (resData.code === 200) {
resolve(resData.data); // 只返回核心数据,简化页面调用
}
// 业务失败code=2
else if (resData.code === 500) {
const errMsg = resData.msg || '请求失败';
uni.showToast({ title: errMsg, icon: "none", duration: 2000 });
resolve(null)
// reject(errMsg);
}
else {
const errMsg = resData.msg || '业务异常';
uni.showToast({ title: errMsg, icon: "none", duration: 2000 });
reject(errMsg);
}
},
fail: (err) => {
uni.hideLoading(); // 关闭加载提示
uni.showToast({ title: '网络连接失败', icon: "none", duration: 2000 });
reject(err);
}
});
});
}
export const get = (url, data) => request(url, data, 'GET');
export const post = (url, data) => request(url, data, 'POST');

File diff suppressed because it is too large Load Diff

View File

@@ -1,325 +1,8 @@
<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="/static/登录/手机号.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<!-- <input
class="input-content"
type="number"
placeholder="请输入手机号"
v-model="phoneNumber"
maxlength="11"
@input="checkForm"
/> -->
</view>
<!-- 验证码输入框 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
<image src="/static/登录/验证码.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode"
placeholder="请输入验证码" :inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<!-- <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>
<!-- 协议勾选 -->
<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>
<!-- 登录按钮 -->
<button
class="login-btn"
:disabled="!canLogin"
@click="handleLogin"
>
登录
</button>
<!-- 底部链接 -->
<view class="bottom-link">
<navigator class="link-item" url="/pages/login/pwd-login">账号密码登录</navigator>
<navigator class="link-item" url="/pages/loginSub/register-new-user">注册新用户</navigator>
</view>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import {getLoginBySms,getSendCode} from '/pages/api/api.js'
// ========== 响应式数据 ==========
// 手机号
const phoneNumber = ref('');
// 验证码
const verifyCode = ref('');
const placeholderStyle = ref('font-size:24rpx')
// 是否同意协议
const agreeAgreement = ref(true); // 默认勾选
// 倒计时(秒)
const countDown = ref(0);
// 定时器
let timer = null;
// ========== 计算属性 ==========
// 是否可以获取验证码(手机号格式正确)
const canGetVerify = computed(() => {
// 手机号正则11位数字以1开头
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 checkForm = () => {
// 无需额外逻辑,计算属性已实时监听
};
// 切换协议勾选状态
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone:phoneNumber.value,
scene:'login'
}
getSendCode(params).then(res =>{
if(res.code === 200){
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
}else{
uni.showToast({
title: '验证码发送失败',
icon: 'error'
});
}
})
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
};
// 登录处理
const handleLogin = () => {
if (!canLogin.value) return;
let params = {
phone:phoneNumber.value,
smsCode:'123456',
scene:'login',
}
getLoginBySms(params).then(res =>{
if(res.code === 200){
//uni.setStorageSync('token', res.token); // 假设后端返回token字段
uni.showToast({ title: '登录成功', icon: 'success' });
}else{
uni.showToast({ title:'登录失败', icon: 'error' });
}
})
uni.switchTab({ url: '/pages/index/index' });
// 模拟登录请求
// uni.showLoading({
// title: '登录中...'
// });
};
// ========== 生命周期 ==========
// 页面卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
<script>
</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);
}
/* 验证码输入项(特殊布局) */
.verify-item {
position: relative;
padding-right: 220rpx;
}
/* 输入框图标 */
.input-icon {
width: 50rpx;
height: 50rpx;
margin-right: 20rpx;
color: #999;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
/* 验证码输入框 */
.verify-input {
padding-right: 20rpx;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #007aff;
color: #fff;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
margin: 0;
}
/* 获取验证码按钮禁用状态 */
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
}
/* 协议勾选项 */
.agreement-item {
display: flex;
align-items: center;
margin: 40rpx 0;
padding-left: 10rpx;
}
/* 协议文本 */
.agreement-text {
font-size: 28rpx;
color: #666;
margin-left: 10rpx;
}
/* 登录按钮 */
.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;
}
/* 底部链接 */
.bottom-link {
display: flex;
justify-content: space-between;
padding: 0 20rpx;
}
/* 链接项 */
.link-item {
font-size: 28rpx;
color: #007aff;
text-decoration: none;
}
<style>
</style>

View File

@@ -1,260 +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="/static/登录/手机号.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="/static/登录/密码.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" @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>
<navigator class="forget-pwd" url="/pages/loginSub/forget-pwd">忘记密码</navigator>
</view>
<!-- 登录按钮 -->
<button
class="login-btn"
@click="handleLogin"
>
登录
</button>
<!-- 底部链接 -->
<view class="bottom-link">
<navigator class="link-item" url="/pages/login/login">验证码登录</navigator>
<navigator class="link-item" url="/pages/loginSub/register-new-user">注册新用户</navigator>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import {onLoad} from "@dcloudio/uni-app"
import {getLoginPassword} from '/pages/api/api.js'
// 响应式数据
const phoneNumber = ref('');
const password = ref('');
const agreeAgreement = ref(true); // 默认勾选
const placeholderStyle = ref('font-size:24rpx')
// 计算属性:是否可登录(帐号+密码+协议都满足)
const canLogin = computed(() => {
const phoneReg = /^[a-zA-Z0-9_-]{4,16}$/; // 4-16位
const pwdReg = /^.{6,16}$/; // 密码6-16位
return phoneReg.test(phoneNumber.value) && pwdReg.test(password.value) && agreeAgreement.value;
});
// 切换协议勾选
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 检查表单
const checkForm = () => {};
onLoad(() =>{
});
const handleLogin = async () => {
if (!canLogin.value) return;
// 防止多次点击触发登录
canLogin.value = false;
// // 2. 空值校验
// if (!phoneNumber.value) {
// uni.showToast({ title: '请输入手机号', icon: 'none' });
// canLogin.value = true; // 解锁
// return;
// }
// if (!password.value) {
// uni.showToast({ title: '请输入密码', icon: 'none' });
// canLogin.value = true; // 解锁
// return;
// }
const params = {
username: phoneNumber.value,
password: password.value
};
try {
// 3. 调用登录接口
const res = await getLoginPassword(params);
// 登录成功逻辑
// 如果后端返回token这里保存
// uni.setStorageSync('token', res.token || '');
uni.showToast({ title: '登录成功', icon: 'success' });
// 登录成功后再跳转
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' });
}, 1500);
} catch (err) {
// 登录失败逻辑(所有错误:账号不存在/密码错/网络异常都走这里)
uni.showToast({ title: `${err || '登录失败'}`, icon: 'none' });
} finally {
// 无论成功/失败,解锁按钮
canLogin.value = true;
}
};
// 登录逻辑
// const handleLogin = () => {
// if (!canLogin.value) return
// let params = {
// username:phoneNumber.value,
// password:password.value
// }
// getLoginPassword(params).then(res =>{
// // console.log('33333',res)
// if(res.code === 200){
// // console.log('33333',res)
// //uni.setStorageSync('token', res.token); // 假设后端返回token字段
// uni.showToast({ title: '登录成功', icon: 'success' });
// uni.switchTab({ url: '/pages/index/index' });
// // uni.reLaunch({ url: '/pages/index/index' });
// }else{
// uni.showToast({ title: `${res.msg},请先注册账号`, icon: 'error' });
// }
// })
// };
</script>
<style scoped>
.login-page {
padding: 80rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar {
height: 160px;
width: 100%;
}
.login-title {
font-size: 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;
}
.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;
}
.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,365 +0,0 @@
<template>
<view class="register-page">
<view class="status_bar"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">注册</view>
</view>
<!-- 注册标题 -->
<view class="register-title">注册用户</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="/static/登录/手机号.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 verify-item">
<view class="input-icon">
<image src="/static/登录/验证码.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">
<view class="input-icon">
<image src="/static/登录/账号.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>
<!-- 密码输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="/static/登录/密码.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="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>
<!-- 立即注册按钮 -->
<button
class="register-btn"
:disabled="!canRegister"
@click="handleRegister"
>
立即注册
</button>
<!-- 跳转登录链接 -->
<view class="login-link">
<navigator class="link-item" url="/pages/login/pwd-login">已有账号去登录</navigator>
</view>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import {getRegisterUser,getSendCode} from '/pages/api/api.js'
// ========== 响应式数据 ==========
// 表单数据
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;
// ========== 计算属性 ==========
// 是否可获取验证码(手机号格式正确)
const canGetVerify = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
// 是否可注册(所有表单项+协议都满足)
const canRegister = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/; // 手机号11位
const verifyReg = /^\d{6}$/; // 验证码6位
const accountReg = /^.{4,20}$/; // 账号4-20位
const pwdReg = /^.{6,16}$/; // 密码6-16位
return phoneReg.test(phoneNumber.value)
&& verifyReg.test(verifyCode.value)
&& accountReg.test(account.value)
&& pwdReg.test(password.value)
&& agreeAgreement.value;
});
// ========== 方法 ==========
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 切换协议勾选状态
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 检查表单状态(实时更新按钮状态)
const checkForm = () => {};
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone:phoneNumber.value,
scene:'register'
}
getSendCode(params).then(res =>{
if(res.code === 200){
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
}else{
uni.showToast({
title: '验证码发送失败',
icon: 'error'
});
}
})
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
};
// 处理注册逻辑
const handleRegister = () => {
if (!canRegister.value) return;
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'
});
}else{
uni.showToast({
title: '注册失败,请重新注册',
icon: 'error'
});
}
})
// 注册成功后跳转到登录页
uni.redirectTo({
url: '/pages/login/pwd-login'
});
// setTimeout(() => {
// }, 1500);
};
// ========== 生命周期 ==========
// 页面卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style scoped>
/* 页面容器 */
.register-page {
padding: 20rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar {
height: 46px;
width: 100%;
}
/* 顶部导航栏 */
.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;
}
/* 注册标题 */
.register-title {
font-size: 48rpx;
font-weight: 600;
color: #333;
margin-bottom: 60rpx;
}
/* 输入框通用样式 */
.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%;
}
.verify-input {
padding-right: 20rpx;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #007aff;
color: #fff;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
margin: 0;
}
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
}
/* 协议勾选项 */
.agreement-item {
display: flex;
align-items: center;
margin: 40rpx 0;
padding-left: 10rpx;
}
.agreement-text {
font-size: 28rpx;
color: #666;
margin-left: 10rpx;
}
/* 立即注册按钮 */
.register-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
margin-bottom: 40rpx;
}
.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

@@ -1,738 +0,0 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="type+'车辆'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y="true">
<!-- 表单区域 -->
<view class="form-container">
<!-- 车牌号 -->
<view class="form-item">
<view class="item-label">车牌号</view>
<input class="item-input" v-model="formData.plateNo" placeholder="请输入"
placeholder-class="input-placeholder" @blur="inputChange('车牌号')" />
</view>
<!-- 车辆品牌选择 -->
<view class="form-item" @click="selectItem('车辆品牌')">
<text class="item-label">车辆品牌</text>
<text class="item-placeholder"
:class="{selected:formData.carBrandName}">{{ formData.carBrandName || '请选择' }}</text>
<text class="item-arrow"></text>
</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('车辆型号')" />
</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('车辆颜色')" />
</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">
<text class="plus-icon">+</text>
<text class="upload-text">上传照片</text>
</view>
<!-- 已上传显示图片 -->
<image v-else :src="formData.carImg" class="car-image" mode="aspectFill" />
</view>
</view>
</view>
</scroll-view>
<!-- 底部下一步按钮 -->
<view class="bottom-btn-wrap">
<button class="next-btn" @click="saveCar">保存</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
onShow() {
},
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 = '/static/propertyImgs/carImg.png';
this.formData.carBrandId = '1';
} else if (uni.getStorageSync('operationType') == 'add') {
this.id = undefined;
this.type = '添加';
}
},
onUnload() {},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
id: undefined,
type: '',
// 表单数据
formData: {
plateNo: uni.getStorageSync('plateNo'),
carBrandName: uni.getStorageSync('carBrandName'),
model: uni.getStorageSync('model'),
color: uni.getStorageSync('color'),
carImg: uni.getStorageSync('carImg') // 车辆图片路径
}
}
},
methods: {
inputChange(type) {
if (type == '车牌号') {
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();
}, 3000);
}
})
},
// 选择项点击事件可替换为picker/弹窗选择)
selectItem(type) {
if (type === '车辆品牌') {
uni.redirectTo({
url: '/pages/my/selectCarBrand',
success: res => {},
fail: () => {},
complete: () => {}
});
}
},
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: '/pages/my/houseDetail?id=' + this.id,
// success: res => {},
// fail: () => {},
// complete: () => {}
// });
// } else if (uni.getStorageSync('operationType') == 'add') {
uni.redirectTo({
url: '/pages/my/myCarIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
// }
},
},
}
</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;
height: 46px;
}
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
box-sizing: border-box;
background-color: #fff;
}
/* 步骤条 */
.step-bar {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 15px 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: 14px;
margin-bottom: 16rpx;
}
.step-item.active .step-circle {
background-color: #1677ff;
}
.step-text {
font-size: 14px;
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 - 220px); */
/* overflow-y: scroll; */
}
/* 表单项 */
.form-item {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1rpx solid #f0f0f0;
}
.item-label {
font-size: 14px;
color: #000;
width: 90px;
font-weight: 600;
}
.item-placeholder {
flex: 1;
font-size: 14px;
color: #999;
}
.item-placeholder.selected {
flex: 1;
font-size: 14px;
color: #333;
}
.item-arrow {
display: inline-block;
width: 5px;
height: 5px;
border-top: 2rpx solid #999;
border-right: 2rpx solid #999;
transform: rotate(45deg);
margin-left: 10rpx;
margin-right: 5px;
}
/* 开关项 */
.switch-item {
justify-content: space-between;
}
/* 底部按钮 */
.bottom-btn-wrap {
padding: 10px 15px 20px;
display: flex;
gap: 20rpx;
background-color: #fff;
}
.next-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background-color: #1677ff;
color: #fff;
font-size: 36rpx;
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: 15px 0;
border-bottom: 1rpx solid #f0f0f0;
}
.label {
font-size: 14px;
color: #000;
width: 90px;
font-weight: 600;
}
.value {
flex: 1;
text-align: left;
font-size: 14px;
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: 15px;
text-align: center;
border-bottom: 1rpx solid #f0f0f0;
position: relative;
}
.title {
font-size: 32rpx;
font-weight: 500;
}
.close {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
font-size: 36rpx;
color: #666;
}
/* 选项列表 */
.list {
padding: 0 15px;
}
.item {
padding: 15px 0;
text-align: center;
font-size: 14px;
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: 14px;
color: #333;
}
.input {
flex: 1;
font-size: 14px;
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: 14px;
color: #999;
padding-right: 40rpx;
}
.item-input {
flex: 1;
height: 40rpx;
font-size: 14px;
color: #333;
border: none;
outline: none;
background: transparent;
}
.input-placeholder {
color: #999 !important;
font-size: 14px !important;
}
/* ===================== 图片上传区域 ===================== */
.upload-item {
align-items: flex-start;
padding-top: 40rpx;
padding-bottom: 40rpx;
border-bottom: none;
}
.upload-box {
width: 280rpx;
height: 280rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f9f9f9;
}
.upload-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.plus-icon {
font-size: 80rpx;
color: #999;
line-height: 1;
margin-bottom: 10rpx;
}
.upload-text {
font-size: 28rpx;
color: #999;
}
.car-image {
width: 100%;
height: 100%;
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: 15px;
text-align: center;
border-bottom: 1rpx solid #eee;
font-size: 32rpx;
font-weight: 500;
position: relative;
}
.close {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
font-size: 36rpx;
color: #666;
}
.popup-list {
padding: 0 15px;
max-height: 40vh;
overflow-y: auto;
}
.item {
padding: 15px 0;
text-align: center;
font-size: 14px;
border-bottom: 1rpx solid #f5f5f5;
}
/* 成功提示弹窗 */
.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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,913 +0,0 @@
<template>
<view class="contentStyle" style="">
<view class="status-bar"></view>
<uni-nav-bar :title="type+'车位'" left-icon="left" @clickLeft="goBack" backgroundColor="transparent"
:border="false">
</uni-nav-bar>
<scroll-view class="page" scroll-y="true">
<!-- 表单区域 -->
<view class="form-container">
<!-- 第一步 -->
<view>
<!-- 小区选择 -->
<view class="form-item" @click="selectItem('小区')">
<text class="item-label">小区</text>
<text class="item-placeholder"
:class="{selected:formData.community}">{{ formData.community || '请选择' }}</text>
<text class="item-arrow"></text>
</view>
<!-- 房屋选择 -->
<view class="form-item" @click="selectItem('绑定房屋')">
<text class="item-label">绑定房屋</text>
<text class="item-placeholder"
:class="{selected:formData.bindHouse}">{{ formData.bindHouse || '请选择' }}</text>
<text class="item-arrow"></text>
</view>
<!-- 停车场选择 -->
<view class="form-item" @click="selectItem('停车场')">
<text class="item-label">停车场</text>
<text class="item-placeholder"
:class="{selected:formData.parkingLot}">{{ formData.parkingLot || '请选择' }}</text>
<text class="item-arrow"></text>
</view>
<!-- 车位编号选择 -->
<view class="form-item" @click="selectItem('车位编号')">
<text class="item-label">车位编号</text>
<text class="item-placeholder"
:class="{selected:formData.parkingSpaceNum}">{{ formData.parkingSpaceNum || '请选择' }}</text>
<text class="item-arrow"></text>
</view>
<!-- 车位状态选择 -->
<view class="form-item">
<text class="label">车位状态</text>
<view class="parkingSpaceStatus-group">
<view class="parkingSpaceStatus-item"
:class="{ active: formData.parkingSpaceStatus == '自用' }"
@click="formData.parkingSpaceStatus = '自用'">
<view class="radio"></view>
<text>自用</text>
</view>
<view class="parkingSpaceStatus-item"
:class="{ active: formData.parkingSpaceStatus == '租赁' }"
@click="formData.parkingSpaceStatus = '租赁'">
<view class="radio"></view>
<text>租赁</text>
</view>
</view>
</view>
<!-- 绑定车辆选择 -->
<view class="form-item" @click="bindCarClick('绑定车辆')">
<text class="item-label">绑定车辆</text>
<text class="item-placeholder"
:class="{selected:formData.bindCar}">{{ formData.bindCar || '请选择' }}</text>
<text class="item-arrow"></text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部下一步按钮 -->
<view class="bottom-btn-wrap">
<button class="next-btn" @click="goSubmit">提交审核</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
onShow() {
},
onLoad(option) {
if (uni.getStorageSync('operationType') == 'update') {
this.id = option.id;
this.type = '修改';
//根据id获取回显数据
this.formData.community = '北境碧桂园小区';
this.formData.bindHouse = '01栋';
this.formData.parkingLot = '地下停车场';
this.formData.parkingSpaceNum = 'B1-02';
this.formData.bindCar = '宝马';
this.formData.communityId = '1';
this.formData.bindHouseId = '2';
this.formData.parkingLotId = '3';
this.formData.parkingSpaceId = '4';
this.formData.bindCarId = '自用';
this.formData.parkingSpaceStatus = '租赁';
} else if (uni.getStorageSync('operationType') == 'add') {
this.id = undefined;
this.type = '添加';
}
},
onUnload() {},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
id: undefined,
type: '',
formData: {
community: uni.getStorageSync('communityName'),
communityId: uni.getStorageSync('communityId'),
bindHouse: uni.getStorageSync('bindHouseName'),
bindHouseId: uni.getStorageSync('bindHouseId'),
parkingLot: uni.getStorageSync('parkingLotName'),
parkingLotId: uni.getStorageSync('parkingLotId'),
parkingSpaceNum: uni.getStorageSync('parkingSpaceName'),
parkingSpaceId: uni.getStorageSync('parkingSpaceId'),
parkingSpaceStatus: '自用',
bindCar: uni.getStorageSync('bindCarName'),
bindCarId: uni.getStorageSync('bindCarId'),
},
}
},
methods: {
// 完整校验
goSubmit() {
if (!this.formData.community) return uni.showToast({
title: '请选择小区',
icon: 'none'
})
if (!this.formData.bindHouse) return uni.showToast({
title: '请绑定房屋',
icon: 'none'
})
if (!this.formData.parkingLot) return uni.showToast({
title: '请选择停车场',
icon: 'none'
})
if (!this.formData.parkingSpaceNum) return uni.showToast({
title: '请选择车位编号',
icon: 'none'
})
if (!this.formData.bindCar) return uni.showToast({
title: '请选择绑定车辆',
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();
this.goBack();
}, 3000);
}
}
})
},
selectClick(item) {
this.formData.status = item;
uni.setStorageSync('houseStatus', item)
this.showPopup = false;
},
bindCarClick() {
uni.redirectTo({
url: '/pages/my/bindCar',
success: res => {},
fail: () => {},
complete: () => {}
});
},
// 选择项点击事件可替换为picker/弹窗选择)
selectItem(type) {
if (type === '小区') {
uni.redirectTo({
url: '/pages/my/selectResidentialCommunity?sourcePage=' + 'addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
}
if (type === '绑定房屋') {
if (!this.formData.community) {
uni.showToast({
title: '请先选择上级',
icon: 'none'
})
} else {
uni.redirectTo({
url: '/pages/my/bindHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
}
}
if (type === '停车场') {
if (!this.formData.community) {
uni.showToast({
title: '请先选择上级',
icon: 'none'
})
} else {
uni.setStorageSync('checkType', '停车场')
uni.redirectTo({
url: '/pages/my/selectParingLot',
success: res => {},
fail: () => {},
complete: () => {}
});
}
}
if (type === '车位编号') {
if (!this.formData.community) {
uni.showToast({
title: '请先选择上级',
icon: 'none'
})
} else {
uni.setStorageSync('checkType', '车位编号')
uni.redirectTo({
url: '/pages/my/selectParingLot',
success: res => {},
fail: () => {},
complete: () => {}
});
}
}
},
// 开关切换
onSwitchChange(e) {
this.formData.isDefault = e.detail.value
uni.setStorageSync('houseIsDefault', e.detail.value)
},
// 下一步
goNext() {
// 基础校验
if (!this.formData.community) {
uni.showToast({
title: '请选择小区',
icon: 'none'
})
return
}
if (!this.formData.building) {
uni.showToast({
title: '请选择楼栋',
icon: 'none'
})
return
}
if (!this.formData.unit) {
uni.showToast({
title: '请选择单元',
icon: 'none'
})
return
}
if (!this.formData.floor) {
uni.showToast({
title: '请选择楼层',
icon: 'none'
})
return
}
if (!this.formData.houseNumber) {
uni.showToast({
title: '请选择户号',
icon: 'none'
})
return
}
if (!this.formData.status) {
uni.showToast({
title: '请选择房屋状态',
icon: 'none'
})
return
}
this.currentStep = 1
//跳转
},
goBack() {
uni.removeStorageSync('communityId');
uni.removeStorageSync('communityName');
uni.removeStorageSync('bindHouseId');
uni.removeStorageSync('bindHouseName');
uni.removeStorageSync('checkType');
uni.removeStorageSync('parkingLotId');
uni.removeStorageSync('parkingLotName');
uni.removeStorageSync('parkingSpaceId');
uni.removeStorageSync('parkingSpaceName');
uni.removeStorageSync('bindCarId');
uni.removeStorageSync('bindCarName');
uni.redirectTo({
url: '/pages/my/myParkingSpaceIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}
</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>
/* 页面整体 */
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
box-sizing: border-box;
background-color: #fff;
}
/* 步骤条 */
.step-bar {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 15px 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: 14px;
margin-bottom: 16rpx;
}
.step-item.active .step-circle {
background-color: #1677ff;
}
.step-text {
font-size: 14px;
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 {}
/* 表单项 */
.form-item {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1rpx solid #f0f0f0;
}
.item-label {
font-size: 14px;
color: #000;
width: 90px;
font-weight: 600;
}
.item-placeholder {
flex: 1;
font-size: 14px;
color: #999;
}
.item-placeholder.selected {
flex: 1;
font-size: 14px;
color: #333;
}
.item-arrow {
display: inline-block;
width: 5px;
height: 5px;
border-top: 2rpx solid #999;
border-right: 2rpx solid #999;
transform: rotate(45deg);
margin-left: 10rpx;
margin-right: 5px;
}
/* 开关项 */
.switch-item {
justify-content: space-between;
}
/* 底部按钮 */
.bottom-btn-wrap {
padding: 10px 15px 20px;
display: flex;
gap: 20rpx;
background-color: #fff;
}
.next-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background-color: #1677ff;
color: #fff;
font-size: 36rpx;
border-radius: 12rpx;
border: none;
}
</style>
<style>
/* 注意:这里不能加 scoped必须写全局样式 */
::v-deep .uni-switch-input:before {
background-color: #cccccd !important;
}
/* 隐藏 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: 15px 0;
border-bottom: 1rpx solid #f0f0f0;
}
.label {
font-size: 14px;
color: #000;
width: 90px;
font-weight: 600;
}
.value {
flex: 1;
text-align: left;
font-size: 14px;
color: #999;
margin-right: 10rpx;
}
.value.selected {
font-size: 14px;
color: #333;
}
.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: 15px;
text-align: center;
border-bottom: 1rpx solid #f0f0f0;
position: relative;
}
.title {
font-size: 32rpx;
font-weight: 500;
}
.close {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
font-size: 36rpx;
color: #666;
}
/* 选项列表 */
.list {
padding: 0 15px;
}
.item {
padding: 15px 0;
text-align: center;
font-size: 14px;
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: 14px;
color: #333;
}
.input {
flex: 1;
font-size: 14px;
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: 14px;
color: #999;
padding-right: 40rpx;
}
/* 性别 */
.parkingSpaceStatus-group {
flex: 1;
display: flex;
justify-content: flex-start;
gap: 40rpx;
}
.parkingSpaceStatus-item {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 14px;
color: #666;
}
.parkingSpaceStatus-item.active {
color: #1677ff;
}
.radio {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #ddd;
position: relative;
}
.parkingSpaceStatus-item.active .radio::after {
content: '';
width: 18rpx;
height: 18rpx;
background: #1677ff;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 手机验证码 */
.phone-item .input {
padding-right: 200rpx;
}
.code-btn {
position: absolute;
right: 0;
/* background: #f5f7fa; */
color: #1677ff;
font-size: 26rpx;
padding: 10rpx 20rpx;
border-radius: 8rpx;
}
/* 上传区域 */
.upload-section {
margin-top: 15px;
}
.upload-title {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 12rpx;
display: block;
font-weight: 600;
}
.upload-desc {
font-size: 12px;
color: #666666;
line-height: 1.5;
margin-bottom: 15px;
}
/* 两列布局 */
.upload-wrap {
margin-top: 15px;
display: flex;
flex-direction: column;
gap: 15px;
}
.upload-item {
/* flex: 1; */
height: 200px;
border-radius: 16rpx;
overflow: hidden;
background: #eff0ff;
position: relative;
}
/* 上传按钮 */
.upload-box {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.plus {
font-size: 50rpx;
color: #ccc;
margin-bottom: 12rpx;
}
.txt {
font-size: 26rpx;
color: #999;
}
/* 预览图 */
.preview-img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 弹窗遮罩 */
.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: 15px;
text-align: center;
border-bottom: 1rpx solid #eee;
font-size: 32rpx;
font-weight: 500;
position: relative;
}
.close {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
font-size: 36rpx;
color: #666;
}
.popup-list {
padding: 0 15px;
max-height: 40vh;
overflow-y: auto;
}
.item {
padding: 15px 0;
text-align: center;
font-size: 14px;
border-bottom: 1rpx solid #f5f5f5;
}
/* 成功提示弹窗 */
.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

@@ -1,378 +0,0 @@
<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>
<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" />
<!-- 中间车辆信息 -->
<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>
</view>
</scroll-view>
<!-- 底部确定绑定按钮 -->
<view class="bottom-btn">
<button class="confirm-btn" @click="confirmBind">确定绑定</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
onShow() {
},
onUnload() {
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
defaultCarImg: "/static/propertyImgs/defaultCarImg.png",
searchKey: "",
selectedId: '',
selectedName: '',
selectedIndex: -1, // 当前选中的索引
// 模拟车辆数据
carList: [{
id: '1',
imgSrc: '/static/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: "未绑定"
}
]
}
},
methods: {
// 选择绑定车辆
handleItemClick(item, index) {
this.selectedIndex = index;
this.selectedId = item.id;
this.selectedName = item.plate;
},
// 确定绑定
confirmBind() {
if (this.selectedIndex === -1) {
return uni.showToast({
title: "请先选择绑定车辆",
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();
}, 3000);
}
},
});
},
goBack() {
uni.redirectTo({
url: '/pages/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
overflow: hidden;
// height: 100vh;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
/* background-color: #fff; */
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
box-sizing: border-box;
}
/* 底部按钮 */
.bottom-btn {
padding: 10px 15px 20px;
}
/* 通用输入框样式 */
.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: 14px;
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: 16px;
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: 20px;
min-height: calc(100vh - 105px);
overflow-y: scroll;
}
/* ====================== 绑定车辆卡片 ====================== */
/* 单个车辆项 */
.car-item {
display: flex;
align-items: center;
padding: 10px;
margin-bottom: 20rpx;
border: 1rpx solid #e6e9ff;
border-radius: 16rpx;
background-color: #fff;
position: relative;
}
.car-item.checked {
background: #f4f7ff;
/* 浅蓝色背景 */
border-color: #1677ff;
}
/* 左侧圆形头像占位 */
.car-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background-color: #f0f4ff;
margin-right: 24rpx;
flex-shrink: 0;
}
/* 中间信息区域 */
.car-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
/* 车牌号 */
.car-plate {
font-size: 16px;
font-weight: bold;
color: #000;
line-height: 1.4;
margin-bottom: 8rpx;
}
/* 车型+颜色 */
.car-model {
font-size: 14px;
color: #666;
line-height: 1.4;
}
/* 状态标签 */
.status-tag {
font-size: 12px;
padding: 3px 7px;
border-radius: 3px;
font-weight: 500;
flex-shrink: 0;
position: absolute;
right: 15px;
top: 15px;
}
/* 未绑定状态(浅绿色背景+绿色文字) */
.status-tag.unbound {
/* background-color: #e6f7e6; */
color: #999999;
}
/* 已绑定状态(可扩展,比如蓝色) */
.status-tag.bound {
/* background-color: #e6f0ff; */
color: #1677ff;
}
/* ====================== 底部按钮 ====================== */
.confirm-btn {
width: 100%;
height: 88rpx;
background-color: #1677ff;
color: #fff;
border-radius: 12rpx;
font-size: 34rpx;
border: none;
line-height: 88rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -1,457 +0,0 @@
<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="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.plateNo}}</text>
</view>
<view class="info-row">
<text class="info-label">车辆品牌</text>
<text class="info-value">{{form.carBrandName}}</text>
</view>
<view class="info-row">
<text class="info-label">车辆型号</text>
<text class="info-value">{{form.model}}</text>
</view>
<view class="info-row">
<text class="info-label">车身颜色</text>
<text class="info-value">{{form.color}}</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>
</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>
</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: '/static/propertyImgs/carImg.png',
},
}
},
methods: {
// 修改按钮事件(仅认证失败显示)
clickUpdate() {
uni.setStorageSync('operationType', 'update')
uni.redirectTo({
url: '/pages/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();
}, 3000);
}
},
});
},
goBack() {
uni.redirectTo({
url: '/pages/my/myCarIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f5f7fa;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
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: 10px;
}
.card-title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #000;
margin-bottom: 30rpx;
}
.title-line {
width: 8rpx;
height: 28rpx;
background-color: #1677ff;
margin-right: 10rpx;
border-radius: 4rpx;
}
.info-row {
display: flex;
margin-bottom: 24rpx;
align-items: center;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
width: 180rpx;
font-size: 28rpx;
color: #000;
}
.info-value {
font-size: 28rpx;
color: #333;
flex: 1;
}
.status-tag {
color: #ff7d00;
font-weight: 500;
}
/* 身份证照片区 */
.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: 10px 15px 20px;
display: flex;
gap: 20rpx;
/* background-color: #fff; */
}
.btn-modify {
flex: 1;
height: 88rpx;
line-height: 88rpx;
background-color: #1677ff;
color: #fff;
font-size: 30rpx;
border-radius: 8rpx;
border: none;
}
.btn-delete {
flex: 1;
height: 88rpx;
line-height: 88rpx;
background-color: #fff;
color: #f53f3f;
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;
}
</style>

View File

@@ -1,296 +0,0 @@
<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>
<!-- 表单区域 -->
<view class="form-container">
<!-- 手机号输入框+86前缀 -->
<view class="input-group phone-input">
<image class="iconStyle" src="/static/propertyImgs/phoneIcon.png">📱</image>
<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="/static/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();
}, 3000);
},
goBack() {
uni.redirectTo({
url: '/pages/my/settingIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}
</script>
<style lang="scss">
page {
background: #f9f9f9;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f9f9f9;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
box-sizing: border-box;
/* background-color: #fff; */
}
/* 底部按钮 */
.bottom-btn {
padding: 10px 15px 20px;
}
/* 表单容器 */
.form-container {
/* padding: 60rpx 30rpx; */
}
/* 输入框组(手机号+验证码行) */
.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;
}
.iconStyle {
width:25px;
height: 25px;
}
.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-btn {
font-size: 12px;
color: #fff;
background: #2F77FD;
border: none;
padding: 0;
line-height: 88rpx;
width: 100px;
}
.code-btn[disabled] {
color: #999;
}
/* 通用输入框样式 */
.input-item {
font-size: 30rpx;
color: #333;
background-color: #fff;
padding-left: 20rpx;
}
.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;
}
</style>

View File

@@ -1,377 +0,0 @@
<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="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-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>
</view>
<!-- 右侧状态标签 -->
<view class="status-tag"
:class="{ underReview: item.status === '审核中', unbound: item.status === '未绑定',bounded: item.status === '已绑定' }">
{{ item.status }}
</view>
</view>
</scroll-view>
<!-- 底部添加车辆按钮 -->
<view class="bottom-btn">
<button class="confirm-btn" @click="addCar">添加车辆</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
onShow() {
},
onUnload() {
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
defaultCarImg: "/static/propertyImgs/defaultCarImg.png",
searchKey: "",
selectedId: '',
selectedName: '',
selectedIndex: -1, // 当前选中的索引
// 模拟车辆数据
carList: [{
id: '1',
imgSrc: '/static/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: "",
}
]
}
},
methods: {
// 选择我的车辆
handleItemClick(item, index) {
uni.setStorageSync('operationType', 'update');
uni.redirectTo({
url: '/pages/my/carDetail?id=' + item.id + '&status=' + item.status,
success: res => {},
fail: () => {},
complete: () => {}
});
},
// 添加车辆
addCar() {
uni.setStorageSync('operationType', 'add');
uni.redirectTo({
url: '/pages/my/addCar',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goBack() {
uni.switchTab({
url: '/pages/myIndex/myIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}
</script>
<style lang="scss">
page {
background: #F3F8FD;
overflow: hidden;
// height: 100vh;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f5f7fa;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
box-sizing: border-box;
}
/* 底部按钮 */
.bottom-btn {
padding: 10px 15px 20px;
}
/* 表单容器 */
.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: 14px;
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: 16px;
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;
padding: 10px;
margin-bottom: 20rpx;
border: 1rpx solid #e6e9ff;
border-radius: 16rpx;
background-color: #fff;
position: relative;
width: calc(100% - 22px);
}
.car-item.checked {
background: #f4f7ff;
/* 浅蓝色背景 */
border-color: #1677ff;
}
/* 左侧圆形头像占位 */
.car-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background-color: #f0f4ff;
margin-right: 24rpx;
flex-shrink: 0;
}
/* 中间信息区域 */
.car-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
/* 车牌号 */
.car-plate {
font-size: 16px;
font-weight: bold;
color: #000;
line-height: 1.4;
margin-bottom: 8rpx;
}
/* 车型+颜色 */
.car-model {
font-size: 14px;
color: #666;
line-height: 1.4;
}
.car-bindParkingSpace {
font-size: 14px;
color: #666;
white-space: nowrap;
/* 强制不换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
/* 超出显示省略号 */
width: 90%;
}
/* 状态标签 */
.status-tag {
font-size: 12px;
padding: 3px 7px;
border-radius: 3px;
font-weight: 500;
flex-shrink: 0;
position: absolute;
right: 15px;
top: 15px;
}
/* 未绑定状态 */
.status-tag.unbound {
/* background-color: #e6f7e6; */
color: #999;
}
/* 已绑定状态) */
.status-tag.bounded {
color: #2F77FD;
}
/* 审核中 */
.status-tag.underReview {
/* background-color: #F8DDBD; */
color: #FF8F40;
}
/* ====================== 底部按钮 ====================== */
.confirm-btn {
width: 100%;
height: 88rpx;
background-color: #1677ff;
color: #fff;
border-radius: 12rpx;
font-size: 34rpx;
border: none;
line-height: 88rpx;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

View File

@@ -1,396 +0,0 @@
<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">
<!-- 房屋列表 v-for 渲染 -->
<view class="house-list">
<view class="house-card" v-for="(item, index) in houseList" :key="index" @click="tapHouse(item)">
<!-- 标题 + 状态 -->
<view class="card-header">
<view class="title-box">
<text class="tag-default" v-if="item.isDefault">默认</text>
<text class="house-name">{{ item.houseName }}</text>
</view>
<text class="status-tag" :class="item.statusClass">{{ item.statusText }}</text>
</view>
<!-- 房屋信息 -->
<view class="card-body">
<view class="row">
<text class="label">房间号</text>
<view class="value-box">
<text class="value">{{ item.roomNo }}</text>
</view>
</view>
<view class="row">
<text class="label">业主</text>
<text class="value">{{ item.owner }}</text>
</view>
<view class="row">
<text class="label">房屋状态</text>
<text class="value">{{ item.houseStatus }}</text>
</view>
<view class="defaultBox" v-if="item.statusText=='认证成功'&&!item.isDefault">
<text class="set-default">设为默认</text>
<switch class="no-cross-switch" :checked="item.isDefault" color="#1677ff"
@change="onSwitchChange($event,index)" @click.stop />
</view>
<!-- <text v-if="item.showSetDefault" class="set-default"
@click="setDefault(item.id)">设为默认</text> -->
</view>
</view>
</view>
</scroll-view>
<!-- 底部添加按钮 -->
<view class="bottom-btn">
<button class="add-btn" @click="addHouse">添加房屋</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,
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,
},
]
}
},
methods: {
// 开关切换
onSwitchChange(e, i) {
uni.showLoading({
title: '设置中...',
mask: true
});
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)
}, 3000);
},
tapHouse(item) {
uni.redirectTo({
url: '/pages/my/houseDetail?id=' + item.id + '&statusClass=' + item.statusClass,
success: res => {},
fail: () => {},
complete: () => {}
});
},
addHouse() {
uni.setStorageSync('operationType', 'add');
uni.redirectTo({
url: '/pages/my/addHouse',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goBack() {
uni.switchTab({
url: '/pages/myIndex/myIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}
</script>
<style lang="scss">
page {
// background: #F3F8FD;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f5f7fa;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
box-sizing: border-box;
}
/* 底部按钮 */
.bottom-btn {
padding: 10px 15px 20px;
}
/* 列表 */
.house-list {
height: 100%;
overflow-y: scroll;
}
.house-card {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
/* 头部 */
.card-header {
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.title-box {
display: flex;
align-items: center;
gap: 16rpx;
}
.house-name {
font-size: 16px;
font-weight: 600;
}
.tag-default {
background: #007aff;
color: #fff;
font-size: 24rpx;
padding: 1px 8px;
border-radius: 15px;
}
/* 状态标签 */
.status-tag {
font-size: 14px;
padding: 6rpx 14rpx;
font-weight: 600;
border-radius: 6rpx;
}
.success {
/* background: #e6f7ee; */
color: #2F77FD;
}
.pending {
/* background: #fff4e4; */
color: #09C2BD;
}
.fail {
/* background: #ffe7e7; */
color: #E34D59;
}
/* 内容行 */
.card-body {
padding: 30rpx;
}
.row {
display: flex;
margin-bottom: 24rpx;
}
.row:last-child {
margin-bottom: 0;
}
.label {
width: 100px;
font-size: 14px;
/* color: #666; */
}
.value-box {
flex: 1;
display: flex;
justify-content: space-between;
}
.value {
font-size: 14px;
font-weight: 600;
color: #333;
}
.defaultBox {
margin-top: 5px;
display: flex;
justify-content: space-between;
z-index: 99 !important;
}
.set-default {
color: #222222;
/* font-weight: 600; */
font-size: 16px;
}
.add-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #007aff;
color: #fff;
border-radius: 12rpx;
font-size: 16px;
border: none;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>
<style>
/* 去掉 switch 内部的 × 和 √ 图标 */
uni-switch::before,
uni-switch::after {
display: none !important;
}
/* 微信小程序专属去除 */
wx-switch::before,
wx-switch::after {
display: none !important;
}
/* 支付宝/百度/抖音小程序兼容 */
my-switch::before,
swan-switch::before,
tt-switch::before {
display: none !important;
}
</style>
<style>
::v-deep .uni-switch-input:before {
background-color: #cccccd !important;
}
/* 隐藏 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>

View File

@@ -1,333 +0,0 @@
<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">
<!-- 车位列表 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>
</view>
<text class="status-tag" :class="item.statusClass">{{ item.statusText }}</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>
</view>
</view>
</view>
<view class="line">
<view class="row">
<text class="label">车位编号</text>
<text class="value">{{ item.parkingSpaceNumber }}</text>
</view>
</view>
<view class="line">
<view class="row">
<text class="label">车位状态</text>
<text class="value">{{ item.parkingSpaceStatus }}</text>
</view>
</view>
<view class="line">
<view class="row">
<text class="label">绑定车辆</text>
<text class="value">{{ item.bindCarNum }}</text>
</view>
</view>
</view>
<view class="right">
<image src="/static/propertyImgs/myParkingSpace.png" class="imgItem" mode="widthFix">
</image>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部添加按钮 -->
<view class="bottom-btn">
<button class="add-btn" @click="addParkingSpace">添加车位</button>
</view>
</view>
</template>
<script>
export default {
onReady: function(e) {},
components: {
},
computed: {
},
created() {
},
onShow() {
},
onUnload() {
// 页面卸载时清除定时器
if (this.timer) clearInterval(this.timer)
},
mounted() {},
data() {
return {
/* 设定状态栏默认高度 */
statusBarHeight: 20,
parkingSpaceList: [{
id: 1,
parkingSpaceName: "北境碧桂园小区地下停车场",
floorNo: "B2",
parkingSpaceNumber: "202",
parkingSpaceStatus: "租赁",
bindCarNum: '京A88888',
statusText: "认证成功",
statusClass: "success",
},
{
id: 2,
parkingSpaceName: "北境碧桂园小区停车场",
floorNo: "B2",
parkingSpaceNumber: "202",
parkingSpaceStatus: "自用",
bindCarNum: '京A88888',
statusText: "认证中",
statusClass: "pending",
},
{
id: 3,
parkingSpaceName: "北境碧桂园小区停车场",
floorNo: "2号楼1层",
floorNo: "B2",
parkingSpaceNumber: "202",
parkingSpaceStatus: "闲置",
bindCarNum: '京A88888',
statusText: "认证失败",
statusClass: "fail",
},
]
}
},
methods: {
tapParkingSpace(item) {
uni.redirectTo({
url: '/pages/my/parkingSpaceDetail?id=' + item.id + '&statusClass=' + item.statusClass,
success: res => {},
fail: () => {},
complete: () => {}
});
},
addParkingSpace() {
uni.setStorageSync('operationType', 'add');
uni.redirectTo({
url: '/pages/my/addParkingSpace',
success: res => {},
fail: () => {},
complete: () => {}
});
},
goBack() {
uni.switchTab({
url: '/pages/myIndex/myIndex',
success: res => {},
fail: () => {},
complete: () => {}
});
},
},
}
</script>
<style lang="scss">
page {
background: #F9F9F9;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
/* 关键:占满屏幕高度 */
background-color: #f5f7fa;
}
.page {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
box-sizing: border-box;
}
/* 底部按钮 */
.bottom-btn {
padding: 10px 15px 20px;
}
/* 列表 */
.parkingSpace-list {}
.parkingSpace-card {
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
/* 头部 */
.card-header {
padding: 30rpx;
/* border-bottom: 1rpx solid #f0f0f0; */
display: flex;
justify-content: space-between;
align-items: center;
}
.title-box {
display: flex;
align-items: center;
gap: 16rpx;
}
.parkingSpace-name {
font-size: 16px;
font-weight: 600;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 状态标签 */
.status-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 15px;
margin-left: 10px;
white-space: nowrap;
}
.success {
background: #2F77FD;
color: #fff;
}
.pending {
background: #09C2BD;
color: #fff;
}
.fail {
background: #E34D59;
color: #fff;
}
/* 内容行 */
.card-body {
padding: 0rpx 30rpx 30rpx 30rpx;
display: flex;
justify-content: space-between;
}
.line {
display: flex;
margin-bottom: 10px;
}
.row {
display: flex;
width: 100%;
}
.label {
width: 80px;
font-size: 14px;
font-weight: 600;
color: #333;
}
.value-box {
flex: 1;
display: flex;
justify-content: space-between;
}
.value {
font-size: 14px;
color: #666;
}
.set-default {
color: #007aff;
font-weight: 600;
font-size: 14px;
}
.add-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #007aff;
color: #fff;
border-radius: 12rpx;
font-size: 16px;
border: none;
}
.left {}
.right {
width: 42%;
}
.imgItem {
width: 100%;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>

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