<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="totalVotes === 0 || isSubmitting">提交投票</button>
<button @click="resetVotes" :disabled="isSubmitting">重置投票</button>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue';
import { useRoute } from 'vue-router';
// 获取路由信息
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');
}
});
console.log(routeUserName )
// 生成200个被投票人数据
const voters = ref([]);
const generateVoters = () => {
const names = [];
// 生成随机中文姓名(简单实现)
const surnames = ['王', '李', '张', '刘', '陈', '杨', '赵', '黄', '周', '吴'];
const names1 = ['伟', '芳', '娜', '洋', '勇', '杰', '明', '霞', '秀', '立'];
for (let i = 1; i <= 200; i++) {
const surname = surnames[Math.floor(Math.random() * surnames.length)];
const namePart = names1[Math.floor(Math.random() * names1.length)];
voters.value.push({
id: i,
name: `${surname}${namePart}`,
vote: null // 初始没有投票
});
}
};
// 初始化投票人列表
generateVoters();
// 投票统计
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 (!isValid.value) {
alert('请填写正确的姓名和身份证信息');
return;
}
// 防止重复提交
if (isSubmitting.value) return;
isSubmitting.value = true;
try {
// 构建请求数据
const requestData = {
voterInfo: {
name: voterInfo.name,
idNumber: voterInfo.idNumber
},
voteStatistics: {
经理: votes.A,
厂长: votes.B,
副厂长: votes.C
},
voteDetails: voters.value
.filter(voter => voter.vote !== null)
.map(voter => ({
id: voter.id,
name: voter.name,
voteType: voter.vote === 'A' ? '经理' :
voter.vote === 'B' ? '厂长' : '副厂长'
}))
};
// 发送POST请求
const response = await fetch('/api/wechat/getInvestigate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
alert('投票提交成功!');
console.log('API响应:', result);
// 可选:提交成功后重置表单
resetVotes();
} catch (error) {
console.error('提交失败:', error);
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>报错:Uncaught runtime errors:
×
ERROR
onMounted is not defined
ReferenceError: onMounted is not defined
at setup (webpack-internal:///./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/views/Vote.vue?vue&type=script&setup=true&lang=js:41:5)
at callWithErrorHandling (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:387:19)
at setupStatefulComponent (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:7161:25)
at setupComponent (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:7122:36)
at mountComponent (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:4835:7)
at processComponent (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:4813:9)
at patch (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:4488:11)
at ReactiveEffect.componentUpdateFn [as fn] (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:5011:9)
at ReactiveEffect.run (webpack-internal:///./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js:323:19)
at ReactiveEffect.runIfDirty (webpack-internal:///./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js:359:12)
ERROR
Cannot read properties of null (reading 'component')
TypeError: Cannot read properties of null (reading 'component')
at locateNonHydratedAsyncRoot (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:5592:41)
at locateNonHydratedAsyncRoot (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:5597:14)
at ReactiveEffect.componentUpdateFn [as fn] (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:4966:40)
at ReactiveEffect.run (webpack-internal:///./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js:323:19)
at ReactiveEffect.runIfDirty (webpack-internal:///./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js:359:12)
at callWithErrorHandling (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:387:33)
at flushJobs (webpack-internal:///./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js:587:9)