品牌活动整改

This commit is contained in:
2026-11-04 12:10:24 +08:00
parent d8313d7822
commit e9866653a9
3 changed files with 406 additions and 58 deletions
@@ -21,37 +21,61 @@ module.exports = {
},
},
watch: {
content: {
immediate: true,
handler() {
// 详情数据通常在组件创建后异步回填,每次内容变化都重新识别并渲染 PDF。
this.init()
}
}
},
data() {
return {
pdf: true,
pdfObj: null,
previewVersion: 0,
}
},
methods: {
init() {
this.destroyPdf()
const currentVersion = ++this.previewVersion
try {
const parser = new DOMParser();
const doc = parser.parseFromString(this.content, 'text/html');
const link = doc.querySelector('a');
const href = link.getAttribute('href');
if(!href) {
const parser = new DOMParser()
const doc = parser.parseFromString(this.content || '', 'text/html')
const link = doc.querySelector('a')
const href = link ? link.getAttribute('href') : ''
if (!href) {
this.pdf = false
return
}
const parsedUrl = new URL(href, window.location.origin)
const linkText = (link.textContent || '').trim().toLowerCase()
const isPdf = linkText.endsWith('.pdf') || parsedUrl.pathname.toLowerCase().endsWith('.pdf')
if (!isPdf) {
this.pdf = false
return
}
// 系统上传文件可能保存了其他环境的绝对域名,内部下载地址统一使用当前站点。
const pdfUrl = parsedUrl.pathname === '/platform/sys/file/download'
? parsedUrl.pathname + parsedUrl.search : parsedUrl.href
this.pdf = true
this.$nextTick(() => {
if (currentVersion !== this.previewVersion || !this.pdf) return
this.pdfObj = new Pdfh5('#pdf-container', {
pdfurl: href,
});
this.pdfObj.on("complete", function () {
const elements = document.querySelectorAll('[class*="canvasImg"]')
let classArray = []
pdfurl: pdfUrl,
})
this.pdfObj.on("complete", () => {
if (currentVersion !== this.previewVersion) return
const elements = this.$el.querySelectorAll('[class*="canvasImg"]')
const classArray = []
elements.forEach(element => {
classArray.push(element.getAttribute('src'))
})
elements.forEach((element,index) => {
element.addEventListener('click', function() {
element.addEventListener('click', () => {
vant.ImagePreview({
images: classArray,
startPosition: index,
@@ -65,9 +89,17 @@ module.exports = {
this.pdf = false
}
},
destroyPdf() {
// 内容切换或组件销毁时释放 PDF 渲染实例,避免重复打开详情产生资源残留。
if (this.pdfObj && this.pdfObj.destroy) {
this.pdfObj.destroy()
}
this.pdfObj = null
},
},
created() {
this.init()
beforeDestroy() {
this.previewVersion += 1
this.destroyPdf()
},
}
</script>
@@ -3,7 +3,175 @@ layout("/layouts/platform.html"){
#-->
<style>
.activity-introduce-dialog {
max-width: calc(100vw - 40px);
border-radius: 8px;
overflow: hidden;
}
.activity-introduce-dialog .el-dialog__header {
padding: 20px 24px;
border-bottom: 1px solid #ebeef5;
}
.activity-introduce-dialog .el-dialog__title {
color: #303133;
font-size: 18px;
font-weight: 600;
}
.activity-introduce-dialog .el-dialog__body {
padding: 0;
}
.activity-introduce-dialog .el-dialog__footer {
padding: 16px 24px;
border-top: 1px solid #ebeef5;
}
.activity-introduce-layout {
display: flex;
height: calc(86vh - 128px);
min-height: 520px;
max-height: 650px;
overflow: hidden;
}
.activity-introduce-summary {
box-sizing: border-box;
width: 280px;
flex-shrink: 0;
padding: 24px;
background: #f7faff;
border-right: 1px solid #ebeef5;
}
.activity-introduce-cover {
display: block;
width: 100%;
height: 152px;
overflow: hidden;
border-radius: 6px;
background: #eef3f8;
}
.activity-introduce-cover-empty {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
color: #a8abb2;
font-size: 40px;
}
.activity-introduce-name {
margin: 16px 0 18px;
color: #303133;
font-size: 22px;
font-weight: 600;
line-height: 1.4;
}
.activity-introduce-meta {
padding-bottom: 20px;
border-bottom: 1px solid #e4e7ed;
}
.activity-introduce-meta-item {
display: flex;
margin-bottom: 16px;
color: #606266;
font-size: 13px;
line-height: 1.7;
}
.activity-introduce-meta-item:last-child {
margin-bottom: 0;
}
.activity-introduce-meta-item > i {
width: 20px;
margin-top: 4px;
color: #909399;
font-size: 16px;
}
.activity-introduce-meta-label {
color: #909399;
}
.activity-introduce-steps {
height: 115px;
margin-top: 22px;
}
.activity-introduce-steps .el-step__title {
font-size: 14px;
}
.activity-introduce-main {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
padding: 24px;
}
.activity-introduce-main-title {
margin-bottom: 16px;
padding-left: 12px;
border-left: 4px solid var(--color-primary);
color: #303133;
font-size: 20px;
font-weight: 600;
line-height: 24px;
}
.activity-introduce-file {
display: flex;
height: 38px;
flex-shrink: 0;
align-items: center;
padding: 0 14px;
border-radius: 4px;
background: #f2f4f7;
color: #606266;
font-size: 13px;
}
.activity-introduce-file i {
margin-right: 8px;
color: #f56c6c;
font-size: 16px;
}
.activity-introduce-preview {
min-height: 0;
flex: 1;
margin-top: 14px;
overflow: hidden;
background: #eef1f5;
}
.activity-introduce-pdf {
width: 100%;
height: 100%;
border: 0;
}
.activity-introduce-content {
box-sizing: border-box;
height: 100%;
overflow-y: auto;
padding: 28px 36px;
background: #fff;
color: #303133;
font-size: 14px;
line-height: 1.8;
}
.activity-introduce-empty {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
color: #909399;
}
.activity-introduce-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.activity-introduce-footer-actions {
display: flex;
gap: 12px;
}
.activity-introduce-footer-actions .el-button {
min-width: 110px;
}
.activity-introduce-footer-actions .el-button--primary {
min-width: 180px;
}
.activity-introduce-countdown {
color: #e6a23c;
font-size: 13px;
}
</style>
<div id="app" v-cloak>
@@ -60,7 +228,7 @@ layout("/layouts/platform.html"){
<template v-slot="{row}">
<template v-if="$moment().isBefore($moment(row.activitySignUpEndTime))">
<el-button @click="onView(row)" size="mini" type="primary">查看介绍</el-button>
<el-button @click="onOpen(row)" size="mini" type="primary">去报名</el-button>
<el-button @click="onView(row)" size="mini" type="primary">去报名</el-button>
</template>
<template v-if="$moment().isAfter($moment(row.activitySignUpEndTime))">
<el-button size="mini" type="info">已结束</el-button>
@@ -76,40 +244,101 @@ layout("/layouts/platform.html"){
</template>
</guava>
<el-dialog title="详细信息" :visible.sync="infoVisible" width="60%">
<el-dialog title="活动介绍"
custom-class="activity-introduce-dialog"
:close-on-click-modal="false"
:visible.sync="infoVisible"
top="7vh"
width="1120px">
<div class="activity-introduce-layout">
<aside class="activity-introduce-summary">
<el-image class="activity-introduce-cover" :src="infoRow.cover" fit="cover">
<div slot="error" class="activity-introduce-cover-empty">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
<div class="activity-introduce-name">{{infoRow.activityName || '品牌活动'}}</div>
<div class="activity-introduce-meta">
<div class="activity-introduce-meta-item">
<i class="el-icon-date"></i>
<div>
<div class="activity-introduce-meta-label">报名时间</div>
<div>{{$moment(infoRow.activitySignUpStartTime).format('YYYY-MM-DD HH:mm')}}</div>
<div>至 {{$moment(infoRow.activitySignUpEndTime).format('YYYY-MM-DD HH:mm')}}</div>
</div>
</div>
<div class="activity-introduce-meta-item">
<i class="el-icon-time"></i>
<div>
<div class="activity-introduce-meta-label">活动时间</div>
<div>{{$moment(infoRow.activityStartTime).format('YYYY-MM-DD HH:mm')}}</div>
<div>至 {{$moment(infoRow.activityEndTime).format('YYYY-MM-DD HH:mm')}}</div>
</div>
</div>
<div class="activity-introduce-meta-item">
<i class="el-icon-user"></i>
<div>
<div class="activity-introduce-meta-label">面向对象</div>
<div>{{infoRow.activityGroupName || '全体工会会员'}}</div>
</div>
</div>
</div>
<el-steps class="activity-introduce-steps" direction="vertical" :active="0">
<el-step title="阅读活动介绍"></el-step>
<el-step title="选择报名项目"></el-step>
</el-steps>
</aside>
<activity-info ref="infoRef"></activity-info>
<section class="activity-introduce-main">
<div class="activity-introduce-main-title">报名须知</div>
<div class="activity-introduce-file">
<i :class="activityIntroducePdfUrl ? 'fa fa-file-pdf-o' : 'fa fa-file-text-o'"></i>
<span>{{activityIntroduceFileName}}</span>
<span v-if="activityIntroducePageCount">&nbsp;·&nbsp;{{activityIntroducePageCount}}页</span>
</div>
<div class="activity-introduce-preview">
<iframe v-if="activityIntroducePdfUrl"
ref="activityPdfFrame"
class="activity-introduce-pdf"
@load="styleActivityPdf"
:src="activityIntroducePdfUrl"></iframe>
<div v-else-if="infoRow.introduce"
class="activity-introduce-content"
v-html="infoRow.introduce"></div>
<div v-else class="activity-introduce-empty">暂无活动介绍</div>
</div>
</section>
</div>
<span slot="footer" class="dialog-footer">
<el-statistic
v-if="time < 0"
format="DD 天 HH 时 mm 分钟 ss 秒"
:value="new Date(infoRow.activitySignUpStartTime)"
time-indices
title="距离开始:"
@finish="time = 0"
>
</el-statistic>
<template v-else>
<el-button @click="infoVisible = false">取消</el-button>
<el-button type="primary" @click="onOpen(infoRow)" :disabled="time < 0">
去报名
</el-button>
</template>
</span>
<div slot="footer" class="activity-introduce-footer">
<div>
<el-checkbox v-model="activityIntroduceConfirmed" :disabled="time < 0">
我已阅读并知晓活动说明
</el-checkbox>
<span class="activity-introduce-countdown" v-if="time < 0">
报名尚未开始
</span>
</div>
<div class="activity-introduce-footer-actions">
<el-button @click="closeIntroduce">暂不报名</el-button>
<el-button type="primary"
:disabled="time < 0 || !activityIntroduceConfirmed"
@click="onOpen(infoRow)">
确定并选择报名项目
</el-button>
</div>
</div>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
<!--#include('courseList.js'){}#-->
<!--#include('../manage/info.js'){}#-->
new Vue({
el: "#app",
mixins: [initTableMixins],
components: {
"course-list": courseList,
"activity-info": info,
},
data() {
return {
@@ -126,25 +355,89 @@ layout("/layouts/platform.html"){
time: 0,
infoRow: {},
infoVisible: false,
activityIntroducePdfUrl: '',
activityIntroduceFileName: '活动介绍',
activityIntroducePageCount: null,
activityIntroduceConfirmed: false,
id: GetQueryString("id"),
}
},
methods: {
onView(row) {
this.infoRow = row
this.time = this.$moment().diff(this.$moment(row.activitySignUpStartTime), 'milliseconds')
this.infoVisible = true
this.$nextTick(() => {
this.$refs.infoRef.initData(row.id)
})
const introduce = this.resolveActivityIntroduce(row.introduce)
this.$set(this.$data, 'infoRow', row)
this.$set(this.$data, 'activityIntroducePdfUrl', introduce.pdfUrl)
this.$set(this.$data, 'activityIntroduceFileName', introduce.fileName)
this.$set(this.$data, 'activityIntroducePageCount', null)
this.$set(this.$data, 'activityIntroduceConfirmed', false)
this.$set(this.$data, 'time', this.$moment().diff(this.$moment(row.activitySignUpStartTime), 'milliseconds'))
this.$set(this.$data, 'infoVisible', true)
},
resolveActivityIntroduce(introduce) {
try {
const parser = new DOMParser()
const doc = parser.parseFromString(introduce || '', 'text/html')
const link = doc.querySelector('a')
const href = link ? link.getAttribute('href') : ''
if (!href) return {pdfUrl: '', fileName: '活动介绍'}
const parsedUrl = new URL(href, window.location.origin)
const linkText = (link.textContent || '').trim().toLowerCase()
const isPdf = linkText.endsWith('.pdf') || parsedUrl.pathname.toLowerCase().endsWith('.pdf')
if (!isPdf) return {pdfUrl: '', fileName: '活动介绍'}
// 系统内部文件使用当前站点地址,避免历史环境域名导致 PDF 无法加载。
const pdfUrl = parsedUrl.pathname === '/platform/sys/file/download'
? parsedUrl.pathname + parsedUrl.search : parsedUrl.href
return {
pdfUrl: '/assets/platform/plugins/pdfJs/web/viewer.html?file=' + encodeURIComponent(pdfUrl),
fileName: (link.textContent || '').trim() || '活动介绍.pdf'
}
} catch (e) {
// 无法识别为 PDF 时按原有富文本内容展示。
return {pdfUrl: '', fileName: '活动介绍'}
}
},
styleActivityPdf() {
try {
const frame = this.$refs.activityPdfFrame
const viewerDocument = frame ? frame.contentDocument : null
if (!viewerDocument) return
// 报名前预览只保留文档阅读区,隐藏 PDF.js 原生工具栏以匹配确认弹框的简洁层级。
const toolbar = viewerDocument.getElementById('toolbarContainer')
const mainContainer = viewerDocument.getElementById('mainContainer')
const viewerContainer = viewerDocument.getElementById('viewerContainer')
if (toolbar) toolbar.style.display = 'none'
if (mainContainer) mainContainer.style.top = '0'
if (viewerContainer) viewerContainer.style.top = '0'
const viewerApplication = frame.contentWindow.PDFViewerApplication
if (viewerApplication && viewerApplication.initializedPromise) {
viewerApplication.initializedPromise.then(() => {
viewerApplication.eventBus.on('pagesloaded', (event) => {
this.$set(this.$data, 'activityIntroducePageCount', event.pagesCount || null)
})
})
}
} catch (e) {
// PDF 查看器样式隔离失败时保留原始查看器,不影响文档阅读和报名流程。
}
},
closeIntroduce() {
this.$set(this.$data, 'infoVisible', false)
},
onOpen(row) {
if(this.$moment().isBefore(this.$moment(row.activitySignUpStartTime))) {
this.onView(row)
return
}
this.infoVisible = false
if (!this.activityIntroduceConfirmed) {
this.$message.warning('请先确认已阅读活动说明')
return
}
this.$set(this.$data, 'infoVisible', false)
this.$nextTick(() => {
this.$refs.guava.view(() => {
this.$refs.courseListRef.onOpen(row)
@@ -55,7 +55,7 @@ const info = {
</span>
</el-dialog>
<el-dialog :visible.sync="viewMapDialog" title="地点" width="60%">
<el-dialog :visible.sync="viewMapDialog" @closed="destroyViewMap" title="地点" width="60%">
<div id="viewMap" style="width: 100%; height: 500px"></div>
</el-dialog>
</div>
@@ -69,6 +69,8 @@ const info = {
courseTimeList: [],
viewMapDialog: false,
viewMap: null,
viewMarkerLayer: null,
welfareProjectName: '',
}
},
@@ -103,28 +105,49 @@ const info = {
this.courseTimeListDialog = true
},
openViewMap(point) {
if (!Array.isArray(point)) {
// 地点坐标按项目现有约定传入 [纬度, 经度],缺失或异常时不打开空白地图。
if (!Array.isArray(point) || point.length < 2) {
this.$message.warning("没有设置点位,无法通过地图查看")
return
}
const latitude = Number(point[0])
const longitude = Number(point[1])
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
this.$message.warning("地点坐标格式有误,无法通过地图查看")
return
}
// 当前公共布局加载腾讯地图,脚本异常时给出提示,避免地图弹窗直接报错。
if (typeof TMap === "undefined") {
this.$message.error("地图加载失败,请刷新页面后重试")
return
}
this.viewMapDialog = true
this.$nextTick(() => {
viewMap = new AMap.Map("viewMap", {
resizeEnable: true,
center: point,
zoom: 16
const center = new TMap.LatLng(latitude, longitude)
this.viewMap = new TMap.Map("viewMap", {
center: center,
zoom: 16,
})
if (viewMarker) {
viewMap.remove(viewMarker)
}
viewMarker = new AMap.Marker({
position: point,
offset: new AMap.Pixel(-13, -30)
this.viewMarkerLayer = new TMap.MultiMarker({
map: this.viewMap,
geometries: [{
id: "view-location",
position: center,
}],
})
viewMap.add(viewMarker)
viewMap.setFitView(null, false, [150, 60, 100, 60])
})
},
destroyViewMap() {
// 弹窗关闭后释放地图覆盖物和实例,避免重复查看地点时残留旧地图资源。
if (this.viewMarkerLayer && this.viewMarkerLayer.setMap) {
this.viewMarkerLayer.setMap(null)
}
if (this.viewMap && this.viewMap.destroy) {
this.viewMap.destroy()
}
this.viewMarkerLayer = null
this.viewMap = null
},
},
style: /*language=CSS*/ `
/deep/ .el-button--text {