<template>
<div class="vote-container">
<!-- 消息提示 -->
<div v-if="showAlert" :class="['alert', alertType]">
{{ alertMessage }}
</div>
<!-- 投票人信息 -->
<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, useRouter} from 'vue-router';
import { onMounted } from 'vue';
const route = useRoute();
const router = useRouter()
const voters = ref([]); //候选人
// 使用props接收参数
const props = defineProps({
queryParams: {
type: Object,
default: () => ({})
}
})
// 使用计算属性
const activeSurvey = ref({
surveyId: route.params.surveyId,//调查令
qydcl: route.params.qydcl,
dclx: route.query.dclx,
tffs: route.query.tffs,
bt: route.query.bt
});
onMounted(async() => {
// 确保路由完全解析完成
await router.isReady()
// console.log('当前路由对象:', route)
// console.log('路径参数:', route.params) // 获取params参数
// console.log('查询参数:', route.query) // 获取query参数
});
// 添加消息提示状态
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');
}
// 获取投票详情
const route = useRoute();
// 加载候选人数据
voters.value = await fetchCandidates();
});
// 添加用于存储投票人信息的变量
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({
name: candidate.dcxbt,
vote: null,
id:idCounter++,
tmlx: candidate.tmlx, // 投票类型
mainId: candidate.mainId, // 主ID
dcbt: candidate.dcbt, // 投票标题
});
});
});
// console.log('候选人数据:', candidateArray);
return candidateArray;
} catch (error) {
console.error('获取候选人失败:', error);
return []; // 返回空数组保持安全
}
};
// 新增计算属性 - 获取已选择的候选人
const selectedCandidates = computed(() => {
return voters.value
.filter(v => v.vote) // 只过滤已投票的
.map(v => ({
mainId: v.mainId, // 候选人原始ID
voteType: v.vote, // 投票类型(A/B/C)
voteValue: v.vote === 'A' ? 17 : // 转换类型值
v.vote === 'B' ? 18 : 19,
name: v.name, // 候选人姓名
tmlx: v.tmlx, // 原始类型
dcbt: v.dcbt // 投票标题
}));
});
// 检查是否有选择的候选人
const hasSelectedCandidates = computed(() => {
return selectedCandidates.value.length > 0;
});
//获取并保存第一个候选人的mainId
const mainIdToSubmit = ref(null);
const firstCandidateMainId = computed(() => {
return selectedCandidates.value[0]?.mainId || null;
});
// 投票统计
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 () => {
// console.log("dclx", activeSurvey.value.dclx);
// 添加防御性检查
if (!hasSelectedCandidates.value) {
showMessage('请先选择候选人', 'error');
return;
}
// 防止重复提交
if (isSubmitting.value) return;
isSubmitting.value = true;
// 设置mainId值
mainIdToSubmit.value = firstCandidateMainId.value;
try {
const mainData = [
{ fieldName: "bt", fieldValue: activeSurvey.value.bt },
{ fieldName: "tprxm", fieldValue: voterName.value },
{ fieldName: "tprsfz", fieldValue: voterIdCard.value },
{ fieldName: "mb", fieldValue: mainIdToSubmit.value },
{ fieldName: "dclx", fieldValue: activeSurvey.value.dclx },
{ fieldName: "tffs", fieldValue: activeSurvey.value.tffs }
];
const workflowRequestTableRecords = selectedCandidates.value.map(candidate => ({
workflowRequestTableFields: [
{ fieldName: "dcbt", fieldValue: candidate.dcbt },
{ fieldName: "tmlx", fieldValue: candidate.tmlx },
{
fieldName: "dcxx",
fieldValue: candidate.vote === 'A' ? 17 :
candidate.vote === 'B' ? 18 : 19
}
]
}));
// console.log('提交数据:', {
// mainData,
// workflowRequestTableRecords
// });
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_status 判断
if (result.api_status === true) {
// 成功处理
const successMsg = result.msg || '投票提交成功!';
showMessage('投票提交成功!', 'success');
// 存储已投票标识
localStorage.setItem('voted_' + voterIdCard.value, 'true');
} else {
// 处理错误情况
if (result.msg === '你已提交或不满足提交条件') {
showMessage(result.msg, 'error');
// 特殊处理:用户已投票,存储标识防止重复提交
localStorage.setItem('voted_' + voterIdCard.value, 'true');
} else {
// 其他错误情况
const errorMsg = result.msg ? `提交失败: ${result.msg}` : '未知错误,请稍后重试';
showMessage(errorMsg, 'error');
}
}
} catch (error) {
console.error('网络请求失败:', error);
showMessage('网络错误,请检查连接后重试', 'error');
} 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 = '';
}
};
//后台响应信息showMessage
const showMessage = (message, type = 'error') => {
// 更新消息提示状态
alertMessage.value = message;
showAlert.value = true;
alertType.value = type;
// 错误提示停留3秒,成功提示停留2秒
const timeout = type === 'error' ? 3000 : 2000;
setTimeout(() => {
showAlert.value = false;
}, timeout);
};
</script>
<style scoped>
/* 移动端垂直布局 */
@media (max-width: 480px) {
.input-group {
flex-direction: column;
}
}
/* 平板/桌面端水平布局 */
@media (min-width: 768px) {
.input-group {
flex-direction: row;
}
}
/* 消息提示样式 */
.alert {
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
text-align: center;
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 300px;
opacity: 0.95;
}
.alert.error {
background-color: #ffebee;
color: #b71c1c;
border: 1px solid #ffcdd2;
}
.alert.success {
background-color: #e8f5e9;
color: #1b5e20;
border: 1px solid #c8e6c9;
}
.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>我想要经理映射值为17,厂长映射值为18,副厂长映射值为19