Merge pull request #889 from nagisa77/feature/icon_park

feature: 图标迁移为字节系IconPark
This commit is contained in:
Tim
2025-09-06 21:09:52 +08:00
committed by GitHub
45 changed files with 418 additions and 264 deletions

View File

@@ -25,7 +25,7 @@
class="app-new-post-icon"
@click="goToNewPost"
>
<i class="fas fa-edit"></i>
<edit />
</div>
</div>
<GlobalPopups />

View File

@@ -1,6 +1,6 @@
<template>
<div class="base-input">
<i v-if="icon" :class="['base-input-icon', icon]" />
<component v-if="icon" :is="icon" class="base-input-icon" size="14" />
<!-- 普通输入框 -->
<input
@@ -29,7 +29,7 @@ export default {
inheritAttrs: false,
props: {
modelValue: { type: [String, Number], default: '' },
icon: { type: String, default: '' },
icon: { type: [String, Object], default: '' },
type: { type: String, default: 'text' },
textarea: { type: Boolean, default: false },
},
@@ -66,7 +66,6 @@ export default {
.base-input-icon {
opacity: 0.5;
font-size: 14px;
}
.base-input-text {

View File

@@ -1,6 +1,6 @@
<template>
<div class="base-placeholder">
<i :class="['base-placeholder-icon', icon]" />
<component :is="icon" class="base-placeholder-icon" theme="outline" />
<div class="base-placeholder-text">
<slot>{{ text }}</slot>
</div>
@@ -12,7 +12,7 @@ export default {
name: 'BasePlaceholder',
props: {
text: { type: String, default: '' },
icon: { type: String, default: 'fas fa-inbox' },
icon: { type: String, default: 'inbox' },
},
}
</script>
@@ -27,6 +27,7 @@ export default {
height: 300px;
opacity: 0.5;
}
.base-placeholder-icon,
.base-placeholder-text {
font-size: 16px;
color: var(--text-color);

View File

@@ -8,7 +8,11 @@
:class="['base-tabs-item', { selected: modelValue === tab.key }]"
@click="$emit('update:modelValue', tab.key)"
>
<i v-if="tab.icon" :class="tab.icon"></i>
<component
v-if="tab.icon && (typeof tab.icon !== 'string' || !tab.icon.includes(' '))"
:is="tab.icon"
class="base-tabs-item-icon"
/>
<div class="base-tabs-item-label">{{ tab.label }}</div>
</div>
</div>
@@ -72,6 +76,7 @@ function onTouchEnd(e) {
align-items: center;
}
.base-tabs-item-icon,
.base-tabs-item i {
margin-right: 6px;
}

View File

@@ -7,7 +7,11 @@
@click="item.iconClick && item.iconClick()"
>
<BaseImage v-if="item.src" :src="item.src" class="timeline-img" alt="timeline item" />
<i v-else-if="item.icon" :class="item.icon"></i>
<component
v-else-if="item.icon && (typeof item.icon !== 'string' || !item.icon.includes(' '))"
:is="item.icon"
:size="20"
/>
<BaseImage v-else-if="item.emoji" :src="item.emoji" class="timeline-emoji" alt="emoji" />
</div>
<div class="timeline-content">

View File

@@ -15,7 +15,7 @@
class="option-icon"
:alt="option.name"
/>
<i v-else :class="['option-icon', option.icon]"></i>
<!-- <i v-else :class="['option-icon', option.icon]"></i> -->
</template>
<span>{{ option.name }}</span>
<span class="option-count" v-if="option.count > 0"> x {{ option.count }}</span>

View File

@@ -7,7 +7,7 @@
<div class="comment-bottom-container">
<div class="comment-submit" :class="{ disabled: isDisabled }" @click="submit">
<template v-if="!loading"> 发布评论 </template>
<template v-else> <i class="fa-solid fa-spinner fa-spin"></i> 发布中... </template>
<template v-else> <loading-four /> 发布中... </template>
</div>
</div>
</div>

View File

@@ -15,16 +15,16 @@
<div class="common-info-content-header">
<div class="info-content-header-left">
<span class="user-name">{{ comment.userName }}</span>
<i class="fas fa-medal medal-icon"></i>
<medal-one class="medal-icon" />
<NuxtLink
v-if="comment.medal"
class="medal-name"
:to="`/users/${comment.userId}?tab=achievements`"
>{{ getMedalTitle(comment.medal) }}</NuxtLink
>
<i v-if="comment.pinned" class="fas fa-thumbtack pin-icon"></i>
<pin v-if="comment.pinned" class="pin-icon" />
<span v-if="level >= 2" class="reply-item">
<i class="fas fa-reply reply-icon"></i>
<next class="reply-icon" />
<span class="reply-info">
<BaseImage
class="reply-avatar"
@@ -40,7 +40,7 @@
<div class="info-content-header-right">
<DropdownMenu v-if="commentMenuItems.length > 0" :items="commentMenuItems">
<template #trigger>
<i class="fas fa-ellipsis-vertical action-menu-icon"></i>
<more-one class="action-menu-icon" />
</template>
</DropdownMenu>
</div>
@@ -53,10 +53,10 @@
<div class="article-footer-container">
<ReactionsGroup v-model="comment.reactions" content-type="comment" :content-id="comment.id">
<div class="make-reaction-item comment-reaction" @click="toggleEditor">
<i class="far fa-comment"></i>
<comment-icon />
</div>
<div class="make-reaction-item copy-link" @click="copyCommentLink">
<i class="fas fa-link"></i>
<link-icon />
</div>
</ReactionsGroup>
</div>
@@ -71,8 +71,8 @@
/>
</div>
<div v-if="replyCount && level < 2" class="reply-toggle" @click="toggleReplies">
<i v-if="showReplies" class="fas fa-chevron-up reply-toggle-icon"></i>
<i v-else class="fas fa-chevron-down reply-toggle-icon"></i>
<up v-if="showReplies" class="reply-toggle-icon" />
<down v-else class="reply-toggle-icon" />
{{ replyCount }}条回复
</div>
<div v-if="showReplies && level < 2" class="reply-list">
@@ -375,7 +375,6 @@ const handleContentClick = (e) => {
}
.reply-toggle-icon {
margin-right: 5px;
}
.common-info-content-header {

View File

@@ -19,7 +19,7 @@
class="option-icon"
:alt="label.name"
/>
<i v-else :class="['option-icon', label.icon]"></i>
<component v-else :is="label.icon" class="option-icon" :size="16" />
</template>
<span>{{ label.name }}</span>
</div>
@@ -38,14 +38,14 @@
class="option-icon"
:alt="selectedLabels[0].name"
/>
<i v-else :class="['option-icon', selectedLabels[0].icon]"></i>
<component v-else :is="selectedLabels[0].icon" class="option-icon" :size="16" />
</template>
<span>{{ selectedLabels[0].name }}</span>
</div>
</span>
<span v-else class="placeholder">{{ placeholder }}</span>
</template>
<i class="fas fa-caret-down dropdown-caret"></i>
<down class="dropdown-caret" />
</slot>
</div>
<div
@@ -54,7 +54,7 @@
v-click-outside="close"
>
<div v-if="showSearch" class="dropdown-search">
<i class="fas fa-search search-icon"></i>
<search-icon class="search-icon" />
<input type="text" v-model="search" placeholder="搜索" />
</div>
<div v-if="loading" class="dropdown-loading">
@@ -75,7 +75,7 @@
class="option-icon"
:alt="o.name"
/>
<i v-else :class="['option-icon', o.icon]"></i>
<component v-else :is="o.icon" class="option-icon" :size="16" />
</template>
<span>{{ o.name }}</span>
</slot>
@@ -85,12 +85,12 @@
<Teleport to="body">
<div v-if="open && isMobile" class="dropdown-mobile-page">
<div class="dropdown-mobile-header">
<i class="fas fa-arrow-left" @click="close"></i>
<next class="back-icon" @click="close" />
<span class="mobile-title">{{ placeholder }}</span>
</div>
<div class="dropdown-mobile-menu">
<div v-if="showSearch" class="dropdown-search">
<i class="fas fa-search search-icon"></i>
<search-icon class="search-icon" />
<input type="text" v-model="search" placeholder="搜索" />
</div>
<div v-if="loading" class="dropdown-loading">
@@ -111,7 +111,7 @@
class="option-icon"
:alt="o.name"
/>
<i v-else :class="['option-icon', o.icon]"></i>
<component v-else :is="o.icon" class="option-icon" :size="16" />
</template>
<span>{{ o.name }}</span>
</slot>
@@ -303,6 +303,10 @@ export default {
overflow-y: auto;
}
.dropdown-caret {
margin-left: 5px;
}
.selected-label {
display: inline-flex;
align-items: center;
@@ -326,6 +330,10 @@ export default {
color: var(--text-color);
}
.search-icon {
font-size: 14px;
}
.dropdown-option {
display: flex;
align-items: center;
@@ -376,6 +384,11 @@ export default {
border-bottom: 1px solid var(--normal-border-color);
}
.back-icon {
transform: rotate(180deg);
cursor: pointer;
}
.dropdown-mobile-menu {
flex: 1;
overflow-y: auto;

View File

@@ -4,7 +4,7 @@
<div class="header-content-left">
<div v-if="showMenuBtn" class="menu-btn-wrapper">
<button class="menu-btn" ref="menuBtn" @click="$emit('toggle-menu')">
<i class="fas fa-bars micon"></i>
<application-menu class="micon"></application-menu>
</button>
<span
v-if="isMobile && (unreadMessageCount > 0 || hasChannelUnread)"
@@ -25,39 +25,39 @@
<ClientOnly>
<div class="header-content-right">
<div v-if="isMobile" class="search-icon" @click="search">
<i class="fas fa-search"></i>
<search-icon />
</div>
<div v-if="isMobile" class="theme-icon" @click="cycleTheme">
<i :class="iconClass"></i>
<component :is="iconClass" />
</div>
<div v-if="!isMobile" class="invite_text" @click="copyInviteLink">
<i class="fas fa-copy"></i>
<copy />
邀请
<i v-if="isCopying" class="fas fa-spinner fa-spin"></i>
<loading v-if="isCopying" />
</div>
<ToolTip v-if="!isMobile" content="当前在线人数" placement="bottom">
<div class="online-count">
<i class="fas fa-people-group"></i>
<peoples-two />
<span>{{ onlineCount }}</span>
</div>
</ToolTip>
<ToolTip content="复制RSS链接" placement="bottom">
<div class="rss-icon" @click="copyRssLink">
<i class="fas fa-rss"></i>
<rss />
</div>
</ToolTip>
<ToolTip v-if="!isMobile && isLogin" content="发帖" placement="bottom">
<div class="new-post-icon" @click="goToNewPost">
<i class="fas fa-edit"></i>
<edit />
</div>
</ToolTip>
<ToolTip v-if="isLogin" content="站内信和频道" placement="bottom">
<div class="messages-icon" @click="goToMessages">
<i class="fas fa-comments"></i>
<message-emoji />
<span v-if="unreadMessageCount > 0" class="unread-badge">{{
unreadMessageCount
}}</span>
@@ -69,7 +69,7 @@
<template #trigger>
<div class="avatar-container">
<img class="avatar-img" :src="avatar" alt="avatar" />
<i class="fas fa-caret-down dropdown-icon"></i>
<down />
</div>
</template>
</DropdownMenu>
@@ -143,7 +143,7 @@ async function sendPing() {
method: 'POST',
})
} catch (e) {
console.error("心跳失败", e)
console.error('心跳失败', e)
}
}
@@ -155,11 +155,10 @@ async function fetchCount() {
})
onlineCount.value = await res.json()
} catch (e) {
console.error("获取在线人数失败", e)
console.error('获取在线人数失败', e)
}
}
const search = () => {
showSearch.value = true
nextTick(() => {
@@ -271,11 +270,11 @@ const headerMenuItems = computed(() => [
const iconClass = computed(() => {
switch (themeState.mode) {
case ThemeMode.DARK:
return 'fas fa-moon'
return 'Moon'
case ThemeMode.LIGHT:
return 'fas fa-sun'
return 'SunOne'
default:
return 'fas fa-desktop'
return 'ComputerOne'
}
})
@@ -311,8 +310,8 @@ onMounted(async () => {
// 新增的在线人数逻辑
sendPing()
fetchCount()
setInterval(sendPing, 120000) // 每 2 分钟发一次心跳
setInterval(fetchCount, 60000) // 每 1 分更新 UI
setInterval(sendPing, 120000) // 每 2 分钟发一次心跳
setInterval(fetchCount, 60000) // 每 1 分更新 UI
})
</script>

View File

@@ -2,7 +2,7 @@
<div class="invite-code-activity">
<div class="invite-code-description">
<div class="invite-code-description-title">
<i class="fas fa-info-circle"></i>
<info-icon />
<span class="invite-code-description-title-text">邀请规则说明</span>
</div>
<div class="invite-code-description-content">
@@ -17,7 +17,7 @@
<div v-if="inviteLink" class="invite-code-link-content">
<p class="invite-code-link-content-text">
邀请链接{{ inviteLink }}
<span @click="copyLink"><i class="fas fa-copy copy-icon"></i></span>
<span @click="copyLink"><copy class="copy-icon" /></span>
</p>
</div>

View File

@@ -2,7 +2,7 @@
<div class="login-overlay">
<div class="login-overlay-blur"></div>
<div class="login-overlay-content">
<i class="fa-solid fa-user login-overlay-icon"></i>
<user-icon class="login-overlay-icon" />
<div class="login-overlay-text">请先登录点击跳转到登录页面</div>
<div class="login-overlay-button" @click="goLogin">登录</div>
</div>

View File

@@ -10,7 +10,7 @@
<span class="prize-row-title">奖品图片</span>
<label class="prize-container">
<BaseImage v-if="data.prizeIcon" :src="data.prizeIcon" class="prize-preview" alt="prize" />
<i v-else class="fa-solid fa-image default-prize-icon"></i>
<image-files v-else class="default-prize-icon" />
<div class="prize-overlay">上传奖品图片</div>
<input type="file" class="prize-input" accept="image/*" @change="onPrizeIconChange" />
</label>

View File

@@ -4,7 +4,7 @@
<div class="menu-content">
<div class="menu-item-container">
<NuxtLink class="menu-item" exact-active-class="selected" to="/" @click="handleItemClick">
<i class="menu-item-icon fas fa-hashtag"></i>
<hashtag-key class="menu-item-icon" />
<span class="menu-item-text">话题</span>
</NuxtLink>
<NuxtLink
@@ -13,7 +13,7 @@
to="/new-post"
@click="handleItemClick"
>
<i class="menu-item-icon fas fa-edit"></i>
<edit class="menu-item-icon" />
<span class="menu-item-text">发帖</span>
</NuxtLink>
<NuxtLink
@@ -22,7 +22,7 @@
to="/message"
@click="handleItemClick"
>
<i class="menu-item-icon fas fa-envelope"></i>
<remind class="menu-item-icon" />
<span class="menu-item-text">我的消息</span>
<span v-if="unreadCount > 0" class="unread-container">
<span class="unread"> {{ showUnreadCount }} </span>
@@ -34,7 +34,7 @@
to="/about"
@click="handleItemClick"
>
<i class="menu-item-icon fas fa-info-circle"></i>
<info-icon class="menu-item-icon" />
<span class="menu-item-text">关于</span>
</NuxtLink>
<NuxtLink
@@ -43,7 +43,7 @@
to="/activities"
@click="handleItemClick"
>
<i class="menu-item-icon fas fa-gift"></i>
<gift class="menu-item-icon" />
<span class="menu-item-text">🔥 活动</span>
</NuxtLink>
<NuxtLink
@@ -53,7 +53,7 @@
to="/about/stats"
@click="handleItemClick"
>
<i class="menu-item-icon fas fa-chart-line"></i>
<chart-line class="menu-item-icon" />
<span class="menu-item-text">站点统计</span>
</NuxtLink>
<NuxtLink
@@ -63,7 +63,7 @@
to="/points"
@click="handleItemClick"
>
<i class="menu-item-icon fas fa-coins"></i>
<finance class="menu-item-icon" />
<span class="menu-item-text">
积分商城
<span v-if="myPoint !== null" class="point-count">{{ myPoint }}</span>
@@ -74,7 +74,8 @@
<div class="menu-section">
<div class="section-header" @click="categoryOpen = !categoryOpen">
<span>类别</span>
<i :class="categoryOpen ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
<up v-if="categoryOpen" class="menu-item-icon" />
<down v-else class="menu-item-icon" />
</div>
<div v-if="categoryOpen" class="section-items">
<div v-if="isLoadingCategory" class="menu-loading-container">
@@ -94,7 +95,7 @@
class="section-item-icon"
:alt="c.name"
/>
<i v-else :class="['section-item-icon', c.smallIcon || c.icon]"></i>
<component v-else :is="c.smallIcon || c.icon" class="section-item-icon" />
</template>
<span class="section-item-text">
{{ c.name }}
@@ -107,7 +108,8 @@
<div class="menu-section">
<div class="section-header" @click="tagOpen = !tagOpen">
<span>标签</span>
<i :class="tagOpen ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
<up v-if="tagOpen" class="menu-item-icon" />
<down v-else class="menu-item-icon" />
</div>
<div v-if="tagOpen" class="section-items">
<div v-if="isLoadingTag" class="menu-loading-container">
@@ -120,7 +122,7 @@
class="section-item-icon"
:alt="t.name"
/>
<i v-else class="section-item-icon fas fa-hashtag"></i>
<tag-one v-else class="section-item-icon" />
<span class="section-item-text"
>{{ t.name }} <span class="section-item-text-count">x {{ t.count }}</span></span
>
@@ -133,7 +135,7 @@
<ClientOnly v-if="!isMobile">
<div class="menu-footer">
<div class="menu-footer-btn" @click="cycleTheme">
<i :class="iconClass"></i>
<component :is="iconClass" class="menu-item-icon" />
</div>
</div>
</ClientOnly>
@@ -193,11 +195,11 @@ const {
const iconClass = computed(() => {
switch (themeState.mode) {
case ThemeMode.DARK:
return 'fas fa-moon'
return 'Moon'
case ThemeMode.LIGHT:
return 'fas fa-sun'
return 'SunOne'
default:
return 'fas fa-desktop'
return 'ComputerOne'
}
})

View File

@@ -6,7 +6,7 @@
<div class="message-bottom-container">
<div class="message-submit" :class="{ disabled: isDisabled }" @click="submit">
<template v-if="!loading"> 发送 </template>
<template v-else> <i class="fa-solid fa-spinner fa-spin"></i> 发送中... </template>
<template v-else> <loading-four /> 发送中... </template>
</div>
</div>
</div>

View File

@@ -3,20 +3,10 @@
<iframe :src="iframeSrc" frameborder="0" ref="iframeRef" @load="injectBaseTag"></iframe>
<div class="float-actions">
<i
class="fas fa-chevron-down"
v-if="floatHeight !== MINI_HEIGHT"
title="收起至 100px"
@click="collapseToMini"
></i>
<i
class="fas fa-chevron-up"
v-if="floatHeight !== DEFAULT_HEIGHT"
title="回弹至 60vh"
@click="reboundToDefault"
></i>
<i class="fas fa-expand" title="在页面中打开" @click="expand"></i>
<i class="fas fa-times" title="关闭" @click="close"></i>
<down v-if="floatHeight !== MINI_HEIGHT" title="收起至 100px" @click="collapseToMini" />
<up v-if="floatHeight !== DEFAULT_HEIGHT" title="回弹至 60vh" @click="reboundToDefault" />
<expand-up title="在页面中打开" @click="expand" />
<close-icon title="关闭" @click="close" />
</div>
</div>
</template>

View File

@@ -2,7 +2,7 @@
<div class="milk-tea-activity">
<div class="milk-tea-description">
<div class="milk-tea-description-title">
<i class="fas fa-info-circle"></i>
<info-icon />
<span class="milk-tea-description-title-text">升级规则说明</span>
</div>
<div class="milk-tea-description-content">
@@ -29,7 +29,7 @@
/>
</div>
<div v-else class="user-level">
<div class="user-level-text"><i class="fas fa-user-circle"></i> 请登录查看自身等级</div>
<div class="user-level-text"><user-icon /> 请登录查看自身等级</div>
</div>
</div>
<div

View File

@@ -10,13 +10,13 @@
:src="lottery.prizeIcon"
alt="prize"
/>
<i v-else class="fa-solid fa-gift default-prize-icon"></i>
<gift v-else class="default-prize-icon" />
</div>
<div class="prize-name">{{ lottery.prizeDescription }}</div>
<div class="prize-count">x {{ lottery.prizeCount }}</div>
</div>
<div class="prize-end-time prize-info-right">
<i v-if="!lotteryEnded" class="fas fa-stopwatch prize-end-time-icon"></i>
<stopwatch v-if="!lotteryEnded" class="prize-end-time-icon" />
<div v-if="!isMobile && !lotteryEnded" class="prize-end-time-title">离结束</div>
<div class="prize-end-time-value">{{ countdown }}</div>
<div v-if="!isMobile" class="join-prize-button-container-desktop">
@@ -26,7 +26,8 @@
@click="joinLottery"
>
<div class="join-prize-button-text">
参与抽奖 <i class="fas fa-coins"></i> {{ lottery.pointCost }}
参与抽奖 <paper-money-two class="join-prize-button-text-icon" />
{{ lottery.pointCost }}
</div>
</div>
<div v-else-if="hasJoined" class="join-prize-button-disabled">
@@ -43,7 +44,7 @@
@click="joinLottery"
>
<div class="join-prize-button-text">
参与抽奖 <i class="fas fa-coins"></i> {{ lottery.pointCost }}
参与抽奖 <paper-money-two class="join-prize-button-text-icon" /> {{ lottery.pointCost }}
</div>
</div>
<div v-else-if="hasJoined" class="join-prize-button-disabled">
@@ -61,7 +62,7 @@
@click="gotoUser(p.id)"
/>
<div v-if="lotteryEnded && lotteryWinners.length" class="prize-member-winner">
<i class="fas fa-medal medal-icon"></i>
<medal-one class="medal-icon"></medal-one>
<span class="prize-member-winner-name">获奖者: </span>
<BaseImage
v-for="w in lotteryWinners"

View File

@@ -34,7 +34,7 @@
<div class="poll-option-title" v-else>单选</div>
<div class="poll-left-time">
<i class="fas fa-stopwatch poll-left-time-icon"></i>
<stopwatch class="poll-left-time-icon" />
<div class="poll-left-time-title">离结束</div>
<div class="poll-left-time-value">{{ countdown }}</div>
</div>
@@ -55,9 +55,7 @@
</div>
<div class="multi-selection-container">
<div class="join-poll-button" @click="submitMultiPoll">
<i class="fas fa-check"></i> 确认投票
</div>
<div class="join-poll-button" @click="submitMultiPoll"><check /> 确认投票</div>
</div>
</template>
<template v-else>
@@ -77,9 +75,7 @@
</div>
<div class="single-selection-container">
<div class="join-poll-button" @click="submitSinglePoll">
<i class="fas fa-check"></i> 确认投票
</div>
<div class="join-poll-button" @click="submitSinglePoll"><check /> 确认投票</div>
</div>
</template>
</div>
@@ -95,22 +91,20 @@
class="poll-option-button"
@click="showPollResult = false"
>
<i class="fas fa-chevron-left"></i> 投票
<arrow-left /> 投票
</div>
<div
v-else-if="!pollEnded && !hasVoted"
class="poll-option-button"
@click="showPollResult = true"
>
<i class="fas fa-chart-bar"></i> 结果
</div>
<div v-else-if="pollEnded" class="poll-option-hint">
<i class="fas fa-stopwatch"></i> 投票已结束
<chart-histogram /> 结果
</div>
<div v-else-if="pollEnded" class="poll-option-hint"><stopwatch /> 投票已结束</div>
<div v-else class="poll-option-hint">
<div>您已投票等待结束查看结果</div>
<div class="poll-left-time">
<i class="fas fa-stopwatch poll-left-time-icon"></i>
<stopwatch class="poll-left-time-icon" />
<div class="poll-left-time-title">离结束</div>
<div class="poll-left-time-value">{{ countdown }}</div>
</div>

View File

@@ -31,9 +31,9 @@ export default {
const fetchTypes = async () => {
return [
{ id: 'NORMAL', name: '普通帖子', icon: 'fa-regular fa-file' },
{ id: 'LOTTERY', name: '抽奖帖子', icon: 'fa-solid fa-gift' },
{ id: 'POLL', name: '投票帖子', icon: 'fa-solid fa-square-poll-vertical' },
{ id: 'NORMAL', name: '普通帖子', icon: 'file-text' },
{ id: 'LOTTERY', name: '抽奖帖子', icon: 'gift' },
{ id: 'POLL', name: '投票帖子', icon: 'ranking-list' },
]
}

View File

@@ -19,8 +19,7 @@
</div>
<div class="reactions-viewer-item placeholder" @click="openPanel">
<i class="far fa-smile reactions-viewer-item-placeholder-icon"></i>
<!-- <span class="reactions-viewer-item-placeholder-text">点击以表态</span> -->
<sly-face-whit-smile class="reactions-viewer-item-placeholder-icon" />
</div>
</template>
<template v-else-if="displayedReactions.length">
@@ -42,8 +41,8 @@
class="make-reaction-item like-reaction"
@click="toggleReaction('LIKE')"
>
<i v-if="!userReacted('LIKE')" class="far fa-heart"></i>
<i v-else class="fas fa-heart"></i>
<like v-if="!userReacted('LIKE')" />
<like v-else theme="filled" />
<span class="reactions-count" v-if="likeCount">{{ likeCount }}</span>
</div>
<slot></slot>

View File

@@ -13,7 +13,7 @@
>
<template #display="{ setSearch }">
<div class="search-input">
<i class="search-input-icon fas fa-search"></i>
<search-icon class="search-input-icon" />
<input
class="text-input"
v-model="keyword"
@@ -24,7 +24,7 @@
</template>
<template #option="{ option }">
<div class="search-option-item">
<i :class="['result-icon', iconMap[option.type] || 'fas fa-question']"></i>
<component :is="iconMap[option.type]" class="result-icon" />
<div class="result-body">
<div class="result-main" v-html="highlight(option.text)"></div>
<div v-if="option.subText" class="result-sub" v-html="highlight(option.subText)"></div>
@@ -83,11 +83,12 @@ const highlight = (text) => {
}
const iconMap = {
user: 'fas fa-user',
post: 'fas fa-file-alt',
comment: 'fas fa-comment',
category: 'fas fa-folder',
tag: 'fas fa-hashtag',
user: 'UserIcon',
post: 'FileText',
post_title: 'FileText',
comment: 'CommentIcon',
category: 'Inbox',
tag: 'TagOne',
}
watch(selected, (val) => {

View File

@@ -13,7 +13,7 @@
>
<template #display="{ setSearch }">
<div class="search-input">
<i class="search-input-icon fas fa-search"></i>
<search-icon class="search-input-icon" />
<input
class="text-input"
v-model="keyword"

View File

@@ -10,14 +10,14 @@
<template #option="{ option }">
<div class="option-container">
<div class="option-main">
<template v-if="option.icon">
<template v-if="option.smallIcon || option.icon">
<BaseImage
v-if="isImageIcon(option.icon)"
:src="option.icon"
v-if="isImageIcon(option.smallIcon || option.icon)"
:src="option.smallIcon || option.icon"
class="option-icon"
:alt="option.name"
/>
<i v-else :class="['option-icon', option.icon]"></i>
<component v-else :is="option.smallIcon || option.icon" class="option-icon" />
</template>
<span>{{ option.name }}</span>
<span class="option-count" v-if="option.count > 0"> x {{ option.count }}</span>

View File

@@ -1,6 +1,6 @@
<template>
<div class="user-list">
<BasePlaceholder v-if="users.length === 0" text="暂无用户" icon="fas fa-inbox" />
<BasePlaceholder v-if="users.length === 0" text="暂无用户" icon="inbox" />
<div v-for="u in users" :key="u.id" class="user-item" @click="handleUserClick(u)">
<BaseImage :src="u.avatar" alt="avatar" class="user-avatar" />
<div class="user-info">

View File

@@ -19,7 +19,12 @@ export default defineNuxtConfig({
telegramBotId: process.env.NUXT_PUBLIC_TELEGRAM_BOT_ID || '',
},
},
css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'],
css: [
'vditor/dist/index.css',
'~/assets/fonts.css',
'~/assets/global.css',
'@icon-park/vue-next/styles/index.css',
],
app: {
pageTransition: { name: 'page', mode: 'out-in' },
head: {
@@ -76,11 +81,11 @@ export default defineNuxtConfig({
rel: 'manifest',
href: '/manifest.webmanifest',
},
{
rel: 'stylesheet',
href: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css',
referrerpolicy: 'no-referrer',
},
// {
// rel: 'stylesheet',
// href: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css',
// referrerpolicy: 'no-referrer',
// },
],
},
baseURL: '/',

View File

@@ -6,6 +6,7 @@
"": {
"name": "frontend_nuxt",
"dependencies": {
"@icon-park/vue-next": "^1.4.2",
"@nuxt/image": "^1.11.0",
"@stomp/stompjs": "^7.0.0",
"cropperjs": "^1.6.2",
@@ -25,6 +26,9 @@
"vue-echarts": "^7.0.3",
"vue-flatpickr-component": "^12.0.0",
"vue-toastification": "^2.0.0-rc.5"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@ampproject/remapping": {
@@ -990,6 +994,19 @@
"integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
"license": "MIT"
},
"node_modules/@icon-park/vue-next": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@icon-park/vue-next/-/vue-next-1.4.2.tgz",
"integrity": "sha512-+QklF255wkfBOabY+xw6FAI0Bwln/RhdwCunNy/9sKdKuChtaU67QZqU67KGAvZUTeeBgsL+yaHHxqfQeGZXEQ==",
"license": "Apache-2.0",
"engines": {
"node": ">= 8.0.0",
"npm": ">= 5.0.0"
},
"peerDependencies": {
"vue": "3.x"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",

View File

@@ -12,6 +12,7 @@
"generate": "nuxt generate"
},
"dependencies": {
"@icon-park/vue-next": "^1.4.2",
"@nuxt/image": "^1.11.0",
"@stomp/stompjs": "^7.0.0",
"cropperjs": "^1.6.2",

View File

@@ -18,7 +18,7 @@
<div class="activity-list-page-card-content">{{ a.content }}</div>
<div class="activity-list-page-card-footer">
<div class="activity-list-page-card-footer-start-time">
<i class="fas fa-clock"></i>
<stopwatch />
<span>开始于 {{ TimeManager.format(a.startTime) }}</span>
</div>
</div>

View File

@@ -2,26 +2,26 @@
<div class="forgot-page">
<div class="forgot-content">
<div class="forgot-title">找回密码</div>
<div v-if="step === 0" class="step-content">
<BaseInput icon="fas fa-envelope" v-model="email" placeholder="邮箱" />
<BaseInput icon="mail" v-model="email" placeholder="邮箱" />
<div v-if="emailError" class="error-message">{{ emailError }}</div>
<div class="primary-button" @click="sendCode" v-if="!isSending">发送验证码</div>
<div class="primary-button disabled" v-else>发送中...</div>
</div>
<div v-else-if="step === 1" class="step-content">
<BaseInput icon="fas fa-envelope" v-model="code" placeholder="邮箱验证码" />
<BaseInput icon="mail" v-model="code" placeholder="邮箱验证码" />
<div class="primary-button" @click="verifyCode" v-if="!isVerifying">验证</div>
<div class="primary-button disabled" v-else>验证中...</div>
</div>
<div v-else class="step-content">
<BaseInput icon="fas fa-lock" v-model="password" type="password" placeholder="新密码" />
<BaseInput icon="lock" v-model="password" type="password" placeholder="新密码" />
<div v-if="passwordError" class="error-message">{{ passwordError }}</div>
<div class="primary-button" @click="resetPassword" v-if="!isResetting">重置密码</div>
<div class="primary-button disabled" v-else>提交中...</div>
</div>
<div class="hint-message">
<i class="fas fa-info-circle"></i>
<info-icon />
使用 Google 注册的用户可使用对应的邮箱进行找回密码
</div>
</div>

View File

@@ -68,13 +68,10 @@
>
<div class="article-main-container">
<NuxtLink class="article-item-title main-item" :to="`/posts/${article.id}`">
<i v-if="article.pinned" class="fas fa-thumbtack pinned-icon"></i>
<i v-if="article.type === 'LOTTERY'" class="fa-solid fa-gift lottery-icon"></i>
<i
v-else-if="article.type === 'POLL'"
class="fa-solid fa-square-poll-vertical poll-icon"
></i>
<i v-if="!article.rssExcluded" class="fa-solid fa-star featured-icon"></i>
<pin v-if="article.pinned" theme="outline" class="pinned-icon" />
<gift v-if="article.type === 'LOTTERY'" class="lottery-icon" />
<ranking-list v-else-if="article.type === 'POLL'" class="poll-icon" />
<star v-if="!article.rssExcluded" class="featured-icon" />
{{ article.title }}
</NuxtLink>
<NuxtLink class="article-item-description main-item" :to="`/posts/${article.id}`">
@@ -141,7 +138,6 @@ import { getToken } from '~/utils/auth'
import { stripMarkdown } from '~/utils/markdown'
import { useIsMobile } from '~/utils/screen'
import TimeManager from '~/utils/time'
useHead({
title: 'OpenIsle - 全面开源的自由社区',
meta: [

View File

@@ -6,9 +6,9 @@
</div>
<div class="email-login-page-content">
<BaseInput icon="fas fa-envelope" v-model="username" placeholder="邮箱/用户名" />
<BaseInput icon="mail" v-model="username" placeholder="邮箱/用户名" />
<BaseInput icon="fas fa-lock" v-model="password" type="password" placeholder="密码" />
<BaseInput icon="lock" v-model="password" type="password" placeholder="密码" />
<div v-if="!isWaitingForLogin" class="login-page-button-primary" @click="submitLogin">
<div class="login-page-button-text">登录</div>
@@ -16,7 +16,7 @@
<div v-else class="login-page-button-primary disabled">
<div class="login-page-button-text">
<i class="fas fa-spinner fa-spin"></i>
<loading-four />
登录中...
</div>
</div>
@@ -28,7 +28,7 @@
>
</div>
<div class="hint-message">
<i class="fas fa-info-circle"></i>
<info-icon />
使用右侧第三方OAuth注册/登录的用户可使用对应的邮箱进行重设密码
</div>
</div>

View File

@@ -3,14 +3,14 @@
<div v-if="!loading" class="chat-header">
<div class="header-main">
<div class="back-button" @click="goBack">
<i class="fas fa-arrow-left"></i>
<arrow-left />
</div>
<h2 class="participant-name">
{{ isChannel ? conversationName : otherParticipant?.username }}
</h2>
</div>
<div v-if="!isFloatMode" class="float-control">
<i class="fas fa-compress" @click="minimize" title="最小化"></i>
<collapse-text-input class="float-control-icon" @click="minimize" title="最小化" />
</div>
</div>
@@ -48,7 +48,7 @@
:content-id="item.id"
@update:modelValue="(v) => (item.reactions = v)"
>
<i class="fas fa-reply reply-btn" @click="setReply(item)"> 写个回复...</i>
<div class="reply-btn"><next @click="setReply(item)" /> 写个回复...</div>
</ReactionsGroup>
</template>
</BaseTimeline>
@@ -56,7 +56,7 @@
<BasePlaceholder
v-if="messages.length === 0"
text="暂无会话,发送消息试试 🎉"
icon="fas fa-inbox"
icon="inbox"
/>
</div>
</template>
@@ -66,7 +66,7 @@
<div v-if="replyTo" class="active-reply">
正在回复 {{ replyTo.sender.username }}:
{{ stripMarkdownLength(replyTo.content, 50) }}
<i class="fas fa-times close-reply" @click="replyTo = null"></i>
<close-icon class="close-reply" @click="replyTo = null" />
</div>
<MessageEditor :loading="sending" @submit="sendMessage" />
</div>
@@ -351,9 +351,9 @@ onMounted(async () => {
})
const subscribeToConversation = () => {
if (!currentUser.value) return;
if (!currentUser.value) return
const destination = `/topic/conversation/${conversationId}`
subscribe(destination, async (message) => {
try {
const parsedMessage = JSON.parse(message.body)
@@ -370,12 +370,12 @@ const subscribeToConversation = () => {
await markConversationAsRead()
await nextTick()
if (isUserNearBottom.value) {
scrollToBottomSmooth()
}
} catch (e) {
console.error("Failed to parse websocket message", e)
console.error('Failed to parse websocket message', e)
}
})
}
@@ -394,7 +394,7 @@ onActivated(async () => {
await nextTick()
scrollToBottomSmooth()
updateNearBottom()
if (isConnected.value) {
// 如果已连接,重新订阅
subscribeToConversation()

View File

@@ -1,11 +1,11 @@
<template>
<div class="messages-container">
<div class="page-title">
<i class="fas fa-comments"></i>
<message-emoji />
<span class="page-title-text">选择聊天</span>
</div>
<div v-if="!isFloatMode" class="float-control">
<i class="fas fa-compress" @click="minimize" title="最小化"></i>
<collapse-text-input class="float-control-icon" @click="minimize" title="最小化" />
</div>
<BaseTabs v-model="activeTab" :tabs="tabs">
<div v-if="activeTab === 'messages'">
@@ -22,7 +22,7 @@
</div>
<div v-if="!loading && conversations.length === 0" class="empty-container">
<BasePlaceholder v-if="conversations.length === 0" text="暂无会话" icon="fas fa-inbox" />
<BasePlaceholder v-if="conversations.length === 0" text="暂无会话" icon="inbox" />
</div>
<div
@@ -73,7 +73,7 @@
</div>
<div v-else>
<div v-if="channels.length === 0" class="empty-container">
<BasePlaceholder text="暂无频道" icon="fas fa-inbox" />
<BasePlaceholder text="暂无频道" icon="inbox" />
</div>
<div
v-for="ch in channels"
@@ -273,9 +273,9 @@ onActivated(async () => {
})
const subscribeToUserMessages = () => {
if (!currentUser.value) return;
if (!currentUser.value) return
const destination = `/topic/user/${currentUser.value.id}/messages`
subscribe(destination, (message) => {
if (activeTab.value === 'messages') {
fetchConversations()

View File

@@ -4,7 +4,7 @@
<template #right>
<div class="message-page-header-right">
<div class="message-page-header-right-item" @click="markAllRead">
<i class="fas fa-bolt message-page-header-right-item-button-icon"></i>
<check-correct class="message-page-header-right-item-button-icon" />
<span class="message-page-header-right-item-button-text"> 已读所有消息 </span>
</div>
</div>
@@ -14,8 +14,12 @@
<div class="message-control-container">
<div class="message-control-title">通知设置</div>
<div class="message-control-item-container">
<template v-for="pref in notificationPrefs">
<div v-if="canShowNotification(pref.type)" :key="pref.type" class="message-control-item">
<template v-for="pref in notificationPrefs">
<div
v-if="canShowNotification(pref.type)"
:key="pref.type"
class="message-control-item"
>
<div class="message-control-item-label">{{ formatType(pref.type) }}</div>
<BaseSwitch
:model-value="pref.enabled"
@@ -47,7 +51,7 @@
<BasePlaceholder
v-else-if="notifications.length === 0"
text="暂时没有消息 :)"
icon="fas fa-inbox"
icon="inbox"
/>
<div class="timeline-container" v-if="notifications.length > 0">
@@ -757,7 +761,12 @@ const formatType = (t) => {
const isAdmin = computed(() => authState.role === 'ADMIN')
const needAdminSet = new Set(['POST_REVIEW_REQUEST','REGISTER_REQUEST', 'POINT_REDEEM', 'ACTIVITY_REDEEM'])
const needAdminSet = new Set([
'POST_REVIEW_REQUEST',
'REGISTER_REQUEST',
'POINT_REDEEM',
'ACTIVITY_REDEEM',
])
const canShowNotification = (type) => {
return !needAdminSet.has(type) || isAdmin.value

View File

@@ -13,13 +13,13 @@
<PostTypeSelect v-model="postType" />
</div>
<div class="post-options-right">
<div class="post-clear" @click="clearPost"><i class="fa-solid fa-eraser"></i> 清空</div>
<div class="post-clear" @click="clearPost"><clear-icon /> 清空</div>
<div class="ai-generate" @click="aiGenerate">
<i class="fa-solid fa-robot"></i>
<smart-optimization />
MD 格式优化
</div>
<div class="post-draft" @click="saveDraft">
<i class="fa-solid fa-floppy-disk"></i>
<save-icon />
存草稿
</div>
<div
@@ -30,9 +30,7 @@
>
发布
</div>
<div v-else class="post-submit-loading">
<i class="fa-solid fa-spinner fa-spin"></i> 发布中...
</div>
<div v-else class="post-submit-loading"><loading-four /> 发布中...</div>
</div>
</div>
<LotteryForm v-if="postType === 'LOTTERY'" :data="lottery" />

View File

@@ -25,7 +25,7 @@
<div class="point-info">
<p v-if="authState.loggedIn && point !== null">
<span><i class="fas fa-coins coin-icon"></i></span>我的积分<span
<span><paper-money-two class="coin-icon" /></span>我的积分<span
class="point-value"
>{{ point }}</span
>
@@ -37,7 +37,7 @@
<BaseImage class="goods-item-image" :src="good.image" alt="good.name" />
<div class="goods-item-name">{{ good.name }}</div>
<div class="goods-item-cost">
<i class="fas fa-coins"></i>
<paper-money-two />
{{ good.cost }} 积分
</div>
<div
@@ -63,11 +63,7 @@
<div class="loading-points-container" v-if="historyLoading">
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
</div>
<BasePlaceholder
v-else-if="histories.length === 0"
text="暂无积分记录"
icon="fas fa-inbox"
/>
<BasePlaceholder v-else-if="histories.length === 0" text="暂无积分记录" icon="inbox" />
<div class="timeline-container" v-else>
<BaseTimeline :items="histories">
<template #item="{ item }">
@@ -189,7 +185,7 @@
参与获得 {{ item.amount }} 积分
</template>
<template v-else-if="item.type === 'SYSTEM_ONLINE'"> 积分历史系统上线 </template>
<i class="fas fa-coins"></i> 你目前的积分是 {{ item.balance }}
<paper-money-two /> 你目前的积分是 {{ item.balance }}
</div>
<div class="history-time">{{ TimeManager.format(item.createdAt) }}</div>
</template>
@@ -242,18 +238,18 @@ const loading = ref(false)
const selectedGood = ref(null)
const iconMap = {
POST: 'fas fa-file-alt',
COMMENT: 'fas fa-comment',
POST_LIKED: 'fas fa-thumbs-up',
COMMENT_LIKED: 'fas fa-thumbs-up',
INVITE: 'fas fa-user-plus',
SYSTEM_ONLINE: 'fas fa-clock',
REDEEM: 'fas fa-gift',
FEATURE: 'fas fa-star',
LOTTERY_JOIN: 'fas fa-ticket-alt',
LOTTERY_REWARD: 'fas fa-ticket-alt',
POST_LIKE_CANCELLED: 'fas fa-thumbs-down',
COMMENT_LIKE_CANCELLED: 'fas fa-thumbs-down',
POST: 'file-text',
COMMENT: 'comment-icon',
POST_LIKED: 'like',
COMMENT_LIKED: 'like',
INVITE: 'add-user',
SYSTEM_ONLINE: 'history-icon',
REDEEM: 'gift',
FEATURE: 'star',
LOTTERY_JOIN: 'medal-one',
LOTTERY_REWARD: 'fireworks',
POST_LIKE_CANCELLED: 'clear-icon',
COMMENT_LIKE_CANCELLED: 'clear-icon',
}
const loadTrend = async () => {

View File

@@ -12,9 +12,9 @@
<TagSelect v-model="selectedTags" creatable />
</div>
<div class="post-options-right">
<div class="post-clear" @click="clearPost"><i class="fa-solid fa-eraser"></i> 清空</div>
<div class="post-clear" @click="clearPost"><clear-icon /> 清空</div>
<div class="ai-generate" @click="aiGenerate">
<i class="fa-solid fa-robot"></i>
<smart-optimization />
MD 格式优化
</div>
<div class="post-cancel" @click="cancelEdit">取消</div>
@@ -26,9 +26,7 @@
>
更新
</div>
<div v-else class="post-submit-loading">
<i class="fa-solid fa-spinner fa-spin"></i> 更新中...
</div>
<div v-else class="post-submit-loading"><loading-four /> 更新中...</div>
</div>
</div>
</div>

View File

@@ -22,7 +22,7 @@
class="article-subscribe-button"
@click="subscribePost"
>
<i class="fas fa-user-plus"></i>
<people-plus />
<div class="article-subscribe-button-text">
{{ isMobile ? '订阅' : '订阅文章' }}
</div>
@@ -32,14 +32,14 @@
class="article-unsubscribe-button"
@click="unsubscribePost"
>
<i class="fas fa-user-minus"></i>
<people-minus-one />
<div class="article-unsubscribe-button-text">
{{ isMobile ? '退订' : '取消订阅' }}
</div>
</div>
<DropdownMenu v-if="articleMenuItems.length > 0" :items="articleMenuItems">
<template #trigger>
<i class="fas fa-ellipsis-vertical action-menu-icon"></i>
<more-one class="action-menu-icon" />
</template>
</DropdownMenu>
</div>
@@ -53,7 +53,7 @@
<div v-if="isMobile" class="info-content-header">
<div class="user-name">
{{ author.username }}
<i class="fas fa-medal medal-icon"></i>
<medal-one class="medal-icon" />
<NuxtLink
v-if="author.displayMedal"
class="user-medal"
@@ -69,7 +69,7 @@
<div v-if="!isMobile" class="info-content-header">
<div class="user-name">
{{ author.username }}
<i class="fas fa-medal medal-icon"></i>
<medal-one class="medal-icon" />
<NuxtLink
v-if="author.displayMedal"
class="user-medal"
@@ -88,7 +88,7 @@
<div class="article-footer-container">
<ReactionsGroup v-model="postReactions" content-type="post" :content-id="postId">
<div class="make-reaction-item copy-link" @click="copyPostLink">
<i class="fas fa-link"></i>
<link-icon />
</div>
</ReactionsGroup>
</div>
@@ -707,8 +707,8 @@ const unsubscribePost = async () => {
const fetchCommentSorts = () => {
return Promise.resolve([
{ id: 'NEWEST', name: '最新', icon: 'fas fa-clock' },
{ id: 'OLDEST', name: '最旧', icon: 'fas fa-hourglass-start' },
{ id: 'NEWEST', name: '最新', icon: 'lightning' },
{ id: 'OLDEST', name: '最旧', icon: 'history-icon' },
// { id: 'MOST_INTERACTIONS', name: '最多互动', icon: 'fas fa-fire' }
])
}

View File

@@ -7,7 +7,7 @@
@crop="onCropped"
/>
<div v-if="isLoadingPage" class="loading-page">
<l-hatch size="20" 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 v-else>
<div class="settings-title">个人资料设置</div>
@@ -23,7 +23,7 @@
</div>
<div class="form-row username-row">
<BaseInput
icon="fas fa-user"
icon="user-icon"
v-model="username"
@input="usernameError = ''"
placeholder="用户名"
@@ -133,15 +133,15 @@ const onCropped = ({ file, url }) => {
}
const fetchPublishModes = () => {
return Promise.resolve([
{ id: 'DIRECT', name: '直接发布', icon: 'fas fa-bolt' },
{ id: 'REVIEW', name: '审核后发布', icon: 'fas fa-search' },
{ id: 'DIRECT', name: '直接发布', icon: 'send-icon' },
{ id: 'REVIEW', name: '审核后发布', icon: 'search-icon' },
])
}
const fetchPasswordStrengths = () => {
return Promise.resolve([
{ id: 'LOW', name: '低', icon: 'fas fa-lock-open' },
{ id: 'MEDIUM', name: '中', icon: 'fas fa-lock' },
{ id: 'HIGH', name: '高', icon: 'fas fa-user-shield' },
{ id: 'LOW', name: '低', icon: 'unlock' },
{ id: 'MEDIUM', name: '中', icon: 'lock-one' },
{ id: 'HIGH', name: '高', icon: 'lock' },
])
}
const fetchAiLimits = () => {
@@ -154,8 +154,8 @@ const fetchAiLimits = () => {
}
const fetchRegisterModes = () => {
return Promise.resolve([
{ id: 'DIRECT', name: '直接注册', icon: 'fas fa-user-check' },
{ id: 'WHITELIST', name: '白名单邀请制', icon: 'fas fa-envelope' },
{ id: 'DIRECT', name: '直接注册', icon: 'send-icon' },
{ id: 'WHITELIST', name: '白名单邀请制', icon: 'search-icon' },
])
}
const loadAdminConfig = async () => {

View File

@@ -6,16 +6,11 @@
</div>
<div v-if="emailStep === 0" class="email-signup-page-content">
<BaseInput
icon="fas fa-envelope"
v-model="email"
@input="emailError = ''"
placeholder="邮箱"
/>
<BaseInput icon="mail" v-model="email" @input="emailError = ''" placeholder="邮箱" />
<div v-if="emailError" class="error-message">{{ emailError }}</div>
<BaseInput
icon="fas fa-user"
icon="user-icon"
v-model="username"
@input="usernameError = ''"
placeholder="用户名"
@@ -23,7 +18,7 @@
<div v-if="usernameError" class="error-message">{{ usernameError }}</div>
<BaseInput
icon="fas fa-lock"
icon="lock"
v-model="password"
@input="passwordError = ''"
type="password"
@@ -40,7 +35,7 @@
</div>
<div v-else class="signup-page-button-primary disabled">
<div class="signup-page-button-text">
<i class="fas fa-spinner fa-spin"></i>
<loading-four />
发送中...
</div>
</div>
@@ -51,7 +46,7 @@
</div>
<div v-if="emailStep === 1" class="email-signup-page-content">
<BaseInput icon="fas fa-envelope" v-model="code" placeholder="邮箱验证码" />
<BaseInput icon="mail" v-model="code" placeholder="邮箱验证码" />
<div
v-if="!isWaitingForEmailVerified"
class="signup-page-button-primary"
@@ -61,7 +56,7 @@
</div>
<div v-else class="signup-page-button-primary disabled">
<div class="signup-page-button-text">
<i class="fas fa-spinner fa-spin"></i>
<loading-four />
验证中...
</div>
</div>

View File

@@ -18,7 +18,7 @@
class="profile-page-header-subscribe-button"
@click="subscribeUser"
>
<i class="fas fa-user-plus"></i>
<add-user />
关注
</div>
<div
@@ -26,11 +26,11 @@
class="profile-page-header-unsubscribe-button"
@click="unsubscribeUser"
>
<i class="fas fa-user-minus"></i>
<reduce-user />
取消关注
</div>
<div v-if="!isMine" class="profile-page-header-subscribe-button" @click="sendMessage">
<i class="fas fa-paper-plane"></i>
<message-one />
发私信
</div>
</div>
@@ -45,7 +45,7 @@
content="经验值可通过发帖、评论等操作获得,达到目标后即可提升等级,解锁更多功能。"
placement="bottom"
>
<i class="fas fa-info-circle profile-exp-info"></i>
<info-icon class="profile-exp-info" />
</ToolTip>
</div>
</div>
@@ -207,7 +207,7 @@
<BasePlaceholder
v-if="filteredTimelineItems.length === 0"
text="暂无时间线"
icon="fas fa-inbox"
icon="inbox"
/>
<div class="timeline-list">
<BaseTimeline :items="filteredTimelineItems">
@@ -305,7 +305,7 @@
</BaseTimeline>
</div>
<div v-else>
<BasePlaceholder text="暂无收藏文章" icon="fas fa-inbox" />
<BasePlaceholder text="暂无收藏文章" icon="inbox" />
</div>
</div>
@@ -368,11 +368,11 @@ const selectedTab = ref(
: 'summary',
)
const tabs = [
{ key: 'summary', label: '总结', icon: 'fas fa-chart-line' },
{ key: 'timeline', label: '时间线', icon: 'fas fa-clock' },
{ key: 'following', label: '关注', icon: 'fas fa-user-plus' },
{ key: 'favorites', label: '收藏', icon: 'fas fa-bookmark' },
{ key: 'achievements', label: '勋章', icon: 'fas fa-medal' },
{ key: 'summary', label: '总结', icon: 'chart-line' },
{ key: 'timeline', label: '时间线', icon: 'alarm-clock' },
{ key: 'following', label: '关注', icon: 'add-user' },
{ key: 'favorites', label: '收藏', icon: 'bookmark' },
{ key: 'achievements', label: '勋章', icon: 'medal-one' },
]
const followTab = ref('followers')
@@ -415,19 +415,19 @@ const fetchSummary = async () => {
const postsRes = await fetch(`${API_BASE_URL}/api/users/${username}/hot-posts`)
if (postsRes.ok) {
const data = await postsRes.json()
hotPosts.value = data.map((p) => ({ icon: 'fas fa-book', post: p }))
hotPosts.value = data.map((p) => ({ icon: 'file-text', post: p }))
}
const repliesRes = await fetch(`${API_BASE_URL}/api/users/${username}/hot-replies`)
if (repliesRes.ok) {
const data = await repliesRes.json()
hotReplies.value = data.map((c) => ({ icon: 'fas fa-comment', comment: c }))
hotReplies.value = data.map((c) => ({ icon: 'comment-icon', comment: c }))
}
const tagsRes = await fetch(`${API_BASE_URL}/api/users/${username}/hot-tags`)
if (tagsRes.ok) {
const data = await tagsRes.json()
hotTags.value = data.map((t) => ({ icon: 'fas fa-tag', tag: t }))
hotTags.value = data.map((t) => ({ icon: 'tag-one', tag: t }))
}
}
@@ -443,19 +443,19 @@ const fetchTimeline = async () => {
const mapped = [
...posts.map((p) => ({
type: 'post',
icon: 'fas fa-book',
icon: 'file-text',
post: p,
createdAt: p.createdAt,
})),
...replies.map((r) => ({
type: r.parentComment ? 'reply' : 'comment',
icon: 'fas fa-comment',
icon: 'comment-icon',
comment: r,
createdAt: r.createdAt,
})),
...tags.map((t) => ({
type: 'tag',
icon: 'fas fa-tag',
icon: 'tag-one',
tag: t,
createdAt: t.createdAt,
})),
@@ -477,7 +477,7 @@ const fetchFavorites = async () => {
const res = await fetch(`${API_BASE_URL}/api/users/${username}/subscribed-posts`)
if (res.ok) {
const data = await res.json()
favoritePosts.value = data.map((p) => ({ icon: 'fas fa-bookmark', post: p }))
favoritePosts.value = data.map((p) => ({ icon: 'bookmark', post: p }))
} else {
favoritePosts.value = []
}

View File

@@ -0,0 +1,132 @@
import { defineNuxtPlugin } from 'nuxt/app'
import {
Pin,
Fireworks,
Gift,
RankingList,
Star,
Edit,
HashtagKey,
Remind,
Info,
ChartLine,
Finance,
Up,
Down,
TagOne,
MedalOne,
Next,
DropDownList,
MoreOne,
SunOne,
Moon,
ComputerOne,
Comment,
Link,
SlyFaceWhitSmile,
Like,
ApplicationMenu,
Search,
Copy,
Loading,
Rss,
MessageEmoji,
AddUser,
ReduceUser,
MessageOne,
AlarmClock,
Bookmark,
Inbox,
LoadingFour,
Mail,
Lock,
User,
Send,
Unlock,
LockOne,
ImageFiles,
ExpandUp,
Close,
ArrowLeft,
CollapseTextInput,
Stopwatch,
PaperMoneyTwo,
Check,
ChartHistogram,
CheckCorrect,
PeoplePlus,
PeopleMinusOne,
SmartOptimization,
Save,
Clear,
FileText,
History,
Lightning,
PeoplesTwo,
} from '@icon-park/vue-next'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('Pin', Pin)
nuxtApp.vueApp.component('Fireworks', Fireworks)
nuxtApp.vueApp.component('Gift', Gift)
nuxtApp.vueApp.component('RankingList', RankingList)
nuxtApp.vueApp.component('Star', Star)
nuxtApp.vueApp.component('Edit', Edit)
nuxtApp.vueApp.component('HashtagKey', HashtagKey)
nuxtApp.vueApp.component('Remind', Remind)
nuxtApp.vueApp.component('InfoIcon', Info)
nuxtApp.vueApp.component('ChartLine', ChartLine)
nuxtApp.vueApp.component('Finance', Finance)
nuxtApp.vueApp.component('Up', Up)
nuxtApp.vueApp.component('Down', Down)
nuxtApp.vueApp.component('TagOne', TagOne)
nuxtApp.vueApp.component('MedalOne', MedalOne)
nuxtApp.vueApp.component('Next', Next)
nuxtApp.vueApp.component('DropDownList', DropDownList)
nuxtApp.vueApp.component('MoreOne', MoreOne)
nuxtApp.vueApp.component('SunOne', SunOne)
nuxtApp.vueApp.component('Moon', Moon)
nuxtApp.vueApp.component('ComputerOne', ComputerOne)
nuxtApp.vueApp.component('CommentIcon', Comment)
nuxtApp.vueApp.component('LinkIcon', Link)
nuxtApp.vueApp.component('SlyFaceWhitSmile', SlyFaceWhitSmile)
nuxtApp.vueApp.component('Like', Like)
nuxtApp.vueApp.component('ApplicationMenu', ApplicationMenu)
nuxtApp.vueApp.component('SearchIcon', Search)
nuxtApp.vueApp.component('Copy', Copy)
nuxtApp.vueApp.component('Loading', Loading)
nuxtApp.vueApp.component('Rss', Rss)
nuxtApp.vueApp.component('MessageEmoji', MessageEmoji)
nuxtApp.vueApp.component('AddUser', AddUser)
nuxtApp.vueApp.component('ReduceUser', ReduceUser)
nuxtApp.vueApp.component('MessageOne', MessageOne)
nuxtApp.vueApp.component('AlarmClock', AlarmClock)
nuxtApp.vueApp.component('Bookmark', Bookmark)
nuxtApp.vueApp.component('Inbox', Inbox)
nuxtApp.vueApp.component('LoadingFour', LoadingFour)
nuxtApp.vueApp.component('UserIcon', User)
nuxtApp.vueApp.component('Mail', Mail)
nuxtApp.vueApp.component('Lock', Lock)
nuxtApp.vueApp.component('SendIcon', Send)
nuxtApp.vueApp.component('Unlock', Unlock)
nuxtApp.vueApp.component('LockOne', LockOne)
nuxtApp.vueApp.component('ImageFiles', ImageFiles)
nuxtApp.vueApp.component('ExpandUp', ExpandUp)
nuxtApp.vueApp.component('CloseIcon', Close)
nuxtApp.vueApp.component('ArrowLeft', ArrowLeft)
nuxtApp.vueApp.component('CollapseTextInput', CollapseTextInput)
nuxtApp.vueApp.component('Stopwatch', Stopwatch)
nuxtApp.vueApp.component('PaperMoneyTwo', PaperMoneyTwo)
nuxtApp.vueApp.component('Check', Check)
nuxtApp.vueApp.component('ChartHistogram', ChartHistogram)
nuxtApp.vueApp.component('CheckCorrect', CheckCorrect)
nuxtApp.vueApp.component('PeoplePlus', PeoplePlus)
nuxtApp.vueApp.component('PeopleMinusOne', PeopleMinusOne)
nuxtApp.vueApp.component('SmartOptimization', SmartOptimization)
nuxtApp.vueApp.component('SaveIcon', Save)
nuxtApp.vueApp.component('ClearIcon', Clear)
nuxtApp.vueApp.component('FileText', FileText)
nuxtApp.vueApp.component('HistoryIcon', History)
nuxtApp.vueApp.component('Lightning', Lightning)
nuxtApp.vueApp.component('PeoplesTwo', PeoplesTwo)
})

View File

@@ -9,28 +9,28 @@ export const notificationState = reactive({
})
const iconMap = {
POST_VIEWED: 'fas fa-eye',
COMMENT_REPLY: 'fas fa-reply',
POST_REVIEWED: 'fas fa-shield-alt',
POST_REVIEW_REQUEST: 'fas fa-gavel',
POST_UPDATED: 'fas fa-comment-dots',
USER_ACTIVITY: 'fas fa-user',
FOLLOWED_POST: 'fas fa-feather-alt',
USER_FOLLOWED: 'fas fa-user-plus',
USER_UNFOLLOWED: 'fas fa-user-minus',
POST_SUBSCRIBED: 'fas fa-bookmark',
POST_UNSUBSCRIBED: 'fas fa-bookmark',
REGISTER_REQUEST: 'fas fa-user-clock',
ACTIVITY_REDEEM: 'fas fa-coffee',
POINT_REDEEM: 'fas fa-gift',
LOTTERY_WIN: 'fas fa-trophy',
LOTTERY_DRAW: 'fas fa-bullhorn',
POLL_VOTE: 'fas fa-square-poll-vertical',
POLL_RESULT_OWNER: 'fas fa-flag-checkered',
POLL_RESULT_PARTICIPANT: 'fas fa-flag-checkered',
MENTION: 'fas fa-at',
POST_DELETED: 'fas fa-trash',
POST_FEATURED: 'fas fa-star',
POST_VIEWED: 'HistoryIcon',
COMMENT_REPLY: 'MessageOne',
POST_REVIEWED: 'CheckCorrect',
POST_REVIEW_REQUEST: 'FileText',
POST_UPDATED: 'Edit',
USER_ACTIVITY: 'UserIcon',
FOLLOWED_POST: 'Pin',
USER_FOLLOWED: 'AddUser',
USER_UNFOLLOWED: 'ReduceUser',
POST_SUBSCRIBED: 'Bookmark',
POST_UNSUBSCRIBED: 'Bookmark',
REGISTER_REQUEST: 'AlarmClock',
ACTIVITY_REDEEM: 'PaperMoneyTwo',
POINT_REDEEM: 'Gift',
LOTTERY_WIN: 'MedalOne',
LOTTERY_DRAW: 'Fireworks',
POLL_VOTE: 'ChartHistogram',
POLL_RESULT_OWNER: 'RankingList',
POLL_RESULT_PARTICIPANT: 'ChartLine',
MENTION: 'HashtagKey',
POST_DELETED: 'ClearIcon',
POST_FEATURED: 'Star',
}
export async function fetchUnreadCount() {

View File

@@ -23,7 +23,7 @@ export default (apiBaseUrl, websiteBaseUrl) => {
return (
body.map((item) => ({
value: `[🔗${item.title}](${websiteBaseUrl}/posts/${item.id})`,
html: `<div><i class="fas fa-link"></i> ${item.title}</div>`,
html: `<div><link-icon /> ${item.title}</div>`,
})) ?? []
)
} else {