Merge branch 'refs/heads/feature_职代会' into release_20260829

# Conflicts:
#	src/main/resources/views/layouts/platform_h5.html
This commit is contained in:
2026-08-29 08:48:32 +08:00
48 changed files with 8172 additions and 1 deletions
@@ -261,7 +261,7 @@
// PJAX 替换 #container 时可能已经移除旧样式节点,清理前需兼容节点脱离 DOM 的情况。
if (window.customStyleList) {
window.customStyleList.forEach((style) => {
if (style.parentNode) {
if (style && style.parentNode) {
style.parentNode.removeChild(style)
}
})
@@ -0,0 +1,370 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="届次">
<el-select v-model="pageForm.sessionId" clearable filterable placeholder="请选择届次">
<el-option v-for="item in sessionOptions" :key="item.id"
:label="item.fullName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="会议名称">
<el-input v-model.trim="pageForm.name" clearable placeholder="请输入会议名称"></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="投票管理">
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd">新增</el-button>
</table-tool>
<el-table :data="tableData" v-loading="tableLoading" class="vi-table">
<el-table-column type="index" :index="indexMethod" label="序号" width="70" align="center"></el-table-column>
<el-table-column prop="name" label="会议名称" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column label="届次" min-width="220">
<template slot-scope="{row}">{{row.sessionName}}</template>
</el-table-column>
<el-table-column prop="typeName" label="投票类型" min-width="130"></el-table-column>
<el-table-column prop="materialsName" label="投票资料" min-width="180" show-overflow-tooltip>
<template slot-scope="{row}">
<el-link v-if="row.fileUrl" type="primary" :href="row.fileUrl" target="_blank">
{{row.materialsName}}
</el-link>
<span v-else>{{row.materialsName || "-"}}</span>
</template>
</el-table-column>
<el-table-column label="投票时间" min-width="300">
<template slot-scope="{row}">
{{formatDate(row.votingStartTime)}} 至 {{formatDate(row.votingEndTime)}}
</template>
</el-table-column>
<el-table-column prop="voteCount" label="已投人数" width="90" align="center"></el-table-column>
<el-table-column label="操作" width="160" fixed="right" align="center">
<template slot-scope="{row}">
<el-button type="text" :disabled="row.voteCount > 0" @click="openEdit(row.id)">编辑</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" class="text-danger" :disabled="row.voteCount > 0"
@click="doDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog :title="formData.id ? '编辑投票' : '新增投票'" :visible.sync="dialogFormVisible"
width="80%" :close-on-click-modal="false">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="届次" prop="sessionId">
<el-select v-model="formData.sessionId" filterable style="width:100%" @change="sessionChanged">
<el-option v-for="item in sessionOptions" :key="item.id"
:label="item.fullName" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="投票类型" prop="typeId">
<el-select v-model="formData.typeId" filterable style="width:100%" @change="typeChanged">
<el-option v-for="item in voteTypeOptions" :key="item.id"
:label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="会议名称" prop="name">
<el-input v-model.trim="formData.name" maxlength="255" show-word-limit></el-input>
</el-form-item>
<el-form-item label="资料" prop="materialsId">
<div style="display:flex; gap:12px; width:100%; align-items:flex-start;">
<el-select v-model="formData.materialsId" filterable style="flex:1;"
:disabled="materialUploadDisabled">
<el-option v-for="item in materialOptions" :key="item.id"
:label="item.name" :value="item.id"></el-option>
</el-select>
<!--<el-upload action="/platform/sys/file/uploadDynamicReturnUrl" :show-file-list="false"
style="width:180px; flex:0 0 180px;"
:disabled="materialUploadDisabled || materialUploadLoading"
:before-upload="beforeMaterialUpload"
:on-success="uploadMaterialSuccess"
:on-error="uploadMaterialError"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx">
<el-button type="primary" icon="el-icon-upload" style="width:180px"
:loading="materialUploadLoading"
:disabled="materialUploadDisabled || materialUploadLoading">上传资料</el-button>
</el-upload>-->
</div>
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="投票开始" prop="votingStartTime">
<el-date-picker v-model="formData.votingStartTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" style="width:100%"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="投票结束" prop="votingEndTime">
<el-date-picker v-model="formData.votingEndTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" style="width:100%"></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="事项表头">
<el-input v-model.trim="formData.votingContentItemHeader" placeholder="表决事项"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="选项表头">
<el-input v-model.trim="formData.votingContentOptionHeader" placeholder="表决内容"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="投票内容" required>
<el-table :data="voteItems" border size="small">
<el-table-column label="表决事项" min-width="230">
<template slot-scope="{row}">
<el-input v-model.trim="row.itemName" maxlength="255" placeholder="请输入表决事项"></el-input>
</template>
</el-table-column>
<el-table-column label="表决选项(逗号分隔)" min-width="330">
<template slot-scope="{row}">
<el-input v-model.trim="row.optionsText" placeholder="赞成,不赞成,弃权"></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template slot-scope="scope">
<el-button type="text" class="text-danger" @click="removeVoteItem(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-button class="mt10" size="small" icon="el-icon-plus" @click="addVoteItem">添加表决事项</el-button>
</el-form-item>
<el-form-item label="投票说明">
<el-input v-model.trim="formData.description" type="textarea" :rows="3"
maxlength="1000" show-word-limit></el-input>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="dialogFormVisible=false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="doSave">确定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const validateEnd = (rule, value, callback) => {
if (!value) return callback(new Error("请选择投票结束时间"))
if (this.formData.votingStartTime && value <= this.formData.votingStartTime) {
return callback(new Error("投票结束时间必须晚于开始时间"))
}
callback()
}
return {
pageDataUrl: "/platform/congress/materials/issue/pageData",
sessionOptions: [],
typeOptions: [],
materialOptions: [],
materialUploadLoading: false,
materialLoadSeq: 0,
voteItems: [],
pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, sessionId: "", name: ""},
formData: {},
formRules: {
sessionId: [{required: true, message: "请选择届次", trigger: "change"}],
typeId: [{required: true, message: "请选择投票类型", trigger: "change"}],
name: [{required: true, message: "请输入会议名称", trigger: "blur"}],
materialsId: [{required: true, message: "请选择资料", trigger: "change"}],
votingStartTime: [{required: true, message: "请选择投票开始时间", trigger: "change"}],
votingEndTime: [{required: true, validator: validateEnd, trigger: "change"}]
}
}
},
computed: {
voteTypeOptions() {
return this.typeOptions.filter(v => Number(v.isVote) === 1)
},
materialUploadDisabled() {
return !this.formData.sessionId || !this.formData.typeId
}
},
methods: {
formatDate(value) {
return value ? this.$moment(value).format("YYYY-MM-DD HH:mm:ss") : "-"
},
async loadOptions() {
const [sessionsRes, typesRes] = await Promise.all([
this.$axios.post("/platform/congress/materials/issue/sessions"),
this.$axios.post("/platform/congress/materials/issue/types")
])
if (sessionsRes.code === 0) this.sessionOptions = sessionsRes.data || []
if (typesRes.code === 0) this.typeOptions = typesRes.data || []
},
async loadMaterials() {
const seq = ++this.materialLoadSeq
this.materialOptions = []
if (!this.formData.sessionId || !this.formData.typeId) return
const res = await this.$axios.post("/platform/congress/materials/issue/materials", {
sessionId: this.formData.sessionId,
typeId: this.formData.typeId
})
if (seq === this.materialLoadSeq && res.code === 0) this.materialOptions = res.data || []
},
getUploadedUrl(res) {
if (!res) return ""
if (typeof res === "string") return res
return res.data || res.url || res.fileUrl || res.path || res.filePath || ""
},
fileNameWithoutExt(name) {
return name ? String(name).replace(/\.[^/.]+$/, "") : ""
},
fileExtension(name) {
const fileName = String(name || "")
const index = fileName.lastIndexOf(".")
return index >= 0 ? fileName.substring(index + 1).toLowerCase() : ""
},
beforeMaterialUpload(file) {
if (this.materialUploadDisabled) {
this.$message.warning("请先选择届次和投票类型")
return false
}
this.materialUploadLoading = true
return true
},
async uploadMaterialSuccess(response, file) {
const fileUrl = this.getUploadedUrl(response)
if (!fileUrl) {
this.materialUploadLoading = false
this.$message.error(response && response.msg ? response.msg : "上传资料失败")
return
}
const fileName = file.name || "附件"
try {
const res = await this.$axios.post("/platform/congress/materials/issue/uploadMaterial", {
sessionId: this.formData.sessionId,
typeId: this.formData.typeId,
name: this.fileNameWithoutExt(fileName),
fileName: fileName,
fileUrl: fileUrl,
fileType: this.fileExtension(fileName),
fileSize: file.raw && file.raw.size ? file.raw.size : ""
})
if (res.code === 0) {
await this.loadMaterials()
this.formData.materialsId = res.data && res.data.id ? res.data.id : res.data
this.$message.success("上传资料成功")
} else {
this.$message.error(res.msg)
}
} finally {
this.materialUploadLoading = false
}
},
uploadMaterialError() {
this.materialUploadLoading = false
this.$message.error("上传资料失败")
},
async sessionChanged() {
this.formData.materialsId = ""
this.materialOptions = []
await this.loadMaterials()
},
async typeChanged() {
this.formData.materialsId = ""
this.materialOptions = []
await this.loadMaterials()
},
addVoteItem() {
this.voteItems.push({itemName: "", optionsText: "赞成,不赞成,弃权"})
},
removeVoteItem(index) {
this.voteItems.splice(index, 1)
},
openAdd() {
this.formData = {
name: "", sessionId: "", typeId: "", materialsId: "",
votingStartTime: "", votingEndTime: "", description: "",
votingContentItemHeader: "表决事项", votingContentOptionHeader: "表决内容"
}
this.voteItems = [{itemName: "", optionsText: "赞成,不赞成,弃权"}]
this.materialOptions = []
this.dialogFormVisible = true
this.$nextTick(() => this.$refs.formRef && this.$refs.formRef.clearValidate())
},
async openEdit(id) {
const res = await this.$axios.post("/platform/congress/materials/issue/get", {id})
if (res.code !== 0) return
const data = res.data || {}
data.votingStartTime = this.formatDate(data.votingStartTime)
data.votingEndTime = this.formatDate(data.votingEndTime)
this.formData = data
try {
this.voteItems = JSON.parse(data.votingContent || "[]").map(item => ({
itemName: item.itemName || item.item || "",
optionsText: (item.options || []).join(",")
}))
} catch (e) {
this.voteItems = []
}
await this.loadMaterials()
this.dialogFormVisible = true
},
buildVotingContent() {
const items = this.voteItems.map(item => ({
itemName: (item.itemName || "").trim(),
options: (item.optionsText || "").split(/[,,]/)
.map(v => v.trim()).filter(Boolean)
}))
if (!items.length || items.some(item => !item.itemName || !item.options.length)) {
this.$message.warning("请完整填写每个表决事项及表决选项")
return null
}
return JSON.stringify(items)
},
doSave() {
this.$refs.formRef.validate(async valid => {
if (!valid) return
const votingContent = this.buildVotingContent()
if (!votingContent) return
this.submitLoading = true
try {
const data = Object.assign({}, this.formData, {votingContent})
const res = await this.$axios.post("/platform/congress/materials/issue/save", data)
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success("保存成功")
this.pageData()
} else {
this.$message.error(res.msg)
}
} finally {
this.submitLoading = false
}
})
},
doDelete(id) {
this.$confirm("确认删除该投票议题吗?", "提示", {type: "warning"}).then(async () => {
const res = await this.$axios.post("/platform/congress/materials/issue/delete", {id})
if (res.code === 0) {
this.$message.success("删除成功")
this.doSearch()
} else {
this.$message.error(res.msg)
}
})
}
},
async created() {
await this.loadOptions()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,236 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="届次">
<el-select v-model="pageForm.sessionId" clearable filterable placeholder="请选择届次">
<el-option v-for="item in sessionOptions" :key="item.id"
:label="item.fullName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="资料类型">
<el-select v-model="pageForm.typeId" clearable filterable placeholder="请选择资料类型">
<el-option v-for="item in typeOptions" :key="item.id"
:label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="资料名称">
<el-input v-model.trim="pageForm.name" clearable placeholder="请输入资料名称"></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="资料管理">
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd">新增</el-button>
</table-tool>
<el-table :data="tableData" v-loading="tableLoading" class="vi-table">
<el-table-column type="index" :index="indexMethod" label="序号" width="70" align="center"></el-table-column>
<el-table-column prop="name" label="资料名称" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="typeName" label="资料类型" min-width="140"></el-table-column>
<el-table-column label="届次" min-width="220">
<template slot-scope="{row}">{{row.sessionName}}</template>
</el-table-column>
<el-table-column prop="sortNo" label="排序号" width="90" align="center"></el-table-column>
<el-table-column prop="fileName" label="文件" min-width="180" show-overflow-tooltip>
<template slot-scope="{row}">
<el-link v-if="row.fileUrl" type="primary" :href="row.fileUrl" target="_blank">
{{row.fileName || row.name}}
</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right" align="center">
<template slot-scope="{row}">
<el-button type="text" @click="openEdit(row.id)">编辑</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" class="text-danger" @click="doDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog :title="formData.id ? '编辑资料' : '新增资料'" :visible.sync="dialogFormVisible"
width="65%" :close-on-click-modal="false">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="届次" prop="sessionId">
<el-select v-model="formData.sessionId" filterable style="width:100%">
<el-option v-for="item in sessionOptions" :key="item.id"
:label="item.fullName" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="资料类型" prop="typeId">
<el-select v-model="formData.typeId" filterable style="width:100%">
<el-option v-for="item in typeOptions" :key="item.id"
:label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="资料名称" prop="name">
<el-input v-model.trim="formData.name" maxlength="255" show-word-limit
placeholder="不需要填写,默认是上传文件名称"></el-input>
</el-form-item>
<el-form-item label="排序号" prop="sortNo">
<el-input-number v-model="formData.sortNo" :min="0" :max="9999" style="width:100%"></el-input-number>
</el-form-item>
<el-form-item label="上传文件" prop="files">
<file-upload :value.sync="formData.files" :upload_number="1"
upload_result_type="url" upload_result_category="array"
complete_result accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"></file-upload>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="dialogFormVisible=false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="doSave">确定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
pageDataUrl: "/platform/congress/materials/manage/pageData",
sessionOptions: [],
typeOptions: [],
pageForm: {
pageNumber: 1, pageSize: 10, totalCount: 0,
sessionId: "", typeId: "", name: ""
},
formData: {},
formRules: {
sessionId: [{required: true, message: "请选择届次", trigger: "change"}],
typeId: [{required: true, message: "请选择资料类型", trigger: "change"}],
name: [{required: true, message: "请输入资料名称", trigger: "blur"}],
files: [{required: true, message: "请上传文件", trigger: "change"}]
}
}
},
methods: {
async loadOptions() {
const [sessionsRes, typesRes] = await Promise.all([
this.$axios.post("/platform/congress/materials/manage/sessions"),
this.$axios.post("/platform/congress/materials/manage/types")
])
if (sessionsRes.code === 0) this.sessionOptions = sessionsRes.data || []
if (typesRes.code === 0) this.typeOptions = typesRes.data || []
},
openAdd() {
this.formData = {
name: "", sessionId: "", typeId: "",
sortNo: 0, fileName: "", fileUrl: "", fileType: "", fileSize: "", files: []
}
this.dialogFormVisible = true
this.$nextTick(() => this.$refs.formRef && this.$refs.formRef.clearValidate())
},
async openEdit(id) {
const res = await this.$axios.post("/platform/congress/materials/manage/get", {id})
if (res.code === 0) {
const data = res.data || {}
this.formData = Object.assign({}, data, {
files: this.buildUploadFiles(data)
})
this.dialogFormVisible = true
}
},
buildUploadFiles(data) {
if (!data || !data.fileUrl) return []
return [{
name: data.fileName || this.fileNameFromUrl(data.fileUrl) || data.name || "附件",
url: data.fileUrl,
status: "success",
response: {
code: 0,
data: data.fileUrl
}
}]
},
fileNameFromUrl(url) {
if (!url) return ""
const path = String(url).split("?")[0]
const name = path.substring(path.lastIndexOf("/") + 1)
try {
return decodeURIComponent(name)
} catch (e) {
return name
}
},
fileNameWithoutExt(name) {
return name ? String(name).replace(/\.[^/.]+$/, "") : ""
},
fileExtension(name) {
const fileName = String(name || "")
const index = fileName.lastIndexOf(".")
return index >= 0 ? fileName.substring(index + 1).toLowerCase() : ""
},
getUploadFileUrl(file) {
return file && file.response && file.response.data ? file.response.data : file && (file.url || file.data) || ""
},
syncUploadFiles(files) {
const file = Array.isArray(files) && files.length > 0 ? files[files.length - 1] : null
if (!file) {
this.$set(this.formData, "fileName", "")
this.$set(this.formData, "fileUrl", "")
this.$set(this.formData, "fileType", "")
this.$set(this.formData, "fileSize", "")
return
}
const fileName = file.name || this.fileNameFromUrl(this.getUploadFileUrl(file))
this.$set(this.formData, "fileName", fileName)
this.$set(this.formData, "fileUrl", this.getUploadFileUrl(file))
this.$set(this.formData, "fileType", this.fileExtension(fileName))
this.$set(this.formData, "fileSize", file.raw && file.raw.size ? file.raw.size : file.size || "")
if (!this.formData.name && fileName) {
this.$set(this.formData, "name", this.fileNameWithoutExt(fileName))
}
},
doSave() {
this.syncUploadFiles(this.formData.files)
this.$refs.formRef.validate(async valid => {
if (!valid) return
this.submitLoading = true
try {
const res = await this.$axios.post("/platform/congress/materials/manage/save", this.formData)
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success("保存成功")
this.pageData()
} else {
this.$message.error(res.msg)
}
} finally {
this.submitLoading = false
}
})
},
doDelete(id) {
this.$confirm("确认删除该会议资料吗?", "提示", {type: "warning"}).then(async () => {
const res = await this.$axios.post("/platform/congress/materials/manage/delete", {id})
if (res.code === 0) {
this.$message.success("删除成功")
this.doSearch()
} else {
this.$message.error(res.msg)
}
})
}
},
watch: {
"formData.files": {
handler(files) {
this.syncUploadFiles(files)
},
deep: true
}
},
async created() {
await this.loadOptions()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,170 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="届次">
<el-select v-model="pageForm.sessionId" clearable filterable placeholder="请选择届次">
<el-option v-for="item in sessionOptions" :key="item.id"
:label="item.fullName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="会议名称">
<el-input v-model.trim="pageForm.name" clearable placeholder="请输入会议名称"></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="投票统计"></table-tool>
<el-table :data="tableData" v-loading="tableLoading" class="vi-table">
<el-table-column type="index" :index="indexMethod" label="序号" width="70" align="center"></el-table-column>
<el-table-column prop="name" label="会议名称" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column prop="typeName" label="投票类型" min-width="130"></el-table-column>
<el-table-column prop="materialsName" label="资料" min-width="180" show-overflow-tooltip>
<template slot-scope="{row}">
<el-link v-if="row.fileUrl" type="primary" :href="row.fileUrl" target="_blank">
{{row.materialsName}}
</el-link>
<span v-else>{{row.materialsName || "-"}}</span>
</template>
</el-table-column>
<el-table-column prop="voteCount" label="投票人数" width="100" align="center"></el-table-column>
<el-table-column label="届次" min-width="220">
<template slot-scope="{row}">{{row.sessionName}}</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template slot-scope="{row}">
<el-button type="text" @click="openDetail(row)">查看统计</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-drawer title="投票明细" :visible.sync="detailVisible" size="72%" :with-header="true">
<div style="padding:0 24px 24px">
<el-descriptions v-if="currentIssue.id" :column="2" border>
<el-descriptions-item label="会议名称">{{currentIssue.name}}</el-descriptions-item>
<el-descriptions-item label="届次">{{currentIssue.sessionName}}</el-descriptions-item>
<el-descriptions-item label="投票类型">{{currentIssue.typeName}}</el-descriptions-item>
<el-descriptions-item label="投票人数">{{currentIssue.voteCount}}</el-descriptions-item>
</el-descriptions>
<el-tabs v-model="detailTab" class="mt10">
<el-tab-pane label="投票统计" name="summary">
<el-table :data="summaryData" border v-loading="detailLoading">
<el-table-column type="index" label="序号" width="70" align="center"></el-table-column>
<el-table-column prop="itemName" label="投票项目/人员" min-width="200"></el-table-column>
<el-table-column prop="option" label="投票选项" min-width="160"></el-table-column>
<el-table-column prop="count" label="票数" width="100" align="center"></el-table-column>
<el-table-column label="占比" width="120" align="center">
<template slot-scope="{row}">{{percent(row.count, row.itemTotal)}}</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="投票记录" name="records">
<el-table :data="recordData" border v-loading="detailLoading">
<el-table-column type="index" label="序号" width="70" align="center"></el-table-column>
<el-table-column prop="emplid" label="工号" min-width="110"></el-table-column>
<el-table-column prop="name" label="代表" min-width="100"></el-table-column>
<el-table-column prop="unionName" label="工会" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="delegationName" label="代表团" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column label="投票结果" min-width="260" show-overflow-tooltip>
<template slot-scope="{row}">{{voteResultText(row.voteResult)}}</template>
</el-table-column>
<el-table-column label="投票时间" min-width="170">
<template slot-scope="{row}">{{formatDate(row.createTime)}}</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</el-drawer>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
pageDataUrl: "/platform/congress/materials/statistical/pageData",
sessionOptions: [],
detailVisible: false,
detailLoading: false,
detailTab: "summary",
currentIssue: {},
recordData: [],
summaryData: [],
pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, sessionId: "", name: ""}
}
},
methods: {
formatDate(value) {
return value ? this.$moment(value).format("YYYY-MM-DD HH:mm:ss") : "-"
},
percent(count, total) {
return total ? (count * 100 / total).toFixed(2) + "%" : "0.00%"
},
parseVoteResult(value) {
if (!value) return []
try {
const data = typeof value === "string" ? JSON.parse(value) : value
return Array.isArray(data) ? data : []
} catch (e) {
return []
}
},
voteResultText(value) {
return this.parseVoteResult(value).map(item => {
return (item.item || item.itemName || "投票项") + ":" +
(item.selected || item.option || item.value || "-")
}).join(";")
},
buildSummary(records) {
const counter = {}
records.forEach(record => {
this.parseVoteResult(record.voteResult).forEach(item => {
const itemName = item.item || item.itemName || "投票项"
const option = item.selected || item.option || item.value || "-"
if (!counter[itemName]) counter[itemName] = {}
counter[itemName][option] = (counter[itemName][option] || 0) + 1
})
})
const rows = []
Object.keys(counter).forEach(itemName => {
const itemTotal = Object.values(counter[itemName]).reduce((sum, count) => sum + count, 0)
Object.keys(counter[itemName]).forEach(option => {
rows.push({itemName, option, count: counter[itemName][option], itemTotal})
})
})
return rows
},
async loadOptions() {
const res = await this.$axios.post("/platform/congress/materials/statistical/sessions")
if (res.code === 0) this.sessionOptions = res.data || []
},
async openDetail(row) {
this.currentIssue = row
this.detailVisible = true
this.detailLoading = true
this.detailTab = "summary"
try {
const res = await this.$axios.post("/platform/congress/materials/statistical/records", {issueId: row.id})
if (res.code === 0) {
this.recordData = res.data || []
this.summaryData = this.buildSummary(this.recordData)
}
} finally {
this.detailLoading = false
}
}
},
async created() {
await this.loadOptions()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,130 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="资料类型">
<el-input v-model.trim="pageForm.name" clearable placeholder="请输入资料类型"></el-input>
</search-item>
<search-item label="是否投票">
<el-select v-model="pageForm.isVote" clearable placeholder="请选择">
<el-option label="是" value="1"></el-option>
<el-option label="否" value="0"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="资料类型">
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd">新增</el-button>
</table-tool>
<el-table :data="tableData" v-loading="tableLoading" class="vi-table">
<el-table-column type="index" :index="indexMethod" label="序号" width="70" align="center"></el-table-column>
<el-table-column prop="name" label="资料类型" min-width="220"></el-table-column>
<el-table-column label="是否投票" width="120" align="center">
<template slot-scope="{row}">
<el-tag :type="row.isVote ? 'success' : 'info'" size="small">{{row.isVote ? "是" : "否"}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="sortNo" label="排序号" width="120" align="center"></el-table-column>
<el-table-column label="操作" width="160" align="center">
<template slot-scope="{row}">
<el-button type="text" @click="openEdit(row.id)">编辑</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" class="text-danger" @click="doDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog :title="formData.id ? '编辑资料类型' : '新增资料类型'"
:visible.sync="dialogFormVisible" width="50%" :close-on-click-modal="false">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="资料类型" prop="name">
<el-input v-model.trim="formData.name" maxlength="100" show-word-limit></el-input>
</el-form-item>
<el-form-item label="是否投票" prop="isVote">
<el-radio-group v-model="formData.isVote">
<el-radio :label="true">是</el-radio>
<el-radio :label="false">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序号" prop="sortNo">
<el-input-number v-model="formData.sortNo" :min="0" :max="9999" style="width:100%"></el-input-number>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="dialogFormVisible=false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="doSave">确定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
pageDataUrl: "/platform/congress/materials/type/pageData",
pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, name: "", isVote: ""},
formData: {},
formRules: {
name: [{required: true, message: "请输入资料类型", trigger: "blur"}],
sortNo: [{required: true, message: "请输入排序号", trigger: "change"}]
}
}
},
methods: {
openAdd() {
this.formData = {name: "", isVote: false, sortNo: 0}
this.dialogFormVisible = true
this.$nextTick(() => this.$refs.formRef && this.$refs.formRef.clearValidate())
},
async openEdit(id) {
const res = await this.$axios.post("/platform/congress/materials/type/get", {id})
if (res.code === 0) {
this.formData = res.data || {}
this.formData.isVote = !!this.formData.isVote
this.dialogFormVisible = true
}
},
doSave() {
this.$refs.formRef.validate(async valid => {
if (!valid) return
this.submitLoading = true
try {
const res = await this.$axios.post("/platform/congress/materials/type/save", this.formData)
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success("保存成功")
this.pageData()
} else {
this.$message.error(res.msg)
}
} finally {
this.submitLoading = false
}
})
},
doDelete(id) {
this.$confirm("确认删除该资料类型吗?", "提示", {type: "warning"}).then(async () => {
const res = await this.$axios.post("/platform/congress/materials/type/delete", {id})
if (res.code === 0) {
this.$message.success("删除成功")
this.doSearch()
} else {
this.$message.error(res.msg)
}
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,84 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="届次">
<el-select v-model="pageForm.sessionId" clearable filterable placeholder="请选择届次" @change="loadMeetings">
<el-option v-for="item in sessionOptions" :key="item.id"
:label="item.fullName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="会议名称">
<el-select v-model="pageForm.meetingId" clearable filterable :disabled="!pageForm.sessionId">
<el-option v-for="item in meetingOptions" :key="item.id"
:label="item.meetingName" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="关键字">
<el-input v-model.trim="pageForm.keyword" clearable placeholder="请输入工号或姓名"></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="签到记录"></table-tool>
<el-table :data="tableData" v-loading="tableLoading" class="vi-table">
<el-table-column type="index" :index="indexMethod" label="序号" width="70" align="center"></el-table-column>
<el-table-column prop="emplid" label="工号" min-width="120"></el-table-column>
<el-table-column prop="name" label="姓名" min-width="100"></el-table-column>
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip></el-table-column>
<el-table-column label="届次" min-width="220">
<template slot-scope="{row}">{{row.sessionName}}</template>
</el-table-column>
<el-table-column prop="delegationName" label="代表团" min-width="160" show-overflow-tooltip></el-table-column>
<el-table-column label="签到状态" width="100" align="center">
<template><el-tag type="success" size="small">已签到</el-tag></template>
</el-table-column>
<el-table-column label="签到时间" min-width="170">
<template slot-scope="{row}">{{formatDate(row.signInTime)}}</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
pageDataUrl: "/platform/congress/record/pageData",
sessionOptions: [],
meetingOptions: [],
pageForm: {
pageNumber: 1, pageSize: 10, totalCount: 0,
sessionId: "", meetingId: "", keyword: ""
}
}
},
methods: {
formatDate(value) {
return value ? this.$moment(value).format("YYYY-MM-DD HH:mm:ss") : "-"
},
async loadOptions() {
const res = await this.$axios.post("/platform/congress/record/sessions")
if (res.code === 0) this.sessionOptions = res.data || []
},
async loadMeetings() {
this.pageForm.meetingId = ""
this.meetingOptions = []
if (!this.pageForm.sessionId) return
const res = await this.$axios.post("/platform/congress/record/meetings", {sessionId: this.pageForm.sessionId})
if (res.code === 0) this.meetingOptions = res.data || []
}
},
async created() {
await this.loadOptions()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,162 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="届次">
<el-select v-model="pageForm.sessionId" clearable filterable placeholder="请选择届次">
<el-option v-for="item in sessionOptions" :key="item.id"
:label="item.fullName" :value="item.id"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="会议管理">
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd">新增</el-button>
</table-tool>
<el-table :data="tableData" v-loading="tableLoading" class="vi-table">
<el-table-column type="index" :index="indexMethod" label="序号" width="70" align="center"></el-table-column>
<el-table-column prop="meetingName" label="会议名称" min-width="220" show-overflow-tooltip></el-table-column>
<el-table-column label="届次" min-width="220">
<template slot-scope="{row}">{{row.sessionName}}</template>
</el-table-column>
<el-table-column label="签到时间" min-width="300">
<template slot-scope="{row}">
{{formatDate(row.signInStartTime)}} 至 {{formatDate(row.signInEndTime)}}
</template>
</el-table-column>
<el-table-column prop="signInCount" label="签到人数" width="100" align="center"></el-table-column>
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="160" fixed="right" align="center">
<template slot-scope="{row}">
<el-button type="text" @click="openEdit(row.id)">编辑</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" class="text-danger" @click="doDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog :title="formData.id ? '编辑会议' : '新增会议'" :visible.sync="dialogFormVisible"
width="65%" :close-on-click-modal="false">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="届次" prop="sessionId">
<el-select v-model="formData.sessionId" filterable style="width:100%">
<el-option v-for="item in sessionOptions" :key="item.id"
:label="item.fullName" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="会议名称" prop="meetingName">
<el-input v-model.trim="formData.meetingName" maxlength="255" show-word-limit></el-input>
</el-form-item>
<el-form-item label="签到开始" prop="signInStartTime">
<el-date-picker v-model="formData.signInStartTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" style="width:100%"></el-date-picker>
</el-form-item>
<el-form-item label="签到结束" prop="signInEndTime">
<el-date-picker v-model="formData.signInEndTime" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" style="width:100%"></el-date-picker>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model.trim="formData.remark" type="textarea" :rows="3"
maxlength="1000" show-word-limit></el-input>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="dialogFormVisible=false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="doSave">确定</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
const validateEnd = (rule, value, callback) => {
if (!value) return callback(new Error("请选择签到结束时间"))
if (this.formData.signInStartTime && value <= this.formData.signInStartTime) {
return callback(new Error("签到结束时间必须晚于开始时间"))
}
callback()
}
return {
pageDataUrl: "/platform/congress/signIn/pageData",
sessionOptions: [],
pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, sessionId: ""},
formData: {},
formRules: {
sessionId: [{required: true, message: "请选择届次", trigger: "change"}],
meetingName: [{required: true, message: "请输入会议名称", trigger: "blur"}],
signInStartTime: [{required: true, message: "请选择签到开始时间", trigger: "change"}],
signInEndTime: [{required: true, validator: validateEnd, trigger: "change"}]
}
}
},
methods: {
formatDate(value) {
return value ? this.$moment(value).format("YYYY-MM-DD HH:mm:ss") : "-"
},
async loadOptions() {
const res = await this.$axios.post("/platform/congress/signIn/sessions")
if (res.code === 0) this.sessionOptions = res.data || []
},
openAdd() {
this.formData = {meetingName: "", sessionId: "", remark: ""}
this.dialogFormVisible = true
this.$nextTick(() => this.$refs.formRef && this.$refs.formRef.clearValidate())
},
async openEdit(id) {
const res = await this.$axios.post("/platform/congress/signIn/get", {id})
if (res.code === 0) {
const data = res.data || {}
data.signInStartTime = this.formatDate(data.signInStartTime)
data.signInEndTime = this.formatDate(data.signInEndTime)
this.formData = data
this.dialogFormVisible = true
}
},
doSave() {
this.$refs.formRef.validate(async valid => {
if (!valid) return
this.submitLoading = true
try {
const res = await this.$axios.post("/platform/congress/signIn/save", this.formData)
if (res.code === 0) {
this.dialogFormVisible = false
this.$message.success("保存成功")
this.pageData()
} else {
this.$message.error(res.msg)
}
} finally {
this.submitLoading = false
}
})
},
doDelete(id) {
this.$confirm("确认删除该会议签到配置吗?", "提示", {type: "warning"}).then(async () => {
const res = await this.$axios.post("/platform/congress/signIn/delete", {id})
if (res.code === 0) {
this.$message.success("删除成功")
this.doSearch()
} else {
this.$message.error(res.msg)
}
})
}
},
async created() {
await this.loadOptions()
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,460 @@
<!--#
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>
<button v-if="timesList.length > 1" type="button" class="header-switch-btn" @click="showTimePicker = true">
<span>切换届次</span>
<van-icon name="arrow-down" size="12"></van-icon>
</button>
</div>
</div>
<div class="module-list-container">
<div class="modules-wrapper">
<div
v-for="module in moduleList"
:key="module.id"
class="module-card"
:class="resolveTheme(module.name)"
@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>
</template>
<van-popup v-model="showTimePicker" position="bottom" :z-index="3000">
<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: "",
selectedSessionId: "",
selectedTimeText: "",
selectedSessionName: "",
selectedRepId: "",
showTimePicker: false,
moduleList: []
}
},
computed: {
formattedTimeColumns() {
return this.timesList.map((item) => ({
text: item.name,
value: item.timeId || item.id || item.sessionId
}))
}
},
methods: {
async initPage() {
const loading = this.$toast.loading({message: "加载中...", forbidClick: true, duration: 0})
try {
await this.fetchRepTimes()
if (this.timesList.length > 0) {
const initialTimeId = this.initialTimeId()
this.selectTime(this.resolveInitialTime(initialTimeId), !!initialTimeId)
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 || []
}
},
initialTimeId() {
const params = new URLSearchParams(window.location.search)
return params.get("timeId") || params.get("sessionId") || ""
},
resolveInitialTime(timeId) {
return this.timesList.find((item) => this.sameTime(item, timeId)) || this.timesList[0]
},
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: "completed",
desc: "点击进行会议签到"
}].concat(modules)
},
selectTime(item, syncUrl) {
this.selectedTimeId = item.timeId || item.id || item.sessionId || ""
this.selectedSessionId = item.sessionId || this.selectedTimeId
this.selectedTimeText = item.name || ""
this.selectedSessionName = item.sessionName || ""
this.selectedRepId = item.repId || ""
window.localStorage.setItem("congressTimeId", this.selectedTimeId)
window.localStorage.setItem("congressSessionId", this.selectedSessionId)
window.localStorage.setItem("timeName", this.selectedTimeText)
window.localStorage.setItem("sessionName", this.selectedSessionName)
window.localStorage.setItem("repId", this.selectedRepId)
if (syncUrl) {
this.syncSelectedTimeToUrl()
}
},
sameTime(item, timeId) {
if (!item || !timeId) return false
return item.id === timeId || item.timeId === timeId || item.sessionId === timeId
},
syncSelectedTimeToUrl() {
if (!this.selectedTimeId) return
const params = new URLSearchParams(window.location.search)
params.set("timeId", this.selectedTimeId)
const nextUrl = window.location.pathname + "?" + params.toString()
const state = Object.assign({}, window.history.state || {})
state.url = nextUrl
window.history.replaceState(state, document.title, nextUrl)
},
onTimeConfirm(value) {
const selected = this.timesList.find((item) => this.sameTime(item, value.value))
if (selected) {
this.selectTime(selected, true)
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) +
"&sessionId=" + encodeURIComponent(this.selectedSessionId))
return
}
this.$pjaxReplace("/platform/h5/congress/materials/list?moduleId=" + encodeURIComponent(module.id) +
"&typeId=" + encodeURIComponent(module.id) +
"&moduleName=" + encodeURIComponent(module.name) +
"&timeId=" + encodeURIComponent(this.selectedTimeId) +
"&sessionId=" + encodeURIComponent(this.selectedSessionId))
},
resolveIcon(name) {
if (!name) return "orders-o"
if (name.indexOf("会议签到") > -1) return "completed"
if (name.indexOf("评议") > -1) return "star"
if (name.indexOf("议题") > -1) return "todo-list"
if (name.indexOf("资料") > -1) return "notes"
if (name.indexOf("公告") > -1 || name.indexOf("结果") > -1) return "bell"
if (name.indexOf("文件") > -1) return "description"
return "orders"
},
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 ""
},
resolveTheme(name) {
if (!name) return "theme-blue"
if (name.indexOf("会议签到") > -1) return "theme-blue"
if (name.indexOf("文件") > -1) return "theme-blue"
if (name.indexOf("表决公告") > -1) return "theme-green"
if (name.indexOf("议题") > -1) return "theme-purple"
if (name.indexOf("评议") > -1) return "theme-orange module-card-wide"
return "theme-blue"
}
},
created() {
this.initPage()
}
})
</script>
<style id="style-congress-h5">
.congress-page {
height: calc(100vh - 46px);
background: #f4f8ff;
overflow: hidden;
display: flex;
flex-direction: column;
}
.page-header {
min-height: 192px;
background: #1686ff url("${base!}/images/congress/h5-congress-header.png") center center / cover no-repeat;
padding: 0px 15px 50px;
color: #fff;
box-shadow: 0 12px 28px rgba(25, 137, 250, 0.18);
display: flex;
justify-content: flex-start;
align-items: center;
position: relative;
overflow: hidden;
flex-shrink: 0;
}
.session-info-card {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
position: relative;
z-index: 3;
max-width: 50%;
}
.session-title {
font-size: 25px;
font-weight: 700;
line-height: 1.25;
color: #fff;
margin: 0 0 10px;
text-shadow: 0 4px 12px rgba(3, 77, 168, 0.18);
}
.header-switch-btn {
margin-top: 6px;
height: 34px;
padding: 0 14px;
border: 0;
border-radius: 18px;
background: rgba(255, 255, 255, 0.18);
color: #fff;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
line-height: 1;
cursor: pointer;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.36), 0 8px 18px rgba(0, 66, 160, 0.12);
backdrop-filter: blur(8px);
}
.header-switch-btn:active {
background: rgba(255, 255, 255, 0.28);
transform: scale(0.98);
}
.module-list-container {
position: relative;
z-index: 2;
flex: 1;
min-height: 0;
margin: -72px 0 0;
padding: 18px 16px 24px;
background: #fff;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.modules-wrapper {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
}
.module-card {
min-height: 98px;
background: linear-gradient(135deg, #f8fbff 0%, #fff 100%);
border-radius: 14px;
box-shadow: 0 8px 18px rgba(27, 56, 105, 0.06);
overflow: hidden;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(237, 242, 250, 0.88);
position: relative;
}
.module-card-wide {
grid-column: auto;
}
.module-card:active {
transform: scale(0.99);
box-shadow: 0 4px 16px rgba(25, 137, 250, 0.08);
}
.module-header {
min-height: 60px;
padding: 14px;
display: flex;
align-items: center;
gap: 12px;
}
.icon-wrapper {
width: 54px;
height: 54px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
background: #eef5ff;
flex-shrink: 0;
box-shadow: 0 8px 18px rgba(25, 137, 250, 0.1);
color: #1989fa;
}
.module-info {
flex: 1;
min-width: 0;
}
.module-title {
font-size: 16px;
font-weight: 700;
color: #111a3a;
margin: 0 0 6px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.module-desc {
font-size: 12px;
color: #7d89a6;
margin: 0;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow-icon {
width: 26px;
height: 26px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #1989fa;
background: rgba(25, 137, 250, 0.1);
flex-shrink: 0;
transition: all 0.3s ease;
}
.theme-green {
background: linear-gradient(135deg, #f3fff9 0%, #fff 100%);
}
.theme-green .icon-wrapper {
color: #17c979;
background: #eafbf2;
box-shadow: 0 8px 18px rgba(23, 201, 121, 0.12);
}
.theme-green .arrow-icon {
color: #17b977;
background: rgba(23, 201, 121, 0.12);
}
.theme-purple {
background: linear-gradient(135deg, #fbf7ff 0%, #fff 100%);
}
.theme-purple .icon-wrapper {
color: #8756e9;
background: #f1eaff;
box-shadow: 0 8px 18px rgba(135, 86, 233, 0.12);
}
.theme-purple .arrow-icon {
color: #8756e9;
background: rgba(135, 86, 233, 0.12);
}
.theme-orange {
background: linear-gradient(135deg, #fff8ed 0%, #fff 100%);
}
.theme-orange .icon-wrapper {
color: #ff9f1a;
background: #fff2df;
box-shadow: 0 8px 18px rgba(255, 159, 26, 0.12);
}
.theme-orange .arrow-icon {
color: #d9a05b;
background: rgba(255, 159, 26, 0.12);
}
.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,432 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar :title="pageTitle" left-text="返回" left-arrow @click-left="backToCongress" placeholder fixed></van-nav-bar>
<div class="file-list-page">
<van-sticky :offset-top="46">
<div class="file-filter-bar">
<van-search v-model="keyword" placeholder="搜索文件名" shape="round" class="file-search" @input="onKeywordChange"></van-search>
<van-dropdown-menu class="file-filter-menu">
<van-dropdown-item v-model="selectedFileType" :options="fileTypeOptions" @change="resetList"></van-dropdown-item>
<van-dropdown-item v-model="uploadTimeOrder" :options="uploadTimeSortOptions" @change="resetList"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<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 class="card-left">
<div class="icon-wrapper">
<img :src="getFileIconUrl(item)" :alt="resolveFileSuffix(item) || 'file'" class="file-type-icon">
</div>
</div>
<div class="card-content">
<div class="card-header">
<span class="file-name">{{ formatFileName(item.name || item.fileName) }}</span>
<div class="card-actions">
<span v-if="item.hasVoted !== null && item.hasVoted !== undefined" class="vote-status" :class="isVoted(item.hasVoted) ? 'voted' : 'pending'">
<van-icon :name="isVoted(item.hasVoted) ? 'checked' : 'clock-o'" size="12"></van-icon>
{{ isVoted(item.hasVoted) ? '已投票' : '待投票' }}
</span>
<van-icon name="arrow" class="arrow-icon"></van-icon>
</div>
</div>
<div class="file-info">
<div class="info-row">
<van-icon name="description" size="14"></van-icon>
<span class="info-text">文件类型:<span class="info-value">{{ fileTypeText(item) }}</span></span>
</div>
<div v-if="item.uploadTime" class="info-row">
<van-icon name="clock-o" size="14"></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="hasFilter ? '未找到匹配文件' : '暂无文件'"></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: "",
sessionId: "",
keyword: "",
selectedFileType: "",
uploadTimeOrder: "desc",
searchTimer: null,
queryVersion: 0,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0
}
}
},
computed: {
fileTypeOptions() {
return [
{text: "全部类型", value: ""},
{text: "PDF", value: "pdf"},
{text: "Word", value: "doc,docx"},
{text: "Excel", value: "xls,xlsx"},
{text: "PPT", value: "ppt,pptx"},
{text: "图片", value: "jpg,jpeg,png,gif,bmp,webp"},
{text: "压缩包", value: "zip,rar"}
]
},
uploadTimeSortOptions() {
return [
{text: "上传时间:最新", value: "desc"},
{text: "上传时间:最早", value: "asc"}
]
},
hasFilter() {
return !!(this.keyword || this.selectedFileType)
}
},
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") || ""
this.sessionId = params.get("sessionId") || this.timeId
},
backToCongress() {
const timeId = this.timeId || this.sessionId || window.localStorage.getItem("congressTimeId") || ""
this.$pjaxReplace("/platform/h5/congress" + (timeId ? "?timeId=" + encodeURIComponent(timeId) : ""))
},
loginName() {
const user = (this.$store.state && this.$store.state.user) || JSON.parse(window.sessionStorage.getItem("user") || "{}")
return user.loginname || user.loginName || ""
},
onRefresh() {
this.resetList()
},
onLoad() {
if (!this.finished) {
this.fetchFileList()
}
},
onKeywordChange() {
clearTimeout(this.searchTimer)
this.searchTimer = setTimeout(() => this.resetList(), 300)
},
resetList() {
this.queryVersion++
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.fileList = []
this.finished = false
this.loadedOnce = false
this.fetchFileList()
},
fetchFileList() {
const queryVersion = this.queryVersion
this.loading = true
this.$axios.get("/platform/h5/congress/materialsList", {
params: {
typeId: this.typeId,
timeId: this.timeId,
emplid: this.loginName(),
searchKeyword: this.keyword,
fileType: this.selectedFileType,
uploadTimeOrder: this.uploadTimeOrder,
pageNumber: this.pageForm.pageNumber,
pageSize: this.pageForm.pageSize
}
}).then((res) => {
if (queryVersion !== this.queryVersion) return
if (res.code === 0) {
const pageData = res.data || {}
const records = pageData.list || []
this.fileList = this.fileList.concat(records)
this.pageForm.totalCount = pageData.totalCount || 0
if (this.fileList.length >= this.pageForm.totalCount) {
this.finished = true
} else {
this.pageForm.pageNumber++
}
} else {
this.finished = true
}
this.loadedOnce = true
}).finally(() => {
if (queryVersion === this.queryVersion) {
this.loading = false
this.refreshing = false
}
})
},
getFileIconUrl(item) {
return this.$commonUtil.getFileItemShowIcon(this.resolveFileSuffix(item))
},
resolveFileSuffix(item) {
const fileType = String(item.fileType || "").replace(".", "").toLowerCase()
if (this.isKnownSuffix(fileType)) return fileType
const fileNameSuffix = this.extractSuffix(item.fileName || item.name || "")
if (fileNameSuffix) return fileNameSuffix
const fileUrlSuffix = this.extractSuffix((item.fileUrl || "").split("?")[0])
if (fileUrlSuffix) return fileUrlSuffix
if (fileType.indexOf("pdf") > -1) return "pdf"
if (fileType.indexOf("word") > -1) return "docx"
if (fileType.indexOf("excel") > -1 || fileType.indexOf("sheet") > -1) return "xlsx"
if (fileType.indexOf("powerpoint") > -1 || fileType.indexOf("presentation") > -1) return "pptx"
if (fileType.indexOf("image") > -1) return "jpg"
if (fileType.indexOf("zip") > -1 || fileType.indexOf("rar") > -1) return "zip"
if (fileType.indexOf("text") > -1) return "txt"
if (fileType.indexOf("video") > -1) return "mp4"
if (fileType.indexOf("audio") > -1) return "mp3"
return ""
},
extractSuffix(value) {
const text = String(value || "").trim()
const index = text.lastIndexOf(".")
if (index < 0 || index === text.length - 1) return ""
const suffix = text.substring(index + 1).toLowerCase()
return this.isKnownSuffix(suffix) ? suffix : ""
},
isKnownSuffix(suffix) {
return ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "zip", "rar", "jpg", "jpeg", "png", "gif", "bmp", "webp", "mp4", "avi", "wmv", "rmvb", "flv", "mkv", "mp3", "wav", "wma", "flac", "ape"].indexOf(suffix) > -1
},
fileTypeText(item) {
const suffix = this.resolveFileSuffix(item)
return suffix ? suffix.toUpperCase() : "未知类型"
},
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 || "") +
"&fileType=" + encodeURIComponent(item.fileType || "") +
"&sysFileId=" + encodeURIComponent(this.resolveSysFileId(item.fileUrl)))
},
openVotePdf(item) {
this.$pjaxReplace("/platform/h5/congress/pdf?pdfPath=" + encodeURIComponent(item.fileUrl || "") +
"&fileName=" + encodeURIComponent(item.fileName || item.name || "") +
"&materialsId=" + encodeURIComponent(item.id || "") +
"&fileType=" + encodeURIComponent(item.fileType || "") +
"&sysFileId=" + encodeURIComponent(this.resolveSysFileId(item.fileUrl)))
},
resolveSysFileId(fileUrl) {
if (!fileUrl) return ""
try {
const url = new URL(fileUrl, window.location.origin)
return url.searchParams.get("id") || ""
} catch (e) {
const match = String(fileUrl).match(/[?&]id=([^&]+)/)
return match ? decodeURIComponent(match[1]) : ""
}
},
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-filter-bar {
background: #fff;
box-shadow: 0 2px 8px rgba(30, 48, 80, 0.04);
}
.file-search {
padding: 10px 16px 6px;
background: transparent;
}
.file-filter-menu {
height: 42px;
box-shadow: none;
}
.file-filter-menu .van-dropdown-menu__bar {
height: 42px;
box-shadow: none;
border-top: 1px solid #f2f3f5;
}
.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:active {
transform: scale(0.99);
box-shadow: 0 4px 16px rgba(25, 137, 250, 0.08);
}
.card-actions {
display: inline-flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.vote-status {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 4px 7px;
font-size: 12px;
font-weight: 600;
line-height: 1;
border-radius: 10px;
}
.vote-status.voted {
color: #17a65b;
background: #edf9f1;
}
.vote-status.pending {
color: #d98a00;
background: #fff6e5;
}
.card-left {
flex-shrink: 0;
display: flex;
align-items: center;
}
.icon-wrapper {
width: 52px;
height: 52px;
border-radius: 12px;
background: #f7f8fa;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #eef0f4;
}
.file-type-icon {
width: 38px;
height: 38px;
display: block;
object-fit: contain;
}
.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;
line-height: 1.5;
}
.info-row + .info-row {
margin-top: 4px;
}
.info-row .van-icon {
color: #969799;
}
.info-value {
color: #646566;
font-weight: 500;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,466 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="会议签到" left-text="返回" left-arrow @click-left="backToCongress" placeholder fixed></van-nav-bar>
<div class="meeting-list-page">
<van-sticky :offset-top="46">
<div class="meeting-filter-bar">
<van-search v-model="keyword" placeholder="搜索会议名称" shape="round" class="meeting-search" @input="onKeywordChange"></van-search>
<van-dropdown-menu class="meeting-filter-menu">
<van-dropdown-item v-model="signTimeStatus" :options="signTimeOptions" @change="resetList"></van-dropdown-item>
<van-dropdown-item v-model="signInCountStatus" :options="signInCountOptions" @change="resetList"></van-dropdown-item>
<van-dropdown-item v-model="signStatus" :options="signStatusOptions" @change="resetList"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<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" :class="{'meeting-card-disabled': !item.signInId && !canSignIn(item)}">
<div class="card-content">
<div class="card-header">
<div class="card-left">
<div class="icon-wrapper" :class="{'signed-icon': item.signInId}">
<van-icon name="completed" size="26"></van-icon>
</div>
</div>
<span class="meeting-name">{{ item.meetingName }}</span>
<button v-if="item.remark" type="button" class="remark-link" @click.stop="openRemark(item)">
<van-icon name="notes-o" size="14"></van-icon>
<span>备注</span>
<van-icon name="arrow" size="12"></van-icon>
</button>
</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">{{ formatTime(item.signInStartTime) }}</span></span>
</div>
<div v-if="item.signInEndTime" class="info-row">
<van-icon name="clock-o" size="14" color="#969799"></van-icon>
<span class="info-text">签到结束:<span class="info-value">{{ formatTime(item.signInEndTime) }}</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.userSignInTime" class="action-section signin-status signed-status">
<van-icon name="checked" size="16"></van-icon>
<span>已签到:{{ formatTime(item.userSignInTime) }}</span>
</div>
<div v-else class="action-section">
<div v-if="!canSignIn(item)" class="signin-status disabled-status">
<van-icon name="clock-o" size="16"></van-icon>
{{ getSignInButtonText(item) }}
</div>
<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="hasFilter ? '未找到匹配会议' : '暂无会议'"></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: "",
sessionId: "",
keyword: "",
signTimeStatus: "",
signInCountStatus: "",
signStatus: "",
searchTimer: null,
queryVersion: 0,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0
}
}
},
computed: {
signTimeOptions() {
return [
{text: "签到时间", value: ""},
{text: "未开始", value: "upcoming"},
{text: "进行中", value: "ongoing"},
{text: "已结束", value: "ended"}
]
},
signInCountOptions() {
return [
{text: "签到人数", value: ""},
{text: "有人签到", value: "has"},
{text: "无人签到", value: "none"}
]
},
signStatusOptions() {
return [
{text: "签到状态", value: ""},
{text: "已签到", value: "signed"},
{text: "未签到", value: "unsigned"}
]
},
hasFilter() {
return !!(this.keyword || this.signTimeStatus || this.signInCountStatus || this.signStatus)
}
},
methods: {
initQuery() {
const params = new URLSearchParams(window.location.search)
this.timeId = params.get("timeId") || ""
this.sessionId = params.get("sessionId") || this.timeId
},
backToCongress() {
const timeId = this.timeId || this.sessionId || window.localStorage.getItem("congressTimeId") || ""
this.$pjaxReplace("/platform/h5/congress" + (timeId ? "?timeId=" + encodeURIComponent(timeId) : ""))
},
loginName() {
const user = (this.$store.state && this.$store.state.user) || JSON.parse(window.sessionStorage.getItem("user") || "{}")
return user.loginname || user.loginName || ""
},
onRefresh() {
this.resetList()
},
onLoad() {
if (!this.finished) {
this.fetchMeetingList()
}
},
onKeywordChange() {
clearTimeout(this.searchTimer)
this.searchTimer = setTimeout(() => this.resetList(), 300)
},
resetList() {
this.queryVersion++
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.meetingList = []
this.finished = false
this.loadedOnce = false
this.fetchMeetingList()
},
fetchMeetingList() {
const queryVersion = this.queryVersion
this.loading = true
this.$axios.get("/platform/h5/congress/user/meetingList", {
params: {
timeId: this.timeId,
emplid: this.loginName(),
searchKeyword: this.keyword,
signTimeStatus: this.signTimeStatus,
signInCountStatus: this.signInCountStatus,
signStatus: this.signStatus,
pageNumber: this.pageForm.pageNumber,
pageSize: this.pageForm.pageSize
}
}).then((res) => {
if (queryVersion !== this.queryVersion) return
if (res.code === 0) {
const pageData = res.data || {}
const records = pageData.list || []
this.meetingList = this.meetingList.concat(records)
this.pageForm.totalCount = pageData.totalCount || 0
if (this.meetingList.length >= this.pageForm.totalCount) {
this.finished = true
} else {
this.pageForm.pageNumber++
}
} else {
this.finished = true
}
this.loadedOnce = true
}).finally(() => {
if (queryVersion === this.queryVersion) {
this.loading = false
this.refreshing = false
}
})
},
openRemark(item) {
this.$pjaxReplace("/platform/h5/congress/meeting/remark?meetingId=" + encodeURIComponent(item.id) +
"&timeId=" + encodeURIComponent(item.timeId || this.timeId) +
"&sessionId=" + encodeURIComponent(item.sessionId || this.sessionId))
},
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-filter-bar {
background: #fff;
box-shadow: 0 2px 8px rgba(30, 48, 80, 0.04);
}
.meeting-search {
padding: 10px 16px 6px;
background: transparent;
}
.meeting-filter-menu {
height: 42px;
box-shadow: none;
}
.meeting-filter-menu .van-dropdown-menu__bar {
height: 42px;
box-shadow: none;
border-top: 1px solid #f2f3f5;
}
.meeting-card {
background: #fff;
margin: 16px;
height: 220px;
padding: 24px;
box-sizing: border-box;
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: stretch;
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;
}
.card-left {
flex-shrink: 0;
padding-top: 0;
}
.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;
}
.meeting-card-disabled .icon-wrapper {
background: #f4f4f5;
color: #c8c9cc;
box-shadow: none;
}
.card-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.card-header {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 12px;
}
.meeting-name {
font-size: 17px;
color: #323233;
font-weight: 600;
line-height: 1.5;
flex: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.remark-link {
display: inline-flex;
align-items: center;
gap: 2px;
margin-left: 8px;
padding: 0;
border: 0;
background: transparent;
color: #969799;
font-size: 13px;
line-height: 1.5;
flex-shrink: 0;
}
.info-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 0;
}
.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.count .info-text {
color: #1989fa;
}
.meeting-card-disabled .info-row.count .van-icon {
color: #c8c9cc !important;
}
.meeting-card-disabled .info-row.count .info-text {
color: #c8c9cc;
}
.action-section {
margin-top: auto;
}
.signin-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
line-height: 1.4;
}
.signed-status {
color: #07c160;
}
.disabled-status {
color: #c8c9cc;
}
.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);
transform: translateY(4px);
}
</style>
<!--#
}
#-->
@@ -0,0 +1,104 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="会议备注" left-text="返回" left-arrow @click-left="backToMeeting" placeholder fixed></van-nav-bar>
<div class="meeting-remark-page">
<div v-if="remarkData" class="remark-content-card">
<h2 class="remark-title">{{ remarkData.meetingName }}</h2>
<div class="remark-divider"></div>
<div class="remark-content">{{ remarkData.remark || "暂无备注" }}</div>
</div>
<van-empty v-else-if="!loading" description="暂无备注"></van-empty>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
store,
data() {
return {
loading: true,
meetingId: "",
timeId: "",
sessionId: "",
remarkData: null
}
},
methods: {
initQuery() {
const params = new URLSearchParams(window.location.search)
this.meetingId = params.get("meetingId") || ""
this.timeId = params.get("timeId") || ""
this.sessionId = params.get("sessionId") || this.timeId
},
backToMeeting() {
this.$pjaxReplace("/platform/h5/congress/meeting/list?timeId=" + encodeURIComponent(this.timeId) +
"&sessionId=" + encodeURIComponent(this.sessionId))
},
loginName() {
const user = (this.$store.state && this.$store.state.user) || JSON.parse(window.sessionStorage.getItem("user") || "{}")
return user.loginname || user.loginName || ""
},
fetchRemark() {
this.$axios.get("/platform/h5/congress/user/meetingRemark", {
params: {meetingId: this.meetingId, timeId: this.timeId, emplid: this.loginName()}
}).then((res) => {
if (res.code === 0) {
this.remarkData = res.data || null
}
}).finally(() => {
this.loading = false
})
}
},
created() {
this.initQuery()
this.fetchRemark()
}
})
</script>
<style id="style-congress-meeting-remark-h5">
.meeting-remark-page {
min-height: calc(100vh - 46px);
padding: 16px;
box-sizing: border-box;
background: #f5f7fa;
}
.remark-content-card {
padding: 20px;
border-radius: 14px;
background: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.remark-title {
margin: 0;
color: #323233;
font-size: 17px;
line-height: 1.5;
}
.remark-divider {
height: 1px;
margin: 16px 0;
background: #ebedf0;
}
.remark-content {
color: #646566;
font-size: 15px;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-word;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,254 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<div class="pdf-viewer-page">
<van-nav-bar :title="fileName || '文件预览'" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="pdf-container" :class="{'has-footer': voteInfo}">
<div v-if="previewMode === 'pdf'" id="congress-pdf-container" class="pdf-render-box"></div>
<img v-else-if="previewMode === 'image'" :src="previewUrl" class="image-preview" @click="previewImage">
<iframe v-else-if="previewMode === 'iframe'" :src="previewUrl" class="pdf-frame"></iframe>
<div v-else class="pdf-empty">
<van-empty description="暂无可预览附件"></van-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: "",
materialsId: "",
sysFileId: "",
fileType: "",
previewUrl: "",
previewMode: "empty",
pdfObj: null,
voteInfo: null
}
},
computed: {
fileSuffix() {
const name = this.fileName || ""
const url = (this.pdfPath || "").split("?")[0]
const source = name.indexOf(".") > -1 ? name : url
const index = source.lastIndexOf(".")
return index > -1 ? source.substring(index + 1).toLowerCase() : ""
}
},
methods: {
initQuery() {
const params = new URLSearchParams(window.location.search)
this.pdfPath = params.get("pdfPath") || ""
this.fileName = params.get("fileName") || ""
this.fileId = params.get("fileId") || ""
this.materialsId = params.get("materialsId") || params.get("materialId") || this.fileId
this.sysFileId = params.get("sysFileId") || ""
this.fileType = params.get("fileType") || ""
},
initPreview() {
if (!this.pdfPath) {
this.previewMode = "empty"
return
}
const suffix = this.resolveSuffix()
if (this.isImage(suffix)) {
this.previewUrl = this.pdfPath
this.previewMode = "image"
return
}
if (suffix === "pdf" || !suffix) {
this.previewUrl = this.pdfPath
this.previewMode = "pdf"
this.renderPdf(this.previewUrl)
return
}
if (this.isOfficeFile(suffix)) {
const fileId = this.resolveFileId()
if (fileId) {
this.previewUrl = "/assets/platform/plugins/pdfJs/web/viewer.html?file=" + encodeURIComponent("/platform/sys/file/convertPDF?id=" + fileId)
this.previewMode = "iframe"
return
}
}
this.previewUrl = this.pdfPath
this.previewMode = "iframe"
},
resolveSuffix() {
if (this.fileSuffix) return this.fileSuffix
const type = String(this.fileType || "").replace(".", "").toLowerCase()
if (["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "jpg", "jpeg", "png", "gif", "bmp", "webp"].indexOf(type) > -1) {
return type
}
if (type.indexOf("pdf") > -1) return "pdf"
if (type.indexOf("word") > -1) return "docx"
if (type.indexOf("excel") > -1 || type.indexOf("sheet") > -1) return "xlsx"
if (type.indexOf("powerpoint") > -1 || type.indexOf("presentation") > -1) return "pptx"
if (type.indexOf("image") > -1) return "jpg"
return ""
},
resolveFileId() {
if (this.sysFileId) return this.sysFileId
if (!this.pdfPath) return ""
try {
const url = new URL(this.pdfPath, window.location.origin)
return url.searchParams.get("id") || ""
} catch (e) {
const match = this.pdfPath.match(/[?&]id=([^&]+)/)
return match ? decodeURIComponent(match[1]) : ""
}
},
isImage(suffix) {
return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].indexOf(suffix) > -1
},
isOfficeFile(suffix) {
return ["doc", "docx", "xls", "xlsx", "ppt", "pptx"].indexOf(suffix) > -1
},
renderPdf(url) {
this.$nextTick(() => {
const container = document.getElementById("congress-pdf-container")
if (!container || !url) return
container.innerHTML = ""
try {
if (this.pdfObj && this.pdfObj.destroy) {
this.pdfObj.destroy()
}
} catch (e) {}
try {
this.pdfObj = new Pdfh5("#congress-pdf-container", {
pdfurl: url,
lazy: true
})
} catch (e) {
this.previewUrl = "/assets/platform/plugins/pdfJs/web/viewer.html?file=" + encodeURIComponent(url)
this.previewMode = "iframe"
}
})
},
previewImage() {
if (!this.previewUrl) return
vant.ImagePreview({
images: [this.previewUrl],
closeable: true
})
},
fetchVoteInfo() {
if (!this.materialsId) return
const repId = window.localStorage.getItem("repId") || ""
this.$axios.get("/platform/h5/congress/votingIssue", {
params: {materialsId: this.materialsId, 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()
this.initPreview()
}
})
</script>
<style id="style-congress-pdf-h5">
.pdf-viewer-page {
min-height: 100vh;
background: #f5f6f8;
}
.pdf-container {
height: calc(100vh - 46px);
background: #f5f6f8;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.pdf-container.has-footer {
height: calc(100vh - 110px);
padding-bottom: 64px;
}
.pdf-frame {
width: 100%;
height: 100%;
border: 0;
background: #fff;
}
.pdf-render-box {
width: 100%;
min-height: 100%;
background: #f5f6f8;
}
.pdf-render-box .pdfjs {
background: #f5f6f8;
}
.image-preview {
display: block;
width: 100%;
height: auto;
min-height: 100%;
object-fit: contain;
background: #fff;
}
.pdf-empty {
width: 100%;
height: 100%;
background: #f5f6f8;
display: flex;
align-items: center;
justify-content: center;
}
.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,445 @@
<!--#
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="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="completed" 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: calc(100vh - 46px);
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;
justify-content: 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;
}
.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);
}
.vote-table-container {
border-color: #dfe5ee;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(30, 48, 80, 0.04);
}
.vote-table {
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
}
.vote-table th,
.vote-table td {
border: 0;
border-right: 1px solid #dfe5ee;
border-bottom: 1px solid #dfe5ee;
}
.vote-table tr > :last-child {
border-right: 0;
}
.vote-table tbody tr:last-child td {
border-bottom: 0;
}
</style>
<!--#
}
#-->
@@ -0,0 +1,435 @@
<!--#
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="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="completed" 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: calc(100vh - 46px);
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;
justify-content: 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;
}
.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;
}
.vote-table-container {
border-color: #dfe5ee;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(30, 48, 80, 0.04);
}
.vote-table {
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
}
.vote-table th,
.vote-table td {
border: 0;
border-right: 1px solid #dfe5ee;
border-bottom: 1px solid #dfe5ee;
}
.vote-table tr > :last-child {
border-right: 0;
}
.vote-table tbody tr:last-child td {
border-bottom: 0;
}
</style>
<!--#
}
#-->