图书馆小程序
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.

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