Compare commits

..

1 Commits

Author SHA1 Message Date
Tim f5f60ae0f4 feat: introduce BaseImg component 2025-08-27 11:46:21 +08:00
38 changed files with 245 additions and 324 deletions
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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>
-67
View File
@@ -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>
+44
View File
@@ -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>
-50
View File
@@ -1,50 +0,0 @@
<template>
<div class="base-tabs" @touchstart="onTouchStart" @touchend="onTouchEnd">
<div
v-for="tab in tabs"
:key="tab.name"
:class="[itemClass, { [activeClass]: tab.name === modelValue }]"
@click="emit('update:modelValue', tab.name)"
>
<slot name="tab" :tab="tab">{{ tab.label }}</slot>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
tabs: { type: Array, required: true },
modelValue: { type: String, required: true },
itemClass: { type: String, default: '' },
activeClass: { type: String, default: 'active' },
})
const emit = defineEmits(['update:modelValue'])
const startX = ref(0)
function onTouchStart(e) {
startX.value = e.changedTouches[0].clientX
}
function onTouchEnd(e) {
const diff = e.changedTouches[0].clientX - startX.value
const threshold = 50
if (Math.abs(diff) > threshold) {
const currentIndex = props.tabs.findIndex((t) => t.name === props.modelValue)
if (currentIndex === -1) return
const newIndex = currentIndex + (diff < 0 ? 1 : -1)
if (newIndex >= 0 && newIndex < props.tabs.length) {
emit('update:modelValue', props.tabs[newIndex].name)
}
}
}
</script>
<style scoped>
.base-tabs {
display: flex;
}
</style>
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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">
+4 -14
View File
@@ -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>
+4 -2
View File
@@ -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'
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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"
+3 -3
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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: {
+20 -22
View File
@@ -1,16 +1,15 @@
<template> <template>
<div class="about-page"> <div class="about-page">
<BaseTabs <div class="about-tabs">
v-model="selectedTab" <div
:tabs="tabs" v-for="tab in tabs"
class="about-tabs" :key="tab.name"
item-class="about-tabs-item" :class="['about-tabs-item', { selected: selectedTab === tab.name }]"
active-class="selected" @click="selectTab(tab.name)"
> >
<template #tab="{ tab }">
<div class="about-tabs-item-label">{{ tab.label }}</div> <div class="about-tabs-item-label">{{ tab.label }}</div>
</template> </div>
</BaseTabs> </div>
<div class="about-loading" v-if="isFetching"> <div class="about-loading" v-if="isFetching">
<l-hatch-spinner size="100" stroke="10" speed="1" color="var(--primary-color)" /> <l-hatch-spinner size="100" stroke="10" speed="1" color="var(--primary-color)" />
</div> </div>
@@ -24,13 +23,11 @@
</template> </template>
<script> <script>
import { ref, watch } from 'vue' import { onMounted, ref } from 'vue'
import BaseTabs from '~/components/BaseTabs.vue'
import { handleMarkdownClick, renderMarkdown } from '~/utils/markdown' import { handleMarkdownClick, renderMarkdown } from '~/utils/markdown'
export default { export default {
name: 'AboutPageView', name: 'AboutPageView',
components: { BaseTabs },
setup() { setup() {
const isFetching = ref(false) const isFetching = ref(false)
const tabs = [ const tabs = [
@@ -74,20 +71,21 @@ export default {
} }
} }
watch( const selectTab = (name) => {
selectedTab, selectedTab.value = name
(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) }
},
{ immediate: true }, onMounted(() => {
) loadContent(tabs[0].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>
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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>
+4 -20
View File
@@ -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>
+3 -4
View File
@@ -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 {
+21 -22
View File
@@ -7,13 +7,14 @@
<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>
<BaseTabs <div class="tabs">
v-model="activeTab" <div :class="['tab', { active: activeTab === 'messages' }]" @click="switchToMessage">
:tabs="tabs" 站内信
class="tabs" </div>
item-class="tab" <div :class="['tab', { active: activeTab === 'channels' }]" @click="switchToChannels">
active-class="active" 频道
/> </div>
</div>
<div v-if="activeTab === 'messages'"> <div v-if="activeTab === 'messages'">
<div v-if="loading" class="loading-message"> <div v-if="loading" class="loading-message">
@@ -40,7 +41,7 @@
@click="goToConversation(convo.id)" @click="goToConversation(convo.id)"
> >
<div class="conversation-avatar"> <div class="conversation-avatar">
<BaseImage <img
:src="getOtherParticipant(convo)?.avatar || '/default-avatar.svg'" :src="getOtherParticipant(convo)?.avatar || '/default-avatar.svg'"
:alt="getOtherParticipant(convo)?.username || '用户'" :alt="getOtherParticipant(convo)?.username || '用户'"
class="avatar-img" class="avatar-img"
@@ -87,7 +88,7 @@
@click="goToChannel(ch.id)" @click="goToChannel(ch.id)"
> >
<div class="conversation-avatar"> <div class="conversation-avatar">
<BaseImage <img
:src="ch.avatar || '/default-avatar.svg'" :src="ch.avatar || '/default-avatar.svg'"
:alt="ch.name" :alt="ch.name"
class="avatar-img" class="avatar-img"
@@ -131,7 +132,6 @@ import TimeManager from '~/utils/time'
import { stripMarkdownLength } from '~/utils/markdown' import { stripMarkdownLength } from '~/utils/markdown'
import SearchPersonDropdown from '~/components/SearchPersonDropdown.vue' import SearchPersonDropdown from '~/components/SearchPersonDropdown.vue'
import BasePlaceholder from '~/components/BasePlaceholder.vue' import BasePlaceholder from '~/components/BasePlaceholder.vue'
import BaseTabs from '~/components/BaseTabs.vue'
const config = useRuntimeConfig() const config = useRuntimeConfig()
const conversations = ref([]) const conversations = ref([])
@@ -146,24 +146,13 @@ const { fetchUnreadCount: refreshGlobalUnreadCount } = useUnreadCount()
const { fetchChannelUnread: refreshChannelUnread, setFromList: setChannelUnreadFromList } = const { fetchChannelUnread: refreshChannelUnread, setFromList: setChannelUnreadFromList } =
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)
const isFloatMode = computed(() => route.query.float === '1') const isFloatMode = computed(() => route.query.float === '1')
const floatRoute = useState('messageFloatRoute') const floatRoute = useState('messageFloatRoute')
watch(activeTab, (tab) => {
if (tab === 'messages') {
fetchConversations()
} else {
fetchChannels()
}
})
async function fetchConversations() { async function fetchConversations() {
const token = getToken() const token = getToken()
if (!token) { if (!token) {
@@ -227,6 +216,16 @@ async function fetchChannels() {
} }
} }
function switchToMessage() {
activeTab.value = 'messages'
fetchConversations()
}
function switchToChannels() {
activeTab.value = 'channels'
fetchChannels()
}
async function goToChannel(id) { async function goToChannel(id) {
const token = getToken() const token = getToken()
if (!token) { if (!token) {
+20 -13
View File
@@ -1,13 +1,26 @@
<template> <template>
<div class="message-page"> <div class="message-page">
<div class="message-page-header"> <div class="message-page-header">
<BaseTabs <div class="message-tabs">
v-model="selectedTab" <div
:tabs="messageTabs" :class="['message-tab-item', { selected: selectedTab === 'all' }]"
class="message-tabs" @click="selectedTab = 'all'"
item-class="message-tab-item" >
active-class="selected" 消息
/> </div>
<div
:class="['message-tab-item', { selected: selectedTab === 'unread' }]"
@click="selectedTab = 'unread'"
>
未读
</div>
<div
:class="['message-tab-item', { selected: selectedTab === 'control' }]"
@click="selectedTab = 'control'"
>
消息设置
</div>
</div>
<div class="message-page-header-right"> <div class="message-page-header-right">
<div class="message-page-header-right-item" @click="markAllRead"> <div class="message-page-header-right-item" @click="markAllRead">
@@ -549,16 +562,10 @@ import {
} from '~/utils/notification' } from '~/utils/notification'
import TimeManager from '~/utils/time' import TimeManager from '~/utils/time'
import BaseSwitch from '~/components/BaseSwitch.vue' import BaseSwitch from '~/components/BaseSwitch.vue'
import BaseTabs from '~/components/BaseTabs.vue'
const config = useRuntimeConfig() const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl const API_BASE_URL = config.public.apiBaseUrl
const route = useRoute() const route = useRoute()
const messageTabs = [
{ name: 'all', label: '消息' },
{ name: 'unread', label: '未读' },
{ name: 'control', label: '消息设置' },
]
const selectedTab = ref( const selectedTab = ref(
['all', 'unread', 'control'].includes(route.query.tab) ? route.query.tab : 'unread', ['all', 'unread', 'control'].includes(route.query.tab) ? route.query.tab : 'unread',
) )
+1 -1
View File
@@ -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" />
+16 -13
View File
@@ -1,12 +1,19 @@
<template> <template>
<div class="point-mall-page"> <div class="point-mall-page">
<BaseTabs <div class="point-tabs">
v-model="selectedTab" <div
:tabs="pointTabs" :class="['point-tab-item', { selected: selectedTab === 'mall' }]"
class="point-tabs" @click="selectedTab = 'mall'"
item-class="point-tab-item" >
active-class="selected" 积分兑换
/> </div>
<div
:class="['point-tab-item', { selected: selectedTab === 'history' }]"
@click="selectedTab = 'history'"
>
积分历史
</div>
</div>
<template v-if="selectedTab === 'mall'"> <template v-if="selectedTab === 'mall'">
<div class="point-mall-page-content"> <div class="point-mall-page-content">
@@ -32,7 +39,7 @@
<section class="goods"> <section class="goods">
<div class="goods-item" v-for="(good, idx) in goods" :key="idx"> <div class="goods-item" v-for="(good, idx) in goods" :key="idx">
<BaseImage class="goods-item-image" :src="good.image" alt="good.name" /> <img class="goods-item-image" :src="good.image" alt="good.name" />
<div class="goods-item-name">{{ good.name }}</div> <div class="goods-item-name">{{ good.name }}</div>
<div class="goods-item-cost"> <div class="goods-item-cost">
<i class="fas fa-coins"></i> <i class="fas fa-coins"></i>
@@ -177,14 +184,10 @@ import BaseTimeline from '~/components/BaseTimeline.vue'
import BasePlaceholder from '~/components/BasePlaceholder.vue' import BasePlaceholder from '~/components/BasePlaceholder.vue'
import { stripMarkdownLength } from '~/utils/markdown' import { stripMarkdownLength } from '~/utils/markdown'
import TimeManager from '~/utils/time' import TimeManager from '~/utils/time'
import BaseTabs from '~/components/BaseTabs.vue'
const config = useRuntimeConfig() const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl const API_BASE_URL = config.public.apiBaseUrl
const pointTabs = [
{ 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)
+4 -4
View File
@@ -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"
+1 -1
View File
@@ -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" />
+4 -20
View File
@@ -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>
+72 -44
View File
@@ -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>
@@ -72,18 +72,43 @@
</div> </div>
</div> </div>
<BaseTabs <div class="profile-tabs">
v-model="selectedTab" <div
:tabs="profileTabs" :class="['profile-tabs-item', { selected: selectedTab === 'summary' }]"
class="profile-tabs" @click="selectedTab = 'summary'"
item-class="profile-tabs-item" >
active-class="selected" <i class="fas fa-chart-line"></i>
> <div class="profile-tabs-item-label">总结</div>
<template #tab="{ tab }"> </div>
<i :class="tab.icon"></i> <div
<div class="profile-tabs-item-label">{{ tab.label }}</div> :class="['profile-tabs-item', { selected: selectedTab === 'timeline' }]"
</template> @click="selectedTab = 'timeline'"
</BaseTabs> >
<i class="fas fa-clock"></i>
<div class="profile-tabs-item-label">时间线</div>
</div>
<div
:class="['profile-tabs-item', { selected: selectedTab === 'following' }]"
@click="selectedTab = 'following'"
>
<i class="fas fa-user-plus"></i>
<div class="profile-tabs-item-label">关注</div>
</div>
<div
:class="['profile-tabs-item', { selected: selectedTab === 'favorites' }]"
@click="selectedTab = 'favorites'"
>
<i class="fas fa-bookmark"></i>
<div class="profile-tabs-item-label">收藏</div>
</div>
<div
:class="['profile-tabs-item', { selected: selectedTab === 'achievements' }]"
@click="selectedTab = 'achievements'"
>
<i class="fas fa-medal"></i>
<div class="profile-tabs-item-label">勋章</div>
</div>
</div>
<div v-if="tabLoading" class="tab-loading"> <div v-if="tabLoading" class="tab-loading">
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)" /> <l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)" />
@@ -194,13 +219,26 @@
</div> </div>
<div v-else-if="selectedTab === 'timeline'" class="profile-timeline"> <div v-else-if="selectedTab === 'timeline'" class="profile-timeline">
<BaseTabs <div class="timeline-tabs">
v-model="timelineFilter" <div
:tabs="timelineTabs" :class="['timeline-tab-item', { selected: timelineFilter === 'all' }]"
class="timeline-tabs" @click="timelineFilter = 'all'"
item-class="timeline-tab-item" >
active-class="selected" 全部
/> </div>
<div
:class="['timeline-tab-item', { selected: timelineFilter === 'articles' }]"
@click="timelineFilter = 'articles'"
>
文章
</div>
<div
:class="['timeline-tab-item', { selected: timelineFilter === 'comments' }]"
@click="timelineFilter = 'comments'"
>
评论和回复
</div>
</div>
<BasePlaceholder <BasePlaceholder
v-if="filteredTimelineItems.length === 0" v-if="filteredTimelineItems.length === 0"
text="暂无时间线" text="暂无时间线"
@@ -267,13 +305,20 @@
</div> </div>
<div v-else-if="selectedTab === 'following'" class="follow-container"> <div v-else-if="selectedTab === 'following'" class="follow-container">
<BaseTabs <div class="follow-tabs">
v-model="followTab" <div
:tabs="followTabs" :class="['follow-tab-item', { selected: followTab === 'followers' }]"
class="follow-tabs" @click="followTab = 'followers'"
item-class="follow-tab-item" >
active-class="selected" 关注者
/> </div>
<div
:class="['follow-tab-item', { selected: followTab === 'following' }]"
@click="followTab = 'following'"
>
正在关注
</div>
</div>
<div class="follow-list"> <div class="follow-list">
<UserList v-if="followTab === 'followers'" :users="followers" /> <UserList v-if="followTab === 'followers'" :users="followers" />
<UserList v-else :users="followings" /> <UserList v-else :users="followings" />
@@ -320,7 +365,6 @@ import { authState, getToken } from '~/utils/auth'
import { prevLevelExp } from '~/utils/level' import { prevLevelExp } from '~/utils/level'
import { stripMarkdown, stripMarkdownLength } from '~/utils/markdown' import { stripMarkdown, stripMarkdownLength } from '~/utils/markdown'
import TimeManager from '~/utils/time' import TimeManager from '~/utils/time'
import BaseTabs from '~/components/BaseTabs.vue'
const config = useRuntimeConfig() const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl const API_BASE_URL = config.public.apiBaseUrl
@@ -336,22 +380,6 @@ const hotReplies = ref([])
const hotTags = ref([]) const hotTags = ref([])
const favoritePosts = ref([]) const favoritePosts = ref([])
const timelineItems = ref([]) const timelineItems = ref([])
const profileTabs = [
{ name: 'summary', label: '总结', icon: 'fas fa-chart-line' },
{ name: 'timeline', label: '时间线', icon: 'fas fa-clock' },
{ name: 'following', label: '关注', icon: 'fas fa-user-plus' },
{ name: 'favorites', label: '收藏', icon: 'fas fa-bookmark' },
{ name: 'achievements', label: '勋章', icon: 'fas fa-medal' },
]
const timelineTabs = [
{ name: 'all', label: '全部' },
{ name: 'articles', label: '文章' },
{ name: 'comments', label: '评论和回复' },
]
const followTabs = [
{ name: 'followers', label: '关注者' },
{ name: 'following', label: '正在关注' },
]
const timelineFilter = ref('all') const timelineFilter = ref('all')
const filteredTimelineItems = computed(() => { const filteredTimelineItems = computed(() => {
if (timelineFilter.value === 'articles') { if (timelineFilter.value === 'articles') {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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",