+
+
+
多选
+
+ 拟议分类:{{ poll.proposedName }}
+
+
+
+
+
单选
+
+
+
离结束
+
{{ countdown }}
+
+
+
+
{{ poll.description }}
+
+
-
-
多选
-
单选
-
-
-
-
离结束
-
{{ countdown }}
-
-
投票已结束
您已投票,等待结束查看结果
-
-
-
离结束
-
{{ countdown }}
-
@@ -130,6 +139,9 @@ const emit = defineEmits(['refresh'])
const loggedIn = computed(() => authState.loggedIn)
const showPollResult = ref(false)
+const isProposal = computed(() =>
+ Object.prototype.hasOwnProperty.call(props.poll || {}, 'proposedName'),
+)
const pollParticipants = computed(() => props.poll?.participants || [])
const pollOptionParticipants = computed(() => props.poll?.optionParticipants || {})
const pollVotes = computed(() => props.poll?.votes || {})
@@ -233,6 +245,34 @@ const submitMultiPoll = async () => {
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 {
color: var(--text-color);
padding: 5px 10px;
@@ -385,12 +425,20 @@ const submitMultiPoll = async () => {
}
.poll-title-section {
- display: flex;
- gap: 30px;
- flex-direction: row;
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 {
font-size: 18px;
font-weight: bold;
diff --git a/frontend_nuxt/components/PostTypeSelect.vue b/frontend_nuxt/components/PostTypeSelect.vue
index 5fa16eab7..cfb605922 100644
--- a/frontend_nuxt/components/PostTypeSelect.vue
+++ b/frontend_nuxt/components/PostTypeSelect.vue
@@ -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' },
]
}
diff --git a/frontend_nuxt/components/ProposalForm.vue b/frontend_nuxt/components/ProposalForm.vue
new file mode 100644
index 000000000..261554069
--- /dev/null
+++ b/frontend_nuxt/components/ProposalForm.vue
@@ -0,0 +1,77 @@
+
+
+
+
+ 提案规则说明
+
+
📛 拟议分类名称需保持唯一,请勿与现有分类或正在提案中的名称重复。
+
📝 请在下方详细说明提案目的、预期价值及补充材料,方便大家快速理解。
+
🗳️ 提案提交后将开放 3 天投票,需达到至少 60% 的赞成率并满 10 人参与方可通过。
+
🤝 讨论请遵循社区守则,保持礼貌和善,欢迎附上相关案例或参考链接。
+
+
+
+ 拟议分类名称
+
+
+
+ 提案描述
+
+
+
+
+
+
+
+
diff --git a/frontend_nuxt/pages/index.vue b/frontend_nuxt/pages/index.vue
index 65b9365e9..78a7160a7 100644
--- a/frontend_nuxt/pages/index.vue
+++ b/frontend_nuxt/pages/index.vue
@@ -1,10 +1,5 @@
-
-
+
{{ article.title }}
@@ -116,7 +112,7 @@
分类浏览功能开发中,敬请期待。
-
+
{
.pinned-icon,
.lottery-icon,
.featured-icon,
+.proposal-icon,
.poll-icon {
margin-right: 4px;
color: var(--primary-color);
diff --git a/frontend_nuxt/pages/message.vue b/frontend_nuxt/pages/message.vue
index 577417a0f..b3948962b 100644
--- a/frontend_nuxt/pages/message.vue
+++ b/frontend_nuxt/pages/message.vue
@@ -75,7 +75,9 @@
@click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.parentComment.id}`"
>
-
+
回复了
@@ -85,7 +87,9 @@
@click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`"
>
-
+
@@ -115,7 +119,9 @@
@click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`"
>
-
+
@@ -162,7 +168,9 @@
@click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`"
>
-
+
进行了表态
@@ -251,6 +259,38 @@
已出结果
+
+
+ 你的分类提案
+
+ {{ stripMarkdownLength(item.post.title, 100) }}
+
+ 已通过
+
+ 未通过,原因:{{ item.content }}
+
+
+
+
+
+ 你参与的分类提案
+
+ {{ stripMarkdownLength(item.post.title, 100) }}
+
+ 已通过
+
+ 未通过,原因:{{ item.content }}
+
+
+
您关注的帖子
@@ -287,7 +327,9 @@
@click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.parentComment.id}`"
>
-
+
回复了
-
+
@@ -775,6 +817,10 @@ const formatType = (t) => {
return '发布的投票结果已公布'
case 'POLL_RESULT_PARTICIPANT':
return '参与的投票结果已公布'
+ case 'CATEGORY_PROPOSAL_RESULT_OWNER':
+ return '分类提案结果已公布'
+ case 'CATEGORY_PROPOSAL_RESULT_PARTICIPANT':
+ return '参与的分类提案结果已公布'
default:
return t
}
diff --git a/frontend_nuxt/pages/new-post.vue b/frontend_nuxt/pages/new-post.vue
index bd13de3f5..5d03f4dad 100644
--- a/frontend_nuxt/pages/new-post.vue
+++ b/frontend_nuxt/pages/new-post.vue
@@ -37,6 +37,7 @@
+
@@ -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,10 @@ const poll = reactive({
endTime: null,
multiple: false,
})
+const proposal = reactive({
+ proposedName: '',
+ proposalDescription: '',
+})
const startTime = ref(null)
const isWaitingPosting = ref(false)
const isAiLoading = ref(false)
@@ -123,6 +129,8 @@ const clearPost = async () => {
poll.options = ['', '']
poll.endTime = null
poll.multiple = false
+ proposal.proposedName = ''
+ proposal.proposalDescription = ''
// 删除草稿
const token = getToken()
@@ -283,6 +291,12 @@ const submitPost = async () => {
return
}
}
+ if (postType.value === 'PROPOSAL') {
+ if (!proposal.proposedName.trim()) {
+ toast.error('请填写拟议分类名称')
+ return
+ }
+ }
try {
const token = getToken()
await ensureTags(token)
@@ -303,35 +317,43 @@ const submitPost = async () => {
}
prizeIconUrl = uploadData.data.url
}
+ const toUtcString = (value) => {
+ if (!value) return undefined
+ return new Date(new Date(value).getTime() + 8.02 * 60 * 60 * 1000).toISOString()
+ }
+
+ const payload = {
+ title: title.value,
+ content: content.value,
+ categoryId: selectedCategory.value,
+ tagIds: selectedTags.value,
+ type: postType.value,
+ }
+
+ if (postType.value === 'LOTTERY') {
+ payload.prizeIcon = prizeIconUrl
+ payload.prizeName = lottery.prizeName
+ payload.prizeCount = lottery.prizeCount
+ payload.prizeDescription = lottery.prizeDescription
+ payload.pointCost = lottery.pointCost
+ payload.startTime = startTime.value ? new Date(startTime.value).toISOString() : undefined
+ payload.endTime = toUtcString(lottery.endTime)
+ } else if (postType.value === 'POLL') {
+ payload.options = poll.options
+ payload.multiple = poll.multiple
+ payload.endTime = toUtcString(poll.endTime)
+ } else if (postType.value === 'PROPOSAL') {
+ payload.proposedName = proposal.proposedName
+ payload.proposalDescription = proposal.proposalDescription
+ }
+
const res = await fetch(`${API_BASE_URL}/api/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
- body: JSON.stringify({
- title: title.value,
- content: content.value,
- categoryId: selectedCategory.value,
- tagIds: selectedTags.value,
- type: postType.value,
- prizeIcon: postType.value === 'LOTTERY' ? prizeIconUrl : undefined,
- prizeName: postType.value === 'LOTTERY' ? lottery.prizeName : undefined,
- prizeCount: postType.value === 'LOTTERY' ? lottery.prizeCount : undefined,
- prizeDescription: postType.value === 'LOTTERY' ? lottery.prizeDescription : undefined,
- options: postType.value === 'POLL' ? poll.options : undefined,
- multiple: postType.value === 'POLL' ? poll.multiple : undefined,
- startTime:
- postType.value === 'LOTTERY' ? new Date(startTime.value).toISOString() : undefined,
- pointCost: postType.value === 'LOTTERY' ? lottery.pointCost : undefined,
- // 将时间转换为 UTC+8.5 时区 todo: 需要优化
- endTime:
- postType.value === 'LOTTERY'
- ? 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,
- }),
+ body: JSON.stringify(payload),
})
const data = await res.json()
if (res.ok) {
diff --git a/frontend_nuxt/plugins/iconpark.client.ts b/frontend_nuxt/plugins/iconpark.client.ts
index 58338d98a..ee11c64f5 100644
--- a/frontend_nuxt/plugins/iconpark.client.ts
+++ b/frontend_nuxt/plugins/iconpark.client.ts
@@ -81,6 +81,7 @@ import {
CheckOne,
Share,
Financing,
+ Hands,
} from '@icon-park/vue-next'
export default defineNuxtPlugin((nuxtApp) => {
@@ -165,4 +166,5 @@ export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('CheckOne', CheckOne)
nuxtApp.vueApp.component('Share', Share)
nuxtApp.vueApp.component('Financing', Financing)
+ nuxtApp.vueApp.component('Hands', Hands)
})
diff --git a/frontend_nuxt/utils/notification.js b/frontend_nuxt/utils/notification.js
index 9d71e2a54..c13f1d9cd 100644
--- a/frontend_nuxt/utils/notification.js
+++ b/frontend_nuxt/utils/notification.js
@@ -28,6 +28,8 @@ const iconMap = {
POLL_VOTE: 'ChartHistogram',
POLL_RESULT_OWNER: 'RankingList',
POLL_RESULT_PARTICIPANT: 'ChartLine',
+ CATEGORY_PROPOSAL_RESULT_OWNER: 'TagOne',
+ CATEGORY_PROPOSAL_RESULT_PARTICIPANT: 'TagOne',
MENTION: 'HashtagKey',
POST_DELETED: 'ClearIcon',
POST_FEATURED: 'Star',
@@ -254,7 +256,9 @@ function createFetchNotifications() {
} else if (
n.type === 'POLL_VOTE' ||
n.type === 'POLL_RESULT_OWNER' ||
- n.type === 'POLL_RESULT_PARTICIPANT'
+ n.type === 'POLL_RESULT_PARTICIPANT' ||
+ n.type === 'CATEGORY_PROPOSAL_RESULT_OWNER' ||
+ n.type === 'CATEGORY_PROPOSAL_RESULT_PARTICIPANT'
) {
arr.push({
...n,