Compare commits

...

11 Commits

Author SHA1 Message Date
Tim
6a27fbe1d7 Fix null multiple field for poll posts 2025-08-31 14:22:44 +08:00
Tim
38ff04c358 Merge pull request #803 from nagisa77/codex/add-baseswitch-component-to-voting-post
feat(poll): use BaseSwitch for multiple selection
2025-08-31 14:13:32 +08:00
Tim
fc27200ac1 feat(poll): use BaseSwitch for multiple selection 2025-08-31 14:13:18 +08:00
Tim
eefefac236 Merge pull request #801 from nagisa77/codex/add-multi-select-support-for-voting
feat: support multi-option polls
2025-08-31 12:13:54 +08:00
Tim
2f339fdbdb feat: enable multi-option polls 2025-08-31 12:13:41 +08:00
tim
3808becc8b fix: 多选ui 2025-08-31 11:25:34 +08:00
tim
18db4d7317 fix: toolbar 层级修改 2025-08-31 11:14:48 +08:00
Tim
52cbb71945 Merge pull request #800 from nagisa77/codex/refactor-voting-and-lottery-into-components-zk6hvx
refactor: extract poll and lottery components
2025-08-31 11:10:46 +08:00
Tim
39c34a9048 feat: add PostPoll and PostLottery components 2025-08-31 11:10:20 +08:00
tim
4baabf2224 Revert "refactor: extract poll and lottery sections"
This reverts commit 27efc493b2.
2025-08-31 11:09:22 +08:00
Tim
8023183bc6 Merge pull request #799 from nagisa77/codex/refactor-voting-and-lottery-into-components
refactor: extract poll and lottery sections
2025-08-31 11:08:05 +08:00
15 changed files with 316 additions and 115 deletions

View File

@@ -44,7 +44,7 @@ public class PostController {
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(), req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
req.getPrizeCount(), req.getPointCost(), req.getPrizeCount(), req.getPointCost(),
req.getStartTime(), req.getEndTime(), req.getStartTime(), req.getEndTime(),
req.getOptions()); req.getOptions(), req.getMultiple());
draftService.deleteDraft(auth.getName()); draftService.deleteDraft(auth.getName());
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName()); PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
dto.setReward(levelService.awardForPost(auth.getName())); dto.setReward(levelService.awardForPost(auth.getName()));
@@ -94,7 +94,7 @@ public class PostController {
} }
@PostMapping("/{id}/poll/vote") @PostMapping("/{id}/poll/vote")
public ResponseEntity<Void> vote(@PathVariable Long id, @RequestParam("option") int option, Authentication auth) { public ResponseEntity<Void> vote(@PathVariable Long id, @RequestParam("option") List<Integer> option, Authentication auth) {
postService.votePoll(id, auth.getName(), option); postService.votePoll(id, auth.getName(), option);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }

View File

@@ -13,4 +13,5 @@ public class PollDto {
private LocalDateTime endTime; private LocalDateTime endTime;
private List<AuthorDto> participants; private List<AuthorDto> participants;
private Map<Integer, List<AuthorDto>> optionParticipants; private Map<Integer, List<AuthorDto>> optionParticipants;
private boolean multiple;
} }

View File

@@ -28,5 +28,6 @@ public class PostRequest {
private LocalDateTime endTime; private LocalDateTime endTime;
// fields for poll posts // fields for poll posts
private List<String> options; private List<String> options;
private Boolean multiple;
} }

View File

@@ -111,6 +111,7 @@ public class PostMapper {
.collect(Collectors.groupingBy(PollVote::getOptionIndex, .collect(Collectors.groupingBy(PollVote::getOptionIndex,
Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList()))); Collectors.mapping(v -> userMapper.toAuthorDto(v.getUser()), Collectors.toList())));
p.setOptionParticipants(optionParticipants); p.setOptionParticipants(optionParticipants);
p.setMultiple(Boolean.TRUE.equals(pp.getMultiple()));
dto.setPoll(p); dto.setPoll(p);
} }
} }

View File

@@ -32,6 +32,9 @@ public class PollPost extends Post {
inverseJoinColumns = @JoinColumn(name = "user_id")) inverseJoinColumns = @JoinColumn(name = "user_id"))
private Set<User> participants = new HashSet<>(); private Set<User> participants = new HashSet<>();
@Column
private Boolean multiple = false;
@Column @Column
private LocalDateTime endTime; private LocalDateTime endTime;

View File

@@ -6,7 +6,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@Entity @Entity
@Table(name = "poll_votes", uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "user_id"})) @Table(name = "poll_votes", uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "user_id", "option_index"}))
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor

View File

@@ -186,7 +186,8 @@ public class PostService {
Integer pointCost, Integer pointCost,
LocalDateTime startTime, LocalDateTime startTime,
LocalDateTime endTime, LocalDateTime endTime,
java.util.List<String> options) { java.util.List<String> options,
Boolean multiple) {
long recent = postRepository.countByAuthorAfter(username, long recent = postRepository.countByAuthorAfter(username,
java.time.LocalDateTime.now().minusMinutes(5)); java.time.LocalDateTime.now().minusMinutes(5));
if (recent >= 1) { if (recent >= 1) {
@@ -227,6 +228,7 @@ public class PostService {
PollPost pp = new PollPost(); PollPost pp = new PollPost();
pp.setOptions(options); pp.setOptions(options);
pp.setEndTime(endTime); pp.setEndTime(endTime);
pp.setMultiple(multiple != null && multiple);
post = pp; post = pp;
} else { } else {
post = new Post(); post = new Post();
@@ -302,7 +304,7 @@ public class PostService {
} }
@Transactional @Transactional
public PollPost votePoll(Long postId, String username, int optionIndex) { public PollPost votePoll(Long postId, String username, java.util.List<Integer> optionIndices) {
PollPost post = pollPostRepository.findById(postId) PollPost post = pollPostRepository.findById(postId)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
if (post.getEndTime() != null && post.getEndTime().isBefore(LocalDateTime.now())) { if (post.getEndTime() != null && post.getEndTime().isBefore(LocalDateTime.now())) {
@@ -313,16 +315,24 @@ public class PostService {
if (post.getParticipants().contains(user)) { if (post.getParticipants().contains(user)) {
throw new IllegalArgumentException("User already voted"); throw new IllegalArgumentException("User already voted");
} }
if (optionIndex < 0 || optionIndex >= post.getOptions().size()) { if (optionIndices == null || optionIndices.isEmpty()) {
throw new IllegalArgumentException("Invalid option"); throw new IllegalArgumentException("No options selected");
}
java.util.Set<Integer> unique = new java.util.HashSet<>(optionIndices);
for (int optionIndex : unique) {
if (optionIndex < 0 || optionIndex >= post.getOptions().size()) {
throw new IllegalArgumentException("Invalid option");
}
} }
post.getParticipants().add(user); post.getParticipants().add(user);
post.getVotes().merge(optionIndex, 1, Integer::sum); for (int optionIndex : unique) {
PollVote vote = new PollVote(); post.getVotes().merge(optionIndex, 1, Integer::sum);
vote.setPost(post); PollVote vote = new PollVote();
vote.setUser(user); vote.setPost(post);
vote.setOptionIndex(optionIndex); vote.setUser(user);
pollVoteRepository.save(vote); vote.setOptionIndex(optionIndex);
pollVoteRepository.save(vote);
}
PollPost saved = pollPostRepository.save(post); PollPost saved = pollPostRepository.save(post);
if (post.getAuthor() != null && !post.getAuthor().getId().equals(user.getId())) { if (post.getAuthor() != null && !post.getAuthor().getId().equals(user.getId())) {
notificationService.createNotification(post.getAuthor(), NotificationType.POLL_VOTE, post, null, null, user, null, null); notificationService.createNotification(post.getAuthor(), NotificationType.POLL_VOTE, post, null, null, user, null, null);

View File

@@ -76,7 +76,7 @@ class PostControllerTest {
post.setTags(Set.of(tag)); post.setTags(Set.of(tag));
when(postService.createPost(eq("alice"), eq(1L), eq("t"), eq("c"), eq(List.of(1L)), when(postService.createPost(eq("alice"), eq(1L), eq("t"), eq("c"), eq(List.of(1L)),
isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull())).thenReturn(post); isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull())).thenReturn(post);
when(postService.viewPost(eq(1L), any())).thenReturn(post); when(postService.viewPost(eq(1L), any())).thenReturn(post);
when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of()); when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of());
when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of()); when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of());
@@ -187,7 +187,7 @@ class PostControllerTest {
.andExpect(status().isBadRequest()); .andExpect(status().isBadRequest());
verify(postService, never()).createPost(any(), any(), any(), any(), any(), verify(postService, never()).createPost(any(), any(), any(), any(), any(),
any(), any(), any(), any(), any(), any(), any(), any()); any(), any(), any(), any(), any(), any(), any(), any(), any());
} }
@Test @Test

View File

@@ -146,7 +146,7 @@ class PostServiceTest {
assertThrows(RateLimitException.class, assertThrows(RateLimitException.class,
() -> service.createPost("alice", 1L, "t", "c", List.of(1L), () -> service.createPost("alice", 1L, "t", "c", List.of(1L),
null, null, null, null, null, null, null, null)); null, null, null, null, null, null, null, null, null));
} }
@Test @Test

View File

@@ -93,7 +93,7 @@ body {
.vditor-toolbar--pin { .vditor-toolbar--pin {
top: calc(var(--header-height) + 1px) !important; top: calc(var(--header-height) + 1px) !important;
z-index: 2000; z-index: 20;
} }
.vditor-panel { .vditor-panel {

View File

@@ -18,6 +18,10 @@
<flat-pickr v-model="data.endTime" :config="dateConfig" class="time-picker" /> <flat-pickr v-model="data.endTime" :config="dateConfig" class="time-picker" />
</client-only> </client-only>
</div> </div>
<div class="poll-multiple-row">
<span class="poll-row-title">多选</span>
<BaseSwitch v-model="data.multiple" />
</div>
</div> </div>
</template> </template>
@@ -25,6 +29,7 @@
import 'flatpickr/dist/flatpickr.css' import 'flatpickr/dist/flatpickr.css'
import FlatPickr from 'vue-flatpickr-component' import FlatPickr from 'vue-flatpickr-component'
import BaseInput from '~/components/BaseInput.vue' import BaseInput from '~/components/BaseInput.vue'
import BaseSwitch from '~/components/BaseSwitch.vue'
const props = defineProps({ const props = defineProps({
data: { data: {
@@ -80,6 +85,11 @@ const removeOption = (idx) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.poll-multiple-row {
display: flex;
align-items: center;
gap: 10px;
}
.time-picker { .time-picker {
max-width: 200px; max-width: 200px;
height: 30px; height: 30px;

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="post-prize-container"> <div class="post-prize-container" v-if="lottery">
<div class="prize-content"> <div class="prize-content">
<div class="prize-info"> <div class="prize-info">
<div class="prize-info-left"> <div class="prize-info-left">
@@ -79,30 +79,20 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, watch, onBeforeUnmount } from 'vue' import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { toast } from '~/main'
import { getToken, authState } from '~/utils/auth' import { getToken, authState } from '~/utils/auth'
import { toast } from '~/main'
import { useRuntimeConfig } from '#imports'
import { useIsMobile } from '~/utils/screen' import { useIsMobile } from '~/utils/screen'
const props = defineProps({ const props = defineProps({
lottery: { lottery: { type: Object, required: true },
type: Object, postId: { type: [String, Number], required: true },
required: true,
},
postId: {
type: [String, Number],
required: true,
},
}) })
const emit = defineEmits(['refresh']) const emit = defineEmits(['refresh'])
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
const loggedIn = computed(() => authState.loggedIn)
const isMobile = useIsMobile() const isMobile = useIsMobile()
const loggedIn = computed(() => authState.loggedIn)
const lotteryParticipants = computed(() => props.lottery?.participants || []) const lotteryParticipants = computed(() => props.lottery?.participants || [])
const lotteryWinners = computed(() => props.lottery?.winners || []) const lotteryWinners = computed(() => props.lottery?.winners || [])
const lotteryEnded = computed(() => { const lotteryEnded = computed(() => {
@@ -115,8 +105,7 @@ const hasJoined = computed(() => {
}) })
const countdown = ref('00:00:00') const countdown = ref('00:00:00')
let countdownTimer = null let timer = null
const updateCountdown = () => { const updateCountdown = () => {
if (!props.lottery || !props.lottery.endTime) { if (!props.lottery || !props.lottery.endTime) {
countdown.value = '00:00:00' countdown.value = '00:00:00'
@@ -125,9 +114,9 @@ const updateCountdown = () => {
const diff = new Date(props.lottery.endTime).getTime() - Date.now() const diff = new Date(props.lottery.endTime).getTime() - Date.now()
if (diff <= 0) { if (diff <= 0) {
countdown.value = '00:00:00' countdown.value = '00:00:00'
if (countdownTimer) { if (timer) {
clearInterval(countdownTimer) clearInterval(timer)
countdownTimer = null timer = null
} }
return return
} }
@@ -136,30 +125,28 @@ const updateCountdown = () => {
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0') const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0')
countdown.value = `${h}:${m}:${s}` countdown.value = `${h}:${m}:${s}`
} }
const startCountdown = () => { const startCountdown = () => {
if (!import.meta.client) return
if (countdownTimer) clearInterval(countdownTimer)
updateCountdown() updateCountdown()
countdownTimer = setInterval(updateCountdown, 1000) if (timer) clearInterval(timer)
timer = setInterval(updateCountdown, 1000)
} }
watch( watch(
() => props.lottery?.endTime, () => props.lottery?.endTime,
() => { () => {
if (props.lottery && props.lottery.endTime) { if (props.lottery && props.lottery.endTime) startCountdown()
startCountdown()
}
}, },
{ immediate: true },
) )
onMounted(() => {
if (props.lottery && props.lottery.endTime) startCountdown()
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (countdownTimer) clearInterval(countdownTimer) if (timer) clearInterval(timer)
}) })
const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true }) const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true })
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
const joinLottery = async () => { const joinLottery = async () => {
const token = getToken() const token = getToken()
if (!token) { if (!token) {
@@ -313,4 +300,11 @@ const joinLottery = async () => {
font-size: 13px; font-size: 13px;
opacity: 0.7; opacity: 0.7;
} }
@media (max-width: 768px) {
.join-prize-button,
.join-prize-button-disabled {
margin-left: 0;
}
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="post-poll-container"> <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 v-if="showPollResult || pollEnded || hasVoted"> <div v-if="showPollResult || pollEnded || hasVoted">
@@ -29,15 +29,42 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<div <template v-if="poll.multiple">
v-for="(opt, idx) in poll.options" <div
:key="idx" v-for="(opt, idx) in poll.options"
class="poll-option" :key="idx"
@click="voteOption(idx)" class="poll-option"
> @click="toggleOption(idx)"
<input type="radio" :checked="false" name="poll-option" class="poll-option-input" /> >
<span class="poll-option-text">{{ opt }}</span> <input
</div> type="checkbox"
:checked="selectedOptions.includes(idx)"
class="poll-option-input"
/>
<span class="poll-option-text">{{ opt }}</span>
</div>
<div class="multi-selection-container">
<div class="multi-selection-title">
<i class="fas fa-info-circle info-icon"></i>
该投票为多选
</div>
<div class="join-poll-button" @click="submitMultiPoll">
<i class="fas fa-plus"></i> 加入投票
</div>
</div>
</template>
<template v-else>
<div
v-for="(opt, idx) in poll.options"
:key="idx"
class="poll-option"
@click="voteOption(idx)"
>
<input type="radio" :checked="false" name="poll-option" class="poll-option-input" />
<span class="poll-option-text">{{ opt }}</span>
</div>
</template>
</div> </div>
</div> </div>
<div class="poll-info"> <div class="poll-info">
@@ -70,26 +97,18 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, watch, onBeforeUnmount } from 'vue' import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { toast } from '~/main'
import { getToken, authState } from '~/utils/auth' import { getToken, authState } from '~/utils/auth'
import { toast } from '~/main'
import { useRuntimeConfig } from '#imports'
const props = defineProps({ const props = defineProps({
poll: { poll: { type: Object, required: true },
type: Object, postId: { type: [String, Number], required: true },
required: true,
},
postId: {
type: [String, Number],
required: true,
},
}) })
const emit = defineEmits(['refresh']) const emit = defineEmits(['refresh'])
const config = useRuntimeConfig() const loggedIn = computed(() => authState.loggedIn)
const API_BASE_URL = config.public.apiBaseUrl
const showPollResult = ref(false) const showPollResult = ref(false)
const pollParticipants = computed(() => props.poll?.participants || []) const pollParticipants = computed(() => props.poll?.participants || [])
@@ -109,17 +128,15 @@ const pollEnded = computed(() => {
return new Date(props.poll.endTime).getTime() <= Date.now() return new Date(props.poll.endTime).getTime() <= Date.now()
}) })
const hasVoted = computed(() => { const hasVoted = computed(() => {
if (!authState.loggedIn) return false if (!loggedIn.value) return false
return pollParticipants.value.some((p) => p.id === Number(authState.userId)) return pollParticipants.value.some((p) => p.id === Number(authState.userId))
}) })
watch([hasVoted, pollEnded], ([voted, ended]) => { watch([hasVoted, pollEnded], ([voted, ended]) => {
if (voted || ended) showPollResult.value = true if (voted || ended) showPollResult.value = true
}) })
const countdown = ref('00:00:00') const countdown = ref('00:00:00')
let countdownTimer = null let timer = null
const updateCountdown = () => { const updateCountdown = () => {
if (!props.poll || !props.poll.endTime) { if (!props.poll || !props.poll.endTime) {
countdown.value = '00:00:00' countdown.value = '00:00:00'
@@ -128,9 +145,9 @@ const updateCountdown = () => {
const diff = new Date(props.poll.endTime).getTime() - Date.now() const diff = new Date(props.poll.endTime).getTime() - Date.now()
if (diff <= 0) { if (diff <= 0) {
countdown.value = '00:00:00' countdown.value = '00:00:00'
if (countdownTimer) { if (timer) {
clearInterval(countdownTimer) clearInterval(timer)
countdownTimer = null timer = null
} }
return return
} }
@@ -139,30 +156,28 @@ const updateCountdown = () => {
const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0') const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0')
countdown.value = `${h}:${m}:${s}` countdown.value = `${h}:${m}:${s}`
} }
const startCountdown = () => { const startCountdown = () => {
if (!import.meta.client) return
if (countdownTimer) clearInterval(countdownTimer)
updateCountdown() updateCountdown()
countdownTimer = setInterval(updateCountdown, 1000) if (timer) clearInterval(timer)
timer = setInterval(updateCountdown, 1000)
} }
watch( watch(
() => props.poll?.endTime, () => props.poll?.endTime,
() => { () => {
if (props.poll && props.poll.endTime) { if (props.poll && props.poll.endTime) startCountdown()
startCountdown()
}
}, },
{ immediate: true },
) )
onMounted(() => {
if (props.poll && props.poll.endTime) startCountdown()
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (countdownTimer) clearInterval(countdownTimer) if (timer) clearInterval(timer)
}) })
const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true }) const gotoUser = (id) => navigateTo(`/users/${id}`, { replace: true })
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
const voteOption = async (idx) => { const voteOption = async (idx) => {
const token = getToken() const token = getToken()
if (!token) { if (!token) {
@@ -182,9 +197,53 @@ const voteOption = async (idx) => {
toast.error(data.error || '操作失败') toast.error(data.error || '操作失败')
} }
} }
const selectedOptions = ref([])
const toggleOption = (idx) => {
const i = selectedOptions.value.indexOf(idx)
if (i >= 0) {
selectedOptions.value.splice(i, 1)
} else {
selectedOptions.value.push(idx)
}
}
const submitMultiPoll = async () => {
const token = getToken()
if (!token) {
toast.error('请先登录')
return
}
if (!selectedOptions.value.length) {
toast.error('请选择至少一个选项')
return
}
const params = selectedOptions.value.map((o) => `option=${o}`).join('&')
const res = await fetch(`${API_BASE_URL}/api/posts/${props.postId}/poll/vote?${params}`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json().catch(() => ({}))
if (res.ok) {
toast.success('投票成功')
emit('refresh')
showPollResult.value = true
} else {
toast.error(data.error || '操作失败')
}
}
</script> </script>
<style scoped> <style scoped>
.post-poll-container {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
background-color: var(--lottery-background-color);
border-radius: 10px;
padding: 10px;
}
.poll-option-button { .poll-option-button {
color: var(--text-color); color: var(--text-color);
padding: 5px 10px; padding: 5px 10px;
@@ -206,6 +265,7 @@ const voteOption = async (idx) => {
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;
flex: 4; flex: 4;
border-right: 1px solid var(--normal-border-color);
} }
.poll-info { .poll-info {
@@ -267,12 +327,14 @@ const voteOption = async (idx) => {
.poll-left-time { .poll-left-time {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center;
justify-content: center;
gap: 5px;
} }
.poll-left-time-title { .poll-left-time-title {
font-size: 13px; font-size: 13px;
opacity: 0.7; opacity: 0.7;
margin-right: 5px;
} }
.poll-left-time-value { .poll-left-time-value {
@@ -281,21 +343,6 @@ const voteOption = async (idx) => {
color: var(--primary-color); color: var(--primary-color);
} }
.post-poll-container {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
background-color: var(--lottery-background-color);
border-radius: 10px;
padding: 10px;
}
.poll-question {
font-weight: bold;
margin-bottom: 10px;
}
.poll-option-progress { .poll-option-progress {
position: relative; position: relative;
background-color: rgb(187, 187, 187); background-color: rgb(187, 187, 187);
@@ -321,11 +368,32 @@ const voteOption = async (idx) => {
color: var(--text-color); color: var(--text-color);
} }
.poll-vote-button { .multi-selection-container {
margin-top: 5px; padding: 20px 15px 20px 5px;
color: var(--primary-color); display: flex;
flex-direction: row;
justify-content: space-between;
}
.multi-selection-title {
font-size: 13px;
color: var(--text-color);
}
.info-icon {
margin-right: 5px;
}
.join-poll-button {
padding: 5px 10px;
background-color: var(--primary-color);
color: white;
border-radius: 8px;
cursor: pointer; cursor: pointer;
width: fit-content; }
.join-poll-button:hover {
background-color: var(--primary-color-hover);
} }
.poll-participants { .poll-participants {

View File

@@ -74,6 +74,7 @@ const lottery = reactive({
const poll = reactive({ const poll = reactive({
options: ['', ''], options: ['', ''],
endTime: null, endTime: null,
multiple: false,
}) })
const startTime = ref(null) const startTime = ref(null)
const isWaitingPosting = ref(false) const isWaitingPosting = ref(false)
@@ -121,6 +122,7 @@ const clearPost = async () => {
startTime.value = null startTime.value = null
poll.options = ['', ''] poll.options = ['', '']
poll.endTime = null poll.endTime = null
poll.multiple = false
// 删除草稿 // 删除草稿
const token = getToken() const token = getToken()
@@ -318,6 +320,7 @@ const submitPost = async () => {
prizeCount: postType.value === 'LOTTERY' ? lottery.prizeCount : undefined, prizeCount: postType.value === 'LOTTERY' ? lottery.prizeCount : undefined,
prizeDescription: postType.value === 'LOTTERY' ? lottery.prizeDescription : undefined, prizeDescription: postType.value === 'LOTTERY' ? lottery.prizeDescription : undefined,
options: postType.value === 'POLL' ? poll.options : undefined, options: postType.value === 'POLL' ? poll.options : undefined,
multiple: postType.value === 'POLL' ? poll.multiple : undefined,
startTime: startTime:
postType.value === 'LOTTERY' ? new Date(startTime.value).toISOString() : undefined, postType.value === 'LOTTERY' ? new Date(startTime.value).toISOString() : undefined,
pointCost: postType.value === 'LOTTERY' ? lottery.pointCost : undefined, pointCost: postType.value === 'LOTTERY' ? lottery.pointCost : undefined,

View File

@@ -1032,7 +1032,120 @@ onMounted(async () => {
cursor: pointer; cursor: pointer;
} }
.action-menu-icon {
cursor: pointer;
font-size: 18px;
padding: 5px;
}
.article-info-container {
display: flex;
flex-direction: row;
margin-top: 10px;
gap: 10px;
align-items: center;
}
.info-content-container {
display: flex;
flex-direction: row;
gap: 10px;
padding: 0px;
border-bottom: 1px solid var(--normal-border-color);
}
.user-avatar-container {
cursor: pointer;
}
.user-avatar-item {
width: 50px;
height: 50px;
}
.user-avatar-item-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.info-content {
display: flex;
flex-direction: column;
gap: 3px;
width: 100%;
}
.info-content-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.user-name {
font-size: 16px;
font-weight: bold;
opacity: 0.7;
}
.user-medal {
font-size: 12px;
margin-left: 4px;
opacity: 0.6;
cursor: pointer;
text-decoration: none;
color: var(--text-color);
}
.post-time {
font-size: 14px;
opacity: 0.5;
}
.info-content-text {
font-size: 16px;
line-height: 1.5;
}
.article-footer-container {
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 0px;
}
.reactions-viewer {
display: flex;
flex-direction: row;
gap: 20px;
align-items: center;
}
.reactions-viewer-item-container {
display: flex;
flex-direction: row;
gap: 2px;
align-items: center;
}
.reactions-viewer-item {
font-size: 16px;
}
.make-reaction-container {
display: flex;
flex-direction: row;
gap: 10px;
}
.copy-link:hover {
background-color: #e2e2e2;
}
.comment-editor-wrapper {
position: relative;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.post-page-main-container { .post-page-main-container {
@@ -1084,8 +1197,5 @@ onMounted(async () => {
.loading-container { .loading-container {
width: 100%; width: 100%;
} }
margin-left: 0;
}
} }
</style> </style>