Merge branch 'refs/heads/feature_职代会' into release_20260829
# Conflicts: # src/main/resources/views/layouts/platform_h5.html
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
+370
@@ -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>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
+236
@@ -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>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
+170
@@ -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>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
Reference in New Issue
Block a user