图书馆小程序
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

616 lines
16 KiB

5 months ago
3 weeks ago
4 months ago
1 month ago
4 months ago
1 month ago
4 months ago
5 months ago
4 months ago
1 month ago
1 month ago
4 months ago
1 month ago
4 months ago
5 months ago
1 month ago
5 months ago
4 months ago
1 month ago
5 months ago
1 month ago
4 months ago
1 month ago
4 months ago
1 month ago
5 months ago
4 months ago
5 months ago
4 months ago
1 month ago
4 months ago
1 month ago
4 months ago
1 month ago
1 month ago
4 months ago
1 month ago
1 month ago
1 month ago
5 months ago
4 months ago
5 months ago
4 months ago
5 months ago
4 months ago
1 month ago
4 months ago
5 months ago
1 month ago
5 months ago
1 month ago
4 months ago
5 months ago
1 month ago
  1. <template>
  2. <view class="activity-detail">
  3. <image class="activity-img" :src="detail.coverImage||detail.cover_image" mode="aspectFill"></image>
  4. <view class="activity-base">
  5. <text class="title">{{ detail.title }}</text>
  6. <view class="time">
  7. <uni-icons class="detail-icon" custom-prefix="iconfont" type="icon-shijian" size="15"></uni-icons>
  8. <text>{{ detail.startTime || formatTime(detail.start_time) }} - {{ detail.endTime || formatTime(detail.end_time) }}</text>
  9. </view>
  10. <view class="location">
  11. <uni-icons class="detail-icon" type="location" size="20"></uni-icons>
  12. <text>{{ detail.activityLocation || detail.activity_location }}</text>
  13. </view>
  14. </view>
  15. <view class="rich-content">
  16. <text class="content-title">活动介绍/回顾</text>
  17. <div v-if="detail.localImg" style="text-align:center; margin:12px 0;">
  18. <image :src="require(`@/static/${detail.localImg}`)" mode="widthFix" style="width:90%; border-radius:8px;"></image>
  19. </div>
  20. <rich-text class="rich-text" :nodes="contentNodes"></rich-text>
  21. </view>
  22. <view class="detail-bottom">
  23. <view class="detail-left">
  24. <button open-type="share" class="handle-btn">
  25. <uni-icons custom-prefix="iconfont" type="icon-fenxiang01" size="20"></uni-icons>
  26. <text>分享</text>
  27. </button>
  28. </view>
  29. <button
  30. class="join-btn"
  31. :class="getJoinBtnClass()"
  32. @click="handleJoin"
  33. >
  34. {{ getJoinBtnText() }}
  35. </button>
  36. </view>
  37. <view class="modal-mask" v-if="showJoinModal" @click="closeJoinModal">
  38. <view class="modal-content" @click.stop>
  39. <view class="modal-header">
  40. <text class="modal-title">报名信息</text>
  41. <uni-icons class="close-icon" type="close" size="24" @click="closeJoinModal"></uni-icons>
  42. </view>
  43. <view class="modal-body">
  44. <view class="form-item">
  45. <text class="form-label">姓名</text>
  46. <input class="form-input" v-model="joinForm.name" placeholder="请输入您的姓名" />
  47. </view>
  48. <view class="form-item">
  49. <text class="form-label">联系方式</text>
  50. <view style="display:flex; justify-content: space-between; align-items:center;">
  51. <input class="form-input" v-model="joinForm.phone" placeholder="请输入手机号码" type="number" maxlength="11" />
  52. <button class="get-phone-btn" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
  53. 一键获取
  54. </button>
  55. </view>
  56. </view>
  57. <view class="form-item">
  58. <text class="form-label">参加人数</text>
  59. <view class="select-wrapper">
  60. <picker
  61. mode="selector"
  62. :range="rangeText"
  63. :value="joinForm.personCount - 1"
  64. @change="onPickerChange"
  65. class="picker-select"
  66. >
  67. <view class="picker-display">
  68. {{ joinForm.personCountText || '请选择参加人数' }}
  69. </view>
  70. </picker>
  71. </view>
  72. </view>
  73. </view>
  74. <view class="modal-footer">
  75. <button class="cancel-btn" @click="closeJoinModal">取消</button>
  76. <button class="submit-btn" @click="submitJoin">确认报名</button>
  77. </view>
  78. </view>
  79. </view>
  80. </view>
  81. </template>
  82. <script>
  83. // import config from '@/utils/config';
  84. // import { getOpenId } from '@/utils/storage';
  85. import { FetchSessionKey, FetchDecryptPhone } from '@/api/user.js';
  86. import { FetchSignActivity, FetchCancelSignActivity } from '@/api/activity.js';
  87. export default {
  88. data() {
  89. return {
  90. detail: {},
  91. activityId: "",
  92. contentNodes: "",
  93. showJoinModal: false,
  94. joinForm: {
  95. name: "",
  96. phone: "",
  97. personCount: 1,
  98. personCountText: ""
  99. },
  100. personOptions: [
  101. { value: 1, text: "1人" },
  102. { value: 2, text: "2人" },
  103. { value: 3, text: "3人" },
  104. { value: 4, text: "4人" },
  105. { value: 5, text: "5人" },
  106. ],
  107. rangeText: [],
  108. sessionKey: "",
  109. openid: ""
  110. };
  111. },
  112. onLoad(options) {
  113. console.log(options);
  114. const item = JSON.parse(decodeURIComponent(options.item));
  115. this.detail = item;
  116. console.log('this.detail',this.detail);
  117. this.activityId = item.id || item.activityId;
  118. this.formatContent();
  119. this.rangeText = this.personOptions.map(item => item.text);
  120. this.joinForm.personCount = this.personOptions[0].value;
  121. this.joinForm.personCountText = this.personOptions[0].text;
  122. this.getSessionKey();
  123. },
  124. methods: {
  125. formatContent() {
  126. let content = this.detail.activityContent || this.detail.activity_content || "";
  127. if (!content) {
  128. this.contentNodes = "";
  129. return;
  130. }
  131. content = content.replace(/&amp;/g, '&');
  132. const imgReg = /<img\s+[^>]*src\s*=\s*["']?([^"'> ]+)["']?[^>]*>/gi;
  133. content = content.replace(imgReg, (match, src) => {
  134. return `<div><img src="${src}" style="width:100%;max-width:750px;display:block;margin:0 auto;"></div>`;
  135. });
  136. // ========== p标签:原有样式保留,缺少line-height自动补充,统一margin ==========
  137. const pHasStyleReg = /<p(\s+[^>]*?)style\s*=\s*["']([^"']+)["']([^>]*)>/gi;
  138. content = content.replace(pHasStyleReg, (match, before, oldStyleStr, after) => {
  139. const hasLineHeight = /line-height\s*:/.test(oldStyleStr);
  140. let append = 'margin:15px 0;';
  141. if (!hasLineHeight) {
  142. append = 'line-height:1.5;margin:15px 0;';
  143. }
  144. const newStyle = oldStyleStr + ';' + append;
  145. return `<p${before}style="${newStyle}"${after}>`;
  146. });
  147. const pNoStyleReg = /<p(?!\s+style\s*=)([^>]*)>/gi;
  148. content = content.replace(pNoStyleReg, '<p$1 style="line-height:1.5;margin:15px 0;">');
  149. // ========== blockquote 引用样式 ==========
  150. content = content.replace(/<blockquote([^>]*)>/gi,
  151. `<blockquote$1 style="background:#f5f7fa;border-left:8px solid #01a4fe;display:block;font-size:100%;line-height:1.5;margin:10px 0;padding:10px;">`);
  152. // ========== 表格基础样式 ==========
  153. content = content.replace(/<table([^>]*)>/gi,
  154. `<table$1 style="width:100% !important;border-collapse:collapse;margin:10px 0;">`);
  155. content = content.replace(/<th([^>]*)>/gi,
  156. `<th$1 style="border:1px solid #dcdfe6;padding:8px 12px;text-align:center;font-size:14px;">`);
  157. content = content.replace(/<td([^>]*)>/gi,
  158. `<td$1 style="border:1px solid #dcdfe6;padding:8px 12px;text-align:center;font-size:14px;">`);
  159. this.contentNodes = content;
  160. },
  161. onPickerChange(e) {
  162. const index = e.detail.value;
  163. this.joinForm.personCount = this.personOptions[index].value;
  164. this.joinForm.personCountText = this.personOptions[index].text;
  165. },
  166. async getSessionKey() {
  167. try {
  168. uni.login({
  169. success: async (loginRes) => {
  170. console.log('loginRes',loginRes)
  171. if (loginRes.code) {
  172. const res = await FetchSessionKey({
  173. libcode: getApp().globalData.libcode,
  174. code: loginRes.code
  175. });
  176. if (res.code === 200) {
  177. this.sessionKey = res.data.session_key;
  178. this.openid = res.data.openid;
  179. }
  180. }
  181. }
  182. });
  183. } catch (error) {
  184. console.error('获取sessionKey失败:', error);
  185. }
  186. },
  187. getPhoneNumber(e) {
  188. if (e.detail.errMsg !== 'getPhoneNumber:ok') {
  189. uni.showToast({ title: '取消授权', icon: 'none' });
  190. return;
  191. }
  192. if (!this.sessionKey) {
  193. uni.showToast({ title: '请稍后再试', icon: 'none' });
  194. return;
  195. }
  196. FetchDecryptPhone({
  197. sessionKey: this.sessionKey,
  198. encryptedData: e.detail.encryptedData,
  199. iv: e.detail.iv
  200. }).then(res => {
  201. if (res.code === 200) {
  202. this.joinForm.phone = res.data.phoneNumber || '';
  203. uni.showToast({ title: '获取成功', icon: 'success' });
  204. } else {
  205. uni.showToast({ title: res.message || '获取失败', icon: 'none' });
  206. }
  207. }).catch(() => {
  208. uni.showToast({ title: '获取手机号失败', icon: 'none' });
  209. });
  210. },
  211. getJoinBtnClass() {
  212. const d = this.detail;
  213. // 已结束 / 已取消 禁用
  214. if (d.status === '已结束' || d.status === '已取消') {
  215. return 'disabled-btn'
  216. }
  217. return ''
  218. },
  219. // 获取按钮文字
  220. getJoinBtnText() {
  221. const d = this.detail;
  222. if(d.status === '已结束') return '活动结束'
  223. if(d.status === '已取消') return '活动取消'
  224. // 未开始,有预约记录,没有取消,显示取消预约
  225. if(d.status === '未开始' && d.isCancel === false && !!d.signId){
  226. return '取消预约'
  227. }
  228. return '我要参加'
  229. },
  230. handleJoin() {
  231. const d = this.detail
  232. // 已结束、已取消直接拦截
  233. if (['已结束', '已取消'].includes(d.status)) {
  234. return;
  235. }
  236. // 未开始、有预约记录、未取消:执行取消报名
  237. if(d.status === '未开始' && d.isCancel === false && !!d.signId){
  238. uni.showModal({
  239. title: '提示',
  240. content: `确定要取消活动【${d.title}】的预约吗?`,
  241. success: async (res) => {
  242. if(res.confirm){
  243. try {
  244. const apiRes = await FetchCancelSignActivity({
  245. id: d.signId
  246. })
  247. if(apiRes.code === 200){
  248. uni.showToast({ title: '取消成功', icon: 'success' })
  249. // 发送全局事件,告诉预约列表页需要刷新
  250. uni.$emit('refreshMySignList')
  251. setTimeout(() => {
  252. uni.navigateBack()
  253. }, 700)
  254. }else{
  255. uni.showToast({ title: apiRes.message || '取消失败', icon: 'none' })
  256. }
  257. } catch (err) {
  258. console.error('取消报名异常', err)
  259. uni.showToast({ title: '网络异常,取消失败', icon: 'none' })
  260. }
  261. }
  262. }
  263. })
  264. return
  265. }
  266. //打开报名弹窗
  267. this.openJoinModal();
  268. },
  269. closeJoinModal() {
  270. this.showJoinModal = false;
  271. },
  272. onPersonChange(e) {
  273. this.joinForm.personCount = e.detail.value + 1;
  274. },
  275. openJoinModal() {
  276. // 每次打开弹窗完整重置表单,包含picker显示文本
  277. this.joinForm = {
  278. name: "",
  279. phone: "",
  280. personCount: 1,
  281. personCountText: "1人"
  282. }
  283. this.showJoinModal = true
  284. },
  285. submitJoin() {
  286. if (!this.joinForm.name) {
  287. uni.showToast({ title: '请输入姓名', icon: 'none' });
  288. return;
  289. }
  290. if (!this.joinForm.phone) {
  291. uni.showToast({ title: '请输入联系方式', icon: 'none' });
  292. return;
  293. }
  294. const phoneReg = /^1[3-9]\d{9}$/;
  295. if (!phoneReg.test(this.joinForm.phone)) {
  296. uni.showToast({ title: '请输入正确的手机号码', icon: 'none' });
  297. return;
  298. }
  299. const params = {
  300. // libcode: getApp().globalData.libcode,
  301. activityId: this.activityId,
  302. signName: this.joinForm.name,
  303. signPhone: this.joinForm.phone,
  304. signNum: this.joinForm.personCount,
  305. openid: this.openid,
  306. // sessionKey: this.sessionKey
  307. }
  308. console.log('params',params)
  309. FetchSignActivity(params).then(res => {
  310. console.log('FetchSignActivity',res)
  311. if (res.code === 200) {
  312. uni.showToast({
  313. title: '报名成功',
  314. icon: 'success'
  315. });
  316. uni.$emit('refreshMySignList')
  317. this.closeJoinModal();
  318. } else {
  319. console.log('error',res)
  320. uni.showToast({
  321. title: res.message || '报名失败',
  322. icon: 'none'
  323. });
  324. }
  325. }).catch((err) => {
  326. console.log('catch err', err)
  327. if(err){
  328. uni.showToast({
  329. title: err,
  330. icon: 'none'
  331. })
  332. }else{
  333. uni.showToast({
  334. title: '报名失败',
  335. icon: 'none'
  336. });
  337. }
  338. });
  339. },
  340. formatTime(timestamp) {
  341. if (!timestamp) return '';
  342. const date = new Date(timestamp);
  343. const y = date.getFullYear();
  344. const m = String(date.getMonth() + 1).padStart(2, '0');
  345. const d = String(date.getDate()).padStart(2, '0');
  346. const h = String(date.getHours()).padStart(2, '0');
  347. const min = String(date.getMinutes()).padStart(2, '0');
  348. return `${y}-${m}-${d} ${h}:${min}`;
  349. },
  350. onShareAppMessage() {
  351. return {
  352. title: this.detail.title,
  353. path: "/subpkg/pages/activity-detail/activity-detail?item=" + encodeURIComponent(JSON.stringify(this.detail)),
  354. imageUrl: this.detail.coverImage
  355. };
  356. }
  357. }
  358. };
  359. </script>
  360. <style lang="scss" scoped>
  361. .activity-detail {
  362. padding-bottom: 60px;
  363. background: #f5f5f5;
  364. }
  365. .activity-img {
  366. width: 100%;
  367. height: 180px;
  368. }
  369. .activity-base {
  370. background: #fff;
  371. padding: 15px;
  372. margin-bottom: 10px;
  373. .title {
  374. font-size: 16px;
  375. font-weight: bold;
  376. padding-bottom: 10px;
  377. }
  378. .time,
  379. .location {
  380. display: flex;
  381. align-items: center;
  382. font-size: 12px;
  383. color: #999;
  384. padding-top: 10px;
  385. .detail-icon {
  386. padding-right: 4px;
  387. }
  388. }
  389. .time{
  390. padding-left: 3px;
  391. }
  392. }
  393. .rich-content {
  394. background: #fff;
  395. padding: 10px;
  396. .content-title{
  397. position: relative;
  398. font-size: 16px;
  399. font-weight: bold;
  400. color: #333;
  401. padding-left: 12px;
  402. margin-bottom: 20px;
  403. &::before{
  404. position: absolute;
  405. left: 0;
  406. top: 50%;
  407. margin-top: -9px;
  408. content: "";
  409. width: 4px;
  410. height: 18px;
  411. background-color: #01a4fe;
  412. }
  413. }
  414. }
  415. .rich-text{
  416. ::v-deep p{
  417. line-height: 1.5;
  418. margin: 15px 0;
  419. }
  420. }
  421. .detail-bottom {
  422. position: fixed;
  423. bottom: 0;
  424. left: 0;
  425. right: 0;
  426. height: 50px;
  427. background: #fff;
  428. display: flex;
  429. align-items: center;
  430. justify-content: space-between;
  431. padding: 0 15px;
  432. box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
  433. }
  434. .detail-left {
  435. display: flex;
  436. gap: 10px;
  437. }
  438. .handle-btn {
  439. display: flex;
  440. align-items: center;
  441. gap: 4px;
  442. background: #f5f5f5;
  443. border-radius: 30px;
  444. padding: 0 12px;
  445. height: 36px;
  446. font-size: 14px;
  447. }
  448. .join-btn {
  449. background: #01a4fe;
  450. color: #fff;
  451. border-radius: 30px;
  452. padding: 0 20px;
  453. height: 36px;
  454. }
  455. .disabled-btn {
  456. background: #ccc !important;
  457. }
  458. .modal-mask {
  459. position: fixed;
  460. top: 0;
  461. left: 0;
  462. right: 0;
  463. bottom: 0;
  464. background: rgba(0, 0, 0, 0.5);
  465. display: flex;
  466. align-items: center;
  467. justify-content: center;
  468. z-index: 1000;
  469. }
  470. .modal-content {
  471. width: 85%;
  472. background: #fff;
  473. border-radius: 8px;
  474. overflow: hidden;
  475. }
  476. .modal-header {
  477. display: flex;
  478. align-items: center;
  479. justify-content: space-between;
  480. padding: 15px;
  481. border-bottom: 1px solid #f0f0f0;
  482. .modal-title {
  483. font-size: 16px;
  484. font-weight: bold;
  485. color: #333;
  486. }
  487. .close-icon {
  488. color: #999;
  489. }
  490. }
  491. .modal-body {
  492. padding: 15px;
  493. }
  494. .form-item {
  495. margin-bottom: 15px;
  496. .form-label {
  497. display: block;
  498. font-size: 14px;
  499. color: #333;
  500. margin-bottom: 8px;
  501. }
  502. .form-input {
  503. width: calc(100% - 25px);
  504. height: 40px;
  505. background: #f8f8f8;
  506. border-radius: 8px;
  507. padding: 0 12px;
  508. font-size: 14px;
  509. }
  510. .select-wrapper {
  511. width: calc(100% - 25px);
  512. height: 40px;
  513. background: #f8f8f8;
  514. border-radius: 8px;
  515. display: flex;
  516. align-items: center;
  517. padding: 0 12px;
  518. font-size: 14px;
  519. ::v-deep .picker-select{
  520. width: 100%;
  521. }
  522. .select-value {
  523. font-size: 14px;
  524. color: #333;
  525. width: 100%;
  526. display: flex;
  527. justify-content: space-between;
  528. align-items: center;
  529. &::after {
  530. content: '';
  531. width: 0;
  532. height: 0;
  533. border-left: 5px solid transparent;
  534. border-right: 5px solid transparent;
  535. border-top: 5px solid #999;
  536. }
  537. }
  538. }
  539. }
  540. .modal-footer {
  541. display: flex;
  542. border-top: 1px solid #f0f0f0;
  543. .cancel-btn,
  544. .submit-btn {
  545. flex: 1;
  546. height: 44px;
  547. line-height: 44px;
  548. text-align: center;
  549. font-size: 15px;
  550. border-radius: 0;
  551. border: none;
  552. margin: 0;
  553. padding: 0;
  554. &::after {
  555. border: none;
  556. border-radius: 0;
  557. }
  558. }
  559. .cancel-btn {
  560. background: #f5f5f5;
  561. color: #666;
  562. // border-right: 1px solid #f0f0f0;
  563. }
  564. .submit-btn {
  565. background: #01a4fe;
  566. color: #fff;
  567. }
  568. }
  569. .get-phone-btn {
  570. font-size: 12px;
  571. color: #fff;
  572. background-color: #01a4fe;
  573. width: 90px;
  574. border-radius: 20px;
  575. padding: 10px;
  576. border: none;
  577. line-height: 1.2;
  578. margin-left: 10px;
  579. &::after {
  580. border: none;
  581. }
  582. }
  583. </style>