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

617 lines
16 KiB

<template>
<view class="activity-detail">
<image class="activity-img" :src="detail.coverImage||detail.cover_image" mode="aspectFill"></image>
<view class="activity-base">
<text class="title">{{ detail.title }}</text>
<view class="time">
<uni-icons class="detail-icon" custom-prefix="iconfont" type="icon-shijian" size="15"></uni-icons>
<text>{{ detail.startTime || formatTime(detail.start_time) }} - {{ detail.endTime || formatTime(detail.end_time) }}</text>
</view>
<view class="location">
<uni-icons class="detail-icon" type="location" size="20"></uni-icons>
<text>{{ detail.activityLocation || detail.activity_location }}</text>
</view>
</view>
<view class="rich-content">
<text class="content-title">活动介绍/回顾</text>
<div v-if="detail.localImg" style="text-align:center; margin:12px 0;">
<image :src="require(`@/static/${detail.localImg}`)" mode="widthFix" style="width:90%; border-radius:8px;"></image>
</div>
<rich-text class="rich-text" :nodes="contentNodes"></rich-text>
</view>
<view class="detail-bottom">
<view class="detail-left">
<button open-type="share" class="handle-btn">
<uni-icons custom-prefix="iconfont" type="icon-fenxiang01" size="20"></uni-icons>
<text>分享</text>
</button>
</view>
<button
class="join-btn"
:class="getJoinBtnClass()"
@click="handleJoin"
>
{{ getJoinBtnText() }}
</button>
</view>
<view class="modal-mask" v-if="showJoinModal" @click="closeJoinModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">报名信息</text>
<uni-icons class="close-icon" type="close" size="24" @click="closeJoinModal"></uni-icons>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">姓名</text>
<input class="form-input" v-model="joinForm.name" placeholder="请输入您的姓名" />
</view>
<view class="form-item">
<text class="form-label">联系方式</text>
<view style="display:flex; justify-content: space-between; align-items:center;">
<input class="form-input" v-model="joinForm.phone" placeholder="请输入手机号码" type="number" maxlength="11" />
<button class="get-phone-btn" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
一键获取
</button>
</view>
</view>
<view class="form-item">
<text class="form-label">参加人数</text>
<view class="select-wrapper">
<picker
mode="selector"
:range="rangeText"
:value="joinForm.personCount - 1"
@change="onPickerChange"
class="picker-select"
>
<view class="picker-display">
{{ joinForm.personCountText || '请选择参加人数' }}
</view>
</picker>
</view>
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" @click="closeJoinModal">取消</button>
<button class="submit-btn" @click="submitJoin">确认报名</button>
</view>
</view>
</view>
</view>
</template>
<script>
// import config from '@/utils/config';
// import { getOpenId } from '@/utils/storage';
import { FetchSessionKey, FetchDecryptPhone } from '@/api/user.js';
import { FetchSignActivity, FetchCancelSignActivity } from '@/api/activity.js';
export default {
data() {
return {
detail: {},
activityId: "",
contentNodes: "",
showJoinModal: false,
joinForm: {
name: "",
phone: "",
personCount: 1,
personCountText: ""
},
personOptions: [
{ value: 1, text: "1人" },
{ value: 2, text: "2人" },
{ value: 3, text: "3人" },
{ value: 4, text: "4人" },
{ value: 5, text: "5人" },
],
rangeText: [],
sessionKey: "",
openid: ""
};
},
onLoad(options) {
console.log(options);
const item = JSON.parse(decodeURIComponent(options.item));
this.detail = item;
console.log('this.detail',this.detail);
this.activityId = item.id || item.activityId;
this.formatContent();
this.rangeText = this.personOptions.map(item => item.text);
this.joinForm.personCount = this.personOptions[0].value;
this.joinForm.personCountText = this.personOptions[0].text;
this.getSessionKey();
},
methods: {
formatContent() {
let content = this.detail.activityContent || this.detail.activity_content || "";
if (!content) {
this.contentNodes = "";
return;
}
content = content.replace(/&amp;/g, '&');
const imgReg = /<img\s+[^>]*src\s*=\s*["']?([^"'> ]+)["']?[^>]*>/gi;
content = content.replace(imgReg, (match, src) => {
return `<div><img src="${src}" style="width:100%;max-width:750px;display:block;margin:0 auto;"></div>`;
});
// ========== p标签:原有样式保留,缺少line-height自动补充,统一margin ==========
const pHasStyleReg = /<p(\s+[^>]*?)style\s*=\s*["']([^"']+)["']([^>]*)>/gi;
content = content.replace(pHasStyleReg, (match, before, oldStyleStr, after) => {
const hasLineHeight = /line-height\s*:/.test(oldStyleStr);
let append = 'margin:15px 0;';
if (!hasLineHeight) {
append = 'line-height:1.5;margin:15px 0;';
}
const newStyle = oldStyleStr + ';' + append;
return `<p${before}style="${newStyle}"${after}>`;
});
const pNoStyleReg = /<p(?!\s+style\s*=)([^>]*)>/gi;
content = content.replace(pNoStyleReg, '<p$1 style="line-height:1.5;margin:15px 0;">');
// ========== blockquote 引用样式 ==========
content = content.replace(/<blockquote([^>]*)>/gi,
`<blockquote$1 style="background:#f5f7fa;border-left:8px solid #01a4fe;display:block;font-size:100%;line-height:1.5;margin:10px 0;padding:10px;">`);
// ========== 表格基础样式 ==========
content = content.replace(/<table([^>]*)>/gi,
`<table$1 style="width:100% !important;border-collapse:collapse;margin:10px 0;">`);
content = content.replace(/<th([^>]*)>/gi,
`<th$1 style="border:1px solid #dcdfe6;padding:8px 12px;text-align:center;font-size:14px;">`);
content = content.replace(/<td([^>]*)>/gi,
`<td$1 style="border:1px solid #dcdfe6;padding:8px 12px;text-align:center;font-size:14px;">`);
this.contentNodes = content;
},
onPickerChange(e) {
const index = e.detail.value;
this.joinForm.personCount = this.personOptions[index].value;
this.joinForm.personCountText = this.personOptions[index].text;
},
async getSessionKey() {
try {
uni.login({
success: async (loginRes) => {
console.log('loginRes',loginRes)
if (loginRes.code) {
const res = await FetchSessionKey({
libcode: getApp().globalData.libcode,
code: loginRes.code
});
if (res.code === 200) {
this.sessionKey = res.data.session_key;
this.openid = res.data.openid;
}
}
}
});
} catch (error) {
console.error('获取sessionKey失败:', error);
}
},
getPhoneNumber(e) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
uni.showToast({ title: '取消授权', icon: 'none' });
return;
}
if (!this.sessionKey) {
uni.showToast({ title: '请稍后再试', icon: 'none' });
return;
}
FetchDecryptPhone({
sessionKey: this.sessionKey,
encryptedData: e.detail.encryptedData,
iv: e.detail.iv
}).then(res => {
if (res.code === 200) {
this.joinForm.phone = res.data.phoneNumber || '';
uni.showToast({ title: '获取成功', icon: 'success' });
} else {
uni.showToast({ title: res.message || '获取失败', icon: 'none' });
}
}).catch(() => {
uni.showToast({ title: '获取手机号失败', icon: 'none' });
});
},
getJoinBtnClass() {
const d = this.detail;
// 已结束 / 已取消 禁用
if (d.status === '已结束' || d.status === '已取消') {
return 'disabled-btn'
}
return ''
},
// 获取按钮文字
getJoinBtnText() {
const d = this.detail;
if(d.status === '已结束') return '活动结束'
if(d.status === '已取消') return '活动取消'
// 未开始,有预约记录,没有取消,显示取消预约
if(d.status === '未开始' && d.isCancel === false && !!d.signId){
return '取消预约'
}
return '我要参加'
},
handleJoin() {
const d = this.detail
// 已结束、已取消直接拦截
if (['已结束', '已取消'].includes(d.status)) {
return;
}
// 未开始、有预约记录、未取消:执行取消报名
if(d.status === '未开始' && d.isCancel === false && !!d.signId){
uni.showModal({
title: '提示',
content: `确定要取消活动【${d.title}】的预约吗?`,
success: async (res) => {
if(res.confirm){
try {
const apiRes = await FetchCancelSignActivity({
id: d.signId
})
if(apiRes.code === 200){
uni.showToast({ title: '取消成功', icon: 'success' })
// 发送全局事件,告诉预约列表页需要刷新
uni.$emit('refreshMySignList')
setTimeout(() => {
uni.navigateBack()
}, 700)
}else{
uni.showToast({ title: apiRes.message || '取消失败', icon: 'none' })
}
} catch (err) {
console.error('取消报名异常', err)
uni.showToast({ title: '网络异常,取消失败', icon: 'none' })
}
}
}
})
return
}
//打开报名弹窗
this.openJoinModal();
},
closeJoinModal() {
this.showJoinModal = false;
},
onPersonChange(e) {
this.joinForm.personCount = e.detail.value + 1;
},
openJoinModal() {
// 每次打开弹窗完整重置表单,包含picker显示文本
this.joinForm = {
name: "",
phone: "",
personCount: 1,
personCountText: "1人"
}
this.showJoinModal = true
},
submitJoin() {
if (!this.joinForm.name) {
uni.showToast({ title: '请输入姓名', icon: 'none' });
return;
}
if (!this.joinForm.phone) {
uni.showToast({ title: '请输入联系方式', icon: 'none' });
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(this.joinForm.phone)) {
uni.showToast({ title: '请输入正确的手机号码', icon: 'none' });
return;
}
const params = {
// libcode: getApp().globalData.libcode,
activityId: this.activityId,
signName: this.joinForm.name,
signPhone: this.joinForm.phone,
signNum: this.joinForm.personCount,
openid: this.openid,
// sessionKey: this.sessionKey
}
console.log('params',params)
FetchSignActivity(params).then(res => {
console.log('FetchSignActivity',res)
if (res.code === 200) {
uni.showToast({
title: '报名成功',
icon: 'success'
});
uni.$emit('refreshMySignList')
this.closeJoinModal();
} else {
console.log('error',res)
uni.showToast({
title: res.message || '报名失败',
icon: 'none'
});
}
}).catch((err) => {
console.log('catch err', err)
if(err){
uni.showToast({
title: err,
icon: 'none'
})
}else{
uni.showToast({
title: '报名失败',
icon: 'none'
});
}
});
},
formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const h = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
return `${y}-${m}-${d} ${h}:${min}`;
},
onShareAppMessage() {
return {
title: this.detail.title,
path: "/subpkg/pages/activity-detail/activity-detail?item=" + encodeURIComponent(JSON.stringify(this.detail)),
imageUrl: this.detail.coverImage
};
}
}
};
</script>
<style lang="scss" scoped>
.activity-detail {
padding-bottom: 60px;
background: #f5f5f5;
}
.activity-img {
width: 100%;
height: 180px;
}
.activity-base {
background: #fff;
padding: 15px;
margin-bottom: 10px;
.title {
font-size: 16px;
font-weight: bold;
padding-bottom: 10px;
}
.time,
.location {
display: flex;
align-items: center;
font-size: 12px;
color: #999;
padding-top: 10px;
.detail-icon {
padding-right: 4px;
}
}
.time{
padding-left: 3px;
}
}
.rich-content {
background: #fff;
padding: 10px;
.content-title{
position: relative;
font-size: 16px;
font-weight: bold;
color: #333;
padding-left: 12px;
margin-bottom: 20px;
&::before{
position: absolute;
left: 0;
top: 50%;
margin-top: -9px;
content: "";
width: 4px;
height: 18px;
background-color: #01a4fe;
}
}
}
.rich-text{
::v-deep p{
line-height: 1.5;
margin: 15px 0;
}
}
.detail-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
}
.detail-left {
display: flex;
gap: 10px;
}
.handle-btn {
display: flex;
align-items: center;
gap: 4px;
background: #f5f5f5;
border-radius: 30px;
padding: 0 12px;
height: 36px;
font-size: 14px;
}
.join-btn {
background: #01a4fe;
color: #fff;
border-radius: 30px;
padding: 0 20px;
height: 36px;
}
.disabled-btn {
background: #ccc !important;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 85%;
background: #fff;
border-radius: 8px;
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
.modal-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.close-icon {
color: #999;
}
}
.modal-body {
padding: 15px;
}
.form-item {
margin-bottom: 15px;
.form-label {
display: block;
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
.form-input {
width: calc(100% - 25px);
height: 40px;
background: #f8f8f8;
border-radius: 8px;
padding: 0 12px;
font-size: 14px;
}
.select-wrapper {
width: calc(100% - 25px);
height: 40px;
background: #f8f8f8;
border-radius: 8px;
display: flex;
align-items: center;
padding: 0 12px;
font-size: 14px;
::v-deep .picker-select{
width: 100%;
}
.select-value {
font-size: 14px;
color: #333;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
&::after {
content: '';
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #999;
}
}
}
}
.modal-footer {
display: flex;
border-top: 1px solid #f0f0f0;
.cancel-btn,
.submit-btn {
flex: 1;
height: 44px;
line-height: 44px;
text-align: center;
font-size: 15px;
border-radius: 0;
border: none;
margin: 0;
padding: 0;
&::after {
border: none;
border-radius: 0;
}
}
.cancel-btn {
background: #f5f5f5;
color: #666;
// border-right: 1px solid #f0f0f0;
}
.submit-btn {
background: #01a4fe;
color: #fff;
}
}
.get-phone-btn {
font-size: 12px;
color: #fff;
background-color: #01a4fe;
width: 90px;
border-radius: 20px;
padding: 10px;
border: none;
line-height: 1.2;
margin-left: 10px;
&::after {
border: none;
}
}
</style>