mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-06-09 03:27:32 +08:00
1.追加文章可见范围功能
2.删除文章右侧滚动条
This commit is contained in:
@@ -66,6 +66,7 @@ public class PostController {
|
|||||||
req.getContent(),
|
req.getContent(),
|
||||||
req.getTagIds(),
|
req.getTagIds(),
|
||||||
req.getType(),
|
req.getType(),
|
||||||
|
req.getPostVisibleScopeType(),
|
||||||
req.getPrizeDescription(),
|
req.getPrizeDescription(),
|
||||||
req.getPrizeIcon(),
|
req.getPrizeIcon(),
|
||||||
req.getPrizeCount(),
|
req.getPrizeCount(),
|
||||||
@@ -101,7 +102,8 @@ public class PostController {
|
|||||||
req.getCategoryId(),
|
req.getCategoryId(),
|
||||||
req.getTitle(),
|
req.getTitle(),
|
||||||
req.getContent(),
|
req.getContent(),
|
||||||
req.getTagIds()
|
req.getTagIds(),
|
||||||
|
req.getPostVisibleScopeType()
|
||||||
);
|
);
|
||||||
return ResponseEntity.ok(postMapper.toDetailDto(post, auth.getName()));
|
return ResponseEntity.ok(postMapper.toDetailDto(post, auth.getName()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.openisle.dto;
|
|||||||
import com.openisle.model.PostType;
|
import com.openisle.model.PostType;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.openisle.model.PostVisibleScopeType;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,6 +21,7 @@ public class PostRequest {
|
|||||||
|
|
||||||
// optional for lottery posts
|
// optional for lottery posts
|
||||||
private PostType type;
|
private PostType type;
|
||||||
|
private PostVisibleScopeType postVisibleScopeType;
|
||||||
private String prizeDescription;
|
private String prizeDescription;
|
||||||
private String prizeIcon;
|
private String prizeIcon;
|
||||||
private Integer prizeCount;
|
private Integer prizeCount;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import com.openisle.model.PostStatus;
|
|||||||
import com.openisle.model.PostType;
|
import com.openisle.model.PostType;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.openisle.model.PostVisibleScopeType;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,4 +36,5 @@ public class PostSummaryDto {
|
|||||||
private PollDto poll;
|
private PollDto poll;
|
||||||
private boolean rssExcluded;
|
private boolean rssExcluded;
|
||||||
private boolean closed;
|
private boolean closed;
|
||||||
|
private PostVisibleScopeType visibleScope;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ public class PostMapper {
|
|||||||
dto.setPinnedAt(post.getPinnedAt());
|
dto.setPinnedAt(post.getPinnedAt());
|
||||||
dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded());
|
dto.setRssExcluded(post.getRssExcluded() == null || post.getRssExcluded());
|
||||||
dto.setClosed(post.isClosed());
|
dto.setClosed(post.isClosed());
|
||||||
|
dto.setVisibleScope(post.getVisibleScope());
|
||||||
|
|
||||||
List<ReactionDto> reactions = reactionService
|
List<ReactionDto> reactions = reactionService
|
||||||
.getReactionsForPost(post.getId())
|
.getReactionsForPost(post.getId())
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ public class Post {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private PostType type = PostType.NORMAL;
|
private PostType type = PostType.NORMAL;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private PostVisibleScopeType visibleScope = PostVisibleScopeType.ALL;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private boolean closed = false;
|
private boolean closed = false;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.openisle.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
|
public enum PostVisibleScopeType {
|
||||||
|
ALL,
|
||||||
|
ONLY_ME,
|
||||||
|
ONLY_REGISTER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防止画面传递错误的值
|
||||||
|
* @param value
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@JsonCreator
|
||||||
|
public static PostVisibleScopeType fromString(String value) {
|
||||||
|
if (value == null) return ALL;
|
||||||
|
for (PostVisibleScopeType type : PostVisibleScopeType.values()) {
|
||||||
|
if (type.name().equalsIgnoreCase(value)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 不匹配时给默认值,而不是抛异常
|
||||||
|
return ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
public String toValue() {
|
||||||
|
return this.name();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.openisle.service;
|
package com.openisle.service;
|
||||||
|
|
||||||
import com.openisle.config.CachingConfig;
|
import com.openisle.config.CachingConfig;
|
||||||
|
import com.openisle.exception.NotFoundException;
|
||||||
import com.openisle.exception.RateLimitException;
|
import com.openisle.exception.RateLimitException;
|
||||||
import com.openisle.mapper.PostMapper;
|
import com.openisle.mapper.PostMapper;
|
||||||
import com.openisle.model.*;
|
import com.openisle.model.*;
|
||||||
@@ -225,6 +226,7 @@ public class PostService {
|
|||||||
String content,
|
String content,
|
||||||
List<Long> tagIds,
|
List<Long> tagIds,
|
||||||
PostType type,
|
PostType type,
|
||||||
|
PostVisibleScopeType postVisibleScopeType,
|
||||||
String prizeDescription,
|
String prizeDescription,
|
||||||
String prizeIcon,
|
String prizeIcon,
|
||||||
Integer prizeCount,
|
Integer prizeCount,
|
||||||
@@ -288,6 +290,14 @@ public class PostService {
|
|||||||
post.setCategory(category);
|
post.setCategory(category);
|
||||||
post.setTags(new HashSet<>(tags));
|
post.setTags(new HashSet<>(tags));
|
||||||
post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED);
|
post.setStatus(publishMode == PublishMode.REVIEW ? PostStatus.PENDING : PostStatus.PUBLISHED);
|
||||||
|
|
||||||
|
// 什么都没设置的情况下,默认为ALL
|
||||||
|
if(Objects.isNull(postVisibleScopeType)){
|
||||||
|
post.setVisibleScope(PostVisibleScopeType.ALL);
|
||||||
|
}else{
|
||||||
|
post.setVisibleScope(postVisibleScopeType);
|
||||||
|
}
|
||||||
|
|
||||||
if (post instanceof LotteryPost) {
|
if (post instanceof LotteryPost) {
|
||||||
post = lotteryPostRepository.save((LotteryPost) post);
|
post = lotteryPostRepository.save((LotteryPost) post);
|
||||||
} else if (post instanceof PollPost) {
|
} else if (post instanceof PollPost) {
|
||||||
@@ -571,7 +581,7 @@ public class PostService {
|
|||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||||
if (post.getStatus() != PostStatus.PUBLISHED) {
|
if (post.getStatus() != PostStatus.PUBLISHED) {
|
||||||
if (viewer == null) {
|
if (viewer == null) {
|
||||||
throw new com.openisle.exception.NotFoundException("Post not found");
|
throw new com.openisle.exception.NotFoundException("User not found");
|
||||||
}
|
}
|
||||||
User viewerUser = userRepository
|
User viewerUser = userRepository
|
||||||
.findByUsername(viewer)
|
.findByUsername(viewer)
|
||||||
@@ -1002,7 +1012,8 @@ public class PostService {
|
|||||||
Long categoryId,
|
Long categoryId,
|
||||||
String title,
|
String title,
|
||||||
String content,
|
String content,
|
||||||
java.util.List<Long> tagIds
|
List<Long> tagIds,
|
||||||
|
PostVisibleScopeType postVisibleScopeType
|
||||||
) {
|
) {
|
||||||
if (tagIds == null || tagIds.isEmpty()) {
|
if (tagIds == null || tagIds.isEmpty()) {
|
||||||
throw new IllegalArgumentException("At least one tag required");
|
throw new IllegalArgumentException("At least one tag required");
|
||||||
@@ -1034,6 +1045,7 @@ public class PostService {
|
|||||||
post.setContent(content);
|
post.setContent(content);
|
||||||
post.setCategory(category);
|
post.setCategory(category);
|
||||||
post.setTags(new java.util.HashSet<>(tags));
|
post.setTags(new java.util.HashSet<>(tags));
|
||||||
|
post.setVisibleScope(postVisibleScopeType);
|
||||||
Post updated = postRepository.save(post);
|
Post updated = postRepository.save(post);
|
||||||
imageUploader.adjustReferences(oldContent, content);
|
imageUploader.adjustReferences(oldContent, content);
|
||||||
notificationService.notifyMentions(content, user, updated, null);
|
notificationService.notifyMentions(content, user, updated, null);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE posts ADD COLUMN visible_scope ENUM('ALL', 'ONLY_ME', 'ONLY_REGISTER') NOT NULL DEFAULT 'ALL'
|
||||||
41
frontend_nuxt/components/PostVisibleScopeSelect.vue
Normal file
41
frontend_nuxt/components/PostVisibleScopeSelect.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<Dropdown
|
||||||
|
v-model="selected"
|
||||||
|
:fetch-options="fetchTypes"
|
||||||
|
placeholder="选择帖子可见范围"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import Dropdown from '~/components/Dropdown.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PostVisibleScopeSelect',
|
||||||
|
components: { Dropdown },
|
||||||
|
props: {
|
||||||
|
modelValue: { type: String, default: 'ALL' },
|
||||||
|
// options: { type: Array, default: () => [] },
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
|
||||||
|
const fetchTypes = async () => {
|
||||||
|
return [
|
||||||
|
{ id: 'ALL', name: '全部可见', icon: 'communication' },
|
||||||
|
{ id: 'ONLY_ME', name: '仅自己可见', icon: 'user-icon' },
|
||||||
|
{ id: 'ONLY_REGISTER', name: '仅注册用户可见', icon: 'peoples-two' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (v) => emit('update:modelValue', v),
|
||||||
|
})
|
||||||
|
|
||||||
|
return { fetchTypes, selected }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<CategorySelect v-model="selectedCategory" />
|
<CategorySelect v-model="selectedCategory" />
|
||||||
<TagSelect v-model="selectedTags" creatable />
|
<TagSelect v-model="selectedTags" creatable />
|
||||||
<PostTypeSelect v-model="postType" />
|
<PostTypeSelect v-model="postType" />
|
||||||
|
<PostVisibleScopeSelect v-model="postVisibleScope"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="post-options-right">
|
<div class="post-options-right">
|
||||||
<div class="post-clear" @click="clearPost"><clear-icon /> 清空</div>
|
<div class="post-clear" @click="clearPost"><clear-icon /> 清空</div>
|
||||||
@@ -52,6 +53,7 @@ import LotteryForm from '~/components/LotteryForm.vue'
|
|||||||
import PollForm from '~/components/PollForm.vue'
|
import PollForm from '~/components/PollForm.vue'
|
||||||
import { toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { authState, getToken } from '~/utils/auth'
|
import { authState, getToken } from '~/utils/auth'
|
||||||
|
import PostVisibleScopeSelect from '~/components/PostVisibleScopeSelect.vue'
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ const content = ref('')
|
|||||||
const selectedCategory = ref('')
|
const selectedCategory = ref('')
|
||||||
const selectedTags = ref([])
|
const selectedTags = ref([])
|
||||||
const postType = ref('NORMAL')
|
const postType = ref('NORMAL')
|
||||||
|
const postVisibleScope = ref('ALL')
|
||||||
const lottery = reactive({
|
const lottery = reactive({
|
||||||
prizeIcon: '',
|
prizeIcon: '',
|
||||||
prizeIconFile: null,
|
prizeIconFile: null,
|
||||||
@@ -94,6 +97,7 @@ const loadDraft = async () => {
|
|||||||
content.value = data.content || ''
|
content.value = data.content || ''
|
||||||
selectedCategory.value = data.categoryId || ''
|
selectedCategory.value = data.categoryId || ''
|
||||||
selectedTags.value = data.tagIds || []
|
selectedTags.value = data.tagIds || []
|
||||||
|
postVisibleScope.value = data.visiblescope
|
||||||
|
|
||||||
toast.success('草稿已加载')
|
toast.success('草稿已加载')
|
||||||
}
|
}
|
||||||
@@ -109,6 +113,7 @@ const clearPost = async () => {
|
|||||||
content.value = ''
|
content.value = ''
|
||||||
selectedCategory.value = ''
|
selectedCategory.value = ''
|
||||||
selectedTags.value = []
|
selectedTags.value = []
|
||||||
|
postVisibleScope.value = 'ALL'
|
||||||
postType.value = 'NORMAL'
|
postType.value = 'NORMAL'
|
||||||
lottery.prizeIcon = ''
|
lottery.prizeIcon = ''
|
||||||
lottery.prizeIconFile = null
|
lottery.prizeIconFile = null
|
||||||
@@ -160,6 +165,7 @@ const saveDraft = async () => {
|
|||||||
content: content.value,
|
content: content.value,
|
||||||
categoryId: selectedCategory.value || null,
|
categoryId: selectedCategory.value || null,
|
||||||
tagIds,
|
tagIds,
|
||||||
|
postVisibleScopeType:postVisibleScope.value
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -314,6 +320,7 @@ const submitPost = async () => {
|
|||||||
content: content.value,
|
content: content.value,
|
||||||
categoryId: selectedCategory.value,
|
categoryId: selectedCategory.value,
|
||||||
tagIds: selectedTags.value,
|
tagIds: selectedTags.value,
|
||||||
|
postVisibleScopeType: postVisibleScope.value,
|
||||||
type: postType.value,
|
type: postType.value,
|
||||||
prizeIcon: postType.value === 'LOTTERY' ? prizeIconUrl : undefined,
|
prizeIcon: postType.value === 'LOTTERY' ? prizeIconUrl : undefined,
|
||||||
prizeName: postType.value === 'LOTTERY' ? lottery.prizeName : undefined,
|
prizeName: postType.value === 'LOTTERY' ? lottery.prizeName : undefined,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<div class="post-options-left">
|
<div class="post-options-left">
|
||||||
<CategorySelect v-model="selectedCategory" />
|
<CategorySelect v-model="selectedCategory" />
|
||||||
<TagSelect v-model="selectedTags" creatable />
|
<TagSelect v-model="selectedTags" creatable />
|
||||||
|
<PostVisibleScopeSelect v-model="selectedVisibleScope"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="post-options-right">
|
<div class="post-options-right">
|
||||||
<div class="post-clear" @click="clearPost"><clear-icon /> 清空</div>
|
<div class="post-clear" @click="clearPost"><clear-icon /> 清空</div>
|
||||||
@@ -44,6 +45,7 @@ import TagSelect from '~/components/TagSelect.vue'
|
|||||||
import { toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { getToken, authState } from '~/utils/auth'
|
import { getToken, authState } from '~/utils/auth'
|
||||||
import LoginOverlay from '~/components/LoginOverlay.vue'
|
import LoginOverlay from '~/components/LoginOverlay.vue'
|
||||||
|
import PostVisibleScopeSelect from '~/components/PostVisibleScopeSelect.vue'
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ const title = ref('')
|
|||||||
const content = ref('')
|
const content = ref('')
|
||||||
const selectedCategory = ref('')
|
const selectedCategory = ref('')
|
||||||
const selectedTags = ref([])
|
const selectedTags = ref([])
|
||||||
|
const selectedVisibleScope = ref('ALL')
|
||||||
const isWaitingPosting = ref(false)
|
const isWaitingPosting = ref(false)
|
||||||
const isAiLoading = ref(false)
|
const isAiLoading = ref(false)
|
||||||
const isLogin = computed(() => authState.loggedIn)
|
const isLogin = computed(() => authState.loggedIn)
|
||||||
@@ -70,6 +73,7 @@ const loadPost = async () => {
|
|||||||
content.value = data.content || ''
|
content.value = data.content || ''
|
||||||
selectedCategory.value = data.category.id || ''
|
selectedCategory.value = data.category.id || ''
|
||||||
selectedTags.value = (data.tags || []).map((t) => t.id)
|
selectedTags.value = (data.tags || []).map((t) => t.id)
|
||||||
|
selectedVisibleScope.value = data.visibleScope
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('加载失败')
|
toast.error('加载失败')
|
||||||
@@ -180,6 +184,7 @@ const submitPost = async () => {
|
|||||||
content: content.value,
|
content: content.value,
|
||||||
categoryId: selectedCategory.value,
|
categoryId: selectedCategory.value,
|
||||||
tagIds: selectedTags.value,
|
tagIds: selectedTags.value,
|
||||||
|
postVisibleScopeType:selectedVisibleScope.value
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|||||||
@@ -3,7 +3,34 @@
|
|||||||
<div v-if="isWaitingFetchingPost" class="loading-container">
|
<div v-if="isWaitingFetchingPost" class="loading-container">
|
||||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="post-page-main-container" ref="mainContainer">
|
<div v-else
|
||||||
|
class="post-page-main-container"
|
||||||
|
ref="mainContainer"
|
||||||
|
>
|
||||||
|
<!-- 🔒 遮罩层 -->
|
||||||
|
<ClientOnly>
|
||||||
|
<div v-if="isRestricted" class="restricted-overlay">
|
||||||
|
<div class="restricted-content">
|
||||||
|
<Lock class="restricted-icon" />
|
||||||
|
|
||||||
|
<!-- 🔒 权限文案 -->
|
||||||
|
<template v-if="visibleScope === 'ONLY_ME'">
|
||||||
|
<p>这是一篇私密文章,仅作者本人及管理员可见</p>
|
||||||
|
<div class="restricted-actions">
|
||||||
|
<NuxtLink to="/" class="restricted-button">返回首页</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="visibleScope === 'ONLY_REGISTER'">
|
||||||
|
<p>请登录后查看这篇文章</p>
|
||||||
|
<div class="restricted-actions">
|
||||||
|
<NuxtLink to="/login" class="restricted-button" v-if="!loggedIn" >登录</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ClientOnly>
|
||||||
|
<div :class="{ 'is-blurred': isRestricted }">
|
||||||
<div class="article-title-container">
|
<div class="article-title-container">
|
||||||
<div class="article-title-container-left">
|
<div class="article-title-container-left">
|
||||||
<div class="article-title">{{ title }}</div>
|
<div class="article-title">{{ title }}</div>
|
||||||
@@ -164,8 +191,9 @@
|
|||||||
</BaseTimeline>
|
</BaseTimeline>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="post-page-scroller-container">
|
<!-- <div class="post-page-scroller-container">
|
||||||
<div class="scroller">
|
<div class="scroller">
|
||||||
<div v-if="isWaitingFetchingPost" class="scroller-time">loading...</div>
|
<div v-if="isWaitingFetchingPost" class="scroller-time">loading...</div>
|
||||||
<div v-else class="scroller-time">{{ scrollerTopTime }}</div>
|
<div v-else class="scroller-time">{{ scrollerTopTime }}</div>
|
||||||
@@ -183,7 +211,7 @@
|
|||||||
<div v-if="isWaitingFetchingPost" class="scroller-time">loading...</div>
|
<div v-if="isWaitingFetchingPost" class="scroller-time">loading...</div>
|
||||||
<div v-else class="scroller-time">{{ lastReplyTime }}</div>
|
<div v-else class="scroller-time">{{ lastReplyTime }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
<vue-easy-lightbox
|
<vue-easy-lightbox
|
||||||
:visible="lightboxVisible"
|
:visible="lightboxVisible"
|
||||||
:index="lightboxIndex"
|
:index="lightboxIndex"
|
||||||
@@ -228,6 +256,7 @@ import { useIsMobile } from '~/utils/screen'
|
|||||||
import Dropdown from '~/components/Dropdown.vue'
|
import Dropdown from '~/components/Dropdown.vue'
|
||||||
import { ClientOnly } from '#components'
|
import { ClientOnly } from '#components'
|
||||||
import { useConfirm } from '~/composables/useConfirm'
|
import { useConfirm } from '~/composables/useConfirm'
|
||||||
|
import { Lock } from '@icon-park/vue-next'
|
||||||
const { confirm } = useConfirm()
|
const { confirm } = useConfirm()
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
@@ -241,6 +270,13 @@ const author = ref('')
|
|||||||
const postContent = ref('')
|
const postContent = ref('')
|
||||||
const category = ref('')
|
const category = ref('')
|
||||||
const tags = ref([])
|
const tags = ref([])
|
||||||
|
const visibleScope = ref('ALL') // 可见范围
|
||||||
|
const isRestricted = computed(() => {
|
||||||
|
return (
|
||||||
|
(visibleScope.value === 'ONLY_ME' && !isAuthor.value && !isAdmin.value) ||
|
||||||
|
(visibleScope.value === 'ONLY_REGISTER' && !loggedIn.value)
|
||||||
|
)
|
||||||
|
})
|
||||||
const postReactions = ref([])
|
const postReactions = ref([])
|
||||||
const postReactionsGroupRef = ref(null)
|
const postReactionsGroupRef = ref(null)
|
||||||
const postLikeCount = computed(
|
const postLikeCount = computed(
|
||||||
@@ -497,16 +533,22 @@ const onCommentDeleted = (id) => {
|
|||||||
fetchTimeline()
|
fetchTimeline()
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const tokenHeader = computed(() => {
|
||||||
data: postData,
|
const token = getToken()
|
||||||
pending: pendingPost,
|
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
error: postError,
|
})
|
||||||
refresh: refreshPost,
|
const { data: postData, pending: pendingPost, error: postError, refresh: refreshPost } =
|
||||||
} = await useAsyncData(`post-${postId}`, () => $fetch(`${API_BASE_URL}/api/posts/${postId}`), {
|
await useAsyncData(`post-${postId}`, async () => {
|
||||||
server: true,
|
try {
|
||||||
|
return await $fetch(`${API_BASE_URL}/api/posts/${postId}`, { headers: tokenHeader.value })
|
||||||
|
} catch (err) {
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
server: false,
|
||||||
lazy: false,
|
lazy: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// 用 pendingPost 驱动现有 UI(替代 isWaitingFetchingPost 手控)
|
// 用 pendingPost 驱动现有 UI(替代 isWaitingFetchingPost 手控)
|
||||||
const isWaitingFetchingPost = computed(() => pendingPost.value)
|
const isWaitingFetchingPost = computed(() => pendingPost.value)
|
||||||
|
|
||||||
@@ -519,6 +561,7 @@ watchEffect(() => {
|
|||||||
title.value = data.title
|
title.value = data.title
|
||||||
category.value = data.category
|
category.value = data.category
|
||||||
tags.value = data.tags || []
|
tags.value = data.tags || []
|
||||||
|
visibleScope.value = data.visibleScope || 'ALL'
|
||||||
postReactions.value = data.reactions || []
|
postReactions.value = data.reactions || []
|
||||||
subscribed.value = !!data.subscribed
|
subscribed.value = !!data.subscribed
|
||||||
status.value = data.status
|
status.value = data.status
|
||||||
@@ -935,7 +978,7 @@ onMounted(async () => {
|
|||||||
<style>
|
<style>
|
||||||
.post-page-container {
|
.post-page-container {
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,9 +991,10 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.post-page-main-container {
|
.post-page-main-container {
|
||||||
|
position: relative;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
width: calc(85% - 40px);
|
width: calc(100% - 40px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-content-text p code {
|
.info-content-text p code {
|
||||||
@@ -1341,6 +1385,80 @@ onMounted(async () => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ======== 权限锁定状态 ======== */
|
||||||
|
.is-blurred {
|
||||||
|
filter: blur(10px);
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 遮罩层 */
|
||||||
|
.restricted-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
animation: fadeIn 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中央提示框 */
|
||||||
|
.restricted-content {
|
||||||
|
background: #ffff;
|
||||||
|
color:var(--primary-color);
|
||||||
|
padding: 50px 60px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.restricted-icon {
|
||||||
|
font-size: 60px;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restricted-button {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restricted-button:hover {
|
||||||
|
background: var(--primary-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.restricted-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淡入动画 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.post-page-main-container {
|
.post-page-main-container {
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
|
|||||||
Reference in New Issue
Block a user