Compare commits

...

10 Commits

Author SHA1 Message Date
Tim
f17b644a9b fix: avatar 以及 auth 重构 2025-10-17 15:10:43 +08:00
Tim
61f8fa4bb7 fix: 新增右间距 2025-10-17 12:19:40 +08:00
Tim
43929bcdc5 Merge pull request #1072 from nagisa77/feature/give_some_money
fix: 移动端ui适配
2025-10-17 12:02:03 +08:00
Tim
0d2e6a9505 Merge pull request #1065 from nagisa77/feature/give_some_money
打赏功能实现
2025-10-17 11:36:34 +08:00
Tim
b2d70b9bde Merge pull request #1069 from nagisa77/codex/add-reinitialize-command-to-contributing.md
docs: document docker compose volume reset workflow
2025-10-17 11:36:15 +08:00
Tim
fa29d255c9 Merge pull request #1067 from smallclover/main
tieba表情函数抽成共通
2025-10-16 22:35:43 +08:00
smallclover
b3fa5e2bef 修复已读 2025-10-16 21:19:13 +09:00
smallclover
a7ef4380d8 问题修复
1.修复网页模式下,markdown代码过长
2.修复网页模实下,按钮文字换行
3.修复网页模式下,消息换行
2025-10-16 21:13:56 +09:00
Tim
596d1558a2 docs: add compose volume reset instructions 2025-10-16 10:13:07 +08:00
smallclover
215c7077d5 tieba表情函数抽成共通 2025-10-15 22:47:48 +09:00
21 changed files with 116 additions and 149 deletions

View File

@@ -73,6 +73,12 @@ cd OpenIsle
docker compose -f docker/docker-compose.yaml --env-file .env --profile dev down docker compose -f docker/docker-compose.yaml --env-file .env --profile dev down
``` ```
5. 开发时若需要**重置所有容器及其挂载的数据卷**,可以执行:
```shell
docker compose -f docker/docker-compose.yaml --env-file .env --profile dev down -v
```
`-v` 参数会在关闭容器的同时移除通过 `volumes` 声明的挂载卷,适用于希望清理数据库、缓存等持久化数据,确保下一次启动时获得全新环境的场景。
如需自定义 Node 依赖缓存、数据库持久化等,可参考 `docker/docker-compose.yaml` 中各卷的定义进行调整。 如需自定义 Node 依赖缓存、数据库持久化等,可参考 `docker/docker-compose.yaml` 中各卷的定义进行调整。
## 启动后端服务 ## 启动后端服务

View File

@@ -41,10 +41,13 @@ import GlobalPopups from '~/components/GlobalPopups.vue'
import ConfirmDialog from '~/components/ConfirmDialog.vue' import ConfirmDialog from '~/components/ConfirmDialog.vue'
import MessageFloatWindow from '~/components/MessageFloatWindow.vue' import MessageFloatWindow from '~/components/MessageFloatWindow.vue'
import { useIsMobile } from '~/utils/screen' import { useIsMobile } from '~/utils/screen'
import { checkToken } from '~/utils/auth'
const isMobile = useIsMobile() const isMobile = useIsMobile()
const menuVisible = ref(!isMobile.value) const menuVisible = ref(!isMobile.value)
await checkToken()
const showNewPostIcon = computed(() => useRoute().path === '/') const showNewPostIcon = computed(() => useRoute().path === '/')
const hideMenu = computed(() => { const hideMenu = computed(() => {

View File

@@ -206,7 +206,6 @@ body {
border-radius: 4px; border-radius: 4px;
background-color: var(--code-highlight-background-color); background-color: var(--code-highlight-background-color);
color: var(--text-color); color: var(--text-color);
white-space: pre; /* 禁止自动换行 */
} }
.copy-code-btn { .copy-code-btn {
@@ -371,7 +370,10 @@ body {
.d2h-code-line { .d2h-code-line {
padding-left: 10px !important; padding-left: 10px !important;
} }
/* 手机端不换行 */
.info-content-text code {
white-space: pre; /* 禁止自动换行 */
}
/* .d2h-diff-table { /* .d2h-diff-table {
font-size: 6px !important; font-size: 6px !important;
} }

View File

@@ -17,7 +17,7 @@ import { computed, ref } from 'vue'
import { useAttrs } from 'vue' import { useAttrs } from 'vue'
const props = defineProps({ const props = defineProps({
src: { type: String, required: true }, src: { type: String, default: '' },
alt: { type: String, default: '' }, alt: { type: String, default: '' },
}) })
@@ -39,9 +39,6 @@ const placeholder = computed(() => {
function onLoad() { function onLoad() {
loaded.value = true loaded.value = true
} }
function onError() {
loaded.value = true
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,22 +1,20 @@
<template> <template>
<NuxtLink <div
:to="resolvedLink"
class="base-user-avatar" class="base-user-avatar"
:class="wrapperClass" :class="wrapperClass"
:style="wrapperStyle" :style="wrapperStyle"
v-bind="wrapperAttrs" v-bind="wrapperAttrs"
@click="handleClick"
> >
<BaseImage :src="currentSrc" :alt="altText" class="base-user-avatar-img" @error="onError" /> <BaseImage :src="props.src" :alt="altText" class="base-user-avatar-img" />
</NuxtLink> </div>
</template> </template>
<script setup> <script setup>
import { computed, ref, watch } from 'vue' import { computed, watch } from 'vue'
import { useAttrs } from 'vue' import { useAttrs } from 'vue'
import BaseImage from './BaseImage.vue' import BaseImage from './BaseImage.vue'
const DEFAULT_AVATAR = '/default-avatar.jpg'
const props = defineProps({ const props = defineProps({
userId: { userId: {
type: [String, Number], type: [String, Number],
@@ -50,15 +48,6 @@ const props = defineProps({
const attrs = useAttrs() const attrs = useAttrs()
const currentSrc = ref(props.src || DEFAULT_AVATAR)
watch(
() => props.src,
(value) => {
currentSrc.value = value || DEFAULT_AVATAR
},
)
const resolvedLink = computed(() => { const resolvedLink = computed(() => {
if (props.to) return props.to if (props.to) return props.to
if (props.userId !== null && props.userId !== undefined && props.userId !== '') { if (props.userId !== null && props.userId !== undefined && props.userId !== '') {
@@ -70,10 +59,16 @@ const resolvedLink = computed(() => {
const altText = computed(() => props.alt || '用户头像') const altText = computed(() => props.alt || '用户头像')
const sizeStyle = computed(() => { const sizeStyle = computed(() => {
if (!props.width && props.width !== 0) return null var style = {}
const value = typeof props.width === 'number' ? `${props.width}px` : props.width
if (!value) return null if (props.width > 0) {
return { width: value, height: value } style.width = `${props.width}px`
}
if (props.height > 0) {
style.height = `${props.height}px`
}
return style
}) })
const wrapperStyle = computed(() => { const wrapperStyle = computed(() => {
@@ -88,10 +83,9 @@ const wrapperAttrs = computed(() => {
return rest return rest
}) })
function onError() { const handleClick = () => {
if (currentSrc.value !== DEFAULT_AVATAR) { if (props.disableLink) return
currentSrc.value = DEFAULT_AVATAR navigateTo(resolvedLink.value)
}
} }
</script> </script>

View File

@@ -78,7 +78,9 @@
<div class="header-icon-item" @click="goToMessages"> <div class="header-icon-item" @click="goToMessages">
<message-emoji class="header-icon" /> <message-emoji class="header-icon" />
<span class="header-label">消息</span> <span class="header-label">消息</span>
<span v-if="unreadMessageCount > 0" class="unread-badge">{{ unreadMessageCount }}</span> <span v-if="unreadMessageCount > 0" class="unread-badge">{{
unreadMessageCount
}}</span>
<span v-else-if="hasChannelUnread" class="unread-dot"></span> <span v-else-if="hasChannelUnread" class="unread-dot"></span>
</div> </div>
</ToolTip> </ToolTip>
@@ -89,10 +91,9 @@
<BaseUserAvatar <BaseUserAvatar
class="avatar-img" class="avatar-img"
:user-id="authState.userId" :user-id="authState.userId"
:src="avatar" :src="authState.avatar"
alt="avatar"
:width="32"
:disable-link="true" :disable-link="true"
:width="32"
/> />
<down /> <down />
</div> </div>
@@ -117,7 +118,7 @@ import DropdownMenu from '~/components/DropdownMenu.vue'
import ToolTip from '~/components/ToolTip.vue' import ToolTip from '~/components/ToolTip.vue'
import SearchDropdown from '~/components/SearchDropdown.vue' import SearchDropdown from '~/components/SearchDropdown.vue'
import BaseUserAvatar from '~/components/BaseUserAvatar.vue' import BaseUserAvatar from '~/components/BaseUserAvatar.vue'
import { authState, clearToken, loadCurrentUser } from '~/utils/auth' import { authState, clearToken } from '~/utils/auth'
import { useUnreadCount } from '~/composables/useUnreadCount' import { useUnreadCount } from '~/composables/useUnreadCount'
import { useChannelsUnreadCount } from '~/composables/useChannelsUnreadCount' import { useChannelsUnreadCount } from '~/composables/useChannelsUnreadCount'
import { useIsMobile } from '~/utils/screen' import { useIsMobile } from '~/utils/screen'
@@ -139,13 +140,11 @@ const isLogin = computed(() => authState.loggedIn)
const isMobile = useIsMobile() const isMobile = useIsMobile()
const { count: unreadMessageCount, fetchUnreadCount } = useUnreadCount() const { count: unreadMessageCount, fetchUnreadCount } = useUnreadCount()
const { hasUnread: hasChannelUnread, fetchChannelUnread } = useChannelsUnreadCount() const { hasUnread: hasChannelUnread, fetchChannelUnread } = useChannelsUnreadCount()
const avatar = ref('')
const showSearch = ref(false) const showSearch = ref(false)
const searchDropdown = ref(null) const searchDropdown = ref(null)
const userMenu = ref(null) const userMenu = ref(null)
const menuBtn = ref(null) const menuBtn = ref(null)
const isCopying = ref(false) const isCopying = ref(false)
const onlineCount = ref(0) const onlineCount = ref(0)
// 心跳检测 // 心跳检测
@@ -208,7 +207,7 @@ const copyInviteLink = async () => {
const token = getToken() const token = getToken()
if (!token) { if (!token) {
toast.error('请先登录') toast.error('请先登录')
isCopying.value = false // 🔥 修复:未登录时立即复原状态 isCopying.value = false // 🔥 修复:未登录时立即复原状态
return return
} }
try { try {
@@ -252,17 +251,7 @@ const copyRssLink = async () => {
} }
const goToProfile = async () => { const goToProfile = async () => {
if (!authState.loggedIn) { let id = authState.username || authState.id
navigateTo('/login', { replace: true })
return
}
let id = authState.username || authState.userId
if (!id) {
const user = await loadCurrentUser()
if (user) {
id = user.username || user.id
}
}
if (id) { if (id) {
navigateTo(`/users/${id}`, { replace: true }) navigateTo(`/users/${id}`, { replace: true })
} }
@@ -306,14 +295,6 @@ const iconClass = computed(() => {
}) })
onMounted(async () => { onMounted(async () => {
const updateAvatar = async () => {
if (authState.loggedIn) {
const user = await loadCurrentUser()
if (user && user.avatar) {
avatar.value = user.avatar
}
}
}
const updateUnread = async () => { const updateUnread = async () => {
if (authState.loggedIn) { if (authState.loggedIn) {
fetchUnreadCount() fetchUnreadCount()
@@ -323,17 +304,8 @@ onMounted(async () => {
} }
} }
await updateAvatar()
await updateUnread() await updateUnread()
watch(
() => authState.loggedIn,
async (isLoggedIn) => {
await updateAvatar()
await updateUnread()
},
)
// 新增的在线人数逻辑 // 新增的在线人数逻辑
sendPing() sendPing()
fetchCount() fetchCount()
@@ -482,7 +454,6 @@ onMounted(async () => {
cursor: pointer; cursor: pointer;
} }
.invite_text:hover { .invite_text:hover {
opacity: 0.8; opacity: 0.8;
text-decoration: underline; text-decoration: underline;
@@ -543,7 +514,10 @@ onMounted(async () => {
color: var(--primary-color); color: var(--primary-color);
cursor: pointer; cursor: pointer;
position: relative; position: relative;
transition: color 0.25s ease, transform 0.15s ease, opacity 0.2s ease; transition:
color 0.25s ease,
transform 0.15s ease,
opacity 0.2s ease;
} }
.header-icon-item:hover { .header-icon-item:hover {
@@ -572,15 +546,14 @@ onMounted(async () => {
position: absolute; position: absolute;
top: -4px; top: -4px;
right: -6px; right: -6px;
color: var(--primary-color); /* 🔹 使用主题主色 */ color: var(--primary-color); /* 🔹 使用主题主色 */
background: none; /* 🔹 去掉背景 */ background: none; /* 🔹 去掉背景 */
font-size: 11px; /* 字体稍微大一点以便清晰 */ font-size: 11px; /* 字体稍微大一点以便清晰 */
font-weight: 600; /* 加一点权重让数字更醒目 */ font-weight: 600; /* 加一点权重让数字更醒目 */
line-height: 1; line-height: 1;
padding: 0; /* 去掉内边距 */ padding: 0; /* 去掉内边距 */
} }
@keyframes rss-glow { @keyframes rss-glow {
0% { 0% {
text-shadow: 0 0 0px var(--primary-color); text-shadow: 0 0 0px var(--primary-color);

View File

@@ -45,6 +45,7 @@ export default {
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
margin-left: 10px; margin-left: 10px;
white-space: nowrap;
} }
.mark-read-button:hover { .mark-read-button:hover {
@@ -53,6 +54,7 @@ export default {
.has-read-button { .has-read-button {
font-size: 12px; font-size: 12px;
white-space: nowrap;
} }
@media (max-width: 768px) { @media (max-width: 768px) {

View File

@@ -322,6 +322,7 @@ onBeforeUnmount(() => {
.reactions-count { .reactions-count {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
margin-right: 15px;
} }
.reactions-panel { .reactions-panel {

View File

@@ -76,7 +76,7 @@
{{ article.title }} {{ article.title }}
</NuxtLink> </NuxtLink>
<NuxtLink class="article-item-description main-item" :to="`/posts/${article.id}`"> <NuxtLink class="article-item-description main-item" :to="`/posts/${article.id}`">
<div v-html="sanitizeDescription(article.description)"></div> <div v-html="stripMarkdownWithTiebaMoji(article.description, 500)"></div>
</NuxtLink> </NuxtLink>
<div class="article-info-container main-item"> <div class="article-info-container main-item">
<ArticleCategory :category="article.category" /> <ArticleCategory :category="article.category" />
@@ -143,6 +143,7 @@ import { useIsMobile } from '~/utils/screen'
import BaseUserAvatar from '~/components/BaseUserAvatar.vue' import BaseUserAvatar from '~/components/BaseUserAvatar.vue'
import TimeManager from '~/utils/time' import TimeManager from '~/utils/time'
import { selectedCategoryGlobal, selectedTagsGlobal } from '~/composables/postFilter' import { selectedCategoryGlobal, selectedTagsGlobal } from '~/composables/postFilter'
import { stripMarkdownWithTiebaMoji } from '~/utils/markdown'
useHead({ useHead({
title: 'OpenIsle - 全面开源的自由社区', title: 'OpenIsle - 全面开源的自由社区',
meta: [ meta: [
@@ -378,27 +379,6 @@ onBeforeUnmount(() => {
/** 供 InfiniteLoadMore 重建用的 key筛选/Tab 改变即重建内部状态 */ /** 供 InfiniteLoadMore 重建用的 key筛选/Tab 改变即重建内部状态 */
const ioKey = computed(() => asyncKey.value.join('::')) const ioKey = computed(() => asyncKey.value.join('::'))
// 在首页摘要加载贴吧表情包
const sanitizeDescription = (text) => {
if (!text) return ''
// 1⃣ 先把 Markdown 转成纯文本
const plain = stripMarkdown(text)
// 2⃣ 替换 :tieba123: 为 <img>
const withEmoji = plain.replace(/:tieba(\d+):/g, (match, num) => {
const key = `tieba${num}`
const file = tiebaEmoji[key]
return file
? `<img loading="lazy" class="emoji" src="${file}" alt="${key}">`
: match // 没有匹配到图片则保留原样
})
// 3 可选截断纯文本长度防止撑太长
const truncated = withEmoji.length > 500 ? withEmoji.slice(0, 500) + '…' : withEmoji
return truncated
}
// 页面选项同步到全局状态 // 页面选项同步到全局状态
watch([selectedCategory, selectedTags], ([newCategory, newTags]) => { watch([selectedCategory, selectedTags], ([newCategory, newTags]) => {

View File

@@ -40,7 +40,7 @@
<script setup> <script setup>
import { toast } from '~/main' import { toast } from '~/main'
import { setToken, loadCurrentUser } from '~/utils/auth' import { setToken } from '~/utils/auth'
import BaseInput from '~/components/BaseInput.vue' import BaseInput from '~/components/BaseInput.vue'
import ThirdPartyAuth from '~/components/ThirdPartyAuth.vue' import ThirdPartyAuth from '~/components/ThirdPartyAuth.vue'
import { registerPush } from '~/utils/push' import { registerPush } from '~/utils/push'
@@ -61,7 +61,6 @@ const submitLogin = async () => {
const data = await res.json() const data = await res.json()
if (res.ok && data.token) { if (res.ok && data.token) {
setToken(data.token) setToken(data.token)
await loadCurrentUser()
toast.success('登录成功') toast.success('登录成功')
registerPush() registerPush()
await navigateTo('/', { replace: true }) await navigateTo('/', { replace: true })

View File

@@ -84,7 +84,7 @@
> >
<div class="conversation-avatar"> <div class="conversation-avatar">
<BaseImage <BaseImage
:src="ch.avatar || '/default-avatar.jpg'" :src="ch.avatar"
:alt="ch.name" :alt="ch.name"
class="avatar-img" class="avatar-img"
@error="handleAvatarError" @error="handleAvatarError"
@@ -194,7 +194,7 @@ function formatTime(timeString) {
// 头像加载失败处理 // 头像加载失败处理
function handleAvatarError(event) { function handleAvatarError(event) {
event.target.src = '/default-avatar.jpg' event.target.src = null
} }
async function fetchChannels() { async function fetchChannels() {

View File

@@ -75,7 +75,7 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.parentComment.id}`" :to="`/posts/${item.post.id}#comment-${item.parentComment.id}`"
> >
{{ stripMarkdownLength(item.parentComment.content, 100) }} <span v-html="stripMarkdownWithTiebaMoji(item.parentComment.content, 500)"></span>
</NuxtLink> </NuxtLink>
</span> </span>
回复了 回复了
@@ -85,7 +85,7 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"
> >
{{ stripMarkdownLength(item.comment.content, 100) }} <span v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"></span>
</NuxtLink> </NuxtLink>
</span> </span>
</NotificationContainer> </NotificationContainer>
@@ -115,7 +115,7 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"
> >
{{ stripMarkdownLength(item.comment.content, 100) }} <span v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"></span>
</NuxtLink> </NuxtLink>
</span> </span>
</NotificationContainer> </NotificationContainer>
@@ -162,7 +162,7 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"
> >
{{ stripMarkdownLength(item.comment.content, 100) }} <span v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"></span>
</NuxtLink> </NuxtLink>
</span> </span>
进行了表态 进行了表态
@@ -267,7 +267,7 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"
> >
{{ stripMarkdownLength(item.comment.content, 100) }} <span v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"></span>
</NuxtLink> </NuxtLink>
</NotificationContainer> </NotificationContainer>
</template> </template>
@@ -287,7 +287,7 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.parentComment.id}`" :to="`/posts/${item.post.id}#comment-${item.parentComment.id}`"
> >
{{ stripMarkdownLength(item.parentComment.content, 100) }} <span v-html="stripMarkdownWithTiebaMoji(item.parentComment.content, 500)"></span>
</NuxtLink> </NuxtLink>
回复了 回复了
<NuxtLink <NuxtLink
@@ -295,7 +295,7 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"
> >
{{ stripMarkdownLength(item.comment.content, 100) }} <span v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"></span>
</NuxtLink> </NuxtLink>
</NotificationContainer> </NotificationContainer>
</template> </template>
@@ -323,7 +323,7 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"
> >
{{ stripMarkdownLength(item.comment.content, 100) }} <span v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"></span>
</NuxtLink> </NuxtLink>
</NotificationContainer> </NotificationContainer>
</template> </template>
@@ -342,7 +342,7 @@
@click="markRead(item.id)" @click="markRead(item.id)"
:to="`/posts/${item.post.id}#comment-${item.comment.id}`" :to="`/posts/${item.post.id}#comment-${item.comment.id}`"
> >
{{ stripMarkdownLength(item.comment.content, 100) }} <span v-html="stripMarkdownWithTiebaMoji(item.comment.content, 500)"></span>
</NuxtLink> </NuxtLink>
</NotificationContainer> </NotificationContainer>
</template> </template>
@@ -577,7 +577,7 @@
</template> </template>
删除了您的帖子 删除了您的帖子
<span class="notif-content-text"> <span class="notif-content-text">
{{ stripMarkdownLength(item.content, 100) }} <span v-html="stripMarkdownWithTiebaMoji(item.content, 500)"></span>
</span> </span>
</NotificationContainer> </NotificationContainer>
</template> </template>
@@ -607,7 +607,7 @@ import InfiniteLoadMore from '~/components/InfiniteLoadMore.vue'
import BaseTabs from '~/components/BaseTabs.vue' import BaseTabs from '~/components/BaseTabs.vue'
import { toast } from '~/main' import { toast } from '~/main'
import { authState, getToken } from '~/utils/auth' import { authState, getToken } from '~/utils/auth'
import { stripMarkdownLength } from '~/utils/markdown' import { stripMarkdownWithTiebaMoji } from '~/utils/markdown'
import { import {
fetchNotifications, fetchNotifications,
fetchUnreadCount, fetchUnreadCount,

View File

@@ -70,7 +70,7 @@
<script setup> <script setup>
import BaseInput from '~/components/BaseInput.vue' import BaseInput from '~/components/BaseInput.vue'
import { toast } from '~/main' import { toast } from '~/main'
import { loadCurrentUser, setToken } from '~/utils/auth' import { setToken } from '~/utils/auth'
import ThirdPartyAuth from '~/components/ThirdPartyAuth.vue' import ThirdPartyAuth from '~/components/ThirdPartyAuth.vue'
const route = useRoute() const route = useRoute()
@@ -172,7 +172,6 @@ const verifyCode = async () => {
if (data.reason_code === 'VERIFIED_AND_APPROVED') { if (data.reason_code === 'VERIFIED_AND_APPROVED') {
toast.success('注册成功') toast.success('注册成功')
setToken(data.token) setToken(data.token)
loadCurrentUser()
navigateTo('/', { replace: true }) navigateTo('/', { replace: true })
} else if (data.reason_code === 'VERIFIED') { } else if (data.reason_code === 'VERIFIED') {
if (registerMode.value === 'WHITELIST') { if (registerMode.value === 'WHITELIST') {

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,33 +1,28 @@
import { reactive } from 'vue' import { reactive } from 'vue'
const TOKEN_KEY = 'token' const TOKEN_KEY = 'token'
const USER_ID_KEY = 'userId'
const USERNAME_KEY = 'username'
const ROLE_KEY = 'role'
export const authState = reactive({ export const authState = reactive({
loggedIn: false, loggedIn: false,
userId: null, userId: null,
username: null, username: null,
role: null, role: null,
avatar: null,
}) })
if (import.meta.client) { if (import.meta.client) {
authState.loggedIn = authState.loggedIn =
localStorage.getItem(TOKEN_KEY) !== null && localStorage.getItem(TOKEN_KEY) !== '' localStorage.getItem(TOKEN_KEY) !== null && localStorage.getItem(TOKEN_KEY) !== ''
authState.userId = localStorage.getItem(USER_ID_KEY)
authState.username = localStorage.getItem(USERNAME_KEY)
authState.role = localStorage.getItem(ROLE_KEY)
} }
export function getToken() { export function getToken() {
return import.meta.client ? localStorage.getItem(TOKEN_KEY) : null return import.meta.client ? localStorage.getItem(TOKEN_KEY) : null
} }
export function setToken(token) { export async function setToken(token) {
if (import.meta.client) { if (import.meta.client) {
localStorage.setItem(TOKEN_KEY, token) localStorage.setItem(TOKEN_KEY, token)
authState.loggedIn = true await loadCurrentUser()
} }
} }
@@ -39,26 +34,20 @@ export function clearToken() {
} }
} }
export function setUserInfo({ id, username }) { export function setUserInfo(user) {
if (import.meta.client) { if (import.meta.client) {
authState.userId = id authState.userId = user.id
authState.username = username authState.username = user.username
if (arguments[0] && arguments[0].role) { authState.avatar = user.avatar
authState.role = arguments[0].role authState.role = user.role
localStorage.setItem(ROLE_KEY, arguments[0].role)
}
if (id !== undefined && id !== null) localStorage.setItem(USER_ID_KEY, id)
if (username) localStorage.setItem(USERNAME_KEY, username)
} }
} }
export function clearUserInfo() { export function clearUserInfo() {
if (import.meta.client) { if (import.meta.client) {
localStorage.removeItem(USER_ID_KEY)
localStorage.removeItem(USERNAME_KEY)
localStorage.removeItem(ROLE_KEY)
authState.userId = null authState.userId = null
authState.username = null authState.username = null
authState.avatar = null
authState.role = null authState.role = null
} }
} }
@@ -82,9 +71,11 @@ export async function fetchCurrentUser() {
export async function loadCurrentUser() { export async function loadCurrentUser() {
const user = await fetchCurrentUser() const user = await fetchCurrentUser()
if (user) { if (user) {
setUserInfo({ id: user.id, username: user.username, role: user.role }) setUserInfo(user)
} else {
clearUserInfo()
} }
return user authState.loggedIn = user !== null
} }
export function isLogin() { export function isLogin() {
@@ -100,10 +91,12 @@ export async function checkToken() {
const res = await fetch(`${API_BASE_URL}/api/auth/check`, { const res = await fetch(`${API_BASE_URL}/api/auth/check`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
}) })
authState.loggedIn = res.ok if (res.ok) {
return res.ok await setToken(token)
} else {
clearToken()
}
} catch (e) { } catch (e) {
authState.loggedIn = false clearToken()
return false
} }
} }

View File

@@ -1,5 +1,5 @@
import { toast } from '../main' import { toast } from '../main'
import { setToken, loadCurrentUser } from './auth' import { setToken } from './auth'
import { registerPush } from './push' import { registerPush } from './push'
export function discordAuthorize(inviteToken = '') { export function discordAuthorize(inviteToken = '') {
@@ -47,7 +47,6 @@ export async function discordExchange(code, inviteToken = '', reason = '') {
if (res.ok && data.token) { if (res.ok && data.token) {
setToken(data.token) setToken(data.token)
await loadCurrentUser()
toast.success('登录成功') toast.success('登录成功')
registerPush?.() registerPush?.()
return { success: true, needReason: false } return { success: true, needReason: false }

View File

@@ -1,5 +1,5 @@
import { toast } from '../main' import { toast } from '../main'
import { setToken, loadCurrentUser } from './auth' import { setToken } from './auth'
import { registerPush } from './push' import { registerPush } from './push'
export function githubAuthorize(inviteToken = '') { export function githubAuthorize(inviteToken = '') {
@@ -45,7 +45,6 @@ export async function githubExchange(code, inviteToken = '', reason = '') {
if (res.ok && data.token) { if (res.ok && data.token) {
setToken(data.token) setToken(data.token)
await loadCurrentUser()
toast.success('登录成功') toast.success('登录成功')
registerPush?.() registerPush?.()
return { success: true, needReason: false } return { success: true, needReason: false }

View File

@@ -1,5 +1,5 @@
import { toast } from '../main' import { toast } from '../main'
import { setToken, loadCurrentUser } from './auth' import { setToken } from './auth'
import { registerPush } from './push' import { registerPush } from './push'
export async function googleGetIdToken() { export async function googleGetIdToken() {
@@ -79,7 +79,6 @@ export async function googleAuthWithToken(
if (res.ok && data && data.token) { if (res.ok && data && data.token) {
setToken(data.token) setToken(data.token)
await loadCurrentUser()
toast.success('登录成功') toast.success('登录成功')
registerPush?.() registerPush?.()
if (typeof redirect_success === 'function') redirect_success() if (typeof redirect_success === 'function') redirect_success()

View File

@@ -265,3 +265,26 @@ export function stripMarkdownLength(text, length) {
} }
return plain.slice(0, length) + '...' return plain.slice(0, length) + '...'
} }
// 朴素文本带贴吧表情
export function stripMarkdownWithTiebaMoji(text, length){
console.error(text)
if (!text) return ''
// Markdown 转成纯文本
const plain = stripMarkdown(text)
console.error(plain)
// 替换 :tieba123: 为 <img>
const withEmoji = plain.replace(/:tieba(\d+):/g, (match, num) => {
const key = `tieba${num}`
const file = tiebaEmoji[key]
return file
? `<img loading="lazy" class="emoji" src="${file}" alt="${key}">`
: match // 没有匹配到图片则保留原样
})
// 截断纯文本长度(防止撑太长)
const truncated = withEmoji.length > length ? withEmoji.slice(0, length) + '...' : withEmoji
return truncated
}

View File

@@ -1,5 +1,5 @@
import { toast } from '../main' import { toast } from '../main'
import { setToken, loadCurrentUser } from './auth' import { setToken } from './auth'
import { registerPush } from './push' import { registerPush } from './push'
export function telegramAuthorize(inviteToken = '') { export function telegramAuthorize(inviteToken = '') {
@@ -34,7 +34,6 @@ export async function telegramExchange(authData, inviteToken = '', reason = '')
const data = await res.json() const data = await res.json()
if (res.ok && data.token) { if (res.ok && data.token) {
setToken(data.token) setToken(data.token)
await loadCurrentUser()
toast.success('登录成功') toast.success('登录成功')
registerPush?.() registerPush?.()
return { success: true, needReason: false } return { success: true, needReason: false }

View File

@@ -1,5 +1,5 @@
import { toast } from '../main' import { toast } from '../main'
import { setToken, loadCurrentUser } from './auth' import { setToken } from './auth'
import { registerPush } from './push' import { registerPush } from './push'
function generateCodeVerifier() { function generateCodeVerifier() {
@@ -99,7 +99,6 @@ export async function twitterExchange(code, state, reason) {
const data = await res.json() const data = await res.json()
if (res.ok && data.token) { if (res.ok && data.token) {
setToken(data.token) setToken(data.token)
await loadCurrentUser()
toast.success('登录成功') toast.success('登录成功')
registerPush() registerPush()
return { success: true, needReason: false } return { success: true, needReason: false }