feat(life): require at least one tag for life posts

Update design spec to mandate tag selection
Add frontend and backend validation for tag requirement
Add localization strings for tag required errors
This commit is contained in:
2026-05-03 11:47:08 +08:00
parent accd6f98cf
commit 95d76522df
5 changed files with 23 additions and 5 deletions

View File

@@ -565,7 +565,7 @@ Life 是社区生活分享信息流,类似轻量社交动态。
Life Post 可配置: Life Post 可配置:
- Post 内容正文 - Post 内容正文
- 标签:使用 Life 标签配置,可多选 - 标签:使用 Life 标签配置,至少选择 1 个,可多选
- 创建者、最后编辑者、创建时间、最后编辑时间 - 创建者、最后编辑者、创建时间、最后编辑时间
- 评论 - 评论
- 评论回复:仅支持回复顶层评论,不做无限嵌套 - 评论回复:仅支持回复顶层评论,不做无限嵌套
@@ -578,7 +578,7 @@ Life Post 可配置:
- 已注册并完成邮箱验证且拥有 `life.posts.create` 权限的用户可以发布 Life Post。 - 已注册并完成邮箱验证且拥有 `life.posts.create` 权限的用户可以发布 Life Post。
- 作者本人拥有 `life.posts.update` / `life.posts.delete` 权限时可以编辑、删除自己的 Life Post删除 Life Post 使用软删除。 - 作者本人拥有 `life.posts.update` / `life.posts.delete` 权限时可以编辑、删除自己的 Life Post删除 Life Post 使用软删除。
- 拥有 `life.posts.update-any` / `life.posts.delete-any` 权限的用户可以管理其他用户的 Life Post。 - 拥有 `life.posts.update-any` / `life.posts.delete-any` 权限的用户可以管理其他用户的 Life Post。
- 已注册并完成邮箱验证且拥有 `life.posts.create``life.posts.update` 权限的用户发布或编辑 Life Post 时可以选择一个或多个 Life 标签。 - 已注册并完成邮箱验证且拥有 `life.posts.create``life.posts.update` 权限的用户发布或编辑 Life Post 时必须选择至少 1 个 Life 标签,可选择多个
- 已注册并完成邮箱验证且拥有 `life.comments.create` 权限的用户可以评论 Life Post并回复顶层评论。 - 已注册并完成邮箱验证且拥有 `life.comments.create` 权限的用户可以评论 Life Post并回复顶层评论。
- 评论作者拥有 `life.comments.delete` 权限时可以删除自己的评论;拥有 `life.comments.delete-any` 权限的用户可以删除其他用户评论;删除评论后正文不再展示,已有回复保留在原位置。 - 评论作者拥有 `life.comments.delete` 权限时可以删除自己的评论;拥有 `life.comments.delete-any` 权限的用户可以删除其他用户评论;删除评论后正文不再展示,已有回复保留在原位置。
- 已软删除的 Life Post 不出现在信息流、搜索或标签筛选结果中,也不能继续编辑、评论或设置 Reaction。 - 已软删除的 Life Post 不出现在信息流、搜索或标签筛选结果中,也不能继续编辑、评论或设置 Reaction。

View File

@@ -2089,10 +2089,14 @@ function cleanLifePostPayload(payload: Record<string, unknown>): LifePostPayload
if (body.length > 2000) { if (body.length > 2000) {
throw validationError('Post is too long'); throw validationError('Post is too long');
} }
const tagIds = cleanIds(payload.tagIds);
if (tagIds.length === 0) {
throw validationError('server.validation.lifeTagRequired');
}
return { return {
body, body,
tagIds: cleanIds(payload.tagIds) tagIds
}; };
} }

View File

@@ -486,7 +486,7 @@ export interface DailyChecklistPayload {
export interface LifePostPayload { export interface LifePostPayload {
body: string; body: string;
tagIds?: number[]; tagIds: number[];
} }
export interface LifeCommentPayload { export interface LifeCommentPayload {

View File

@@ -218,10 +218,14 @@ function resetForm() {
function payload() { function payload() {
return { return {
body: body.value.trim(), body: body.value.trim(),
tagIds: selectedTagIds.value.map((tagId) => Number(tagId)).filter((tagId) => Number.isInteger(tagId) && tagId > 0) tagIds: selectedLifeTagIds()
}; };
} }
function selectedLifeTagIds() {
return selectedTagIds.value.map((tagId) => Number(tagId)).filter((tagId) => Number.isInteger(tagId) && tagId > 0);
}
function submitSearch() { function submitSearch() {
const nextSearch = searchDraft.value.trim(); const nextSearch = searchDraft.value.trim();
if (nextSearch === submittedSearch.value && !loadError.value) { if (nextSearch === submittedSearch.value && !loadError.value) {
@@ -281,6 +285,12 @@ async function submitPost() {
return; return;
} }
if (selectedLifeTagIds().length === 0) {
formError.value = t('pages.life.tagRequired');
document.getElementById('life-post-tags')?.focus();
return;
}
busy.value = true; busy.value = true;
formError.value = ''; formError.value = '';

View File

@@ -436,6 +436,7 @@ export const systemWordingMessages = {
saveFailed: 'Save failed', saveFailed: 'Save failed',
deleteFailed: 'Delete failed', deleteFailed: 'Delete failed',
bodyRequired: 'Please enter a post.', bodyRequired: 'Please enter a post.',
tagRequired: 'Please select at least one tag.',
byUnknown: 'Community member', byUnknown: 'Community member',
edited: 'Edited', edited: 'Edited',
deleteConfirm: 'Delete this post?', deleteConfirm: 'Delete this post?',
@@ -648,6 +649,7 @@ export const systemWordingMessages = {
taskDoesNotExist: 'Task does not exist', taskDoesNotExist: 'Task does not exist',
postRequired: 'Please enter a post', postRequired: 'Please enter a post',
postTooLong: 'Post is too long', postTooLong: 'Post is too long',
lifeTagRequired: 'Please select at least one tag',
commentRequired: 'Please enter a comment', commentRequired: 'Please enter a comment',
commentTooLong: 'Comment is too long', commentTooLong: 'Comment is too long',
reactionInvalid: 'Reaction is invalid', reactionInvalid: 'Reaction is invalid',
@@ -1144,6 +1146,7 @@ export const systemWordingMessages = {
saveFailed: '保存失败', saveFailed: '保存失败',
deleteFailed: '删除失败', deleteFailed: '删除失败',
bodyRequired: '请输入动态内容。', bodyRequired: '请输入动态内容。',
tagRequired: '请至少选择 1 个标签。',
byUnknown: '社区成员', byUnknown: '社区成员',
edited: '已编辑', edited: '已编辑',
deleteConfirm: '确认删除这条动态?', deleteConfirm: '确认删除这条动态?',
@@ -1356,6 +1359,7 @@ export const systemWordingMessages = {
taskDoesNotExist: '任务不存在', taskDoesNotExist: '任务不存在',
postRequired: '请输入动态内容', postRequired: '请输入动态内容',
postTooLong: '动态内容过长', postTooLong: '动态内容过长',
lifeTagRequired: '请至少选择 1 个标签',
commentRequired: '请输入评论内容', commentRequired: '请输入评论内容',
commentTooLong: '评论内容过长', commentTooLong: '评论内容过长',
reactionInvalid: '互动类型不合法', reactionInvalid: '互动类型不合法',