feat:浦发项目模块迁移-职代会h5端初始化
This commit is contained in:
@@ -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>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
Reference in New Issue
Block a user