mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-06-06 10:07:36 +08:00
fix: 分类提案投票UI
This commit is contained in:
20
backend/src/main/java/com/openisle/dto/ProposalDto.java
Normal file
20
backend/src/main/java/com/openisle/dto/ProposalDto.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package com.openisle.dto;
|
||||||
|
|
||||||
|
import com.openisle.model.CategoryProposalStatus;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ProposalDto extends PollDto {
|
||||||
|
|
||||||
|
private CategoryProposalStatus proposalStatus;
|
||||||
|
private String proposedName;
|
||||||
|
private String description;
|
||||||
|
private int approveThreshold;
|
||||||
|
private int quorum;
|
||||||
|
private LocalDateTime startAt;
|
||||||
|
private String resultSnapshot;
|
||||||
|
private String rejectReason;
|
||||||
|
}
|
||||||
@@ -6,7 +6,9 @@ import com.openisle.dto.LotteryDto;
|
|||||||
import com.openisle.dto.PollDto;
|
import com.openisle.dto.PollDto;
|
||||||
import com.openisle.dto.PostDetailDto;
|
import com.openisle.dto.PostDetailDto;
|
||||||
import com.openisle.dto.PostSummaryDto;
|
import com.openisle.dto.PostSummaryDto;
|
||||||
|
import com.openisle.dto.ProposalDto;
|
||||||
import com.openisle.dto.ReactionDto;
|
import com.openisle.dto.ReactionDto;
|
||||||
|
import com.openisle.model.CategoryProposalPost;
|
||||||
import com.openisle.model.CommentSort;
|
import com.openisle.model.CommentSort;
|
||||||
import com.openisle.model.LotteryPost;
|
import com.openisle.model.LotteryPost;
|
||||||
import com.openisle.model.PollPost;
|
import com.openisle.model.PollPost;
|
||||||
@@ -113,26 +115,40 @@ public class PostMapper {
|
|||||||
dto.setLottery(l);
|
dto.setLottery(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (post instanceof PollPost pp) {
|
if (post instanceof CategoryProposalPost cp) {
|
||||||
PollDto p = new PollDto();
|
ProposalDto proposalDto = (ProposalDto) buildPollDto(cp, new ProposalDto());
|
||||||
p.setOptions(pp.getOptions());
|
proposalDto.setProposalStatus(cp.getProposalStatus());
|
||||||
p.setVotes(pp.getVotes());
|
proposalDto.setProposedName(cp.getProposedName());
|
||||||
p.setEndTime(pp.getEndTime());
|
proposalDto.setDescription(cp.getDescription());
|
||||||
p.setParticipants(
|
proposalDto.setApproveThreshold(cp.getApproveThreshold());
|
||||||
pp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList())
|
proposalDto.setQuorum(cp.getQuorum());
|
||||||
);
|
proposalDto.setStartAt(cp.getStartAt());
|
||||||
Map<Integer, List<AuthorDto>> optionParticipants = pollVoteRepository
|
proposalDto.setResultSnapshot(cp.getResultSnapshot());
|
||||||
.findByPostId(pp.getId())
|
proposalDto.setRejectReason(cp.getRejectReason());
|
||||||
.stream()
|
dto.setPoll(proposalDto);
|
||||||
.collect(
|
} else if (post instanceof PollPost pp) {
|
||||||
Collectors.groupingBy(
|
dto.setPoll(buildPollDto(pp, new PollDto()));
|
||||||
PollVote::getOptionIndex,
|
|
||||||
Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
p.setOptionParticipants(optionParticipants);
|
|
||||||
p.setMultiple(Boolean.TRUE.equals(pp.getMultiple()));
|
|
||||||
dto.setPoll(p);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PollDto buildPollDto(PollPost pollPost, PollDto target) {
|
||||||
|
target.setOptions(pollPost.getOptions());
|
||||||
|
target.setVotes(pollPost.getVotes());
|
||||||
|
target.setEndTime(pollPost.getEndTime());
|
||||||
|
target.setParticipants(
|
||||||
|
pollPost.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
Map<Integer, List<AuthorDto>> optionParticipants = pollVoteRepository
|
||||||
|
.findByPostId(pollPost.getId())
|
||||||
|
.stream()
|
||||||
|
.collect(
|
||||||
|
Collectors.groupingBy(
|
||||||
|
PollVote::getOptionIndex,
|
||||||
|
Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
target.setOptionParticipants(optionParticipants);
|
||||||
|
target.setMultiple(Boolean.TRUE.equals(pollPost.getMultiple()));
|
||||||
|
return target;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,30 @@
|
|||||||
<div class="post-poll-container" v-if="poll">
|
<div class="post-poll-container" v-if="poll">
|
||||||
<div class="poll-top-container">
|
<div class="poll-top-container">
|
||||||
<div class="poll-options-container">
|
<div class="poll-options-container">
|
||||||
|
<div class="poll-title-section">
|
||||||
|
<div class="poll-title-section-row">
|
||||||
|
<div class="poll-option-title" v-if="poll.multiple">多选</div>
|
||||||
|
<div class="poll-option-title" v-else-if="isProposal">
|
||||||
|
拟议分类:{{ poll.proposedName }}
|
||||||
|
<ToolTip
|
||||||
|
content="🗳️ 提案提交后将开放3天投票,需达到至少60%的赞成率并满10人参与方可通过。"
|
||||||
|
placement="bottom"
|
||||||
|
v-if="isProposal"
|
||||||
|
>
|
||||||
|
<info-icon class="info-icon" />
|
||||||
|
</ToolTip>
|
||||||
|
</div>
|
||||||
|
<div class="poll-option-title" v-else>单选</div>
|
||||||
|
<div class="poll-left-time">
|
||||||
|
<stopwatch class="poll-left-time-icon" />
|
||||||
|
<div class="poll-left-time-title">离结束</div>
|
||||||
|
<div class="poll-left-time-value">{{ countdown }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="poll-title-section-row">
|
||||||
|
<div v-if="poll.description" class="proposal-description">{{ poll.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="showPollResult || pollEnded || hasVoted">
|
<div v-if="showPollResult || pollEnded || hasVoted">
|
||||||
<div v-for="(opt, idx) in poll.options" :key="idx" class="poll-option-result">
|
<div v-for="(opt, idx) in poll.options" :key="idx" class="poll-option-result">
|
||||||
<div class="poll-option-info-container">
|
<div class="poll-option-info-container">
|
||||||
@@ -29,16 +53,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="poll-title-section">
|
|
||||||
<div class="poll-option-title" v-if="poll.multiple">多选</div>
|
|
||||||
<div class="poll-option-title" v-else>单选</div>
|
|
||||||
|
|
||||||
<div class="poll-left-time">
|
|
||||||
<stopwatch class="poll-left-time-icon" />
|
|
||||||
<div class="poll-left-time-title">离结束</div>
|
|
||||||
<div class="poll-left-time-value">{{ countdown }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="poll.multiple">
|
<template v-if="poll.multiple">
|
||||||
<div
|
<div
|
||||||
v-for="(opt, idx) in poll.options"
|
v-for="(opt, idx) in poll.options"
|
||||||
@@ -103,11 +117,6 @@
|
|||||||
<div v-else-if="pollEnded" class="poll-option-hint"><stopwatch /> 投票已结束</div>
|
<div v-else-if="pollEnded" class="poll-option-hint"><stopwatch /> 投票已结束</div>
|
||||||
<div v-else class="poll-option-hint">
|
<div v-else class="poll-option-hint">
|
||||||
<div>您已投票,等待结束查看结果</div>
|
<div>您已投票,等待结束查看结果</div>
|
||||||
<div class="poll-left-time">
|
|
||||||
<stopwatch class="poll-left-time-icon" />
|
|
||||||
<div class="poll-left-time-title">离结束</div>
|
|
||||||
<div class="poll-left-time-value">{{ countdown }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,6 +139,9 @@ const emit = defineEmits(['refresh'])
|
|||||||
const loggedIn = computed(() => authState.loggedIn)
|
const loggedIn = computed(() => authState.loggedIn)
|
||||||
const showPollResult = ref(false)
|
const showPollResult = ref(false)
|
||||||
|
|
||||||
|
const isProposal = computed(() =>
|
||||||
|
Object.prototype.hasOwnProperty.call(props.poll || {}, 'proposedName'),
|
||||||
|
)
|
||||||
const pollParticipants = computed(() => props.poll?.participants || [])
|
const pollParticipants = computed(() => props.poll?.participants || [])
|
||||||
const pollOptionParticipants = computed(() => props.poll?.optionParticipants || {})
|
const pollOptionParticipants = computed(() => props.poll?.optionParticipants || {})
|
||||||
const pollVotes = computed(() => props.poll?.votes || {})
|
const pollVotes = computed(() => props.poll?.votes || {})
|
||||||
@@ -233,6 +245,34 @@ const submitMultiPoll = async () => {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.proposal-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proposal-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proposal-status {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proposal-description {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
.poll-option-button {
|
.poll-option-button {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
@@ -385,12 +425,20 @@ const submitMultiPoll = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.poll-title-section {
|
.poll-title-section {
|
||||||
display: flex;
|
|
||||||
gap: 30px;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poll-title-section-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.poll-option-title {
|
.poll-option-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
Reference in New Issue
Block a user