mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-07-17 18:16:10 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5f60ae0f4 |
@@ -1,9 +1,9 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<BaseImage alt="OpenIsle" src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png" width="200">
|
<img alt="OpenIsle" src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png" width="200">
|
||||||
<br>
|
<br>
|
||||||
高效的开源社区前后端平台
|
高效的开源社区前后端平台
|
||||||
<br><br><br>
|
<br><br><br>
|
||||||
<BaseImage alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200">
|
<img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 💡 简介
|
## 💡 简介
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class RssController {
|
|||||||
|
|
||||||
// 兼容 Markdown/HTML 两类图片写法(用于 enclosure)
|
// 兼容 Markdown/HTML 两类图片写法(用于 enclosure)
|
||||||
private static final Pattern MD_IMAGE = Pattern.compile("!\\[[^\\]]*\\]\\(([^)]+)\\)");
|
private static final Pattern MD_IMAGE = Pattern.compile("!\\[[^\\]]*\\]\\(([^)]+)\\)");
|
||||||
private static final Pattern HTML_IMAGE = Pattern.compile("<BaseImage[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>");
|
private static final Pattern HTML_IMAGE = Pattern.compile("<img[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>");
|
||||||
|
|
||||||
private static final DateTimeFormatter RFC1123 = DateTimeFormatter.RFC_1123_DATE_TIME;
|
private static final DateTimeFormatter RFC1123 = DateTimeFormatter.RFC_1123_DATE_TIME;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
]"
|
]"
|
||||||
@click="selectMedal(medal)"
|
@click="selectMedal(medal)"
|
||||||
>
|
>
|
||||||
<BaseImage
|
<img
|
||||||
:src="medal.icon"
|
:src="medal.icon"
|
||||||
:alt="medal.title"
|
:alt="medal.title"
|
||||||
:class="['achievements-list-item-icon', { not_completed: !medal.completed }]"
|
:class="['achievements-list-item-icon', { not_completed: !medal.completed }]"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasePopup :visible="visible" @close="close">
|
<BasePopup :visible="visible" @close="close">
|
||||||
<div class="activity-popup">
|
<div class="activity-popup">
|
||||||
<BaseImage v-if="icon" :src="icon" class="activity-popup-icon" alt="activity icon" />
|
<img v-if="icon" :src="icon" class="activity-popup-icon" alt="activity icon" />
|
||||||
<div class="activity-popup-text">{{ text }}</div>
|
<div class="activity-popup-text">{{ text }}</div>
|
||||||
<div class="activity-popup-actions">
|
<div class="activity-popup-actions">
|
||||||
<div class="activity-popup-button" @click="gotoActivity">立即前往</div>
|
<div class="activity-popup-button" @click="gotoActivity">立即前往</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="article-category-container" v-if="category">
|
<div class="article-category-container" v-if="category">
|
||||||
<div class="article-info-item" @click="gotoCategory">
|
<div class="article-info-item" @click="gotoCategory">
|
||||||
<BaseImage
|
<img
|
||||||
v-if="category.smallIcon"
|
v-if="category.smallIcon"
|
||||||
class="article-info-item-img"
|
class="article-info-item-img"
|
||||||
:src="category.smallIcon"
|
:src="category.smallIcon"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
:key="tag.id || tag.name"
|
:key="tag.id || tag.name"
|
||||||
@click="gotoTag(tag)"
|
@click="gotoTag(tag)"
|
||||||
>
|
>
|
||||||
<BaseImage
|
<img
|
||||||
v-if="tag.smallIcon"
|
v-if="tag.smallIcon"
|
||||||
class="article-info-item-img"
|
class="article-info-item-img"
|
||||||
:src="tag.smallIcon"
|
:src="tag.smallIcon"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div v-if="show" class="cropper-modal">
|
<div v-if="show" class="cropper-modal">
|
||||||
<div class="cropper-body">
|
<div class="cropper-body">
|
||||||
<div class="cropper-wrapper">
|
<div class="cropper-wrapper">
|
||||||
<BaseImage ref="image" :src="src" alt="to crop" />
|
<img ref="image" :src="src" alt="to crop" />
|
||||||
</div>
|
</div>
|
||||||
<div class="cropper-actions">
|
<div class="cropper-actions">
|
||||||
<button class="cropper-btn" @click="$emit('close')">取消</button>
|
<button class="cropper-btn" @click="$emit('close')">取消</button>
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<NuxtImg
|
|
||||||
v-bind="passAttrs"
|
|
||||||
:src="src"
|
|
||||||
:alt="alt"
|
|
||||||
loading="lazy"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
placeholder-class="base-image-ph"
|
|
||||||
@load="onLoad"
|
|
||||||
@error="onError"
|
|
||||||
:class="['base-image', passAttrs.class, { 'is-loaded': loaded }]"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { useAttrs } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
src: { type: String, required: true },
|
|
||||||
alt: { type: String, default: '' },
|
|
||||||
})
|
|
||||||
|
|
||||||
const attrs = useAttrs()
|
|
||||||
|
|
||||||
const passAttrs = computed(() => {
|
|
||||||
const { placeholder, ...rest } = attrs
|
|
||||||
return rest
|
|
||||||
})
|
|
||||||
|
|
||||||
const loaded = ref(false)
|
|
||||||
const img = useImage()
|
|
||||||
|
|
||||||
const placeholder = computed(() => {
|
|
||||||
if (!props.src) return undefined
|
|
||||||
return img(props.src, { w: 16, h: 16, f: 'webp', q: 20, blur: 1 })
|
|
||||||
})
|
|
||||||
|
|
||||||
function onLoad() {
|
|
||||||
loaded.value = true
|
|
||||||
}
|
|
||||||
function onError() {
|
|
||||||
loaded.value = true
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.base-image {
|
|
||||||
display: block;
|
|
||||||
transition:
|
|
||||||
filter 0.35s ease,
|
|
||||||
transform 0.35s ease,
|
|
||||||
opacity 0.35s ease;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.base-image-ph {
|
|
||||||
filter: blur(10px) saturate(0.85);
|
|
||||||
transform: scale(1.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.base-image.is-loaded {
|
|
||||||
filter: none;
|
|
||||||
transform: none;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtImg
|
||||||
|
:src="src"
|
||||||
|
:alt="alt"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
placeholder-class="ph"
|
||||||
|
:loading="loading"
|
||||||
|
@load="loaded = true"
|
||||||
|
:class="['base-img', { 'is-loaded': loaded }]"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: { type: String, required: true },
|
||||||
|
alt: { type: String, default: '' },
|
||||||
|
loading: { type: String, default: 'lazy' },
|
||||||
|
placeholderOptions: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ w: 16, h: 16, f: 'webp', q: 40, blur: 2 }),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const img = useImage()
|
||||||
|
const loaded = ref(false)
|
||||||
|
const placeholder = computed(() => img(props.src, props.placeholderOptions))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.base-img {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
}
|
||||||
|
.base-img.is-loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
:deep(img.ph) {
|
||||||
|
filter: blur(10px);
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
:class="{ clickable: !!item.iconClick }"
|
:class="{ clickable: !!item.iconClick }"
|
||||||
@click="item.iconClick && item.iconClick()"
|
@click="item.iconClick && item.iconClick()"
|
||||||
>
|
>
|
||||||
<BaseImage v-if="item.src" :src="item.src" class="timeline-img" alt="timeline item" />
|
<img v-if="item.src" :src="item.src" class="timeline-img" alt="timeline item" />
|
||||||
<i v-else-if="item.icon" :class="item.icon"></i>
|
<i v-else-if="item.icon" :class="item.icon"></i>
|
||||||
<BaseImage v-else-if="item.emoji" :src="item.emoji" class="timeline-emoji" alt="emoji" />
|
<img v-else-if="item.emoji" :src="item.emoji" class="timeline-emoji" alt="emoji" />
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<slot name="item" :item="item">{{ item.content }}</slot>
|
<slot name="item" :item="item">{{ item.content }}</slot>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="option-container">
|
<div class="option-container">
|
||||||
<div class="option-main">
|
<div class="option-main">
|
||||||
<template v-if="option.icon">
|
<template v-if="option.icon">
|
||||||
<BaseImage
|
<img
|
||||||
v-if="isImageIcon(option.icon)"
|
v-if="isImageIcon(option.icon)"
|
||||||
:src="option.icon"
|
:src="option.icon"
|
||||||
class="option-icon"
|
class="option-icon"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<!-- <div class="user-avatar-container">
|
<!-- <div class="user-avatar-container">
|
||||||
<div class="user-avatar-item">
|
<div class="user-avatar-item">
|
||||||
<BaseImage class="user-avatar-item-img" :src="comment.avatar" alt="avatar" />
|
<img class="user-avatar-item-img" :src="comment.avatar" alt="avatar" />
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="info-content">
|
<div class="info-content">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<template v-for="(label, idx) in selectedLabels" :key="label.id">
|
<template v-for="(label, idx) in selectedLabels" :key="label.id">
|
||||||
<div class="selected-label">
|
<div class="selected-label">
|
||||||
<template v-if="label.icon">
|
<template v-if="label.icon">
|
||||||
<BaseImage
|
<img
|
||||||
v-if="isImageIcon(label.icon)"
|
v-if="isImageIcon(label.icon)"
|
||||||
:src="label.icon"
|
:src="label.icon"
|
||||||
class="option-icon"
|
class="option-icon"
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<span v-if="selectedLabels.length">
|
<span v-if="selectedLabels.length">
|
||||||
<div class="selected-label">
|
<div class="selected-label">
|
||||||
<template v-if="selectedLabels[0].icon">
|
<template v-if="selectedLabels[0].icon">
|
||||||
<BaseImage
|
<img
|
||||||
v-if="isImageIcon(selectedLabels[0].icon)"
|
v-if="isImageIcon(selectedLabels[0].icon)"
|
||||||
:src="selectedLabels[0].icon"
|
:src="selectedLabels[0].icon"
|
||||||
class="option-icon"
|
class="option-icon"
|
||||||
@@ -69,12 +69,7 @@
|
|||||||
>
|
>
|
||||||
<slot name="option" :option="o" :isSelected="isSelected(o.id)">
|
<slot name="option" :option="o" :isSelected="isSelected(o.id)">
|
||||||
<template v-if="o.icon">
|
<template v-if="o.icon">
|
||||||
<BaseImage
|
<img v-if="isImageIcon(o.icon)" :src="o.icon" class="option-icon" :alt="o.name" />
|
||||||
v-if="isImageIcon(o.icon)"
|
|
||||||
:src="o.icon"
|
|
||||||
class="option-icon"
|
|
||||||
:alt="o.name"
|
|
||||||
/>
|
|
||||||
<i v-else :class="['option-icon', o.icon]"></i>
|
<i v-else :class="['option-icon', o.icon]"></i>
|
||||||
</template>
|
</template>
|
||||||
<span>{{ o.name }}</span>
|
<span>{{ o.name }}</span>
|
||||||
@@ -105,12 +100,7 @@
|
|||||||
>
|
>
|
||||||
<slot name="option" :option="o" :isSelected="isSelected(o.id)">
|
<slot name="option" :option="o" :isSelected="isSelected(o.id)">
|
||||||
<template v-if="o.icon">
|
<template v-if="o.icon">
|
||||||
<BaseImage
|
<img v-if="isImageIcon(o.icon)" :src="o.icon" class="option-icon" :alt="o.name" />
|
||||||
v-if="isImageIcon(o.icon)"
|
|
||||||
:src="o.icon"
|
|
||||||
class="option-icon"
|
|
||||||
:alt="o.name"
|
|
||||||
/>
|
|
||||||
<i v-else :class="['option-icon', o.icon]"></i>
|
<i v-else :class="['option-icon', o.icon]"></i>
|
||||||
</template>
|
</template>
|
||||||
<span>{{ o.name }}</span>
|
<span>{{ o.name }}</span>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<NuxtLink class="logo-container" :to="`/`" @click="refrechData">
|
<NuxtLink class="logo-container" :to="`/`" @click="refrechData">
|
||||||
<BaseImage
|
<img
|
||||||
alt="OpenIsle"
|
alt="OpenIsle"
|
||||||
src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png"
|
src="https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png"
|
||||||
width="60"
|
width="60"
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<DropdownMenu v-if="isLogin" ref="userMenu" :items="headerMenuItems">
|
<DropdownMenu v-if="isLogin" ref="userMenu" :items="headerMenuItems">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<BaseImage class="avatar-img" :src="avatar" alt="avatar" />
|
<BaseImg class="avatar-img" :src="avatar" alt="avatar" width="32" height="32" />
|
||||||
<i class="fas fa-caret-down dropdown-icon"></i>
|
<i class="fas fa-caret-down dropdown-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
|
|
||||||
<SearchDropdown ref="searchDropdown" v-if="isMobile && showSearch" @close="closeSearch" />
|
<SearchDropdown ref="searchDropdown" v-if="isMobile && showSearch" @close="closeSearch" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -86,6 +87,7 @@ import { computed, nextTick, ref, watch } from 'vue'
|
|||||||
import DropdownMenu from '~/components/DropdownMenu.vue'
|
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 BaseImg from '~/components/BaseImg.vue'
|
||||||
import { authState, clearToken, loadCurrentUser } from '~/utils/auth'
|
import { authState, clearToken, loadCurrentUser } from '~/utils/auth'
|
||||||
import { useUnreadCount } from '~/composables/useUnreadCount'
|
import { useUnreadCount } from '~/composables/useUnreadCount'
|
||||||
import { useChannelsUnreadCount } from '~/composables/useChannelsUnreadCount'
|
import { useChannelsUnreadCount } from '~/composables/useChannelsUnreadCount'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="medal-popup-title">恭喜你获得以下勋章</div>
|
<div class="medal-popup-title">恭喜你获得以下勋章</div>
|
||||||
<div class="medal-popup-list">
|
<div class="medal-popup-list">
|
||||||
<div v-for="medal in medals" :key="medal.type" class="medal-popup-item">
|
<div v-for="medal in medals" :key="medal.type" class="medal-popup-item">
|
||||||
<BaseImage :src="medal.icon" :alt="medal.title" class="medal-popup-item-icon" />
|
<img :src="medal.icon" :alt="medal.title" class="medal-popup-item-icon" />
|
||||||
<div class="medal-popup-item-title">{{ medal.title }}</div>
|
<div class="medal-popup-item-title">{{ medal.title }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
@click="gotoCategory(c)"
|
@click="gotoCategory(c)"
|
||||||
>
|
>
|
||||||
<template v-if="c.smallIcon || c.icon">
|
<template v-if="c.smallIcon || c.icon">
|
||||||
<BaseImage
|
<img
|
||||||
v-if="isImageIcon(c.smallIcon || c.icon)"
|
v-if="isImageIcon(c.smallIcon || c.icon)"
|
||||||
:src="c.smallIcon || c.icon"
|
:src="c.smallIcon || c.icon"
|
||||||
class="section-item-icon"
|
class="section-item-icon"
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<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 v-for="t in tagData" :key="t.id" class="section-item" @click="gotoTag(t)">
|
<div v-else v-for="t in tagData" :key="t.id" class="section-item" @click="gotoTag(t)">
|
||||||
<BaseImage
|
<img
|
||||||
v-if="isImageIcon(t.smallIcon || t.icon)"
|
v-if="isImageIcon(t.smallIcon || t.icon)"
|
||||||
:src="t.smallIcon || t.icon"
|
:src="t.smallIcon || t.icon"
|
||||||
class="section-item-icon"
|
class="section-item-icon"
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="multi-tabs">
|
|
||||||
<div class="multi-tabs-header" :class="headerClass">
|
|
||||||
<div
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:key="tab.name"
|
|
||||||
:class="[itemClass, { selected: current === tab.name }]"
|
|
||||||
@click="select(tab.name)"
|
|
||||||
>
|
|
||||||
<div :class="labelClass">
|
|
||||||
<i v-if="tab.icon" :class="tab.icon"></i>
|
|
||||||
{{ tab.label }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<slot name="header-extra" />
|
|
||||||
</div>
|
|
||||||
<div class="multi-tabs-content" @touchstart="onTouchStart" @touchend="onTouchEnd">
|
|
||||||
<slot :selected="current" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
tabs: { type: Array, required: true },
|
|
||||||
modelValue: { type: String, required: true },
|
|
||||||
headerClass: { type: String, default: '' },
|
|
||||||
itemClass: { type: String, default: '' },
|
|
||||||
labelClass: { type: String, default: '' },
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
|
||||||
|
|
||||||
const current = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (val) => emit('update:modelValue', val),
|
|
||||||
})
|
|
||||||
|
|
||||||
const select = (name) => {
|
|
||||||
current.value = name
|
|
||||||
}
|
|
||||||
|
|
||||||
const startX = ref(0)
|
|
||||||
const startY = ref(0)
|
|
||||||
|
|
||||||
const onTouchStart = (e) => {
|
|
||||||
const t = e.touches[0]
|
|
||||||
startX.value = t.clientX
|
|
||||||
startY.value = t.clientY
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTouchEnd = (e) => {
|
|
||||||
const t = e.changedTouches[0]
|
|
||||||
const dx = t.clientX - startX.value
|
|
||||||
const dy = t.clientY - startY.value
|
|
||||||
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
|
|
||||||
const idx = props.tabs.findIndex((t) => t.name === current.value)
|
|
||||||
if (dx < 0 && idx < props.tabs.length - 1) {
|
|
||||||
current.value = props.tabs[idx + 1].name
|
|
||||||
} else if (dx > 0 && idx > 0) {
|
|
||||||
current.value = props.tabs[idx - 1].name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.multi-tabs-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
overflow-x: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
.multi-tabs-header::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.multi-tabs-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
:class="{ selected: userReacted(r.type) }"
|
:class="{ selected: userReacted(r.type) }"
|
||||||
@click="toggleReaction(r.type)"
|
@click="toggleReaction(r.type)"
|
||||||
>
|
>
|
||||||
<BaseImage :src="reactionEmojiMap[r.type]" class="emoji" alt="emoji" />
|
<img :src="reactionEmojiMap[r.type]" class="emoji" alt="emoji" />
|
||||||
<div>{{ counts[r.type] }}</div>
|
<div>{{ counts[r.type] }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
class="reactions-viewer-item"
|
class="reactions-viewer-item"
|
||||||
@click="openPanel"
|
@click="openPanel"
|
||||||
>
|
>
|
||||||
<BaseImage :src="reactionEmojiMap[r.type]" class="emoji" alt="emoji" />
|
<img :src="reactionEmojiMap[r.type]" class="emoji" alt="emoji" />
|
||||||
</div>
|
</div>
|
||||||
<div class="reactions-count">{{ totalCount }}</div>
|
<div class="reactions-count">{{ totalCount }}</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
@click="toggleReaction(t)"
|
@click="toggleReaction(t)"
|
||||||
:class="{ selected: userReacted(t) }"
|
:class="{ selected: userReacted(t) }"
|
||||||
>
|
>
|
||||||
<BaseImage :src="reactionEmojiMap[t]" class="emoji" alt="emoji" /><span v-if="counts[t]">{{
|
<img :src="reactionEmojiMap[t]" class="emoji" alt="emoji" /><span v-if="counts[t]">{{
|
||||||
counts[t]
|
counts[t]
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #option="{ option }">
|
<template #option="{ option }">
|
||||||
<div class="search-option-item">
|
<div class="search-option-item">
|
||||||
<BaseImage
|
<img
|
||||||
:src="option.avatar || '/default-avatar.svg'"
|
:src="option.avatar || '/default-avatar.svg'"
|
||||||
class="avatar"
|
class="avatar"
|
||||||
@error="handleAvatarError"
|
@error="handleAvatarError"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="option-container">
|
<div class="option-container">
|
||||||
<div class="option-main">
|
<div class="option-main">
|
||||||
<template v-if="option.icon">
|
<template v-if="option.icon">
|
||||||
<BaseImage
|
<img
|
||||||
v-if="isImageIcon(option.icon)"
|
v-if="isImageIcon(option.icon)"
|
||||||
:src="option.icon"
|
:src="option.icon"
|
||||||
class="option-icon"
|
class="option-icon"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="user-list">
|
<div class="user-list">
|
||||||
<BasePlaceholder v-if="users.length === 0" text="暂无用户" icon="fas fa-inbox" />
|
<BasePlaceholder v-if="users.length === 0" text="暂无用户" icon="fas fa-inbox" />
|
||||||
<div v-for="u in users" :key="u.id" class="user-item" @click="handleUserClick(u)">
|
<div v-for="u in users" :key="u.id" class="user-item" @click="handleUserClick(u)">
|
||||||
<BaseImage :src="u.avatar" alt="avatar" class="user-avatar" />
|
<img :src="u.avatar" alt="avatar" class="user-avatar" />
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="user-name">{{ u.username }}</div>
|
<div class="user-name">{{ u.username }}</div>
|
||||||
<div v-if="u.introduction" class="user-intro">{{ u.introduction }}</div>
|
<div v-if="u.introduction" class="user-intro">{{ u.introduction }}</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { defineNuxtConfig } from 'nuxt/config'
|
|||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
ssr: true,
|
ssr: true,
|
||||||
modules: ['@nuxt/image'],
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || '',
|
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || '',
|
||||||
@@ -14,6 +13,7 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'],
|
css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'],
|
||||||
|
modules: ['@nuxt/image'],
|
||||||
app: {
|
app: {
|
||||||
pageTransition: { name: 'page', mode: 'out-in' },
|
pageTransition: { name: 'page', mode: 'out-in' },
|
||||||
head: {
|
head: {
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="about-page">
|
<div class="about-page">
|
||||||
<MultiTabs
|
<div class="about-tabs">
|
||||||
:tabs="tabs"
|
<div
|
||||||
v-model="selectedTab"
|
v-for="tab in tabs"
|
||||||
header-class="about-tabs"
|
:key="tab.name"
|
||||||
item-class="about-tabs-item"
|
:class="['about-tabs-item', { selected: selectedTab === tab.name }]"
|
||||||
label-class="about-tabs-item-label"
|
@click="selectTab(tab.name)"
|
||||||
>
|
>
|
||||||
<template #default>
|
<div class="about-tabs-item-label">{{ tab.label }}</div>
|
||||||
<div class="about-loading" v-if="isFetching">
|
</div>
|
||||||
<l-hatch-spinner size="100" stroke="10" speed="1" color="var(--primary-color)" />
|
</div>
|
||||||
</div>
|
<div class="about-loading" v-if="isFetching">
|
||||||
<div
|
<l-hatch-spinner size="100" stroke="10" speed="1" color="var(--primary-color)" />
|
||||||
v-else
|
</div>
|
||||||
class="about-content"
|
<div
|
||||||
v-html="renderMarkdown(content)"
|
v-else
|
||||||
@click="handleContentClick"
|
class="about-content"
|
||||||
></div>
|
v-html="renderMarkdown(content)"
|
||||||
</template>
|
@click="handleContentClick"
|
||||||
</MultiTabs>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { handleMarkdownClick, renderMarkdown } from '~/utils/markdown'
|
import { handleMarkdownClick, renderMarkdown } from '~/utils/markdown'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -71,21 +71,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(selectedTab, (name) => {
|
const selectTab = (name) => {
|
||||||
|
selectedTab.value = name
|
||||||
const tab = tabs.find((t) => t.name === name)
|
const tab = tabs.find((t) => t.name === name)
|
||||||
if (tab) loadContent(tab.file)
|
if (tab) loadContent(tab.file)
|
||||||
})
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const first = tabs[0]
|
loadContent(tabs[0].file)
|
||||||
if (first) loadContent(first.file)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleContentClick = (e) => {
|
const handleContentClick = (e) => {
|
||||||
handleMarkdownClick(e)
|
handleMarkdownClick(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { tabs, selectedTab, content, renderMarkdown, isFetching, handleContentClick }
|
return { tabs, selectedTab, content, renderMarkdown, selectTab, isFetching, handleContentClick }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="activity-list-page-card" v-for="a in activities" :key="a.id">
|
<div class="activity-list-page-card" v-for="a in activities" :key="a.id">
|
||||||
<div class="activity-list-page-card-normal">
|
<div class="activity-list-page-card-normal">
|
||||||
<div v-if="a.icon" class="activity-card-normal-left">
|
<div v-if="a.icon" class="activity-card-normal-left">
|
||||||
<BaseImage :src="a.icon" alt="avatar" class="activity-card-left-avatar-img" />
|
<img :src="a.icon" alt="avatar" class="activity-card-left-avatar-img" />
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-card-normal-right">
|
<div class="activity-card-normal-right">
|
||||||
<div class="activity-card-normal-right-header">
|
<div class="activity-card-normal-right-header">
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
class="article-member-avatar-item"
|
class="article-member-avatar-item"
|
||||||
:to="`/users/${member.id}`"
|
:to="`/users/${member.id}`"
|
||||||
>
|
>
|
||||||
<BaseImage class="article-member-avatar-item-img" :src="member.avatar" alt="avatar" />
|
<img class="article-member-avatar-item-img" :src="member.avatar" alt="avatar" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -36,35 +36,19 @@
|
|||||||
|
|
||||||
<div class="other-login-page-content">
|
<div class="other-login-page-content">
|
||||||
<div class="login-page-button" @click="loginWithGoogle">
|
<div class="login-page-button" @click="loginWithGoogle">
|
||||||
<BaseImage
|
<img class="login-page-button-icon" src="../assets/icons/google.svg" alt="Google Logo" />
|
||||||
class="login-page-button-icon"
|
|
||||||
src="../assets/icons/google.svg"
|
|
||||||
alt="Google Logo"
|
|
||||||
/>
|
|
||||||
<div class="login-page-button-text">Google 登录</div>
|
<div class="login-page-button-text">Google 登录</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-page-button" @click="loginWithGithub">
|
<div class="login-page-button" @click="loginWithGithub">
|
||||||
<BaseImage
|
<img class="login-page-button-icon" src="../assets/icons/github.svg" alt="GitHub Logo" />
|
||||||
class="login-page-button-icon"
|
|
||||||
src="../assets/icons/github.svg"
|
|
||||||
alt="GitHub Logo"
|
|
||||||
/>
|
|
||||||
<div class="login-page-button-text">GitHub 登录</div>
|
<div class="login-page-button-text">GitHub 登录</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-page-button" @click="loginWithDiscord">
|
<div class="login-page-button" @click="loginWithDiscord">
|
||||||
<BaseImage
|
<img class="login-page-button-icon" src="../assets/icons/discord.svg" alt="Discord Logo" />
|
||||||
class="login-page-button-icon"
|
|
||||||
src="../assets/icons/discord.svg"
|
|
||||||
alt="Discord Logo"
|
|
||||||
/>
|
|
||||||
<div class="login-page-button-text">Discord 登录</div>
|
<div class="login-page-button-text">Discord 登录</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-page-button" @click="loginWithTwitter">
|
<div class="login-page-button" @click="loginWithTwitter">
|
||||||
<BaseImage
|
<img class="login-page-button-icon" src="../assets/icons/twitter.svg" alt="Twitter Logo" />
|
||||||
class="login-page-button-icon"
|
|
||||||
src="../assets/icons/twitter.svg"
|
|
||||||
alt="Twitter Logo"
|
|
||||||
/>
|
|
||||||
<div class="login-page-button-text">Twitter 登录</div>
|
<div class="login-page-button-text">Twitter 登录</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
{{ loadingMore ? '加载中...' : '查看更多消息' }}
|
{{ loadingMore ? '加载中...' : '查看更多消息' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BaseTimeline :items="messages">
|
<BaseTimeline :items="messages" hover>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
<div class="user-name">
|
<div class="user-name">
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
{{ TimeManager.format(item.createdAt) }}
|
{{ TimeManager.format(item.createdAt) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.replyTo" class="reply-preview info-content-text">
|
<div v-if="item.replyTo" class="reply-preview">
|
||||||
<div class="reply-author">{{ item.replyTo.sender.username }}</div>
|
<div class="reply-author">{{ item.replyTo.sender.username }}</div>
|
||||||
<div class="reply-content" v-html="renderMarkdown(item.replyTo.content)"></div>
|
<div class="reply-content" v-html="renderMarkdown(item.replyTo.content)"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -597,11 +597,10 @@ function goBack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reply-preview {
|
.reply-preview {
|
||||||
padding: 10px;
|
padding: 5px 10px;
|
||||||
border-left: 5px solid var(--primary-color);
|
border-left: 5px solid var(--primary-color);
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
background-color: var(--menu-selected-background-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-author {
|
.reply-author {
|
||||||
|
|||||||
@@ -7,118 +7,116 @@
|
|||||||
<div v-if="!isFloatMode" class="float-control">
|
<div v-if="!isFloatMode" class="float-control">
|
||||||
<i class="fas fa-compress" @click="minimize" title="最小化"></i>
|
<i class="fas fa-compress" @click="minimize" title="最小化"></i>
|
||||||
</div>
|
</div>
|
||||||
<MultiTabs :tabs="tabs" v-model="activeTab" header-class="tabs" item-class="tab">
|
<div class="tabs">
|
||||||
<template #default="{ selected }">
|
<div :class="['tab', { active: activeTab === 'messages' }]" @click="switchToMessage">
|
||||||
<div v-if="selected === 'messages'">
|
站内信
|
||||||
<div v-if="loading" class="loading-message">
|
</div>
|
||||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
<div :class="['tab', { active: activeTab === 'channels' }]" @click="switchToChannels">
|
||||||
|
频道
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'messages'">
|
||||||
|
<div v-if="loading" class="loading-message">
|
||||||
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="error-container">
|
||||||
|
<div class="error-text">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loading && !isFloatMode" class="search-container">
|
||||||
|
<SearchPersonDropdown />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loading && conversations.length === 0" class="empty-container">
|
||||||
|
<BasePlaceholder v-if="conversations.length === 0" text="暂无会话" icon="fas fa-inbox" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!loading"
|
||||||
|
v-for="convo in conversations"
|
||||||
|
:key="convo.id"
|
||||||
|
class="conversation-item"
|
||||||
|
@click="goToConversation(convo.id)"
|
||||||
|
>
|
||||||
|
<div class="conversation-avatar">
|
||||||
|
<img
|
||||||
|
:src="getOtherParticipant(convo)?.avatar || '/default-avatar.svg'"
|
||||||
|
:alt="getOtherParticipant(convo)?.username || '用户'"
|
||||||
|
class="avatar-img"
|
||||||
|
@error="handleAvatarError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="conversation-content">
|
||||||
|
<div class="conversation-header">
|
||||||
|
<div class="participant-name">
|
||||||
|
{{ getOtherParticipant(convo)?.username || '未知用户' }}
|
||||||
|
</div>
|
||||||
|
<div class="message-time">
|
||||||
|
{{ formatTime(convo.lastMessage?.createdAt || convo.createdAt) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="error" class="error-container">
|
<div class="last-message-row">
|
||||||
<div class="error-text">{{ error }}</div>
|
<div class="last-message">
|
||||||
|
{{
|
||||||
|
convo.lastMessage ? stripMarkdownLength(convo.lastMessage.content, 100) : '暂无消息'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div v-if="convo.unreadCount > 0" class="unread-count-badge">
|
||||||
|
{{ convo.unreadCount }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="!loading && !isFloatMode" class="search-container">
|
<div v-else>
|
||||||
<SearchPersonDropdown />
|
<div v-if="loadingChannels" class="loading-message">
|
||||||
</div>
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
|
</div>
|
||||||
<div v-if="!loading && conversations.length === 0" class="empty-container">
|
<div v-else>
|
||||||
<BasePlaceholder
|
<div v-if="channels.length === 0" class="empty-container">
|
||||||
v-if="conversations.length === 0"
|
<BasePlaceholder text="暂无频道" icon="fas fa-inbox" />
|
||||||
text="暂无会话"
|
</div>
|
||||||
icon="fas fa-inbox"
|
<div
|
||||||
|
v-for="ch in channels"
|
||||||
|
:key="ch.id"
|
||||||
|
class="conversation-item"
|
||||||
|
@click="goToChannel(ch.id)"
|
||||||
|
>
|
||||||
|
<div class="conversation-avatar">
|
||||||
|
<img
|
||||||
|
:src="ch.avatar || '/default-avatar.svg'"
|
||||||
|
:alt="ch.name"
|
||||||
|
class="avatar-img"
|
||||||
|
@error="handleAvatarError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="conversation-content">
|
||||||
<div
|
<div class="conversation-header">
|
||||||
v-if="!loading"
|
<div class="participant-name">
|
||||||
v-for="convo in conversations"
|
{{ ch.name }}
|
||||||
:key="convo.id"
|
<span v-if="ch.unreadCount > 0" class="unread-dot"></span>
|
||||||
class="conversation-item"
|
</div>
|
||||||
@click="goToConversation(convo.id)"
|
<div class="message-time">
|
||||||
>
|
{{ formatTime(ch.lastMessage?.createdAt || ch.createdAt) }}
|
||||||
<div class="conversation-avatar">
|
</div>
|
||||||
<BaseImage
|
|
||||||
:src="getOtherParticipant(convo)?.avatar || '/default-avatar.svg'"
|
|
||||||
:alt="getOtherParticipant(convo)?.username || '用户'"
|
|
||||||
class="avatar-img"
|
|
||||||
@error="handleAvatarError"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="last-message-row">
|
||||||
<div class="conversation-content">
|
<div class="last-message">
|
||||||
<div class="conversation-header">
|
{{
|
||||||
<div class="participant-name">
|
ch.lastMessage ? stripMarkdownLength(ch.lastMessage.content, 100) : ch.description
|
||||||
{{ getOtherParticipant(convo)?.username || '未知用户' }}
|
}}
|
||||||
</div>
|
|
||||||
<div class="message-time">
|
|
||||||
{{ formatTime(convo.lastMessage?.createdAt || convo.createdAt) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="last-message-row">
|
|
||||||
<div class="last-message">
|
|
||||||
{{
|
|
||||||
convo.lastMessage
|
|
||||||
? stripMarkdownLength(convo.lastMessage.content, 100)
|
|
||||||
: '暂无消息'
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div v-if="convo.unreadCount > 0" class="unread-count-badge">
|
|
||||||
{{ convo.unreadCount }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="member-count">成员 {{ ch.memberCount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
</div>
|
||||||
<div v-if="loadingChannels" class="loading-message">
|
</div>
|
||||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div v-if="channels.length === 0" class="empty-container">
|
|
||||||
<BasePlaceholder text="暂无频道" icon="fas fa-inbox" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="ch in channels"
|
|
||||||
:key="ch.id"
|
|
||||||
class="conversation-item"
|
|
||||||
@click="goToChannel(ch.id)"
|
|
||||||
>
|
|
||||||
<div class="conversation-avatar">
|
|
||||||
<BaseImage
|
|
||||||
:src="ch.avatar || '/default-avatar.svg'"
|
|
||||||
:alt="ch.name"
|
|
||||||
class="avatar-img"
|
|
||||||
@error="handleAvatarError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="conversation-content">
|
|
||||||
<div class="conversation-header">
|
|
||||||
<div class="participant-name">
|
|
||||||
{{ ch.name }}
|
|
||||||
<span v-if="ch.unreadCount > 0" class="unread-dot"></span>
|
|
||||||
</div>
|
|
||||||
<div class="message-time">
|
|
||||||
{{ formatTime(ch.lastMessage?.createdAt || ch.createdAt) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="last-message-row">
|
|
||||||
<div class="last-message">
|
|
||||||
{{
|
|
||||||
ch.lastMessage
|
|
||||||
? stripMarkdownLength(ch.lastMessage.content, 100)
|
|
||||||
: ch.description
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="member-count">成员 {{ ch.memberCount }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</MultiTabs>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -149,10 +147,6 @@ const { fetchChannelUnread: refreshChannelUnread, setFromList: setChannelUnreadF
|
|||||||
useChannelsUnreadCount()
|
useChannelsUnreadCount()
|
||||||
let subscription = null
|
let subscription = null
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{ name: 'messages', label: '站内信' },
|
|
||||||
{ name: 'channels', label: '频道' },
|
|
||||||
]
|
|
||||||
const activeTab = ref('channels')
|
const activeTab = ref('channels')
|
||||||
const channels = ref([])
|
const channels = ref([])
|
||||||
const loadingChannels = ref(false)
|
const loadingChannels = ref(false)
|
||||||
@@ -222,13 +216,15 @@ async function fetchChannels() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(activeTab, (val) => {
|
function switchToMessage() {
|
||||||
if (val === 'messages') {
|
activeTab.value = 'messages'
|
||||||
fetchConversations()
|
fetchConversations()
|
||||||
} else {
|
}
|
||||||
fetchChannels()
|
|
||||||
}
|
function switchToChannels() {
|
||||||
})
|
activeTab.value = 'channels'
|
||||||
|
fetchChannels()
|
||||||
|
}
|
||||||
|
|
||||||
async function goToChannel(id) {
|
async function goToChannel(id) {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
|
|||||||
+534
-526
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@
|
|||||||
<div class="prize-row">
|
<div class="prize-row">
|
||||||
<span class="prize-row-title">奖品图片</span>
|
<span class="prize-row-title">奖品图片</span>
|
||||||
<label class="prize-container">
|
<label class="prize-container">
|
||||||
<BaseImage v-if="prizeIcon" :src="prizeIcon" class="prize-preview" alt="prize" />
|
<img v-if="prizeIcon" :src="prizeIcon" class="prize-preview" alt="prize" />
|
||||||
<i v-else class="fa-solid fa-image default-prize-icon"></i>
|
<i v-else class="fa-solid fa-image default-prize-icon"></i>
|
||||||
<div class="prize-overlay">上传奖品图片</div>
|
<div class="prize-overlay">上传奖品图片</div>
|
||||||
<input type="file" class="prize-input" accept="image/*" @change="onPrizeIconChange" />
|
<input type="file" class="prize-input" accept="image/*" @change="onPrizeIconChange" />
|
||||||
|
|||||||
+169
-163
@@ -1,166 +1,177 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="point-mall-page">
|
<div class="point-mall-page">
|
||||||
<MultiTabs
|
<div class="point-tabs">
|
||||||
:tabs="tabs"
|
<div
|
||||||
v-model="selectedTab"
|
:class="['point-tab-item', { selected: selectedTab === 'mall' }]"
|
||||||
header-class="point-tabs"
|
@click="selectedTab = 'mall'"
|
||||||
item-class="point-tab-item"
|
>
|
||||||
>
|
积分兑换
|
||||||
<template #default="{ selected }">
|
</div>
|
||||||
<template v-if="selected === 'mall'">
|
<div
|
||||||
<div class="point-mall-page-content">
|
:class="['point-tab-item', { selected: selectedTab === 'history' }]"
|
||||||
<section class="rules">
|
@click="selectedTab = 'history'"
|
||||||
<div class="section-title">🎉 积分规则</div>
|
>
|
||||||
<div class="section-content">
|
积分历史
|
||||||
<div class="section-item" v-for="(rule, idx) in pointRules" :key="idx">
|
</div>
|
||||||
{{ rule }}
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<template v-if="selectedTab === 'mall'">
|
||||||
</section>
|
<div class="point-mall-page-content">
|
||||||
<div class="loading-points-container" v-if="isLoading">
|
<section class="rules">
|
||||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
<div class="section-title">🎉 积分规则</div>
|
||||||
</div>
|
<div class="section-content">
|
||||||
<div class="point-info">
|
<div class="section-item" v-for="(rule, idx) in pointRules" :key="idx">{{ rule }}</div>
|
||||||
<p v-if="authState.loggedIn && point !== null">
|
|
||||||
<span><i class="fas fa-coins coin-icon"></i></span>我的积分:<span
|
|
||||||
class="point-value"
|
|
||||||
>{{ point }}</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<section class="goods">
|
|
||||||
<div class="goods-item" v-for="(good, idx) in goods" :key="idx">
|
|
||||||
<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>
|
|
||||||
{{ good.cost }} 积分
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="goods-item-button"
|
|
||||||
:class="{ disabled: !authState.loggedIn || point === null || point < good.cost }"
|
|
||||||
@click="openRedeem(good)"
|
|
||||||
>
|
|
||||||
兑换
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<RedeemPopup
|
|
||||||
:visible="dialogVisible"
|
|
||||||
v-model="contact"
|
|
||||||
:loading="loading"
|
|
||||||
@close="closeRedeem"
|
|
||||||
@submit="submitRedeem"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</section>
|
||||||
<template v-else>
|
|
||||||
<div class="loading-points-container" v-if="historyLoading">
|
<div class="loading-points-container" v-if="isLoading">
|
||||||
<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 class="point-info">
|
||||||
|
<p v-if="authState.loggedIn && point !== null">
|
||||||
|
<span><i class="fas fa-coins coin-icon"></i></span>我的积分:<span
|
||||||
|
class="point-value"
|
||||||
|
>{{ point }}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="goods">
|
||||||
|
<div class="goods-item" v-for="(good, idx) in goods" :key="idx">
|
||||||
|
<img 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>
|
||||||
|
{{ good.cost }} 积分
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="goods-item-button"
|
||||||
|
:class="{ disabled: !authState.loggedIn || point === null || point < good.cost }"
|
||||||
|
@click="openRedeem(good)"
|
||||||
|
>
|
||||||
|
兑换
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BasePlaceholder
|
</section>
|
||||||
v-else-if="histories.length === 0"
|
<RedeemPopup
|
||||||
text="暂无积分记录"
|
:visible="dialogVisible"
|
||||||
icon="fas fa-inbox"
|
v-model="contact"
|
||||||
/>
|
:loading="loading"
|
||||||
<div class="timeline-container" v-else>
|
@close="closeRedeem"
|
||||||
<BaseTimeline :items="histories">
|
@submit="submitRedeem"
|
||||||
<template #item="{ item }">
|
/>
|
||||||
<div class="history-content">
|
</div>
|
||||||
<template v-if="item.type === 'POST'">
|
</template>
|
||||||
发送帖子
|
|
||||||
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
<template v-else>
|
||||||
item.postTitle
|
<div class="loading-points-container" v-if="historyLoading">
|
||||||
}}</NuxtLink>
|
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
,获得{{ item.amount }}积分
|
</div>
|
||||||
</template>
|
<BasePlaceholder v-else-if="histories.length === 0" text="暂无积分记录" icon="fas fa-inbox" />
|
||||||
<template v-else-if="item.type === 'COMMENT'">
|
<div class="timeline-container" v-else>
|
||||||
在文章
|
<BaseTimeline :items="histories">
|
||||||
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
<template #item="{ item }">
|
||||||
item.postTitle
|
<div class="history-content">
|
||||||
}}</NuxtLink>
|
<template v-if="item.type === 'POST'">
|
||||||
中
|
发送帖子
|
||||||
<template v-if="!item.fromUserId">
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
发送评论
|
item.postTitle
|
||||||
<NuxtLink
|
}}</NuxtLink>
|
||||||
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
,获得{{ item.amount }}积分
|
||||||
class="timeline-link"
|
|
||||||
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
|
||||||
>
|
|
||||||
,获得{{ item.amount }}积分
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
被评论
|
|
||||||
<NuxtLink
|
|
||||||
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
|
||||||
class="timeline-link"
|
|
||||||
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
|
||||||
>
|
|
||||||
,获得{{ item.amount }}积分
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="item.type === 'POST_LIKED' && item.fromUserId">
|
|
||||||
帖子
|
|
||||||
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
|
||||||
item.postTitle
|
|
||||||
}}</NuxtLink>
|
|
||||||
被
|
|
||||||
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
|
||||||
item.fromUserName
|
|
||||||
}}</NuxtLink>
|
|
||||||
按赞,获得{{ item.amount }}积分
|
|
||||||
</template>
|
|
||||||
<template v-else-if="item.type === 'COMMENT_LIKED' && item.fromUserId">
|
|
||||||
评论
|
|
||||||
<NuxtLink
|
|
||||||
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
|
||||||
class="timeline-link"
|
|
||||||
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
|
||||||
>
|
|
||||||
被
|
|
||||||
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
|
||||||
item.fromUserName
|
|
||||||
}}</NuxtLink>
|
|
||||||
按赞,获得{{ item.amount }}积分
|
|
||||||
</template>
|
|
||||||
<template v-else-if="item.type === 'INVITE' && item.fromUserId">
|
|
||||||
邀请了好友
|
|
||||||
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
|
||||||
item.fromUserName
|
|
||||||
}}</NuxtLink>
|
|
||||||
加入社区 🎉,获得 {{ item.amount }} 积分
|
|
||||||
</template>
|
|
||||||
<template v-else-if="item.type === 'FEATURE'">
|
|
||||||
文章
|
|
||||||
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
|
||||||
item.postTitle
|
|
||||||
}}</NuxtLink>
|
|
||||||
被收录为精选,获得 {{ item.amount }} 积分
|
|
||||||
</template>
|
|
||||||
<template v-else-if="item.type === 'REDEEM'">
|
|
||||||
兑换商品,消耗 {{ -item.amount }} 积分
|
|
||||||
</template>
|
|
||||||
<template v-else-if="item.type === 'LOTTERY_JOIN'">
|
|
||||||
参与抽奖帖
|
|
||||||
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
|
||||||
item.postTitle
|
|
||||||
}}</NuxtLink>
|
|
||||||
,消耗 {{ -item.amount }} 积分
|
|
||||||
</template>
|
|
||||||
<template v-else-if="item.type === 'LOTTERY_REWARD'">
|
|
||||||
你的抽奖帖
|
|
||||||
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
|
||||||
item.postTitle
|
|
||||||
}}</NuxtLink>
|
|
||||||
被抽中,消耗 {{ -item.amount }} 积分
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</BaseTimeline>
|
<template v-else-if="item.type === 'COMMENT'">
|
||||||
</div>
|
在文章
|
||||||
</template>
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
</template>
|
item.postTitle
|
||||||
</MultiTabs>
|
}}</NuxtLink>
|
||||||
|
中
|
||||||
|
<template v-if="!item.fromUserId">
|
||||||
|
发送评论
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
||||||
|
class="timeline-link"
|
||||||
|
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
||||||
|
>
|
||||||
|
,获得{{ item.amount }}积分
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
被评论
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
||||||
|
class="timeline-link"
|
||||||
|
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
||||||
|
>
|
||||||
|
,获得{{ item.amount }}积分
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'POST_LIKED' && item.fromUserId">
|
||||||
|
帖子
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
被
|
||||||
|
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||||
|
item.fromUserName
|
||||||
|
}}</NuxtLink>
|
||||||
|
按赞,获得{{ item.amount }}积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'COMMENT_LIKED' && item.fromUserId">
|
||||||
|
评论
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/posts/${item.postId}#comment-${item.commentId}`"
|
||||||
|
class="timeline-link"
|
||||||
|
>{{ stripMarkdownLength(item.commentContent, 100) }}</NuxtLink
|
||||||
|
>
|
||||||
|
被
|
||||||
|
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||||
|
item.fromUserName
|
||||||
|
}}</NuxtLink>
|
||||||
|
按赞,获得{{ item.amount }}积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'INVITE' && item.fromUserId">
|
||||||
|
邀请了好友
|
||||||
|
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||||
|
item.fromUserName
|
||||||
|
}}</NuxtLink>
|
||||||
|
加入社区 🎉,获得 {{ item.amount }} 积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'FEATURE'">
|
||||||
|
文章
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
被收录为精选,获得 {{ item.amount }} 积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'REDEEM'">
|
||||||
|
兑换商品,消耗 {{ -item.amount }} 积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'LOTTERY_JOIN'">
|
||||||
|
参与抽奖帖
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
,消耗 {{ -item.amount }} 积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'LOTTERY_REWARD'">
|
||||||
|
你的抽奖帖
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
被
|
||||||
|
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||||
|
item.fromUserName
|
||||||
|
}}</NuxtLink>
|
||||||
|
参与,获得 {{ item.amount }} 积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'SYSTEM_ONLINE'"> 积分历史系统上线 </template>
|
||||||
|
<i class="fas fa-coins"></i> 你目前的积分是 {{ item.balance }}
|
||||||
|
</div>
|
||||||
|
<div class="history-time">{{ TimeManager.format(item.createdAt) }}</div>
|
||||||
|
</template>
|
||||||
|
</BaseTimeline>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -177,11 +188,6 @@ import TimeManager from '~/utils/time'
|
|||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{ name: 'mall', label: '积分兑换' },
|
|
||||||
{ name: 'history', label: '积分历史' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const selectedTab = ref('mall')
|
const selectedTab = ref('mall')
|
||||||
const point = ref(null)
|
const point = ref(null)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
<div class="info-content-container author-info-container">
|
<div class="info-content-container author-info-container">
|
||||||
<div class="user-avatar-container" @click="gotoProfile">
|
<div class="user-avatar-container" @click="gotoProfile">
|
||||||
<div class="user-avatar-item">
|
<div class="user-avatar-item">
|
||||||
<BaseImage class="user-avatar-item-img" :src="author.avatar" alt="avatar" />
|
<img class="user-avatar-item-img" :src="author.avatar" alt="avatar" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isMobile" class="info-content-header">
|
<div v-if="isMobile" class="info-content-header">
|
||||||
<div class="user-name">
|
<div class="user-name">
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<div class="prize-info">
|
<div class="prize-info">
|
||||||
<div class="prize-info-left">
|
<div class="prize-info-left">
|
||||||
<div class="prize-icon">
|
<div class="prize-icon">
|
||||||
<BaseImage
|
<img
|
||||||
class="prize-icon-img"
|
class="prize-icon-img"
|
||||||
v-if="lottery.prizeIcon"
|
v-if="lottery.prizeIcon"
|
||||||
:src="lottery.prizeIcon"
|
:src="lottery.prizeIcon"
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="prize-member-container">
|
<div class="prize-member-container">
|
||||||
<BaseImage
|
<img
|
||||||
v-for="p in lotteryParticipants"
|
v-for="p in lotteryParticipants"
|
||||||
:key="p.id"
|
:key="p.id"
|
||||||
class="prize-member-avatar"
|
class="prize-member-avatar"
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
<div v-if="lotteryEnded && lotteryWinners.length" class="prize-member-winner">
|
<div v-if="lotteryEnded && lotteryWinners.length" class="prize-member-winner">
|
||||||
<i class="fas fa-medal medal-icon"></i>
|
<i class="fas fa-medal medal-icon"></i>
|
||||||
<span class="prize-member-winner-name">获奖者: </span>
|
<span class="prize-member-winner-name">获奖者: </span>
|
||||||
<BaseImage
|
<img
|
||||||
v-for="w in lotteryWinners"
|
v-for="w in lotteryWinners"
|
||||||
:key="w.id"
|
:key="w.id"
|
||||||
class="prize-member-avatar"
|
class="prize-member-avatar"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div class="avatar-row">
|
<div class="avatar-row">
|
||||||
<!-- label 充当点击区域,内部隐藏 input -->
|
<!-- label 充当点击区域,内部隐藏 input -->
|
||||||
<label class="avatar-container">
|
<label class="avatar-container">
|
||||||
<BaseImage :src="avatar" class="avatar-preview" alt="avatar" />
|
<img :src="avatar" class="avatar-preview" alt="avatar" />
|
||||||
<!-- 半透明蒙层:hover 时出现 -->
|
<!-- 半透明蒙层:hover 时出现 -->
|
||||||
<div class="avatar-overlay">更换头像</div>
|
<div class="avatar-overlay">更换头像</div>
|
||||||
<input type="file" class="avatar-input" accept="image/*" @change="onAvatarChange" />
|
<input type="file" class="avatar-input" accept="image/*" @change="onAvatarChange" />
|
||||||
|
|||||||
@@ -70,35 +70,19 @@
|
|||||||
|
|
||||||
<div class="other-signup-page-content">
|
<div class="other-signup-page-content">
|
||||||
<div class="signup-page-button" @click="signupWithGoogle">
|
<div class="signup-page-button" @click="signupWithGoogle">
|
||||||
<BaseImage
|
<img class="signup-page-button-icon" src="~/assets/icons/google.svg" alt="Google Logo" />
|
||||||
class="signup-page-button-icon"
|
|
||||||
src="~/assets/icons/google.svg"
|
|
||||||
alt="Google Logo"
|
|
||||||
/>
|
|
||||||
<div class="signup-page-button-text">Google 注册</div>
|
<div class="signup-page-button-text">Google 注册</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="signup-page-button" @click="signupWithGithub">
|
<div class="signup-page-button" @click="signupWithGithub">
|
||||||
<BaseImage
|
<img class="signup-page-button-icon" src="~/assets/icons/github.svg" alt="GitHub Logo" />
|
||||||
class="signup-page-button-icon"
|
|
||||||
src="~/assets/icons/github.svg"
|
|
||||||
alt="GitHub Logo"
|
|
||||||
/>
|
|
||||||
<div class="signup-page-button-text">GitHub 注册</div>
|
<div class="signup-page-button-text">GitHub 注册</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="signup-page-button" @click="signupWithDiscord">
|
<div class="signup-page-button" @click="signupWithDiscord">
|
||||||
<BaseImage
|
<img class="signup-page-button-icon" src="~/assets/icons/discord.svg" alt="Discord Logo" />
|
||||||
class="signup-page-button-icon"
|
|
||||||
src="~/assets/icons/discord.svg"
|
|
||||||
alt="Discord Logo"
|
|
||||||
/>
|
|
||||||
<div class="signup-page-button-text">Discord 注册</div>
|
<div class="signup-page-button-text">Discord 注册</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="signup-page-button" @click="signupWithTwitter">
|
<div class="signup-page-button" @click="signupWithTwitter">
|
||||||
<BaseImage
|
<img class="signup-page-button-icon" src="~/assets/icons/twitter.svg" alt="Twitter Logo" />
|
||||||
class="signup-page-button-icon"
|
|
||||||
src="~/assets/icons/twitter.svg"
|
|
||||||
alt="Twitter Logo"
|
|
||||||
/>
|
|
||||||
<div class="signup-page-button-text">Twitter 注册</div>
|
<div class="signup-page-button-text">Twitter 注册</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="profile-page-header">
|
<div class="profile-page-header">
|
||||||
<div class="profile-page-header-avatar">
|
<div class="profile-page-header-avatar">
|
||||||
<BaseImage :src="user.avatar" alt="avatar" class="profile-page-header-avatar-img" />
|
<img :src="user.avatar" alt="avatar" class="profile-page-header-avatar-img" />
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-page-header-user-info">
|
<div class="profile-page-header-user-info">
|
||||||
<div class="profile-page-header-user-info-name">{{ user.username }}</div>
|
<div class="profile-page-header-user-info-name">{{ user.username }}</div>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function tiebaEmojiPlugin(md) {
|
|||||||
md.renderer.rules['tieba-emoji'] = (tokens, idx) => {
|
md.renderer.rules['tieba-emoji'] = (tokens, idx) => {
|
||||||
const name = tokens[idx].content
|
const name = tokens[idx].content
|
||||||
const file = tiebaEmoji[name]
|
const file = tiebaEmoji[name]
|
||||||
return `<BaseImage class="emoji" src="${file}" alt="${name}">`
|
return `<img class="emoji" src="${file}" alt="${name}">`
|
||||||
}
|
}
|
||||||
md.inline.ruler.before('emphasis', 'tieba-emoji', (state, silent) => {
|
md.inline.ruler.before('emphasis', 'tieba-emoji', (state, silent) => {
|
||||||
const pos = state.pos
|
const pos = state.pos
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export function createVditor(editorId, options = {}) {
|
|||||||
const list = await fetchMentions(key)
|
const list = await fetchMentions(key)
|
||||||
return list.map((u) => ({
|
return list.map((u) => ({
|
||||||
value: `@[${u.username}]`,
|
value: `@[${u.username}]`,
|
||||||
html: `<BaseImage src="${u.avatar}" /> @${u.username}`,
|
html: `<img src="${u.avatar}" /> @${u.username}`,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "openisle",
|
"name": "openisle",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "<p align=\"center\"> <BaseImage alt=\"OpenIsle\" src=\"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png\" width=\"200\"> <br><br> 高效的开源社区前后端端平台 <br><br> <a href=\"LICENSE\"><BaseImage src=\"https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square\"></a> </p>",
|
"description": "<p align=\"center\"> <img alt=\"OpenIsle\" src=\"https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png\" width=\"200\"> <br><br> 高效的开源社区前后端端平台 <br><br> <a href=\"LICENSE\"><img src=\"https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square\"></a> </p>",
|
||||||
"author": "nagisa77",
|
"author": "nagisa77",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"homepage": "https://www.open-isle.com",
|
"homepage": "https://www.open-isle.com",
|
||||||
|
|||||||
Reference in New Issue
Block a user