<template>
<div class="vote-container">
<!-- 投票人信息 -->
<div class="voter-info" v-if="voterName && voterIdCard">
<p>投票人:{{ voterName }}</p>
<p>身份证:{{ formattedIdCard }}</p>
</div>
<!-- 投票统计信息 -->
<div class="stats">
<div class="stat">
<h3>经理投票</h3>
<div class="progress">
<div class="progress-bar" :style="{ width: (votes.A / 5) * 100 + '%' }"></div>
</div>
<p>{{ votes.A }} / 5</p>
</div>
<div class="stat">
<h3>厂长投票</h3>
<div class="progress">
<div class="progress-bar" :style="{ width: (votes.B / 5) * 100 + '%' }"></div>
</div>
<p>{{ votes.B }} / 5</p>
</div>
<div class="stat">
<h3>副厂长投票</h3>
<div class="progress">
<div class="progress-bar" :style="{ width: (votes.C / 15) * 100 + '%' }"></div>
</div>
<p>{{ votes.C }} / 15</p>
</div>
<div class="stat">
<h3>总票数</h3>
<div class="progress">
<div class="progress-bar" :style="{ width: (totalVotes / 25) * 100 + '%' }"></div>
</div>
<p>{{ totalVotes }} / 25</p>
</div>
</div>
<!-- 被投票人列表 -->
<div class="voters-grid">
<div v-for="voter in voters" :key="voter.id" class="voter-card">
<h4>{{ voter.name }}</h4>
<p class="voter-id">ID: {{ voter.id }}</p>
<div class="vote-options">
<button
@click="castVote(voter, 'A')"
:disabled="!canVote(voter, 'A')"
:class="{
'selected': voter.vote === 'A',
'disabled': !canVote(voter, 'A')
}"
>
经理
</button>
<button
@click="castVote(voter, 'B')"
:disabled="!canVote(voter, 'B')"
:class="{
'selected': voter.vote === 'B',
'disabled': !canVote(voter, 'B')
}"
>
厂长
</button>
<button
@click="castVote(voter, 'C')"
:disabled="!canVote(voter, 'C')"
:class="{
'selected': voter.vote === 'C',
'disabled': !canVote(voter, 'C')
}"
>
副厂长
</button>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button @click="submitVotes" :disabled="!hasSelectedCandidates || isSubmitting">提交投票</button>
<button @click="resetVotes" :disabled="isSubmitting">重置投票</button>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue';
import { useRoute } from 'vue-router';
import { onMounted } from 'vue'
const voters = ref([]); //候选人
const activeSurvey = ref({
id: null, //投票ID
bt: '', // 标题
qydcl: '',
dclx: '', //投票类型
tffs: ''
});
// 添加消息提示状态
const alertMessage = ref('');
const showAlert = ref(false);
const alertType = ref(''); // 'success' 或 'error'
// 安全序列化函数
function safeStringify(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
// 检测到循环引用,返回占位符或跳过
return "[Circular Reference Removed]";
}
seen.add(value);
}
return value;
});
}
// onMounted生命周期钩子
onMounted(async () => {
// 从sessionStorage获取投票人信息并立即清除
const voterInfo = sessionStorage.getItem('voterInfo');
if (voterInfo) {
const { name, idCard } = JSON.parse(voterInfo);
voterName.value = name;
voterIdCard.value = idCard;
sessionStorage.removeItem('voterInfo');
}
// 加载候选人数据
voters.value = await fetchCandidates();
});
// 获取路由信息
const route = useRoute();
// 添加用于存储投票人信息的变量
const voterName = ref('');
const voterIdCard = ref('');
// 格式化身份证显示(安全脱敏)
const formattedIdCard = computed(() => {
if (!voterIdCard.value) return '';
// 显示前6位和后4位,中间用*代替
return voterIdCard.value.substring(0, 6) + '******' +
voterIdCard.value.substring(voterIdCard.value.length - 4);
});
onMounted(() => {
// 从sessionStorage获取数据并立即清除
const voterInfo = sessionStorage.getItem('voterInfo');
if (voterInfo) {
const { name, idCard } = JSON.parse(voterInfo);
voterName.value = name;
voterIdCard.value = idCard;
// 关键:立即清除存储防止数据残留
sessionStorage.removeItem('voterInfo');
}
});
//获取候选人明细
const fetchCandidates = async () => {
try {
const response = await fetch('/api/wechat/getInvestigate', {
method: 'POST',
body: JSON.stringify({ id: '9', dcl: '123' })
});
console.log('API响应:', response);
const result = await response.json();
if (!result || !result.root) throw new Error('无效API响应');
// 提取候选人数据
const candidateArray = [];
let idCounter = 1; // 自增计数器,名称序号
result.root.forEach(rootItem => {
if (!rootItem.childEntList) return;
rootItem.childEntList.forEach(candidate => {
if (!candidate.dcbt || !candidate.dcxbt) return;
candidateArray.push({
originalid: candidate.dcbt,
name: candidate.dcxbt,
vote: null,
id:idCounter++,
tmlx: candidate.tmlx, // 投票类型
});
});
});
return candidateArray;
} catch (error) {
console.error('获取候选人失败:', error);
return []; // 返回空数组保持安全
}
};
// 新增计算属性 - 获取已选择的候选人
const selectedCandidates = computed(() => {
return voters.value
.filter(v => v.vote) // 只过滤已投票的
.map(v => ({
id: v.originalid, // 候选人原始ID
voteType: v.vote, // 投票类型(A/B/C)
voteValue: v.vote === 'A' ? 17 : // 转换类型值
v.vote === 'B' ? 18 : 19,
name: v.name, // 候选人姓名
tmlx: v.tmlx // 原始类型
}));
});
// 检查是否有选择的候选人
const hasSelectedCandidates = computed(() => {
return selectedCandidates.value.length > 0;
});
// 投票统计
const votes = reactive({
A: 0,
B: 0,
C: 0
});
// 计算总票数
const totalVotes = computed(() => {
return votes.A + votes.B + votes.C;
});
// 投票方法
const canVote = (voter, type) => {
// 情况1:用户取消当前选择的类型(总是允许)
if (voter.vote === type) return true;
// 情况2:用户从其他类型转换到当前类型
if (voter.vote && voter.vote !== type) {
if (type === 'A' && votes.A >= 5) return false;
if (type === 'B' && votes.B >= 5) return false;
if (type === 'C' && votes.C >= 15) return false;
}
// 情况3:用户首次投票
if (!voter.vote) {
if (type === 'A' && votes.A >= 5) return false;
if (type === 'B' && votes.B >= 5) return false;
if (type === 'C' && votes.C >= 15) return false;
if (totalVotes.value >= 25) return false;
}
return true;
};
// 投票方法
const castVote = (voter, type) => {
// 如果已投票且点击相同类型,取消投票
if (voter.vote === type) {
voter.vote = null;
votes[type]--;
return;
}
// 如果之前有投票,先取消
if (voter.vote !== null) {
votes[voter.vote]--;
}
// 投新票
voter.vote = type;
votes[type]++;
};
//投票人信息
// 添加投票人信息数据模型
const voterInfo = reactive({
name: '',
idNumber: ''
});
// // 添加基本信息验证
// const isValid = computed(() => {
// return voterInfo.name.trim() !== '' &&
// voterInfo.idNumber.trim() !== '' &&
// /^\d{17}[\dXx]$/.test(voterInfo.idNumber);
// });
// 提交投票
// 防止重复提交
const isSubmitting = ref(false);
// 提交投票到API
const submitVotes = async () => {
// 添加防御性检查
if (!hasSelectedCandidates.value) {
alert('请先选择候选人', 'error');
return;
}
// 防止重复提交
if (isSubmitting.value) return;
isSubmitting.value = true;
try {
// 按接口规范构建JSON数据结构
// const requestData = {
// mainData: [
// { fieldName: "bt", fieldValue: activeSurvey.value.bt },
// { fieldName: "tprxm", fieldValue: voterName },
// { fieldName: "tprsfz", fieldValue: voterIdCard },
// { fieldName: "mb", fieldValue: activeSurvey.value.id },
// { fieldName: "dclx", fieldValue: activeSurvey.value.dclx },
// { fieldName: "tffs", fieldValue: activeSurvey.value.tffs },
// ],
// workflowRequestTableRecords: selectedCandidates.value.map(candidate => ({
// workflowRequestTableFields: [
// {
// fieldName: "dcbt",
// fieldValue: candidate.originalid
// },
// {
// fieldName: "tmlx",
// fieldValue: candidate.tmlx
// },
// {
// fieldName: "dcxx",
// fieldValue: candidate.voteValue // 使用转换后的值
// }
// ]
// }))
// };
const mainData = [
{ fieldName: "bt", fieldValue: activeSurvey.value.bt },
{ fieldName: "tprxm", fieldValue: voterName.value },
{ fieldName: "tprsfz", fieldValue: voterIdCard.value },
{ fieldName: "mb", fieldValue: activeSurvey.value.id },
{ fieldName: "dclx", fieldValue: activeSurvey.value.dclx },
{ fieldName: "tffs", fieldValue: activeSurvey.value.tffs }
];
const workflowRequestTableRecords = selectedCandidates.value.map(candidate => ({
workflowRequestTableFields: [
{ fieldName: "dcbt", fieldValue: candidate.originalid },
{ fieldName: "tmlx", fieldValue: candidate.tmlx },
{
fieldName: "dcxx",
fieldValue: candidate.vote === 'A' ? 17 :
candidate.vote === 'B' ? 18 : 19
}
]
}));
const requestBody = {
requestName: activeSurvey.value.bt, // 投票标题
workflowId: 118, // 固定工作流ID
mainData,
workflowRequestTableRecords
};
// 发送POST请求
const response = await fetch('/api/wechat/addInvestigateWorkflow1', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: safeStringify(requestBody)// 使用安全序列化,避免重复引用
});
const result = await response.json();
// 根据API响应显示相应提示
if (result.code === 200) {
// 成功处理
if (result.msg) {
showMessage(result.msg, 'success');
}else {
showMessage('投票提交成功!', 'success');
}
// 存储已投票标识
localStorage.setItem('voted_' + voterIdCard.value, 'true');
} else {
// 特殊处理"已提交"消息
if (result.msg === '你已提交或不满足提交条件') {
showMessage(result.msg, 'error');
} else {
// 其他错误处理
showMessage(`提交失败: ${result.msg || '未知错误'}`, 'error');
}
}
} catch (error) {
console.error('提交失败:', error);
// 检查是否已投票错误(假设后端返回409状态码)
if (error.response?.status === 409) {
alert('您已投过票,无法重复提交');
} else {
alert('投票提交失败,请重试');
}
} finally {
isSubmitting.value = false;
}
};
// 重置投票
const resetVotes = () => {
if (confirm('确定要重置所有投票吗?')) {
voters.value.forEach(voter => {
voter.vote = null;
});
votes.A = 0;
votes.B = 0;
votes.C = 0;
voterInfo.name = '';
voterInfo.idNumber = '';
}
};
</script>
<style scoped>
/* 移动端垂直布局 */
@media (max-width: 480px) {
.input-group {
flex-direction: column;
}
}
/* 平板/桌面端水平布局 */
@media (min-width: 768px) {
.input-group {
flex-direction: row;
}
}
.vote-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.stats {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
background: #f5f7fa;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.stat {
flex: 1;
text-align: center;
padding: 0 15px;
}
.progress {
height: 20px;
background: #e0e0e0;
border-radius: 10px;
margin: 10px 0;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #3498db;
transition: width 0.3s;
}
.voters-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px;
}
.voter-card {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.voter-card:hover {
transform: translateY(-5px);
}
.voter-id {
color: #777;
font-size: 0.9rem;
margin-bottom: 15px;
}
.vote-options {
display: flex;
justify-content: space-between;
}
.vote-options button {
flex: 1;
margin: 0 5px;
padding: 8px 0;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.vote-options button:not(.selected):hover {
opacity: 0.9;
transform: scale(1.05);
}
.vote-options button:first-child {
background: #ff6b6b;
color: white;
}
.vote-options button:nth-child(2) {
background: #4ecdc4;
color: white;
}
.vote-options button:last-child {
background: #ffd166;
color: white;
}
.selected {
border: 2px solid #2c3e50 !important;
font-weight: bold;
box-shadow: 0 0 2 rgba(61, 60, 60, 0.5);
}
.disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
}
.action-buttons {
margin-top: 30px;
display: flex;
justify-content: center;
gap: 20px;
}
.action-buttons button {
padding: 12px 30px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.2s;
}
.action-buttons button:first-child {
background: #3498db;
color: white;
}
.action-buttons button:first-child:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.action-buttons button:last-child {
background: #e74c3c;
color: white;
}
.action-buttons button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
</style>帮我修改报错:提交失败: ReferenceError: showMessage is not defined
at submitVotes (Vote.vue:386:1)
overrideMethod @ hook.js:608
submitVotes @ Vote.vue:390
await in submitVotes
callWithErrorHandling @ runtime-core.esm-bundler.js:199
callWithAsyncErrorHandling @ runtime-core.esm-bundler.js:206
invoker @ runtime-dom.esm-bundler.js:729
最新发布