feat: 添加分类提案功能,包括提案表单和相关后端逻辑

This commit is contained in:
sivdead
2025-09-25 17:00:17 +08:00
parent 4fc7c861ee
commit 76962d6d1c
15 changed files with 485 additions and 19 deletions

View File

@@ -16,6 +16,7 @@ JWT_EXPIRATION=2592000000
# === Redis ===
REDIS_HOST=<Redis 地址>
REDIS_PORT=<Redis 端口>
REDIS_PASS=<Redis 密码>
# === Resend ===
RESEND_API_KEY=<你的resend-api-key>

View File

@@ -73,7 +73,12 @@ public class PostController {
req.getStartTime(),
req.getEndTime(),
req.getOptions(),
req.getMultiple()
req.getMultiple(),
req.getProposedName(),
req.getProposedSlug(),
req.getProposalDescription(),
req.getApproveThreshold(),
req.getQuorum()
);
draftService.deleteDraft(auth.getName());
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());

View File

@@ -28,4 +28,11 @@ public class PostRequest {
// fields for poll posts
private List<String> options;
private Boolean multiple;
// fields for category proposal posts
private String proposedName;
private String proposedSlug;
private String proposalDescription;
private Integer approveThreshold;
private Integer quorum;
}

View File

@@ -0,0 +1,65 @@
package com.openisle.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Index;
import jakarta.persistence.PrimaryKeyJoinColumn;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
/**
* A specialized post type used for proposing new categories.
* It reuses poll mechanics (participants, votes, endTime) by extending PollPost.
*/
@Entity
@Table(name = "category_proposal_posts", indexes = {
@Index(name = "idx_category_proposal_posts_status", columnList = "status"),
@Index(name = "idx_category_proposal_posts_slug", columnList = "proposed_slug", unique = true)
})
@Getter
@Setter
@NoArgsConstructor
@PrimaryKeyJoinColumn(name = "post_id")
public class CategoryProposalPost extends PollPost {
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private CategoryProposalStatus proposalStatus = CategoryProposalStatus.PENDING;
@Column(name = "proposed_name", nullable = false)
private String proposedName;
@Column(name = "proposed_slug", nullable = false, unique = true)
private String proposedSlug;
@Column(name = "description")
private String description;
// Approval threshold as percentage (0-100), default 60
@Column(name = "approve_threshold", nullable = false)
private int approveThreshold = 60;
// Minimum number of participants required to meet quorum
@Column(name = "quorum", nullable = false)
private int quorum = 10;
// Optional voting start time (end time inherited from PollPost)
@Column(name = "start_at")
private LocalDateTime startAt;
// Snapshot of poll results at finalization (e.g., JSON)
@Column(name = "result_snapshot", columnDefinition = "TEXT")
private String resultSnapshot;
// Reason when proposal is rejected
@Column(name = "reject_reason")
private String rejectReason;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.model;
public enum CategoryProposalStatus {
PENDING,
APPROVED,
REJECTED
}

View File

@@ -4,4 +4,5 @@ public enum PostType {
NORMAL,
LOTTERY,
POLL,
PROPOSAL
}

View File

@@ -0,0 +1,18 @@
package com.openisle.repository;
import com.openisle.model.CategoryProposalPost;
import com.openisle.model.CategoryProposalStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDateTime;
import java.util.List;
public interface CategoryProposalPostRepository extends JpaRepository<CategoryProposalPost, Long> {
List<CategoryProposalPost> findByEndTimeAfterAndProposalStatus(LocalDateTime now, CategoryProposalStatus status);
List<CategoryProposalPost> findByEndTimeBeforeAndProposalStatus(LocalDateTime now, CategoryProposalStatus status);
boolean existsByProposedSlug(String proposedSlug);
}

View File

@@ -1,6 +1,30 @@
package com.openisle.service;
import com.openisle.config.CachingConfig;
import com.openisle.exception.NotFoundException;
import com.openisle.exception.RateLimitException;
import com.openisle.model.*;
import com.openisle.repository.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import com.openisle.exception.RateLimitException;
import com.openisle.mapper.PostMapper;
import com.openisle.model.*;
@@ -26,22 +50,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@Slf4j
@Service
@@ -53,6 +61,7 @@ public class PostService {
private final TagRepository tagRepository;
private final LotteryPostRepository lotteryPostRepository;
private final PollPostRepository pollPostRepository;
private final CategoryProposalPostRepository categoryProposalPostRepository;
private final PollVoteRepository pollVoteRepository;
private PublishMode publishMode;
private final NotificationService notificationService;
@@ -86,6 +95,7 @@ public class PostService {
TagRepository tagRepository,
LotteryPostRepository lotteryPostRepository,
PollPostRepository pollPostRepository,
CategoryProposalPostRepository categoryProposalPostRepository,
PollVoteRepository pollVoteRepository,
NotificationService notificationService,
SubscriptionService subscriptionService,
@@ -155,6 +165,19 @@ public class PostService {
for (PollPost pp : pollPostRepository.findByEndTimeBeforeAndResultAnnouncedFalse(now)) {
applicationContext.getBean(PostService.class).finalizePoll(pp.getId());
}
for (CategoryProposalPost cp : categoryProposalPostRepository
.findByEndTimeAfterAndProposalStatus(now, CategoryProposalStatus.PENDING)) {
if (cp.getEndTime() != null) {
ScheduledFuture<?> future = taskScheduler.schedule(
() -> applicationContext.getBean(PostService.class).finalizeProposal(cp.getId()),
java.util.Date.from(cp.getEndTime().atZone(ZoneId.systemDefault()).toInstant()));
scheduledFinalizations.put(cp.getId(), future);
}
}
for (CategoryProposalPost cp : categoryProposalPostRepository
.findByEndTimeBeforeAndProposalStatus(now, CategoryProposalStatus.PENDING)) {
applicationContext.getBean(PostService.class).finalizeProposal(cp.getId());
}
}
public PublishMode getPublishMode() {
@@ -227,7 +250,12 @@ public class PostService {
LocalDateTime startTime,
LocalDateTime endTime,
java.util.List<String> options,
Boolean multiple
Boolean multiple,
String proposedName,
String proposedSlug,
String proposalDescription,
Integer approveThreshold,
Integer quorum
) {
// 限制访问次数
boolean limitResult = postRateLimit(username);
@@ -273,6 +301,42 @@ public class PostService {
pp.setEndTime(endTime);
pp.setMultiple(multiple != null && multiple);
post = pp;
} else if (actualType == PostType.PROPOSAL) {
CategoryProposalPost cp = new CategoryProposalPost();
if (proposedName == null || proposedName.isBlank()) {
throw new IllegalArgumentException("Proposed name required");
}
if (proposedSlug == null || proposedSlug.isBlank()) {
throw new IllegalArgumentException("Proposed slug required");
}
if (categoryProposalPostRepository.existsByProposedSlug(proposedSlug)) {
throw new IllegalArgumentException("Proposed slug already exists: " + proposedSlug);
}
cp.setProposedName(proposedName);
cp.setProposedSlug(proposedSlug);
cp.setDescription(proposalDescription);
if (approveThreshold != null) {
if (approveThreshold < 0 || approveThreshold > 100) {
throw new IllegalArgumentException("approveThreshold must be between 0 and 100");
}
cp.setApproveThreshold(approveThreshold);
}
if (quorum != null) {
if (quorum < 0) {
throw new IllegalArgumentException("quorum must be >= 0");
}
cp.setQuorum(quorum);
}
cp.setStartAt(startTime);
cp.setEndTime(endTime);
// default yes/no options if not provided
if (options == null || options.size() < 2) {
cp.setOptions(List.of("同意", "反对"));
} else {
cp.setOptions(options);
}
cp.setMultiple(false);
post = cp;
} else {
post = new Post();
}
@@ -285,6 +349,8 @@ public class PostService {
post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED);
if (post instanceof LotteryPost) {
post = lotteryPostRepository.save((LotteryPost) post);
}else if (post instanceof CategoryProposalPost categoryProposalPost) {
post = categoryProposalPostRepository.save(categoryProposalPost);
} else if (post instanceof PollPost) {
post = pollPostRepository.save((PollPost) post);
} else {
@@ -339,6 +405,11 @@ public class PostService {
java.util.Date.from(lp.getEndTime().atZone(ZoneId.systemDefault()).toInstant())
);
scheduledFinalizations.put(lp.getId(), future);
} else if (post instanceof CategoryProposalPost cp && cp.getEndTime() != null) {
ScheduledFuture<?> future = taskScheduler.schedule(
() -> applicationContext.getBean(PostService.class).finalizeProposal(cp.getId()),
java.util.Date.from(cp.getEndTime().atZone(ZoneId.systemDefault()).toInstant()));
scheduledFinalizations.put(cp.getId(), future);
} else if (post instanceof PollPost pp && pp.getEndTime() != null) {
ScheduledFuture<?> future = taskScheduler.schedule(
() -> applicationContext.getBean(PostService.class).finalizePoll(pp.getId()),
@@ -349,6 +420,50 @@ public class PostService {
return post;
}
@CacheEvict(
value = CachingConfig.POST_CACHE_NAME, allEntries = true
)
@Transactional
public void finalizeProposal(Long postId) {
scheduledFinalizations.remove(postId);
categoryProposalPostRepository.findById(postId).ifPresent(cp -> {
if (cp.getProposalStatus() != CategoryProposalStatus.PENDING) {
return;
}
int totalParticipants = cp.getParticipants() != null ? cp.getParticipants().size() : 0;
int approveVotes = 0;
if (cp.getVotes() != null) {
approveVotes = cp.getVotes().getOrDefault(0, 0);
}
boolean quorumMet = totalParticipants >= cp.getQuorum();
int approvePercent = totalParticipants > 0 ? (approveVotes * 100) / totalParticipants : 0;
boolean thresholdMet = approvePercent >= cp.getApproveThreshold();
if (quorumMet && thresholdMet) {
cp.setProposalStatus(CategoryProposalStatus.APPROVED);
} else {
cp.setProposalStatus(CategoryProposalStatus.REJECTED);
String reason;
if (!quorumMet && !thresholdMet) {
reason = "未达到法定人数且赞成率不足";
} else if (!quorumMet) {
reason = "未达到法定人数";
} else {
reason = "赞成率不足";
}
cp.setRejectReason(reason);
}
cp.setResultSnapshot("approveVotes=" + approveVotes + ", totalParticipants=" + totalParticipants + ", approvePercent=" + approvePercent);
categoryProposalPostRepository.save(cp);
if (cp.getAuthor() != null) {
notificationService.createNotification(cp.getAuthor(), NotificationType.POLL_RESULT_OWNER, cp, null, null, null, null, null);
}
for (User participant : cp.getParticipants()) {
notificationService.createNotification(participant, NotificationType.POLL_RESULT_PARTICIPANT, cp, null, null, null, null, null);
}
postChangeLogService.recordVoteResult(cp);
});
}
/**
* 限制发帖频率
* @param username

View File

@@ -13,6 +13,7 @@ spring.jpa.hibernate.ddl-auto=update
spring.data.redis.host=${REDIS_HOST:localhost}
spring.data.redis.port=${REDIS_PORT:6379}
spring.data.redis.database=${REDIS_DATABASE:0}
spring.data.redis.password=${REDIS_PASS: null}
# for jwt
app.jwt.secret=${JWT_SECRET:jwt_sec}

View File

@@ -0,0 +1,25 @@
-- Create table for category proposal posts (subclass of poll_posts)
CREATE TABLE IF NOT EXISTS category_proposal_posts (
post_id BIGINT NOT NULL,
status VARCHAR(50) NOT NULL,
proposed_name VARCHAR(255) NOT NULL,
proposed_slug VARCHAR(255) NOT NULL,
description VARCHAR(255),
approve_threshold INT NOT NULL DEFAULT 60,
quorum INT NOT NULL DEFAULT 10,
start_at DATETIME(6) NULL,
result_snapshot LONGTEXT NULL,
reject_reason VARCHAR(255),
PRIMARY KEY (post_id),
CONSTRAINT fk_category_proposal_posts_parent
FOREIGN KEY (post_id) REFERENCES poll_posts (post_id)
);
CREATE INDEX IF NOT EXISTS idx_category_proposal_posts_status
ON category_proposal_posts (status);
CREATE UNIQUE INDEX IF NOT EXISTS idx_category_proposal_posts_slug
ON category_proposal_posts (proposed_slug);

View File

@@ -117,6 +117,11 @@ class PostControllerTest {
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull(),
isNull()
)
).thenReturn(post);
@@ -266,6 +271,10 @@ class PostControllerTest {
any(),
any(),
any(),
any(),
any(),
any(),
any(),
any()
);
}

View File

@@ -25,6 +25,7 @@ class PostServiceTest {
TagRepository tagRepo = mock(TagRepository.class);
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
CategoryProposalPostRepository proposalRepo = mock(CategoryProposalPostRepository.class);
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
NotificationService notifService = mock(NotificationService.class);
SubscriptionService subService = mock(SubscriptionService.class);
@@ -50,6 +51,7 @@ class PostServiceTest {
tagRepo,
lotteryRepo,
pollPostRepo,
proposalRepo,
pollVoteRepo,
notifService,
subService,
@@ -101,6 +103,7 @@ class PostServiceTest {
TagRepository tagRepo = mock(TagRepository.class);
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
CategoryProposalPostRepository proposalRepo = mock(CategoryProposalPostRepository.class);
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
NotificationService notifService = mock(NotificationService.class);
SubscriptionService subService = mock(SubscriptionService.class);
@@ -126,6 +129,7 @@ class PostServiceTest {
tagRepo,
lotteryRepo,
pollPostRepo,
proposalRepo,
pollVoteRepo,
notifService,
subService,
@@ -190,6 +194,7 @@ class PostServiceTest {
TagRepository tagRepo = mock(TagRepository.class);
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
CategoryProposalPostRepository proposalRepo = mock(CategoryProposalPostRepository.class);
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
NotificationService notifService = mock(NotificationService.class);
SubscriptionService subService = mock(SubscriptionService.class);
@@ -215,6 +220,7 @@ class PostServiceTest {
tagRepo,
lotteryRepo,
pollPostRepo,
proposalRepo,
pollVoteRepo,
notifService,
subService,
@@ -253,6 +259,11 @@ class PostServiceTest {
null,
null,
null,
null,
null,
null,
null,
null,
null
)
);
@@ -266,6 +277,7 @@ class PostServiceTest {
TagRepository tagRepo = mock(TagRepository.class);
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
CategoryProposalPostRepository proposalRepo = mock(CategoryProposalPostRepository.class);
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
NotificationService notifService = mock(NotificationService.class);
SubscriptionService subService = mock(SubscriptionService.class);
@@ -291,6 +303,7 @@ class PostServiceTest {
tagRepo,
lotteryRepo,
pollPostRepo,
proposalRepo,
pollVoteRepo,
notifService,
subService,
@@ -358,6 +371,7 @@ class PostServiceTest {
TagRepository tagRepo = mock(TagRepository.class);
LotteryPostRepository lotteryRepo = mock(LotteryPostRepository.class);
PollPostRepository pollPostRepo = mock(PollPostRepository.class);
CategoryProposalPostRepository proposalRepo = mock(CategoryProposalPostRepository.class);
PollVoteRepository pollVoteRepo = mock(PollVoteRepository.class);
NotificationService notifService = mock(NotificationService.class);
SubscriptionService subService = mock(SubscriptionService.class);
@@ -383,6 +397,7 @@ class PostServiceTest {
tagRepo,
lotteryRepo,
pollPostRepo,
proposalRepo,
pollVoteRepo,
notifService,
subService,

View File

@@ -34,6 +34,7 @@ export default {
{ id: 'NORMAL', name: '普通帖子', icon: 'file-text' },
{ id: 'LOTTERY', name: '抽奖帖子', icon: 'gift' },
{ id: 'POLL', name: '投票帖子', icon: 'ranking-list' },
{ id: 'PROPOSAL', name: '分类提案', icon: 'tag-one' },
]
}

View File

@@ -0,0 +1,133 @@
<template>
<div class="proposal-section">
<div class="proposal-row">
<span class="proposal-row-title">拟议分类名称</span>
<BaseInput v-model="data.proposedName" placeholder="请输入分类名称" />
</div>
<div class="proposal-row">
<span class="proposal-row-title">拟议分类 Slug</span>
<BaseInput v-model="data.proposedSlug" placeholder="小写短横线分隔,如: tech-news" />
</div>
<div class="proposal-row">
<span class="proposal-row-title">提案描述</span>
<BaseInput v-model="data.proposalDescription" placeholder="简要说明提案目的与理由" />
</div>
<div class="proposal-row two-col">
<div class="proposal-col">
<span class="proposal-row-title">通过阈值(%)</span>
<input
class="number-input"
type="number"
v-model.number="data.approveThreshold"
min="0"
max="100"
/>
</div>
<div class="proposal-col">
<span class="proposal-row-title">法定最小参与数</span>
<input class="number-input" type="number" v-model.number="data.quorum" min="0" />
</div>
</div>
<div class="proposal-row">
<span class="proposal-row-title">投票结束时间</span>
<client-only>
<flat-pickr v-model="data.endTime" :config="dateConfig" class="time-picker" />
</client-only>
</div>
<div class="proposal-row">
<span class="proposal-row-title">投票选项</span>
<div class="poll-option-item" v-for="(opt, idx) in data.options" :key="idx">
<BaseInput v-model="data.options[idx]" placeholder="选项内容" />
<i
v-if="data.options.length > 2"
class="fa-solid fa-xmark remove-option-icon"
@click="removeOption(idx)"
></i>
</div>
<div class="add-option" @click="addOption">添加选项</div>
</div>
</div>
</template>
<script setup>
import 'flatpickr/dist/flatpickr.css'
import FlatPickr from 'vue-flatpickr-component'
import BaseInput from '~/components/BaseInput.vue'
const props = defineProps({
data: { type: Object, required: true },
})
const dateConfig = { enableTime: true, time_24hr: true, dateFormat: 'Y-m-d H:i' }
const addOption = () => {
props.data.options.push('')
}
const removeOption = (idx) => {
if (props.data.options.length > 2) {
props.data.options.splice(idx, 1)
}
}
</script>
<style scoped>
.proposal-section {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 200px;
}
.proposal-row-title {
font-size: 16px;
color: var(--text-color);
font-weight: bold;
margin-bottom: 10px;
}
.proposal-row {
display: flex;
flex-direction: column;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.proposal-col {
display: flex;
flex-direction: column;
}
.number-input {
max-width: 120px;
height: 30px;
border-radius: 5px;
border: 1px solid var(--border-color);
padding: 0 10px;
font-size: 16px;
color: var(--text-color);
background-color: var(--lottery-background-color);
}
.poll-option-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.remove-option-icon {
cursor: pointer;
}
.add-option {
color: var(--primary-color);
cursor: pointer;
width: fit-content;
margin-top: 5px;
}
.time-picker {
max-width: 200px;
height: 30px;
background-color: var(--lottery-background-color);
border-radius: 5px;
border: 1px solid var(--border-color);
}
</style>

View File

@@ -37,6 +37,7 @@
</div>
<LotteryForm v-if="postType === 'LOTTERY'" :data="lottery" />
<PollForm v-if="postType === 'POLL'" :data="poll" />
<ProposalForm v-if="postType === 'PROPOSAL'" :data="proposal" />
</div>
</div>
</template>
@@ -50,6 +51,7 @@ import PostTypeSelect from '~/components/PostTypeSelect.vue'
import TagSelect from '~/components/TagSelect.vue'
import LotteryForm from '~/components/LotteryForm.vue'
import PollForm from '~/components/PollForm.vue'
import ProposalForm from '~/components/ProposalForm.vue'
import { toast } from '~/main'
import { authState, getToken } from '~/utils/auth'
const config = useRuntimeConfig()
@@ -76,6 +78,15 @@ const poll = reactive({
endTime: null,
multiple: false,
})
const proposal = reactive({
proposedName: '',
proposedSlug: '',
proposalDescription: '',
approveThreshold: 60,
quorum: 10,
endTime: null,
options: ['同意', '反对'],
})
const startTime = ref(null)
const isWaitingPosting = ref(false)
const isAiLoading = ref(false)
@@ -123,6 +134,13 @@ const clearPost = async () => {
poll.options = ['', '']
poll.endTime = null
poll.multiple = false
proposal.proposedName = ''
proposal.proposedSlug = ''
proposal.proposalDescription = ''
proposal.approveThreshold = 60
proposal.quorum = 10
proposal.endTime = null
proposal.options = ['同意', '反对']
// 删除草稿
const token = getToken()
@@ -283,6 +301,32 @@ const submitPost = async () => {
return
}
}
if (postType.value === 'PROPOSAL') {
if (!proposal.proposedName.trim()) {
toast.error('请填写拟议分类名称')
return
}
if (!proposal.proposedSlug.trim()) {
toast.error('请填写拟议分类 Slug')
return
}
if (proposal.approveThreshold < 0 || proposal.approveThreshold > 100) {
toast.error('通过阈值需在0到100之间')
return
}
if (proposal.quorum < 0) {
toast.error('最小参与数需大于或等于0')
return
}
if (proposal.options.length < 2 || proposal.options.some((o) => !o.trim())) {
toast.error('请填写至少两个投票选项')
return
}
if (!proposal.endTime) {
toast.error('请选择投票结束时间')
return
}
}
try {
const token = getToken()
await ensureTags(token)
@@ -321,6 +365,18 @@ const submitPost = async () => {
prizeDescription: postType.value === 'LOTTERY' ? lottery.prizeDescription : undefined,
options: postType.value === 'POLL' ? poll.options : undefined,
multiple: postType.value === 'POLL' ? poll.multiple : undefined,
proposedName: postType.value === 'PROPOSAL' ? proposal.proposedName : undefined,
proposedSlug: postType.value === 'PROPOSAL' ? proposal.proposedSlug : undefined,
proposalDescription:
postType.value === 'PROPOSAL' ? proposal.proposalDescription : undefined,
approveThreshold: postType.value === 'PROPOSAL' ? proposal.approveThreshold : undefined,
quorum: postType.value === 'PROPOSAL' ? proposal.quorum : undefined,
options:
postType.value === 'POLL'
? poll.options
: postType.value === 'PROPOSAL'
? proposal.options
: undefined,
startTime:
postType.value === 'LOTTERY' ? new Date(startTime.value).toISOString() : undefined,
pointCost: postType.value === 'LOTTERY' ? lottery.pointCost : undefined,
@@ -330,7 +386,11 @@ const submitPost = async () => {
? new Date(new Date(lottery.endTime).getTime() + 8.02 * 60 * 60 * 1000).toISOString()
: postType.value === 'POLL'
? new Date(new Date(poll.endTime).getTime() + 8.02 * 60 * 60 * 1000).toISOString()
: undefined,
: postType.value === 'PROPOSAL'
? new Date(
new Date(proposal.endTime).getTime() + 8.02 * 60 * 60 * 1000,
).toISOString()
: undefined,
}),
})
const data = await res.json()