feat:浦发项目模块迁移-职代会h5端初始化

This commit is contained in:
2026-07-28 17:27:29 +08:00
parent 785b9a4b80
commit 704bc278fa
9 changed files with 3240 additions and 0 deletions
@@ -0,0 +1,372 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="职代会" left-text="返回" left-arrow @click-left="returnH5Home('apps')" placeholder fixed></van-nav-bar>
<div class="congress-page">
<div class="empty-box" v-if="!loading && timesList.length === 0">
<van-icon name="notes-o" class="empty-icon"></van-icon>
<p class="empty-text">暂无参与届次信息</p>
</div>
<template v-else>
<div class="page-header">
<div class="session-info-card">
<h2 class="session-title">{{ selectedSessionName }}</h2>
<p class="session-subtitle">{{ selectedTimeText }}</p>
</div>
</div>
<div class="module-list-container">
<div class="modules-wrapper">
<div
v-for="module in moduleList"
:key="module.id"
class="module-card"
@click="handleModuleClick(module)"
>
<div class="module-header">
<div class="icon-wrapper">
<van-icon :name="module.icon" size="36"></van-icon>
</div>
<div class="module-info">
<h4 class="module-title">{{ module.name }}</h4>
<p class="module-desc" v-if="module.desc">{{ module.desc }}</p>
</div>
<van-icon name="arrow" class="arrow-icon"></van-icon>
</div>
</div>
</div>
</div>
<div v-if="timesList.length > 1" class="bottom-switch-bar">
<span class="switch-text" @click="showTimePicker = true">
<van-icon name="arrow-down" size="12"></van-icon>
切换届次
</span>
</div>
</template>
<van-popup v-model="showTimePicker" position="bottom">
<van-picker
show-toolbar
:columns="formattedTimeColumns"
@confirm="onTimeConfirm"
@cancel="showTimePicker = false"
></van-picker>
</van-popup>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
data() {
return {
loading: true,
timesList: [],
selectedTimeId: "",
selectedTimeText: "",
selectedSessionName: "",
selectedRepId: "",
showTimePicker: false,
moduleList: []
}
},
computed: {
formattedTimeColumns() {
return this.timesList.map((item) => ({
text: item.name,
value: item.id
}))
}
},
methods: {
async initPage() {
const loading = this.$toast.loading({message: "加载中...", forbidClick: true, duration: 0})
try {
await this.fetchRepTimes()
if (this.timesList.length > 0) {
this.selectTime(this.timesList[0])
await this.fetchModules()
}
} finally {
this.loading = false
loading.close()
}
},
loginName() {
const user = (this.$store.state && this.$store.state.user) || JSON.parse(window.sessionStorage.getItem("user") || "{}")
return user.loginname || user.loginName || ""
},
async fetchRepTimes() {
const res = await this.$axios.get("/platform/h5/congress/repTimes", {params: {emplid: this.loginName()}})
if (res.code === 0) {
this.timesList = res.data || []
}
},
async fetchModules() {
const res = await this.$axios.get("/platform/h5/congress/materialsTypeList")
if (res.code !== 0) return
const modules = (res.data || []).map((item) => ({
id: item.id,
name: item.name,
icon: this.resolveIcon(item.name),
desc: this.resolveDesc(item.name)
}))
const hasSignin = modules.some((item) => item.name && item.name.indexOf("会议签到") > -1)
this.moduleList = hasSignin ? modules : [{
id: "signin",
name: "会议签到",
icon: "calendar-o",
desc: "点击进行会议签到"
}].concat(modules)
},
selectTime(item) {
this.selectedTimeId = item.id || item.timeId || ""
this.selectedTimeText = item.name || ""
this.selectedSessionName = item.sessionName || ""
this.selectedRepId = item.repId || ""
window.localStorage.setItem("timeName", this.selectedTimeText)
window.localStorage.setItem("sessionName", this.selectedSessionName)
window.localStorage.setItem("repId", this.selectedRepId)
},
onTimeConfirm(value) {
const selected = this.timesList.find((item) => item.id === value.value)
if (selected) {
this.selectTime(selected)
this.fetchModules()
}
this.showTimePicker = false
},
handleModuleClick(module) {
if (!this.selectedTimeId) {
this.$toast("请先选择届次")
return
}
if (module.id === "signin" || module.name.indexOf("会议签到") > -1) {
this.$pjaxReplace("/platform/h5/congress/meeting/list?moduleId=signin&moduleName=会议签到&timeId=" + encodeURIComponent(this.selectedTimeId))
return
}
this.$pjaxReplace("/platform/h5/congress/materials/list?moduleId=" + encodeURIComponent(module.id) +
"&typeId=" + encodeURIComponent(module.id) +
"&moduleName=" + encodeURIComponent(module.name) +
"&timeId=" + encodeURIComponent(this.selectedTimeId))
},
resolveIcon(name) {
if (!name) return "orders-o"
if (name.indexOf("会议签到") > -1) return "calendar-o"
if (name.indexOf("评议") > -1) return "star"
if (name.indexOf("议题") > -1) return "todo-list-o"
if (name.indexOf("资料") > -1) return "notes-o"
if (name.indexOf("公告") > -1 || name.indexOf("结果") > -1) return "bullhorn-o"
if (name.indexOf("文件") > -1) return "description"
return "orders-o"
},
resolveDesc(name) {
if (!name) return ""
if (name.indexOf("会议签到") > -1) return "点击进行会议签到"
if (name.indexOf("文件") > -1) return "查看会议文件公告"
if (name.indexOf("评议") > -1) return "查看并参与评议表决"
if (name.indexOf("议题") > -1) return "查看并参与议题表决"
if (name.indexOf("表决公告") > -1) return "查看表决结果公告"
if (name.indexOf("资料") > -1) return "查看会议准备资料"
return ""
}
},
created() {
this.initPage()
}
})
</script>
<style id="style-congress-h5">
.congress-page {
min-height: calc(100vh - 46px);
background: #f5f7fa;
}
.page-header {
background: linear-gradient(135deg, #1989fa 0%, #0d6efd 100%);
padding: 24px 16px 28px;
color: #fff;
box-shadow: 0 4px 16px rgba(25, 137, 250, 0.15);
display: flex;
justify-content: center;
align-items: center;
}
.session-info-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.session-title {
font-size: 20px;
font-weight: 600;
line-height: 1.4;
color: #fff;
margin: 0 0 8px;
}
.session-subtitle {
font-size: 15px;
line-height: 1.3;
color: #fff;
margin: 0;
}
.module-list-container {
padding: 16px;
}
.modules-wrapper {
display: flex;
flex-direction: column;
gap: 14px;
}
.module-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(0, 0, 0, 0.03);
position: relative;
}
.module-card::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(180deg, #1989fa 0%, #0d6efd 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.module-card:hover {
box-shadow: 0 8px 24px rgba(25, 137, 250, 0.12);
transform: translateY(-3px);
border-color: rgba(25, 137, 250, 0.1);
}
.module-card:hover::before {
opacity: 1;
}
.module-card:active {
transform: translateY(-1px) scale(0.99);
box-shadow: 0 4px 16px rgba(25, 137, 250, 0.08);
}
.module-header {
padding: 16px 18px;
display: flex;
align-items: center;
gap: 14px;
}
.icon-wrapper {
width: 56px;
height: 56px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e6f0ff 0%, #f0f5ff 100%);
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(25, 137, 250, 0.12);
color: #1989fa;
}
.module-info {
flex: 1;
min-width: 0;
}
.module-title {
font-size: 17px;
font-weight: 600;
color: #4a4a4a;
margin: 0 0 6px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.module-desc {
font-size: 13px;
color: #969799;
margin: 0;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow-icon {
font-size: 18px;
color: #c8c9cc;
flex-shrink: 0;
transition: all 0.3s ease;
}
.module-card:hover .arrow-icon {
color: #1989fa;
transform: translateX(4px);
}
.bottom-switch-bar {
padding: 20px 16px calc(20px + env(safe-area-inset-bottom));
display: flex;
justify-content: center;
}
.switch-text {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #969799;
cursor: pointer;
user-select: none;
}
.empty-box {
min-height: calc(100vh - 46px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 24px;
box-sizing: border-box;
text-align: center;
}
.empty-icon {
font-size: 64px;
color: #d9d9d9;
margin-bottom: 16px;
}
.empty-text {
font-size: 14px;
color: #999;
margin: 0;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,279 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar :title="pageTitle" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="file-list-page">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" :finished-text="fileList.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="item in fileList" :key="item.id" class="file-card" @click="handleFileClick(item)">
<div v-if="item.hasVoted !== null && item.hasVoted !== undefined" class="vote-badge">
<span v-if="isVoted(item.hasVoted)" class="badge voted">已投票</span>
<span v-else class="badge pending">待投票</span>
</div>
<div class="card-left">
<div class="icon-wrapper">
<van-icon :name="getFileIcon(item.fileType)" size="36"></van-icon>
</div>
</div>
<div class="card-content">
<div class="card-header">
<span class="file-name">{{ formatFileName(item.name || item.fileName) }}</span>
<van-icon name="arrow" class="arrow-icon"></van-icon>
</div>
<div v-if="item.uploadTime" class="file-info">
<div class="info-row">
<van-icon name="clock-o" size="14" color="#969799"></van-icon>
<span class="info-text">上传时间:<span class="info-value">{{ formatTime(item.uploadTime) }}</span></span>
</div>
</div>
</div>
</div>
<van-empty v-if="!loading && fileList.length === 0" description="暂无文件"></van-empty>
</van-list>
</van-pull-refresh>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
data() {
return {
pageTitle: "文件公告",
fileList: [],
loading: false,
finished: false,
refreshing: false,
loadedOnce: false,
typeId: "",
timeId: ""
}
},
methods: {
initQuery() {
const params = new URLSearchParams(window.location.search)
this.pageTitle = params.get("moduleName") || "文件公告"
this.typeId = params.get("typeId") || params.get("moduleId") || ""
this.timeId = params.get("timeId") || ""
},
loginName() {
const user = (this.$store.state && this.$store.state.user) || JSON.parse(window.sessionStorage.getItem("user") || "{}")
return user.loginname || user.loginName || ""
},
onRefresh() {
this.fileList = []
this.finished = false
this.loadedOnce = false
this.fetchFileList()
},
onLoad() {
if (!this.loadedOnce) {
this.fetchFileList()
}
},
fetchFileList() {
this.loading = true
this.$axios.get("/platform/h5/congress/materialsList", {
params: {
typeId: this.typeId,
timeId: this.timeId,
emplid: this.loginName()
}
}).then((res) => {
if (res.code === 0) {
this.fileList = res.data || []
}
this.finished = true
this.loadedOnce = true
}).finally(() => {
this.loading = false
this.refreshing = false
})
},
getFileIcon(mimeType) {
if (!mimeType) return "description"
const type = String(mimeType).toLowerCase()
if (type.indexOf("pdf") > -1) return "description"
if (type.indexOf("word") > -1) return "edit"
if (type.indexOf("excel") > -1 || type.indexOf("sheet") > -1) return "chart-trending-o"
if (type.indexOf("powerpoint") > -1 || type.indexOf("presentation") > -1) return "play-circle-o"
if (type.indexOf("image") > -1) return "photo"
if (type.indexOf("zip") > -1 || type.indexOf("rar") > -1) return "bag-o"
return "description"
},
formatTime(time) {
if (!time) return ""
const date = new Date(time)
if (Number.isNaN(date.getTime())) return time
const pad = (value) => String(value).padStart(2, "0")
return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes())
},
formatFileName(fileName) {
return fileName ? String(fileName).trim().replace(/\s/g, "") : ""
},
handleFileClick(item) {
if (item.hasVoted !== null && item.hasVoted !== undefined) {
this.openVotePdf(item)
return
}
if (!item.fileUrl) {
this.$toast("暂无可预览附件")
return
}
this.$pjaxReplace("/platform/h5/congress/pdf?pdfPath=" + encodeURIComponent(item.fileUrl) +
"&fileName=" + encodeURIComponent(item.fileName || item.name || ""))
},
openVotePdf(item) {
this.$pjaxReplace("/platform/h5/congress/pdf?pdfPath=" + encodeURIComponent(item.fileUrl || "") +
"&fileName=" + encodeURIComponent(item.fileName || item.name || "") +
"&fileId=" + encodeURIComponent(item.id || ""))
},
isVoted(value) {
return value === true || value === 1 || value === "1" || value === "true"
}
},
created() {
this.initQuery()
}
})
</script>
<style id="style-congress-materials-h5">
.file-list-page {
min-height: calc(100vh - 46px);
background: #f5f7fa;
padding-top: 1px;
}
.file-card {
background: #fff;
margin: 12px 16px;
padding: 18px 16px;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
gap: 16px;
position: relative;
overflow: hidden;
border: 1px solid rgba(0, 0, 0, 0.03);
}
.file-card::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(180deg, #1989fa 0%, #0d6efd 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.file-card:hover {
box-shadow: 0 8px 24px rgba(25, 137, 250, 0.12);
transform: translateY(-3px);
border-color: rgba(25, 137, 250, 0.1);
}
.file-card:hover::before {
opacity: 1;
}
.file-card:active {
transform: translateY(-1px) scale(0.99);
box-shadow: 0 4px 16px rgba(25, 137, 250, 0.08);
}
.vote-badge {
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
.badge {
display: inline-block;
padding: 6px 14px;
font-size: 12px;
font-weight: 600;
border-bottom-right-radius: 12px;
color: #fff;
}
.badge.voted {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
}
.badge.pending {
background: linear-gradient(135deg, #faad14 0%, #d48806 100%);
}
.card-left {
flex-shrink: 0;
margin-top: 12px;
}
.icon-wrapper {
width: 52px;
height: 52px;
border-radius: 14px;
background: linear-gradient(135deg, #f0f5ff 0%, #e6f0ff 100%);
display: flex;
align-items: center;
justify-content: center;
color: #1989fa;
}
.card-content {
flex: 1;
min-width: 0;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.file-name {
font-size: 15px;
font-weight: 600;
color: #4a4a4a;
line-height: 1.4;
word-break: break-word;
flex: 1;
margin-right: 8px;
}
.arrow-icon {
color: #c8c9cc;
font-size: 16px;
flex-shrink: 0;
}
.info-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #969799;
}
.info-value {
color: #646566;
font-weight: 500;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,306 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="会议签到" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="meeting-list-page">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" :finished-text="meetingList.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="item in meetingList" :key="item.id" class="meeting-card">
<div class="card-left">
<div class="icon-wrapper" :class="{'signed-icon': item.signInId}">
<van-icon name="calendar-o" size="26"></van-icon>
</div>
</div>
<div class="card-content">
<div class="card-header">
<span class="meeting-name">{{ item.meetingName }}</span>
</div>
<div class="info-list">
<div v-if="item.signInStartTime" class="info-row">
<van-icon name="clock-o" size="14" color="#969799"></van-icon>
<span class="info-text">签到时间:<span class="info-value">{{ formatTimeRange(item.signInStartTime, item.signInEndTime) }}</span></span>
</div>
<div v-if="item.userSignInTime" class="info-row signed">
<van-icon name="checked" size="14" color="#07c160"></van-icon>
<span class="info-text">已签到:<span class="info-value">{{ formatTime(item.userSignInTime) }}</span></span>
</div>
<div class="info-row count">
<van-icon name="friends-o" size="14" color="#1989fa"></van-icon>
<span class="info-text">签到人数:<span class="info-value">{{ item.signInCount || 0 }} 人</span></span>
</div>
</div>
<div v-if="!item.signInId" class="action-section">
<van-button v-if="!canSignIn(item)" disabled type="default" class="signin-btn disabled">
<van-icon name="clock-o" size="16"></van-icon>
{{ getSignInButtonText(item) }}
</van-button>
<van-button v-else type="primary" class="signin-btn" @click.stop="handleSignInClick(item)">
<van-icon name="checked" size="16"></van-icon>
立即签到
</van-button>
</div>
</div>
</div>
<van-empty v-if="!loading && meetingList.length === 0" description="暂无会议"></van-empty>
</van-list>
</van-pull-refresh>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
data() {
return {
meetingList: [],
loading: false,
finished: false,
refreshing: false,
loadedOnce: false,
timeId: ""
}
},
methods: {
initQuery() {
const params = new URLSearchParams(window.location.search)
this.timeId = params.get("timeId") || ""
},
loginName() {
const user = (this.$store.state && this.$store.state.user) || JSON.parse(window.sessionStorage.getItem("user") || "{}")
return user.loginname || user.loginName || ""
},
onRefresh() {
this.meetingList = []
this.finished = false
this.loadedOnce = false
this.fetchMeetingList()
},
onLoad() {
if (!this.loadedOnce) {
this.fetchMeetingList()
}
},
fetchMeetingList() {
this.loading = true
this.$axios.get("/platform/h5/congress/user/meetingList", {
params: {timeId: this.timeId, emplid: this.loginName()}
}).then((res) => {
if (res.code === 0) {
this.meetingList = res.data || []
}
this.finished = true
this.loadedOnce = true
}).finally(() => {
this.loading = false
this.refreshing = false
})
},
handleSignInClick(item) {
if (item.signInId) {
this.$toast.success("您已签到,无需重复签到")
return
}
if (!this.canSignIn(item)) {
this.$toast(this.getSignInButtonText(item))
return
}
this.$dialog.confirm({
title: "签到确认",
message: "确认要对 \"" + item.meetingName + "\" 进行签到吗?",
confirmButtonText: "确认签到",
cancelButtonText: "取消",
confirmButtonColor: "#1989fa"
}).then(() => {
const body = new URLSearchParams()
body.append("repId", item.repId || window.localStorage.getItem("repId") || "")
body.append("sessionId", item.sessionId || "")
body.append("timeId", item.timeId || this.timeId)
body.append("meetingId", item.id)
return this.$axios.post("/platform/h5/congress/signin", body).then((res) => {
if (res.code === 0) {
this.$toast.success("签到成功")
this.$set(item, "signInId", Date.now())
this.$set(item, "userSignInTime", new Date())
this.$set(item, "signInCount", (item.signInCount || 0) + 1)
}
})
}).catch(() => {})
},
canSignIn(item) {
if (!item.signInStartTime || !item.signInEndTime) return true
const now = Date.now()
const start = new Date(item.signInStartTime).getTime()
const end = new Date(item.signInEndTime).getTime()
if (Number.isNaN(start) || Number.isNaN(end)) return true
return now >= start && now <= end
},
getSignInButtonText(item) {
if (!item.signInStartTime || !item.signInEndTime) return "立即签到"
const now = Date.now()
const start = new Date(item.signInStartTime).getTime()
const end = new Date(item.signInEndTime).getTime()
if (Number.isNaN(start) || Number.isNaN(end)) return "立即签到"
if (now < start) return "未到签到时间"
if (now > end) return "已超过签到时间"
return "立即签到"
},
formatTimeRange(startTime, endTime) {
if (!startTime || !endTime) return ""
const start = this.formatTime(startTime)
const end = this.formatTime(endTime)
return start.slice(0, 10) === end.slice(0, 10) ? start + " ~ " + end.slice(11) : start + " ~ " + end
},
formatTime(time) {
if (!time) return ""
const date = new Date(time)
if (Number.isNaN(date.getTime())) return time
const pad = (value) => String(value).padStart(2, "0")
return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes())
}
},
created() {
this.initQuery()
}
})
</script>
<style id="style-congress-meeting-h5">
.meeting-list-page {
min-height: calc(100vh - 46px);
background: #f5f7fa;
padding-top: 1px;
}
.meeting-card {
background: #fff;
margin: 16px;
padding: 24px;
border-radius: 18px;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: flex-start;
gap: 18px;
position: relative;
overflow: hidden;
border: 1px solid rgba(0, 0, 0, 0.03);
}
.meeting-card::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 5px;
background: linear-gradient(180deg, #1989fa 0%, #0d6efd 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.meeting-card:hover {
box-shadow: 0 8px 24px rgba(25, 137, 250, 0.12);
transform: translateY(-3px);
border-color: rgba(25, 137, 250, 0.1);
}
.meeting-card:hover::before {
opacity: 1;
}
.card-left {
flex-shrink: 0;
padding-top: 2px;
}
.icon-wrapper {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f0f5ff 0%, #e6f0ff 100%);
color: #1989fa;
box-shadow: 0 2px 8px rgba(25, 137, 250, 0.15);
}
.icon-wrapper.signed-icon {
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
color: #fff;
}
.card-content {
flex: 1;
min-width: 0;
}
.card-header {
margin-bottom: 12px;
}
.meeting-name {
font-size: 17px;
color: #323233;
font-weight: 600;
line-height: 1.5;
word-break: break-word;
}
.info-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.info-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
line-height: 1.4;
}
.info-text {
flex: 1;
color: #646566;
word-break: break-word;
}
.info-row.signed .info-text {
color: #07c160;
}
.info-row.count .info-text {
color: #1989fa;
}
.signin-btn {
width: 100%;
padding: 10px 16px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
background: linear-gradient(135deg, #1989fa 0%, #0d6efd 100%);
border: none;
box-shadow: 0 3px 10px rgba(25, 137, 250, 0.3);
}
.signin-btn.disabled {
background: linear-gradient(135deg, rgba(25, 137, 250, 0.3) 0%, rgba(13, 110, 253, 0.3) 100%);
color: #fff;
border: 1px solid rgba(25, 137, 250, 0.4);
opacity: 0.7;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,140 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<div class="pdf-viewer-page">
<van-nav-bar :title="fileName || 'PDF查看'" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="pdf-container" :class="{'has-footer': voteInfo}">
<iframe v-if="pdfPath" :src="previewUrl" class="pdf-frame"></iframe>
<div v-else class="pdf-empty"></div>
</div>
<div v-if="voteInfo" class="footer-bar">
<van-button type="primary" block @click="handleVoteClick">{{ isVoted(voteInfo.voted) ? '查看投票' : '投票' }}</van-button>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
pdfPath: "",
fileName: "",
fileId: "",
voteInfo: null
}
},
computed: {
previewUrl() {
if (!this.pdfPath) return ""
return this.pdfPath
}
},
methods: {
initQuery() {
const params = new URLSearchParams(window.location.search)
this.pdfPath = params.get("pdfPath") || ""
this.fileName = params.get("fileName") || ""
this.fileId = params.get("fileId") || ""
},
fetchVoteInfo() {
if (!this.fileId) return
const repId = window.localStorage.getItem("repId") || ""
this.$axios.get("/platform/h5/congress/votingIssue", {
params: {materialsId: this.fileId, repId: repId}
}).then((res) => {
if (res.code === 0 && res.data) {
this.voteInfo = res.data
}
})
},
handleVoteClick() {
if (!this.voteInfo) {
this.$toast("投票信息未加载")
return
}
window.localStorage.setItem("voteInfo", JSON.stringify(this.voteInfo))
this.$pjaxReplace(this.isVoted(this.voteInfo.voted) ? "/platform/h5/congress/voted" : "/platform/h5/congress/vote")
},
isVoted(value) {
return value === true || value === 1 || value === "1" || value === "true"
}
},
created() {
this.initQuery()
this.fetchVoteInfo()
}
})
</script>
<style id="style-congress-pdf-h5">
.pdf-viewer-page {
min-height: 100vh;
background: #f5f6f8;
}
.pdf-viewer-page .van-nav-bar {
background: #1989fa;
box-shadow: 0 4px 16px rgba(25, 137, 250, 0.15);
}
.pdf-viewer-page .van-nav-bar__title,
.pdf-viewer-page .van-nav-bar .van-icon,
.pdf-viewer-page .van-nav-bar__text {
color: #fff;
font-weight: 600;
}
.pdf-container {
height: calc(100vh - 46px);
background: #f5f6f8;
overflow: hidden;
}
.pdf-container.has-footer {
height: calc(100vh - 110px);
padding-bottom: 64px;
}
.pdf-frame {
width: 100%;
height: 100%;
border: 0;
background: #fff;
}
.pdf-empty {
width: 100%;
height: 100%;
background: #f5f6f8;
}
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 99;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
}
.footer-bar .van-button {
height: 46px;
border-radius: 10px;
background: #1989fa;
border: 0;
font-size: 15px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(25, 137, 250, 0.3);
}
</style>
<!--#
}
#-->
@@ -0,0 +1,417 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<div class="vote-page">
<div class="vote-content">
<div class="vote-header-card">
<h2 class="vote-title">{{ voteInfo.name }}</h2>
<div v-if="voteTicketName" class="vote-type-text">{{ voteTicketName }}</div>
<div class="vote-meta">
<div class="meta-item">
<van-icon name="calendar-o" size="14"></van-icon>
<span>{{ sessionName }}</span>
</div>
<div class="meta-divider"></div>
<div class="meta-item">
<van-icon name="clock-o" size="14"></van-icon>
<span>{{ timeName }}</span>
</div>
</div>
</div>
<div v-if="voteItems.length > 0">
<div v-if="voteItems.length === 1" class="vote-card-mode">
<div class="vote-item-card">
<div class="card-header">
<span class="item-name">{{ voteItems[0].itemName }}</span>
</div>
<div class="card-options">
<label v-for="option in voteItems[0].options" :key="option" class="option-label">
<input type="radio" name="vote-0" :value="option" v-model="selectedVotes[0]">
<span class="option-text">{{ option }}</span>
</label>
</div>
</div>
</div>
<div v-else class="vote-table-container">
<table class="vote-table">
<thead>
<tr>
<th class="col-name">{{ voteInfo.votingContentItemHeader || '表决事项' }}</th>
<th class="col-evaluate" colspan="2">{{ voteInfo.votingContentOptionHeader || '表决内容' }}</th>
</tr>
</thead>
<tbody>
<template v-for="(item, index) in voteItems">
<tr v-for="rowIndex in getRowCount(item)" :key="'row-' + index + '-' + rowIndex">
<td v-if="rowIndex === 1" class="col-name" :rowspan="getRowCount(item)">{{ item.itemName }}</td>
<td class="col-option" v-for="optIndex in 2" :key="'opt-' + optIndex">
<template v-if="(rowIndex - 1) * 2 + optIndex - 1 < item.options.length">
<label class="radio-label">
<input
type="radio"
:name="'vote-' + index"
:value="item.options[(rowIndex - 1) * 2 + optIndex - 1]"
v-model="selectedVotes[index]">
<span class="radio-text">{{ item.options[(rowIndex - 1) * 2 + optIndex - 1] }}</span>
</label>
</template>
<span v-else class="empty-placeholder"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<div v-if="voteInfo.description" class="vote-description">
<div class="description-header">
<van-icon name="info-o" size="16"></van-icon>
<span>投票说明</span>
</div>
<div class="description-content">
<p v-for="(line, index) in descriptionLines" :key="index" class="description-line">{{ line }}</p>
</div>
</div>
<van-empty v-if="voteItems.length === 0" description="暂无投票内容"></van-empty>
</div>
<div class="vote-footer">
<van-button type="primary" block @click="submitVote">确认投票</van-button>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
voteInfo: {},
voteItems: [],
selectedVotes: {},
sessionName: "",
timeName: ""
}
},
computed: {
voteTicketName() {
const typeName = this.voteInfo.typeName || ""
if (!typeName) return ""
return typeName.indexOf("票") > -1 ? typeName : typeName + "票"
},
descriptionLines() {
if (!this.voteInfo.description) return []
return String(this.voteInfo.description).split("\n").filter((line) => line.trim() !== "")
}
},
methods: {
initVoteInfo() {
const cached = window.localStorage.getItem("voteInfo")
this.sessionName = window.localStorage.getItem("sessionName") || ""
this.timeName = window.localStorage.getItem("timeName") || ""
if (!cached) return
try {
this.voteInfo = JSON.parse(cached) || {}
} catch (e) {
this.voteInfo = {}
}
this.sessionName = this.sessionName || this.voteInfo.sessionName || ""
this.timeName = this.timeName || this.voteInfo.timeName || ""
this.parseVoteItems()
},
parseVoteItems() {
if (!this.voteInfo.votingContent) return
let content = this.voteInfo.votingContent
try {
if (typeof content === "string") content = JSON.parse(content)
} catch (e) {
this.voteItems = []
return
}
const list = Array.isArray(content) ? content : (content.items || [content])
this.voteItems = list.map((item, index) => {
let options = item.options || []
if (item.selections && typeof item.selections === "object") {
options = Object.keys(item.selections)
}
if (!Array.isArray(options) || options.length === 0) {
options = ["赞成", "不赞成", "弃权"]
}
return {
itemName: item.itemName || item.item || item.name || item.title || ("投票项" + (index + 1)),
options: options.map((option) => typeof option === "string" ? option : (option.label || option.name || option.value))
}
})
this.selectedVotes = {}
this.voteItems.forEach((_, index) => this.$set(this.selectedVotes, index, ""))
},
getRowCount(item) {
return Math.ceil(item.options.length / 2)
},
submitVote() {
const missing = this.voteItems.filter((_, index) => !this.selectedVotes[index])
if (missing.length > 0) {
this.$toast("请完成所有投票项的选择")
return
}
const voteResult = this.voteItems.map((item, index) => ({
item: item.itemName,
result: this.selectedVotes[index]
}))
this.$dialog.confirm({
title: "确认投票",
message: "您选择了 " + this.voteItems.length + " 项评价,确定要提交吗?",
confirmButtonText: "确认",
cancelButtonText: "取消",
confirmButtonColor: "#1989fa"
}).then(() => {
const body = new URLSearchParams()
body.append("issueId", this.voteInfo.id || "")
body.append("repId", window.localStorage.getItem("repId") || "")
body.append("sessionId", this.voteInfo.sessionId || "")
body.append("timeId", this.voteInfo.timeId || "")
body.append("delegationId", this.voteInfo.delegationId || "")
body.append("voteResult", JSON.stringify(voteResult))
return this.$axios.post("/platform/h5/congress/api/vote", body).then((res) => {
if (res.code === 0) {
this.$toast.success("投票成功")
this.voteInfo.voted = true
this.voteInfo.voteResult = JSON.stringify(voteResult)
window.localStorage.setItem("voteInfo", JSON.stringify(this.voteInfo))
window.localStorage.setItem("voteStatusUpdated", "true")
setTimeout(() => this.historyBack(), 800)
}
})
}).catch(() => {})
}
},
created() {
this.initVoteInfo()
}
})
</script>
<style id="style-congress-vote-h5">
.vote-page {
min-height: 100vh;
background: #f5f7fa;
display: flex;
flex-direction: column;
}
.vote-content {
flex: 1;
overflow-y: auto;
padding: 16px 16px 96px;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
}
.vote-header-card {
background: #fff;
margin-bottom: 16px;
padding: 20px;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
border: 1px solid rgba(0, 0, 0, 0.03);
}
.vote-title {
font-size: 18px;
font-weight: 600;
color: #323233;
margin: 0 0 8px;
line-height: 1.4;
text-align: center;
}
.vote-type-text {
width: 100%;
font-size: 14px;
color: #969799;
margin-bottom: 12px;
line-height: 1.5;
text-align: center;
}
.vote-meta {
display: flex;
align-items: center;
gap: 12px;
padding-top: 8px;
border-top: 1px solid #ebedf0;
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #646566;
font-weight: 500;
}
.meta-item .van-icon {
color: #1989fa;
}
.meta-divider {
width: 1px;
height: 16px;
background: #e5e5e5;
}
.vote-table-container {
width: 100%;
overflow: hidden;
margin-bottom: 16px;
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
border: 1px solid #000;
}
.vote-table {
width: 100%;
border-collapse: collapse;
background: #fff;
}
.vote-table th {
background: linear-gradient(135deg, #f0f5ff 0%, #e6f0ff 100%);
border: 1px solid #000;
padding: 14px 10px;
font-weight: 600;
font-size: 14px;
color: #1989fa;
text-align: center;
}
.vote-table td {
border: 1px solid #000;
padding: 12px 10px;
text-align: center;
vertical-align: middle;
font-size: 14px;
color: #646566;
}
.vote-table .col-name {
width: 88px;
min-width: 88px;
background: #f7f8fa;
color: #323233;
font-weight: 500;
}
.radio-label {
display: flex;
align-items: center;
justify-content: flex-start;
white-space: nowrap;
cursor: pointer;
padding-left: 8px;
}
.radio-label input {
width: 16px;
height: 16px;
margin-right: 8px;
accent-color: #1989fa;
}
.radio-text {
font-size: 14px;
color: #646566;
}
.vote-card-mode,
.vote-description {
margin-bottom: 16px;
}
.vote-item-card,
.vote-description {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.vote-item-card .card-header {
padding: 18px;
background: linear-gradient(135deg, #f0f5ff 0%, #e6f0ff 100%);
color: #1989fa;
font-weight: 600;
}
.card-options {
padding: 18px;
}
.option-label {
display: flex;
align-items: center;
padding: 12px;
background: #f7f8fa;
border-radius: 10px;
margin-bottom: 10px;
}
.option-label input {
margin-right: 10px;
accent-color: #1989fa;
}
.vote-description {
padding: 20px;
border-left: 4px solid #1989fa;
}
.description-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 15px;
font-weight: 600;
color: #1989fa;
}
.description-line {
margin: 0 0 8px;
font-size: 14px;
line-height: 1.8;
color: #646566;
}
.vote-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
}
.vote-footer .van-button {
height: 48px;
border-radius: 12px;
font-weight: 600;
font-size: 15px;
background: linear-gradient(135deg, #1989fa 0%, #0d6efd 100%);
border: 0;
box-shadow: 0 4px 12px rgba(25, 137, 250, 0.3);
}
</style>
<!--#
}
#-->
@@ -0,0 +1,407 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<div class="vote-page">
<div class="vote-content">
<div class="vote-header-card">
<h2 class="vote-title">{{ voteInfo.name }}</h2>
<div v-if="voteTicketName" class="vote-type-text">{{ voteTicketName }}</div>
<div class="vote-meta">
<div class="meta-item">
<van-icon name="calendar-o" size="14"></van-icon>
<span>{{ sessionName }}</span>
</div>
<div class="meta-divider"></div>
<div class="meta-item">
<van-icon name="clock-o" size="14"></van-icon>
<span>{{ timeName }}</span>
</div>
</div>
</div>
<div v-if="voteItems.length > 0">
<div v-if="voteItems.length === 1" class="vote-card-mode">
<div class="vote-item-card">
<div class="card-header">
<span class="item-name">{{ voteItems[0].itemName }}</span>
</div>
<div class="card-options">
<label v-for="option in voteItems[0].options" :key="option" class="option-label readonly-option">
<input type="radio" disabled :checked="selectedVotes[0] === option">
<span class="option-text">{{ option }}</span>
</label>
</div>
</div>
</div>
<div v-else class="vote-table-container">
<table class="vote-table">
<thead>
<tr>
<th class="col-name">{{ voteInfo.votingContentItemHeader || '表决事项' }}</th>
<th class="col-evaluate" colspan="2">{{ voteInfo.votingContentOptionHeader || '表决内容' }}</th>
</tr>
</thead>
<tbody>
<template v-for="(item, index) in voteItems">
<tr v-for="rowIndex in getRowCount(item)" :key="'row-' + index + '-' + rowIndex">
<td v-if="rowIndex === 1" class="col-name" :rowspan="getRowCount(item)">{{ item.itemName }}</td>
<td class="col-option" v-for="optIndex in 2" :key="'opt-' + optIndex">
<template v-if="(rowIndex - 1) * 2 + optIndex - 1 < item.options.length">
<label class="radio-label readonly-option">
<input
type="radio"
disabled
:checked="selectedVotes[index] === item.options[(rowIndex - 1) * 2 + optIndex - 1]">
<span class="radio-text">{{ item.options[(rowIndex - 1) * 2 + optIndex - 1] }}</span>
</label>
</template>
<span v-else class="empty-placeholder"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<div v-if="voteInfo.description" class="vote-description">
<div class="description-header">
<van-icon name="info-o" size="16"></van-icon>
<span>投票说明</span>
</div>
<div class="description-content">
<p v-for="(line, index) in descriptionLines" :key="index" class="description-line">{{ line }}</p>
</div>
</div>
<van-empty v-if="voteItems.length === 0" description="暂无投票结果"></van-empty>
</div>
<div class="vote-footer">
<van-button type="primary" block disabled>已完成投票</van-button>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
voteInfo: {},
voteItems: [],
selectedVotes: {},
sessionName: "",
timeName: ""
}
},
computed: {
voteTicketName() {
const typeName = this.voteInfo.typeName || ""
if (!typeName) return ""
return typeName.indexOf("票") > -1 ? typeName : typeName + "票"
},
descriptionLines() {
if (!this.voteInfo.description) return []
return String(this.voteInfo.description).split("\n").filter((line) => line.trim() !== "")
}
},
methods: {
initVoteInfo() {
const cached = window.localStorage.getItem("voteInfo")
this.sessionName = window.localStorage.getItem("sessionName") || ""
this.timeName = window.localStorage.getItem("timeName") || ""
if (!cached) return
try {
this.voteInfo = JSON.parse(cached) || {}
} catch (e) {
this.voteInfo = {}
}
this.sessionName = this.sessionName || this.voteInfo.sessionName || ""
this.timeName = this.timeName || this.voteInfo.timeName || ""
this.parseVoteItems()
this.parseVoteResult()
},
parseVoteItems() {
if (!this.voteInfo.votingContent) return
let content = this.voteInfo.votingContent
try {
if (typeof content === "string") content = JSON.parse(content)
} catch (e) {
this.voteItems = []
return
}
const list = Array.isArray(content) ? content : (content.items || [content])
this.voteItems = list.map((item, index) => {
let options = item.options || []
if (item.selections && typeof item.selections === "object") {
options = Object.keys(item.selections)
}
if (!Array.isArray(options) || options.length === 0) {
options = ["赞成", "不赞成", "弃权"]
}
return {
itemName: item.itemName || item.item || item.name || item.title || ("投票项" + (index + 1)),
options: options.map((option) => typeof option === "string" ? option : (option.label || option.name || option.value))
}
})
},
parseVoteResult() {
let result = this.voteInfo.voteResult || []
try {
if (typeof result === "string") result = JSON.parse(result || "[]")
} catch (e) {
result = []
}
const resultMap = {}
if (Array.isArray(result)) {
result.forEach((item) => {
const name = item.item || item.itemName || item.name || item.title
const value = item.result || item.vote || item.value
if (name) resultMap[name] = value
})
} else if (result && typeof result === "object") {
Object.keys(result).forEach((key) => resultMap[key] = result[key])
}
this.selectedVotes = {}
this.voteItems.forEach((item, index) => {
this.$set(this.selectedVotes, index, resultMap[item.itemName] || "")
})
},
getRowCount(item) {
return Math.ceil(item.options.length / 2)
}
},
created() {
this.initVoteInfo()
}
})
</script>
<style id="style-congress-voted-h5">
.vote-page {
min-height: 100vh;
background: #f5f7fa;
display: flex;
flex-direction: column;
}
.vote-content {
flex: 1;
overflow-y: auto;
padding: 16px 16px 96px;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
}
.vote-header-card {
background: #fff;
margin-bottom: 16px;
padding: 20px;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
border: 1px solid rgba(0, 0, 0, 0.03);
}
.vote-title {
font-size: 18px;
font-weight: 600;
color: #323233;
margin: 0 0 8px;
line-height: 1.4;
text-align: center;
}
.vote-type-text {
width: 100%;
font-size: 14px;
color: #969799;
margin-bottom: 12px;
line-height: 1.5;
text-align: center;
}
.vote-meta {
display: flex;
align-items: center;
gap: 12px;
padding-top: 8px;
border-top: 1px solid #ebedf0;
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #646566;
font-weight: 500;
}
.meta-item .van-icon {
color: #1989fa;
}
.meta-divider {
width: 1px;
height: 16px;
background: #e5e5e5;
}
.vote-table-container {
width: 100%;
overflow: hidden;
margin-bottom: 16px;
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
border: 1px solid #000;
}
.vote-table {
width: 100%;
border-collapse: collapse;
background: #fff;
}
.vote-table th {
background: linear-gradient(135deg, #f0f5ff 0%, #e6f0ff 100%);
border: 1px solid #000;
padding: 14px 10px;
font-weight: 600;
font-size: 14px;
color: #1989fa;
text-align: center;
}
.vote-table td {
border: 1px solid #000;
padding: 12px 10px;
text-align: center;
vertical-align: middle;
font-size: 14px;
color: #646566;
}
.vote-table .col-name {
width: 88px;
min-width: 88px;
background: #f7f8fa;
color: #323233;
font-weight: 500;
}
.radio-label {
display: flex;
align-items: center;
justify-content: flex-start;
white-space: nowrap;
padding-left: 8px;
}
.radio-label input {
width: 16px;
height: 16px;
margin-right: 8px;
accent-color: #1989fa;
opacity: 1;
}
.radio-text {
font-size: 14px;
color: #646566;
}
.vote-card-mode,
.vote-description {
margin-bottom: 16px;
}
.vote-item-card,
.vote-description {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.vote-item-card .card-header {
padding: 18px;
background: linear-gradient(135deg, #f0f5ff 0%, #e6f0ff 100%);
color: #1989fa;
font-weight: 600;
}
.card-options {
padding: 18px;
}
.option-label {
display: flex;
align-items: center;
padding: 12px;
background: #f7f8fa;
border-radius: 10px;
margin-bottom: 10px;
}
.option-label input {
margin-right: 10px;
accent-color: #1989fa;
opacity: 1;
}
.vote-description {
padding: 20px;
border-left: 4px solid #1989fa;
}
.description-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 15px;
font-weight: 600;
color: #1989fa;
}
.description-line {
margin: 0 0 8px;
font-size: 14px;
line-height: 1.8;
color: #646566;
}
.readonly-option {
cursor: default;
}
.vote-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
}
.vote-footer .van-button {
height: 48px;
border-radius: 12px;
font-weight: 600;
font-size: 15px;
color: #fff;
background: #7dbbff;
border: 0;
opacity: 1;
}
</style>
<!--#
}
#-->