mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-13 00:50:40 +08:00
chore: 批量新增项目依赖、工具函数、页面与组件资源
1. 新增mp-html、qs等生产依赖,补全项目基础库 2. 新增平台判断、缓存、工具函数等通用工具集 3. 新增标签页、网站浏览页、关于页等业务页面 4. 新增分类卡片、通知弹窗等业务组件 5. 新增uts-progressNotification、liu-poster、uhalo-upgrade等uni模块 6. 补充audio/video组件样式补件,修复uni-components路径缺失问题 7. 新增环境变量Halo个人令牌配置项 8. 重构store导出结构,新增appConfig/halo/setting三个状态模块 9. 新增tsconfig编译目标配置,适配更高版本ES语法
This commit is contained in:
+147
-14
@@ -1,7 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
defineOptions({
|
||||
name: 'Index',
|
||||
})
|
||||
/**
|
||||
* 入口页(源自旧项目 pages/index/index.vue,新建复刻)
|
||||
* 职责:检查插件可用性 → 获取配置 → 二维码 scene 跳文章 → 审计模式 mock → 启动页/首页分流
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getQRCodeInfo } from '@/api/uni-halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import { checkJsonAndParse } from '@/utils/json'
|
||||
|
||||
definePage({
|
||||
// 使用 type: "home" 属性设置首页,其他页面不需要设置,默认为page
|
||||
type: 'home',
|
||||
@@ -11,23 +19,148 @@ definePage({
|
||||
navigationBarTitleText: '初始页面',
|
||||
},
|
||||
})
|
||||
|
||||
console.log('index/index 首页打印了')
|
||||
|
||||
function toHome() {
|
||||
uni.switchTab({
|
||||
url: '/pages/tabbar/home/home',
|
||||
})
|
||||
/* ---------------- 常量 ---------------- */
|
||||
const homePagePath = '/pages/tabbar/home/home'
|
||||
const startPagePath = '/pages/start/start'
|
||||
const articleDetailPath = '/pages-blog/article-detail/article-detail'
|
||||
|
||||
// 本地开发快速跳转页面,发布请置为 false
|
||||
const DEV_MODE = false
|
||||
const DEV_TO_TYPE = 'page' as 'page' | 'tabbar'
|
||||
const DEV_TO_PATH = '/pages-blog/data-visual/data-visual'
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const uniHaloPluginId = 'plugin-uni-halo'
|
||||
const uniHaloPluginAvailableError = '阿偶,检测到当前插件没有安装或者启用,无法启动 uni-halo 哦,请联系管理员'
|
||||
const uniHaloPluginAvailable = ref(true)
|
||||
|
||||
/* ---------------- 逻辑 ---------------- */
|
||||
/** 检查插件可用性 */
|
||||
async function handleCheckPluginAvailable(): Promise<boolean> {
|
||||
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
|
||||
return uniHaloPluginAvailable.value
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
console.log('测试 uni API 自动引入: onLoad')
|
||||
/** 通过二维码 scene 获取文章 id */
|
||||
async function getPostIdByQRCode(key: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await getQRCodeInfo(key)
|
||||
if (response.data?.postId)
|
||||
return response.data.postId as string
|
||||
}
|
||||
catch (err) {
|
||||
console.error('二维码解析失败', err)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 处理审计模式 mock 数据 */
|
||||
async function handleAuditMode(res: Record<string, unknown>) {
|
||||
const auditConfig = (res?.auditConfig ?? {}) as {
|
||||
auditModeEnabled?: boolean
|
||||
auditModeData?: { jsonUrl?: string, jsonData?: string }
|
||||
}
|
||||
if (!auditConfig.auditModeEnabled)
|
||||
return
|
||||
if (auditConfig.auditModeData?.jsonUrl) {
|
||||
await appConfigStore.fetchMockJson()
|
||||
}
|
||||
else {
|
||||
const mockJson = checkJsonAndParse(auditConfig.auditModeData?.jsonData || '')
|
||||
if (mockJson.ok) {
|
||||
appConfigStore.setMockJson(mockJson.jsonData as Record<string, unknown>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动页/首页分流 */
|
||||
function handleCheckShowStarted() {
|
||||
const appConfig = (appConfigStore.configs.appConfig ?? {}) as {
|
||||
startConfig?: { enabled?: boolean, alwaysShow?: boolean }
|
||||
}
|
||||
const startConfig = appConfig.startConfig
|
||||
|
||||
// 未开启启动页,直接进首页
|
||||
if (!startConfig?.enabled) {
|
||||
uni.switchTab({ url: homePagePath })
|
||||
return
|
||||
}
|
||||
|
||||
// 是否每次都显示启动页
|
||||
if (startConfig.alwaysShow) {
|
||||
uni.removeStorageSync('APP_HAS_STARTED')
|
||||
uni.redirectTo({ url: startPagePath })
|
||||
return
|
||||
}
|
||||
|
||||
// 只显示一次启动页
|
||||
if (uni.getStorageSync('APP_HAS_STARTED')) {
|
||||
uni.switchTab({ url: homePagePath })
|
||||
}
|
||||
else {
|
||||
uni.redirectTo({ url: startPagePath })
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async (options) => {
|
||||
// 本地开发,快速跳转页面,发布请设置 DEV_MODE = false
|
||||
if (DEV_MODE && DEV_TO_PATH) {
|
||||
if (DEV_TO_TYPE === 'tabbar') {
|
||||
uni.switchTab({ url: DEV_TO_PATH })
|
||||
}
|
||||
else {
|
||||
uni.navigateTo({ url: DEV_TO_PATH })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查插件
|
||||
if (!(await handleCheckPluginAvailable()))
|
||||
return
|
||||
|
||||
// 获取配置
|
||||
try {
|
||||
const res = await appConfigStore.fetchConfigs()
|
||||
if (!res) {
|
||||
uni.switchTab({ url: homePagePath })
|
||||
return
|
||||
}
|
||||
|
||||
// 二维码 scene 进入:解析 postId 跳文章详情
|
||||
if (options.scene && options.scene !== '') {
|
||||
const postId = await getPostIdByQRCode(decodeURIComponent(options.scene))
|
||||
if (postId) {
|
||||
uni.redirectTo({
|
||||
url: `${articleDetailPath}?name=${postId}`,
|
||||
animationType: 'slide-in-right',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 审计模式 mock
|
||||
await handleAuditMode(res as Record<string, unknown>)
|
||||
|
||||
// 启动页分流
|
||||
handleCheckShowStarted()
|
||||
}
|
||||
catch (err) {
|
||||
console.error('入口页初始化失败', err)
|
||||
uni.switchTab({ url: homePagePath })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="box-border bg-white px-4 pt-safe w-screen h-screen flex flex-col items-center justify-center">
|
||||
uni-halo
|
||||
<wd-button @click="toHome()">去首页</wd-button>
|
||||
<view class="app-page h-screen w-screen flex items-center justify-center" style="background-color: #fff;">
|
||||
<uh-plugin-unavailable
|
||||
v-if="!uniHaloPluginAvailable"
|
||||
:plugin-id="uniHaloPluginId"
|
||||
:error-text="uniHaloPluginAvailableError"
|
||||
:use-border="false"
|
||||
:use-decoration="false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 启动页(源自旧项目 pagesA/start,新建复刻)
|
||||
* 支持颜色/图片/视频/星空四种背景类型 + logo/标题/描述 + 开始按钮 + 波浪
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { checkImageUrl, checkUrl } from '@/utils/url'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: 'uni-halo',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
|
||||
const startConfig = computed(() => (haloConfigs.value.appConfig?.startConfig as {
|
||||
title?: string
|
||||
logo?: string
|
||||
desc1?: string
|
||||
desc2?: string
|
||||
btnText?: string
|
||||
btnClass?: string
|
||||
btnStyle?: string
|
||||
titleStyle?: string
|
||||
descStyle?: string
|
||||
backgroundType?: string
|
||||
bg?: string
|
||||
bgImage?: string
|
||||
bgImageFit?: string
|
||||
bgVideo?: string
|
||||
bgVideoFit?: string
|
||||
useWave?: boolean
|
||||
} | undefined) || {})
|
||||
|
||||
const calcBackgroundType = computed(() => startConfig.value.backgroundType || 'star')
|
||||
|
||||
const calcPageClass = computed(() => {
|
||||
if (calcBackgroundType.value === 'color') {
|
||||
return [startConfig.value.bg]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const calcPageStyle = computed(() => {
|
||||
if (calcBackgroundType.value === 'color') {
|
||||
return {}
|
||||
}
|
||||
if (calcBackgroundType.value === 'image') {
|
||||
return {
|
||||
backgroundImage: `url(${checkImageUrl(startConfig.value.bgImage)}) !important`,
|
||||
backgroundSize: startConfig.value.bgImageFit || 'cover',
|
||||
}
|
||||
}
|
||||
if (calcBackgroundType.value === 'video') {
|
||||
return {
|
||||
background: '#ffffff',
|
||||
}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
function handleStart() {
|
||||
uni.switchTab({
|
||||
url: '/pages/tabbar/home/home',
|
||||
success: () => {
|
||||
uni.setStorageSync('APP_HAS_STARTED', true)
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="app-page relative h-screen w-screen" :class="calcPageClass" :style="[calcPageStyle]">
|
||||
<!-- 星空背景 -->
|
||||
<view v-if="calcBackgroundType !== 'video'" class="star-bg fixed z-998 h-[600px] w-full shrink-0 overflow-hidden">
|
||||
<view class="stars absolute z-1 h-[400px] w-full">
|
||||
<view class="falling-stars">
|
||||
<view class="star-fall" />
|
||||
<view class="star-fall" />
|
||||
<view class="star-fall" />
|
||||
<view class="star-fall" />
|
||||
</view>
|
||||
<view class="small-stars">
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
<view class="star" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 视频背景 -->
|
||||
<video
|
||||
v-else
|
||||
class="video-bg absolute left-0 top-0 z-0 h-screen w-screen"
|
||||
:object-fit="(startConfig.bgVideoFit as 'contain' | 'cover') || 'cover'"
|
||||
:src="checkUrl(startConfig.bgVideo)"
|
||||
:loop="true"
|
||||
:autoplay="true"
|
||||
:muted="true"
|
||||
:controls="false"
|
||||
:show-fullscreen-btn="false"
|
||||
:show-play-btn="false"
|
||||
:show-center-play-btn="false"
|
||||
:show-loading="false"
|
||||
:enable-progress-gesture="false"
|
||||
:show-progress="false"
|
||||
/>
|
||||
|
||||
<!-- 标题区域 -->
|
||||
<view v-if="startConfig.title || startConfig.logo" class="title-container absolute left-0 top-[20vh] z-999 w-screen flex flex-col items-center justify-center">
|
||||
<view v-if="startConfig.logo" class="app-logo h-[200rpx] w-[200rpx]">
|
||||
<view class="app-logo-border box-border h-full w-full overflow-hidden border-8 border-white/35 rounded-full">
|
||||
<image class="app-logo-image h-full w-full rounded-full" :src="checkImageUrl(startConfig.logo)" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="startConfig.title" class="app-title mt-6 text-center text-[36rpx] text-white font-semibold" :style="startConfig.titleStyle">
|
||||
「 {{ startConfig.title }} 」
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部区域 -->
|
||||
<view class="bottom-container absolute bottom-[50rpx] left-1/2 z-999 flex flex-col items-center -translate-x-1/2">
|
||||
<view class="desc-area pt-[60vh] text-white" :style="startConfig.descStyle">
|
||||
<view v-show="startConfig.desc1" class="desc1 text-center text-[44rpx]">
|
||||
{{ startConfig.desc1 }}
|
||||
</view>
|
||||
<view v-show="startConfig.desc2" class="desc2 mt-8 text-center text-[26rpx]">
|
||||
{{ startConfig.desc2 }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="start-btn mb-[120rpx] mt-[60rpx] box-border border-2 border-white rounded-[50rpx] px-12 py-4 text-center text-[28rpx] text-white" :class="[startConfig.btnClass]" :style="[startConfig.btnStyle]" @click="handleStart">
|
||||
{{ startConfig.btnText || '开始体验' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 波浪效果 -->
|
||||
<image v-if="startConfig.useWave" class="wave-img absolute bottom-0 left-0 z-99 h-[100rpx] w-full" src="/static/wave/wave-1.png" mode="scaleToFill" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-page {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background: linear-gradient(180deg, #0f1e3d 0%, #1a3a6b 100%);
|
||||
}
|
||||
|
||||
/* 星空背景(动画无法用 UnoCSS 表达,保留样式) */
|
||||
.star-bg {
|
||||
.star {
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 6px 0 rgb(255 255 255 / 80%);
|
||||
}
|
||||
|
||||
.small-stars .star {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
opacity: 0;
|
||||
animation: star-blink 1.2s linear infinite alternate;
|
||||
|
||||
&:nth-child(1) {
|
||||
left: 40px;
|
||||
bottom: 50px;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
left: 200px;
|
||||
bottom: 40px;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
left: 60px;
|
||||
bottom: 120px;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
left: 140px;
|
||||
bottom: 250px;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
left: 400px;
|
||||
bottom: 300px;
|
||||
}
|
||||
&:nth-child(6) {
|
||||
left: 170px;
|
||||
bottom: 80px;
|
||||
}
|
||||
&:nth-child(7) {
|
||||
left: 200px;
|
||||
bottom: 360px;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
&:nth-child(8) {
|
||||
left: 250px;
|
||||
bottom: 320px;
|
||||
}
|
||||
&:nth-child(9) {
|
||||
left: 300px;
|
||||
bottom: 340px;
|
||||
}
|
||||
&:nth-child(10) {
|
||||
left: 130px;
|
||||
bottom: 320px;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
&:nth-child(11) {
|
||||
left: 230px;
|
||||
bottom: 330px;
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
&:nth-child(12) {
|
||||
left: 300px;
|
||||
bottom: 360px;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.star-fall {
|
||||
position: relative;
|
||||
border-radius: 2px;
|
||||
width: 80px;
|
||||
height: 2px;
|
||||
overflow: hidden;
|
||||
transform: rotate(-20deg);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 2px;
|
||||
background: linear-gradient(to left, rgb(0 0 0 / 0%) 0%, rgb(255 255 255 / 40%) 100%);
|
||||
left: 100%;
|
||||
animation: star-fall 3.6s linear infinite;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
left: 80px;
|
||||
bottom: -100px;
|
||||
&::after {
|
||||
animation-delay: 2.4s;
|
||||
}
|
||||
}
|
||||
&:nth-child(2) {
|
||||
left: 200px;
|
||||
bottom: -200px;
|
||||
&::after {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
left: 430px;
|
||||
bottom: -50px;
|
||||
&::after {
|
||||
animation-delay: 3.6s;
|
||||
}
|
||||
}
|
||||
&:nth-child(4) {
|
||||
left: 400px;
|
||||
bottom: 100px;
|
||||
&::after {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes star-blink {
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes star-fall {
|
||||
20% {
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 波浪混合模式(无法用 UnoCSS 表达) */
|
||||
.wave-img {
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,334 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 关于页(源自旧项目 pages/tabbar/about/about.vue,新建复刻)
|
||||
* 功能:博主信息 + 站点统计 + 功能导航 + 版权
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { getBlogStatistics } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||
import { checkHasAdminLogin } from '@/utils/auth'
|
||||
import { t } from '@/locale'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import type { IBlogStats } from '@/api/types/halo'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '关于',
|
||||
enablePullDownRefresh: true,
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
|
||||
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
|
||||
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
const bloggerInfo = computed(() => {
|
||||
const blogger = haloConfigs.value.authorConfig?.blogger as
|
||||
| { nickname?: string, avatar?: string, description?: string }
|
||||
| undefined
|
||||
return {
|
||||
nickname: blogger?.nickname || '',
|
||||
avatar: checkAvatarUrl(blogger?.avatar),
|
||||
description: blogger?.description || '',
|
||||
}
|
||||
})
|
||||
|
||||
const pageConfig = computed(() => haloConfigs.value.pageConfig?.aboutConfig as
|
||||
| { bgImageUrl?: string, waveImageUrl?: string }
|
||||
| undefined)
|
||||
|
||||
const calcProfileStyle = computed(() => ({
|
||||
backgroundImage: `url(${checkImageUrl(pageConfig.value?.bgImageUrl)})`,
|
||||
}))
|
||||
|
||||
const calcWaveUrl = computed(() => checkImageUrl(pageConfig.value?.waveImageUrl))
|
||||
|
||||
const basicConfig = computed(() => haloConfigs.value.basicConfig as
|
||||
| {
|
||||
copyrightConfig?: { enabled?: boolean, content?: string }
|
||||
disclaimers?: { enabled?: boolean }
|
||||
showAboutSystem?: boolean
|
||||
}
|
||||
| undefined)
|
||||
|
||||
const copyrightConfig = computed(() => basicConfig.value?.copyrightConfig)
|
||||
|
||||
const loveEnabled = computed(() => !!(haloConfigs.value.loveConfig as { loveEnabled?: boolean } | undefined)?.loveEnabled)
|
||||
const socialEnabled = computed(() => !!(haloConfigs.value.authorConfig?.social as { enabled?: boolean } | undefined)?.enabled)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const statisticsShowMore = ref(false)
|
||||
const statistics = ref<IBlogStats>({ post: 0, comment: 0, category: 0, visit: 0, upvote: 0 })
|
||||
const navList = ref<{
|
||||
key: string
|
||||
title: string
|
||||
icon: string
|
||||
iconColor: string
|
||||
rightText: string
|
||||
path: string | null
|
||||
isAdmin?: boolean
|
||||
openType?: string
|
||||
show: boolean
|
||||
}[]>([])
|
||||
|
||||
/* ---------------- 功能导航 ---------------- */
|
||||
async function handleGetNavList() {
|
||||
let isWx = false
|
||||
// #ifdef MP-WEIXIN
|
||||
isWx = true
|
||||
// #endif
|
||||
|
||||
const dataVisualAvailable = await usePluginAvailable('plugin-data-statistics')
|
||||
|
||||
navList.value = [
|
||||
{
|
||||
key: 'data-visual',
|
||||
title: '数据看板',
|
||||
icon: 'chart',
|
||||
iconColor: '#2196f3',
|
||||
rightText: '站点数据可视化',
|
||||
path: '/pages-blog/data-visual/data-visual',
|
||||
show: dataVisualAvailable,
|
||||
},
|
||||
{
|
||||
key: 'archives',
|
||||
title: calcAuditModeEnabled.value ? '内容归档' : '文章归档',
|
||||
icon: 'folder',
|
||||
iconColor: '#f44336',
|
||||
rightText: calcAuditModeEnabled.value ? '全部已归档内容' : '全部已归档文章',
|
||||
path: '/pages-blog/archives/archives',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
key: 'love',
|
||||
title: '恋爱日记',
|
||||
icon: 'heart',
|
||||
iconColor: '#f44336',
|
||||
rightText: '博主的恋爱日记',
|
||||
path: '/pages-blog/love/love',
|
||||
show: loveEnabled.value,
|
||||
},
|
||||
{
|
||||
key: 'vote',
|
||||
title: '投票中心',
|
||||
icon: 'box',
|
||||
iconColor: '#f44336',
|
||||
rightText: '查看和进行投票',
|
||||
path: '/pages-blog/votes/votes',
|
||||
show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value,
|
||||
},
|
||||
{
|
||||
key: 'friend-links',
|
||||
title: '友情链接',
|
||||
icon: 'link',
|
||||
iconColor: '#2196f3',
|
||||
rightText: '看看博主朋友们吧',
|
||||
path: '/pages-blog/friend-links/friend-links',
|
||||
show: calcLinksPluginEnabled.value,
|
||||
},
|
||||
{
|
||||
key: 'disclaimers',
|
||||
title: '免责声明',
|
||||
icon: 'map',
|
||||
iconColor: '#f44336',
|
||||
rightText: '博客内容免责声明',
|
||||
path: '/pages-blog/disclaimers/disclaimers',
|
||||
show: !!basicConfig.value?.disclaimers?.enabled,
|
||||
},
|
||||
{
|
||||
key: 'contact-blogger',
|
||||
title: '联系博主',
|
||||
icon: 'message',
|
||||
iconColor: '#ff9800',
|
||||
rightText: '博主常用联系方式',
|
||||
path: '/pages-blog/contact/contact',
|
||||
show: socialEnabled.value,
|
||||
},
|
||||
{
|
||||
key: 'about',
|
||||
title: '关于项目',
|
||||
icon: 'info',
|
||||
iconColor: '#2196f3',
|
||||
rightText: '小莫唐尼开源项目',
|
||||
path: '/pages-blog/about/about',
|
||||
show: !!basicConfig.value?.showAboutSystem,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetData() {
|
||||
try {
|
||||
const res = await getBlogStatistics()
|
||||
statistics.value = res.data
|
||||
}
|
||||
catch (err) {
|
||||
console.error('获取统计失败', err)
|
||||
uni.showToast({ icon: 'none', title: t('common.loadFailedRetry') })
|
||||
}
|
||||
finally {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 交互 ---------------- */
|
||||
function handleOnNav(data: { path: string | null, isAdmin?: boolean }) {
|
||||
const { path, isAdmin } = data
|
||||
if (!path)
|
||||
return
|
||||
|
||||
// 拦截后台管理页面(需超管登录)
|
||||
if (isAdmin && !checkHasAdminLogin()) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '未登录超管账号或登录状态已过期,是否立即登录?',
|
||||
showCancel: true,
|
||||
cancelText: '否',
|
||||
cancelColor: '#999999',
|
||||
confirmText: '是',
|
||||
confirmColor: '#03a9f4',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({ url: '/pages/auth/login' })
|
||||
}
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({ url: path })
|
||||
}
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
watch(haloConfigs, () => {
|
||||
handleGetNavList()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
handleGetData()
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
handleGetData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mt-10 text-center text-green-500">
|
||||
关于页面
|
||||
<view class="app-page min-h-screen w-screen pb-6">
|
||||
<!-- 博主信息 -->
|
||||
<view class="blogger-info relative h-[600rpx] w-full" :style="[calcProfileStyle]">
|
||||
<image class="avatar absolute left-1/2 top-[200rpx] z-2 h-[130rpx] w-[130rpx] border-6 border-white rounded-full -translate-x-1/2" :src="bloggerInfo.avatar" mode="aspectFill" />
|
||||
<view class="profile absolute left-0 top-[340rpx] z-6 w-full text-center text-white">
|
||||
<view class="author text-[34rpx] font-bold">
|
||||
{{ bloggerInfo.nickname }}
|
||||
</view>
|
||||
<view class="desc mt-4 px-12 text-[26rpx] opacity-90">
|
||||
{{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
|
||||
</view>
|
||||
</view>
|
||||
<image v-if="calcWaveUrl" :src="calcWaveUrl" mode="scaleToFill" class="gif-wave absolute bottom-0 left-0 z-99 h-[100rpx] w-full" style="mix-blend-mode: screen;" />
|
||||
</view>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<view class="statistics-wrap overflow-hidden rounded-b-3xl bg-white shadow-sm">
|
||||
<view class="statistics flex pb-3 pt-3">
|
||||
<view class="item flex-1 py-6 text-center">
|
||||
<view class="number text-[40rpx] font-bold" style="color: #ff9800;">
|
||||
{{ statistics.post }}
|
||||
</view>
|
||||
<view class="mt-1 text-center text-[24rpx] text-[#999]">
|
||||
内容数量
|
||||
</view>
|
||||
</view>
|
||||
<view class="item flex-1 py-6 text-center">
|
||||
<view class="number text-[40rpx] font-bold" style="color: #4caf50;">
|
||||
{{ statistics.visit }}
|
||||
</view>
|
||||
<view class="mt-1 text-[24rpx] text-[#999]">
|
||||
访客数量
|
||||
</view>
|
||||
</view>
|
||||
<view class="item flex-1 py-6 text-center">
|
||||
<view class="number text-[40rpx] font-bold" style="color: #2196f3;">
|
||||
{{ statistics.category }}
|
||||
</view>
|
||||
<view class="mt-1 text-center text-[24rpx] text-[#999]">
|
||||
分类总数
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="statisticsShowMore" class="statistics flex border-t-2 border-[#fafafa] pb-3 pt-3">
|
||||
<view class="item flex-1 py-6 text-center">
|
||||
<view class="number text-[40rpx] font-bold" style="color: #ff9800;">
|
||||
{{ statistics.comment }}
|
||||
</view>
|
||||
<view class="mt-1 text-center text-[24rpx] text-[#999]">
|
||||
评论数量
|
||||
</view>
|
||||
</view>
|
||||
<view class="item flex-1 py-6 text-center">
|
||||
<view class="number text-[40rpx] font-bold" style="color: #2196f3;">
|
||||
{{ statistics.upvote }}
|
||||
</view>
|
||||
<view class="mt-1 text-[24rpx] text-[#999]">
|
||||
点赞数量
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="show-more-btn pb-4 text-center text-[24rpx] text-[#999]" @click="statisticsShowMore = !statisticsShowMore">
|
||||
{{ statisticsShowMore ? '收起' : '展开' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能导航 -->
|
||||
<view class="nav-wrap mx-6 mt-6 overflow-hidden rounded-xl bg-white shadow-sm">
|
||||
<template v-for="nav in navList.filter(n => n.show)" :key="nav.key">
|
||||
<view class="nav-item flex items-center justify-between border-b-2 border-[#f5f5f5] px-3 py-7" @click="handleOnNav(nav)">
|
||||
<view class="nav-left flex items-center gap-4">
|
||||
<wd-icon :name="nav.icon" size="18px" :color="nav.iconColor" />
|
||||
<text class="nav-title text-[28rpx] text-[#303133]">{{ nav.title }}</text>
|
||||
</view>
|
||||
<view class="nav-right flex items-center gap-2">
|
||||
<text class="nav-right-text text-[24rpx] text-[#c0c4cc]">{{ nav.rightText }}</text>
|
||||
<wd-icon name="arrow-right" size="12px" color="#c0c4cc" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 版权 -->
|
||||
<view v-if="copyrightConfig?.enabled" class="copyright mt-10 px-6 text-center text-[22rpx] text-[#c0c4c7]">
|
||||
<view>{{ copyrightConfig.content }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-page {
|
||||
.blogger-info {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: rgb(0 0 0 / 30%);
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-wrap {
|
||||
.nav-item {
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,376 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 分类页(源自旧项目 pages/tabbar/category/category.vue,新建复刻)
|
||||
* 两种视图:list(分类卡片网格)/ list-post(左侧分类导航 + 右侧文章列表)
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getCategoryList, getCategoryPostList } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkThumbnailUrl } from '@/utils/url'
|
||||
import { t } from '@/locale'
|
||||
import type { ICategory, IPost } from '@/api/types/halo'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '分类',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const mockJson = computed(() => appConfigStore.mockJson)
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
|
||||
const categoryConfig = computed(() => haloConfigs.value.pageConfig?.categoryConfig)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const queryParams = ref({
|
||||
size: 20,
|
||||
page: 1,
|
||||
fieldSelector: ['spec.hideFromList=false'],
|
||||
})
|
||||
const hasNext = ref(false)
|
||||
const dataList = ref<ICategory[]>([])
|
||||
const categoryList = ref<ICategory[]>([])
|
||||
const isLoadMore = ref(false)
|
||||
const loadMoreText = ref(t('common.loading'))
|
||||
const currentCategoryConfig = ref<{ type?: string }>({ type: 'list' })
|
||||
const currentCategoryName = ref('')
|
||||
const postQueryParams = ref({ size: 10, page: 0 })
|
||||
const postList = ref<IPost[]>([])
|
||||
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
const calcShowType = computed(() => currentCategoryConfig.value.type)
|
||||
|
||||
/* ---------------- 视图切换 ---------------- */
|
||||
function handleChangeShowType() {
|
||||
currentCategoryConfig.value.type = calcShowType.value === 'list-post' ? 'list' : 'list-post'
|
||||
handleInitPage()
|
||||
}
|
||||
|
||||
function handleResetInit() {
|
||||
postList.value = []
|
||||
dataList.value = []
|
||||
categoryList.value = []
|
||||
queryParams.value.page = 1
|
||||
postQueryParams.value.page = 0
|
||||
hasNext.value = false
|
||||
isLoadMore.value = false
|
||||
loadMoreText.value = t('common.loading')
|
||||
currentCategoryName.value = ''
|
||||
}
|
||||
|
||||
function handleInitPage() {
|
||||
handleResetInit()
|
||||
if (calcShowType.value === 'list-post') {
|
||||
queryParams.value.size = 99999
|
||||
}
|
||||
handleGetData()
|
||||
}
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetData() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
currentCategoryConfig.value.type = 'list'
|
||||
const categoryMock = mockJson.value.category as { list?: { title?: string, cover?: string }[] } | undefined
|
||||
dataList.value = (categoryMock?.list || []).map(item => ({
|
||||
metadata: { name: String(Date.now() * Math.random()) },
|
||||
spec: {
|
||||
displayName: item.title || '',
|
||||
slug: '',
|
||||
priority: 0,
|
||||
cover: checkThumbnailUrl(item.cover, true),
|
||||
},
|
||||
postCount: 0,
|
||||
}))
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ mask: true, title: t('common.loading') })
|
||||
if (!isLoadMore.value) {
|
||||
loading.value = 'loading'
|
||||
}
|
||||
loadMoreText.value = t('common.loading')
|
||||
|
||||
try {
|
||||
const res = await getCategoryList({ ...queryParams.value })
|
||||
|
||||
if (calcShowType.value === 'list') {
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
hasNext.value = res.data.hasNext
|
||||
|
||||
const tempItems = res.data.items.map(item => ({
|
||||
...item,
|
||||
postCount: item.postCount ?? 0,
|
||||
spec: { ...item.spec, cover: checkThumbnailUrl(item.spec.cover, true) },
|
||||
}))
|
||||
|
||||
dataList.value = isLoadMore.value
|
||||
? dataList.value.concat(tempItems)
|
||||
: tempItems
|
||||
}
|
||||
else {
|
||||
dataList.value = res.data.items
|
||||
categoryList.value = res.data.items.map(item => ({
|
||||
...item,
|
||||
postCount: item.postCount ?? 0,
|
||||
}))
|
||||
loading.value = 'success'
|
||||
if (dataList.value.length !== 0) {
|
||||
currentCategoryName.value = dataList.value[0].metadata.name
|
||||
handleGetPostByCategory()
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
}
|
||||
finally {
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取当前分类下的文章 */
|
||||
async function handleGetPostByCategory(isPulldownRefresh = true) {
|
||||
if (!isPulldownRefresh) {
|
||||
if (hasNext.value) {
|
||||
postQueryParams.value.page += 1
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
return
|
||||
}
|
||||
}
|
||||
else {
|
||||
postQueryParams.value.page = 0
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getCategoryPostList(currentCategoryName.value, postQueryParams.value)
|
||||
hasNext.value = res.data.hasNext
|
||||
postList.value = isPulldownRefresh
|
||||
? res.data.items
|
||||
: postList.value.concat(res.data.items)
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
}
|
||||
catch (err) {
|
||||
loadMoreText.value = t('common.loadFailedShort')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 交互 ---------------- */
|
||||
function handleOnCategoryChange(e: { detail: { current: number } }) {
|
||||
const index = e.detail.current
|
||||
if (!dataList.value[index])
|
||||
return
|
||||
currentCategoryName.value = dataList.value[index].metadata.name
|
||||
postList.value = []
|
||||
handleGetPostByCategory()
|
||||
}
|
||||
|
||||
function handleToCategory(category: ICategory) {
|
||||
if (calcAuditModeEnabled.value)
|
||||
return
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`,
|
||||
})
|
||||
}
|
||||
|
||||
function handleToArticleDetail(post: IPost) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/article-detail/article-detail?name=${post.metadata.name}`,
|
||||
animationType: 'slide-in-right',
|
||||
})
|
||||
}
|
||||
|
||||
function handleScrollTop() {
|
||||
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
|
||||
}
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
watch(categoryConfig, (newVal) => {
|
||||
if (!newVal)
|
||||
return
|
||||
currentCategoryConfig.value = newVal
|
||||
uni.setNavigationBarTitle({ title: t('page.category.title') })
|
||||
handleInitPage()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
isLoadMore.value = false
|
||||
queryParams.value.page = 1
|
||||
handleGetData()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
return
|
||||
}
|
||||
if (hasNext.value) {
|
||||
if (calcShowType.value === 'list') {
|
||||
queryParams.value.page += 1
|
||||
isLoadMore.value = true
|
||||
handleGetData()
|
||||
}
|
||||
else {
|
||||
postQueryParams.value.page += 1
|
||||
handleGetPostByCategory(false)
|
||||
}
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mt-10 text-center text-green-500">
|
||||
分类
|
||||
<view class="app-page min-h-screen w-screen flex flex-col" :style="{ padding: calcShowType === 'list-post' ? '0' : '24rpx 0' }">
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading !== 'success'" class="loading-wrap px-3">
|
||||
<wd-skeleton :row="3" :animated="true" />
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view v-else class="app-page-content flex flex-wrap gap-y-5 px-1.5" :class="[calcShowType === 'list-post' ? 'list-post' : '']">
|
||||
<view v-if="dataList.length === 0" class="h-[70vh] flex items-center justify-center content-empty">
|
||||
<wd-empty :description="t('common.empty')" />
|
||||
</view>
|
||||
|
||||
<block v-else>
|
||||
<!-- list 视图:分类卡片网格 -->
|
||||
<block v-if="calcAuditModeEnabled || calcShowType === 'list'">
|
||||
<view
|
||||
v-for="(item, index) in dataList"
|
||||
:key="index"
|
||||
class="catgory-card box-border w-1/2 p-1"
|
||||
:style="{ backgroundImage: `url(${item.spec.cover})` }"
|
||||
>
|
||||
<view class="catgory-card-content h-[200rpx] flex flex-col items-center justify-center overflow-hidden rounded-xl shadow-sm" @click="handleToCategory(item)">
|
||||
<view class="catgory-name z-2 text-[32rpx] text-white">
|
||||
{{ item.spec.displayName }}
|
||||
</view>
|
||||
<view v-if="!calcAuditModeEnabled" class="catgory-count z-2 mt-1 text-[24rpx] text-white">
|
||||
共 {{ item.postCount }} 篇文章
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="load-text w-full py-5 text-center text-[24rpx] text-[#999]">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- list-post 视图:左侧分类 + 右侧文章 -->
|
||||
<view v-else class="list-post-wrapper min-h-screen w-screen flex">
|
||||
<scroll-view class="left-nav w-[180rpx] shrink-0 bg-white" :scroll-y="true">
|
||||
<view
|
||||
v-for="(item, index) in categoryList"
|
||||
:key="item.metadata.name"
|
||||
class="left-nav-item border-l-4 px-4 py-8 text-center text-[26rpx] text-[#606266]"
|
||||
:class="{ active: currentCategoryName === item.metadata.name }"
|
||||
@click="handleOnCategoryChange({ detail: { current: index } })"
|
||||
>
|
||||
{{ item.spec.displayName }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<scroll-view class="right-content box-border h-screen flex-1" :scroll-y="true">
|
||||
<view v-if="postList.length === 0" class="article-empty flex items-center justify-center py-10">
|
||||
<wd-empty description="该分类下暂无文章~" />
|
||||
</view>
|
||||
<block v-else>
|
||||
<uh-article-min-card
|
||||
v-for="(post, index) in postList"
|
||||
:key="index"
|
||||
:article="post"
|
||||
@on-click="handleToArticleDetail"
|
||||
/>
|
||||
<view class="load-text w-full py-5 text-center text-[24rpx] text-[#999]">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 悬浮按钮 -->
|
||||
<view class="flot-buttons fixed bottom-[100rpx] right-8 z-999 flex flex-col gap-1.5">
|
||||
<view class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleScrollTop">
|
||||
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
||||
</view>
|
||||
<view v-if="!calcAuditModeEnabled" class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleChangeShowType">
|
||||
<wd-icon :name="calcShowType === 'list' ? 'list' : 'grid'" size="20px" color="#03a9f4" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-page {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.app-page-content {
|
||||
&.list-post {
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.catgory-card {
|
||||
> view {
|
||||
position: relative;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.catgory-card-content::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgb(0 0 0 / 15%);
|
||||
backdrop-filter: blur(3rpx);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-post-wrapper {
|
||||
.left-nav {
|
||||
.left-nav-item {
|
||||
border-left-color: transparent;
|
||||
|
||||
&.active {
|
||||
color: #03a9f4;
|
||||
border-left-color: #03a9f4;
|
||||
background-color: #f5f7fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flot-buttons {
|
||||
.fab-btn {
|
||||
box-shadow: 0 4rpx 16rpx rgb(0 0 0 / 10%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,277 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 图库页(源自旧项目 pages/tabbar/gallery/gallery.vue,新建复刻)
|
||||
* 功能:相册分组切换 + 图片列表(瀑布流/网格) + 图片预览
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getPhotoGroupList, getPhotoListByGroupName } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkImageUrl } from '@/utils/url'
|
||||
import { t } from '@/locale'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import type { IPhoto } from '@/api/types/halo'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '图库',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const mockJson = computed(() => appConfigStore.mockJson)
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
|
||||
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
|
||||
|
||||
/** 依赖插件(plugin-photos) */
|
||||
const uniHaloPluginId = 'plugin-photos'
|
||||
const uniHaloPluginAvailable = ref(true)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const category = ref<{ activeIndex: number, list: { name?: string, displayName: string, priority: number }[] }>({
|
||||
activeIndex: 0,
|
||||
list: [],
|
||||
})
|
||||
const queryParams = ref({ size: 10, page: 1, group: '' })
|
||||
const isLoadMore = ref(false)
|
||||
const loadMoreText = ref('')
|
||||
const hasNext = ref(false)
|
||||
const dataList = ref<IPhoto[]>([])
|
||||
const lock = ref(false)
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetCategory() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
handleGetData(true)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
||||
category.value.list = (res.data.items || [])
|
||||
.map(item => ({
|
||||
name: item.metadata.name,
|
||||
displayName: item.spec.displayName,
|
||||
priority: item.spec.priority ?? 0,
|
||||
}))
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
category.value.list.unshift({ name: undefined, displayName: '全部', priority: 0 })
|
||||
if (category.value.list.length !== 0) {
|
||||
queryParams.value.group = category.value.list[0].name || ''
|
||||
handleGetData(true)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
loading.value = 'error'
|
||||
category.value = { activeIndex: 0, list: [] }
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetData(isClearList = false) {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
const galleryMock = mockJson.value.gallery as { list?: string[] } | undefined
|
||||
dataList.value = (galleryMock?.list || []).map(item => ({
|
||||
metadata: { name: String(Date.now() * Math.random()) },
|
||||
spec: {
|
||||
displayName: '',
|
||||
url: checkImageUrl(item),
|
||||
},
|
||||
}))
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
lock.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!isLoadMore.value) {
|
||||
loading.value = 'loading'
|
||||
}
|
||||
loadMoreText.value = ''
|
||||
|
||||
try {
|
||||
const res = await getPhotoListByGroupName({ ...queryParams.value })
|
||||
hasNext.value = res.data.hasNext
|
||||
loading.value = 'success'
|
||||
if (res.data.items.length !== 0) {
|
||||
const list = res.data.items.map(item => ({
|
||||
...item,
|
||||
spec: { ...item.spec, url: checkImageUrl(item.spec.url || item.spec.cover) },
|
||||
}))
|
||||
dataList.value = isLoadMore.value
|
||||
? dataList.value.concat(list)
|
||||
: list
|
||||
}
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
}
|
||||
finally {
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
lock.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
function handleGetDataByCategory(index: number) {
|
||||
const item = category.value.list[index]
|
||||
if (!item)
|
||||
return
|
||||
queryParams.value.group = item.name || ''
|
||||
queryParams.value.page = 1
|
||||
uni.pageScrollTo({ scrollTop: 0, duration: 500 })
|
||||
dataList.value = []
|
||||
handleGetData(true)
|
||||
}
|
||||
|
||||
function handleOnCategoryChange(e: { detail: { current: number } }) {
|
||||
if (lock.value)
|
||||
return
|
||||
handleGetDataByCategory(e.detail.current)
|
||||
}
|
||||
|
||||
/* ---------------- 图片预览 ---------------- */
|
||||
function handlePreview(data: IPhoto) {
|
||||
const current = dataList.value.findIndex(x => x.metadata.name === data.metadata.name)
|
||||
uni.previewImage({
|
||||
current,
|
||||
urls: dataList.value.map(x => x.spec.url),
|
||||
indicator: 'number',
|
||||
loop: true,
|
||||
})
|
||||
}
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
onLoad(async () => {
|
||||
// 检查插件可用性
|
||||
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
|
||||
if (!uniHaloPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
watch(galleryConfig, (newVal) => {
|
||||
if (!newVal)
|
||||
return
|
||||
uni.setNavigationBarTitle({ title: newVal.pageTitle || t('page.gallery.title') })
|
||||
handleGetCategory()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
if (!uniHaloPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
dataList.value = []
|
||||
isLoadMore.value = false
|
||||
queryParams.value.page = 1
|
||||
handleGetData(true)
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!uniHaloPluginAvailable.value)
|
||||
return
|
||||
if (calcAuditModeEnabled.value) {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
return
|
||||
}
|
||||
if (hasNext.value) {
|
||||
queryParams.value.page += 1
|
||||
isLoadMore.value = true
|
||||
handleGetData(false)
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mt-10 text-center text-green-500">
|
||||
相册图库
|
||||
<view class="app-page min-h-screen w-screen flex flex-col pb-6" style="background-color: #fafafa;">
|
||||
<uh-plugin-unavailable
|
||||
v-if="!uniHaloPluginAvailable"
|
||||
:plugin-id="uniHaloPluginId"
|
||||
error-text="检测到当前插件没有安装或者启用,无法使用图库功能哦,请联系管理员"
|
||||
@on-refresh="handleGetCategory"
|
||||
/>
|
||||
<template v-else>
|
||||
<!-- 顶部切换 -->
|
||||
<view v-if="category.list.length > 0" class="category-tabs fixed inset-x-0 top-0 z-6 bg-white">
|
||||
<wd-tabs
|
||||
v-model="category.activeIndex"
|
||||
:tabs="category.list.map(item => ({ title: item.displayName }))"
|
||||
align="left"
|
||||
@change="handleOnCategoryChange"
|
||||
/>
|
||||
</view>
|
||||
<!-- 占位区域 -->
|
||||
<view v-if="category.list.length > 0" class="h-[90rpx] w-screen" />
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading === 'loading'" class="loading-wrap box-border p-3">
|
||||
<wd-skeleton :row="4" :animated="true" />
|
||||
</view>
|
||||
|
||||
<!-- 错误态 -->
|
||||
<view v-else-if="loading === 'error'" class="error-wrap h-[60vh] w-full flex flex-col items-center justify-center gap-6">
|
||||
<wd-empty description="阿偶,获取数据失败了~" />
|
||||
<wd-button size="small" plain type="primary" @click="handleGetCategory()">
|
||||
刷新试试
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view v-else class="content box-border w-full p-3">
|
||||
<view v-if="dataList.length === 0" class="h-[70vh] w-full flex items-center justify-center content-empty">
|
||||
<wd-empty description="博主还没有分享图片~" />
|
||||
</view>
|
||||
<block v-else>
|
||||
<!-- 瀑布流(双列) -->
|
||||
<view class="waterfall flex flex-wrap gap-1.5">
|
||||
<view
|
||||
v-for="(item, index) in dataList"
|
||||
:key="index"
|
||||
class="waterfall-item h-[250rpx] w-[calc(50%-6rpx)] overflow-hidden rounded-xl"
|
||||
:class="{ 'is-even mt-3': index % 2 === 1 }"
|
||||
>
|
||||
<image
|
||||
class="waterfall-img h-full w-full"
|
||||
:src="item.spec.url"
|
||||
mode="aspectFill"
|
||||
lazy-load
|
||||
@click="handlePreview(item)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="load-text w-full py-5 text-center text-[24rpx] text-[#999]">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.error-wrap {
|
||||
.error-wrap-inner {
|
||||
/* 无额外样式 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,560 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 首页(源自旧项目 pages/tabbar/home/home.vue,新建复刻)
|
||||
* 功能:顶部栏 + 轮播 Banner + 快捷导航 + 精选分类 + 最新文章列表(分页) + 通知弹窗
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getCategoryList, getPostList } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { useSettingStore } from '@/store/setting'
|
||||
import { checkAvatarUrl, checkImageUrl, checkThumbnailUrl } from '@/utils/url'
|
||||
import { t } from '@/locale'
|
||||
import type { ICategory, IPost } from '@/api/types/halo'
|
||||
import type { IBannerItem } from '@/components/uh-swiper/uh-swiper.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '首页',
|
||||
enablePullDownRefresh: true,
|
||||
enablePullDownRefresh: true,
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const mockJson = computed(() => appConfigStore.mockJson)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const isLoadMore = ref(false)
|
||||
const loadMoreText = ref(t('common.loading'))
|
||||
const articleList = ref<IPost[]>([])
|
||||
const categoryList = ref<ICategory[]>([])
|
||||
const bannerList = ref<IBannerItem[]>([])
|
||||
const result = ref<{ hasNext: boolean }>({ hasNext: false })
|
||||
|
||||
const notify = ref({
|
||||
show: false,
|
||||
data: {} as IBannerItem,
|
||||
})
|
||||
|
||||
const queryParams = ref({
|
||||
size: 5,
|
||||
page: 1,
|
||||
sort: ['spec.pinned,desc', 'spec.publishTime,desc'],
|
||||
})
|
||||
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
const appInfo = computed(() => {
|
||||
const appInfoData = haloConfigs.value.appConfig?.appInfo as { name?: string, logo?: string } | undefined
|
||||
return {
|
||||
name: appInfoData?.name || 'uni-halo',
|
||||
logo: checkImageUrl(appInfoData?.logo),
|
||||
}
|
||||
})
|
||||
|
||||
const bloggerInfo = computed(() => {
|
||||
const blogger = haloConfigs.value.authorConfig?.blogger as { nickname?: string, avatar?: string } | undefined
|
||||
return {
|
||||
nickname: blogger?.nickname || '',
|
||||
avatar: checkAvatarUrl(blogger?.avatar),
|
||||
}
|
||||
})
|
||||
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
|
||||
const calcIsShowQuickNavigationEnabled = computed(() => !!haloConfigs.value.pageConfig?.homeConfig?.useQuickNavigation)
|
||||
|
||||
const calcIsShowCategory = computed(() => {
|
||||
if (calcAuditModeEnabled.value)
|
||||
return false
|
||||
return !!haloConfigs.value.pageConfig?.homeConfig?.useCategory
|
||||
})
|
||||
|
||||
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
|
||||
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
|
||||
|
||||
const bannerConfig = computed(() => haloConfigs.value.pageConfig?.homeConfig?.bannerConfig)
|
||||
|
||||
const globalAppSettings = computed(() => settingStore.settings)
|
||||
|
||||
/** 快捷导航列表(由配置控制显隐) */
|
||||
const navList = computed(() => {
|
||||
const loveEnabled = !!(haloConfigs.value.loveConfig as { loveEnabled?: boolean })?.loveEnabled
|
||||
const socialEnabled = !!(haloConfigs.value.authorConfig?.social as { enabled?: boolean } | undefined)?.enabled
|
||||
return [
|
||||
{
|
||||
key: 'archives',
|
||||
title: calcAuditModeEnabled.value ? '内容归档' : '文章归档',
|
||||
bgColor: 'rgba(3, 169, 244, 0.95)',
|
||||
icon: 'news',
|
||||
path: '/pages-blog/archives/archives',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
key: 'vote',
|
||||
title: '投票中心',
|
||||
bgColor: 'rgba(0, 188, 212, 0.95)',
|
||||
icon: 'box',
|
||||
path: '/pages-blog/votes/votes',
|
||||
show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value,
|
||||
},
|
||||
{
|
||||
key: 'disclaimers',
|
||||
title: '友情链接',
|
||||
bgColor: 'rgba(0, 150, 136, 0.95)',
|
||||
icon: 'link',
|
||||
path: '/pages-blog/friend-links/friend-links',
|
||||
show: calcLinksPluginEnabled.value,
|
||||
},
|
||||
{
|
||||
key: 'love',
|
||||
title: '恋爱日记',
|
||||
bgColor: 'rgba(255, 76, 103, 0.95)',
|
||||
icon: 'heart',
|
||||
path: '/pages-blog/love/love',
|
||||
show: loveEnabled,
|
||||
},
|
||||
{
|
||||
key: 'contact-blogger',
|
||||
title: '联系博主',
|
||||
bgColor: 'rgba(255, 152, 0, 0.95)',
|
||||
icon: 'message',
|
||||
path: '/pages-blog/contact/contact',
|
||||
show: socialEnabled,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleQuery() {
|
||||
handleGetBanner()
|
||||
await Promise.all([handleGetArticleList(), handleGetCategoryList()])
|
||||
}
|
||||
|
||||
/** 轮播图 */
|
||||
function handleGetBanner() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
const homeMock = mockJson.value.home as { bannerList?: { title?: string, cover?: string, time?: string }[] } | undefined
|
||||
bannerList.value = (homeMock?.bannerList || []).map(item => ({
|
||||
id: Date.now() * Math.random(),
|
||||
title: item.title,
|
||||
image: checkThumbnailUrl(item.cover),
|
||||
src: checkThumbnailUrl(item.cover),
|
||||
type: 'custom',
|
||||
content: '',
|
||||
url: '',
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
if (!bannerConfig.value?.enabled)
|
||||
return
|
||||
|
||||
if (bannerConfig.value.type === 'custom') {
|
||||
bannerList.value = (bannerConfig.value.list as { title?: string, cover?: string, content?: string, url?: string }[]).map(item => ({
|
||||
id: Date.now() * Math.random(),
|
||||
title: item.title,
|
||||
image: checkThumbnailUrl(item.cover),
|
||||
src: checkThumbnailUrl(item.cover),
|
||||
type: 'custom',
|
||||
content: item.content || '',
|
||||
url: item.url || '',
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// post 类型:取最新文章作为轮播
|
||||
const list = articleList.value.slice(0, 5).map(item => ({
|
||||
id: item.metadata.name,
|
||||
title: item.spec.title,
|
||||
image: checkThumbnailUrl(item.spec.cover),
|
||||
src: checkThumbnailUrl(item.spec.cover),
|
||||
type: 'post',
|
||||
content: item.status?.excerpt || '',
|
||||
url: '',
|
||||
}))
|
||||
bannerList.value = list
|
||||
}
|
||||
|
||||
/** 精选分类 */
|
||||
async function handleGetCategoryList() {
|
||||
if (calcAuditModeEnabled.value || !calcIsShowCategory.value) {
|
||||
loading.value = 'success'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getCategoryList({ fieldSelector: ['spec.hideFromList=false'], size: 10 })
|
||||
categoryList.value = res.data.items
|
||||
.map(item => ({ ...item, postCount: item.postCount ?? 0 }))
|
||||
.sort((a, b) => (b.postCount || 0) - (a.postCount || 0))
|
||||
loading.value = 'success'
|
||||
}
|
||||
catch (err) {
|
||||
console.error('获取分类失败', err)
|
||||
loading.value = 'error'
|
||||
}
|
||||
finally {
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
/** 文章列表 */
|
||||
async function handleGetArticleList() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
const homeMock = mockJson.value.home as { postList?: { title?: string, cover?: string, time?: string, desc?: string }[] } | undefined
|
||||
articleList.value = (homeMock?.postList || []).map(item => ({
|
||||
metadata: { name: String(Date.now() * Math.random()) },
|
||||
spec: {
|
||||
title: item.title || '',
|
||||
slug: '',
|
||||
cover: item.cover,
|
||||
pinned: false,
|
||||
publishTime: item.time,
|
||||
deleted: false,
|
||||
publish: true,
|
||||
allowComment: true,
|
||||
visible: 'PUBLIC' as const,
|
||||
priority: 0,
|
||||
categories: [],
|
||||
tags: [],
|
||||
},
|
||||
status: { permalink: '', inProgress: false, excerpt: item.desc },
|
||||
stats: { visit: 0 },
|
||||
}))
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
|
||||
if (!isLoadMore.value) {
|
||||
loading.value = 'loading'
|
||||
}
|
||||
loadMoreText.value = t('common.loading')
|
||||
|
||||
try {
|
||||
const res = await getPostList({ ...queryParams.value })
|
||||
result.value.hasNext = res.data.hasNext
|
||||
articleList.value = isLoadMore.value
|
||||
? articleList.value.concat(res.data.items)
|
||||
: res.data.items
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
// post 型轮播依赖文章列表,若启用则刷新
|
||||
if (bannerConfig.value?.enabled && bannerConfig.value.type !== 'custom') {
|
||||
handleGetBanner()
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
console.error('获取文章失败', err)
|
||||
}
|
||||
finally {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 跳转 ---------------- */
|
||||
function handleToArticleDetail(article: IPost) {
|
||||
if (calcAuditModeEnabled.value)
|
||||
return
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/article-detail/article-detail?name=${article.metadata.name}`,
|
||||
animationType: 'slide-in-right',
|
||||
})
|
||||
}
|
||||
|
||||
function handleToCategoryPage() {
|
||||
uni.switchTab({ url: '/pages/tabbar/category/category' })
|
||||
}
|
||||
|
||||
function handleToArticlesPage() {
|
||||
uni.navigateTo({ url: '/pages-blog/articles/articles' })
|
||||
}
|
||||
|
||||
function handleToCategoryBy(category: ICategory) {
|
||||
if (calcAuditModeEnabled.value)
|
||||
return
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`,
|
||||
})
|
||||
}
|
||||
|
||||
function handleToSearch() {
|
||||
uni.navigateTo({ url: '/pages-blog/articles/articles' })
|
||||
}
|
||||
|
||||
function handleOnLogoToPage() {
|
||||
uni.switchTab({ url: '/pages/tabbar/about/about' })
|
||||
}
|
||||
|
||||
function handleClickNav(item: { path: string }) {
|
||||
uni.navigateTo({ url: item.path })
|
||||
}
|
||||
|
||||
function handleToTopPage(duration = 500) {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration,
|
||||
fail: (err) => {
|
||||
console.error('回顶失败', err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleOnBannerClick(item: IBannerItem) {
|
||||
if (calcAuditModeEnabled.value)
|
||||
return
|
||||
if (item.type === 'custom') {
|
||||
if (item.content) {
|
||||
notify.value = { show: true, data: item }
|
||||
return
|
||||
}
|
||||
if (item.url) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/website/website?data=${JSON.stringify({
|
||||
title: item.title || t('common.loading'),
|
||||
url: encodeURIComponent(item.url),
|
||||
})}`,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!item.id)
|
||||
return
|
||||
handleToArticleDetail({ metadata: { name: String(item.id) } } as IPost)
|
||||
}
|
||||
|
||||
function handleOnNotifyChange(show: boolean) {
|
||||
notify.value.show = show
|
||||
}
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
onLoad(() => {
|
||||
uni.setNavigationBarTitle({ title: t('page.home.title') })
|
||||
})
|
||||
|
||||
watch(haloConfigs, () => {
|
||||
// 配置就绪后重新拉取(导航显隐依赖配置)
|
||||
}, { deep: true })
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
isLoadMore.value = false
|
||||
queryParams.value.page = 1
|
||||
handleQuery()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
return
|
||||
}
|
||||
if (result.value.hasNext) {
|
||||
queryParams.value.page += 1
|
||||
isLoadMore.value = true
|
||||
handleGetArticleList()
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
}
|
||||
})
|
||||
|
||||
// 首次加载
|
||||
handleQuery()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mt-10 text-center text-green-500">
|
||||
首页
|
||||
<view class="app-page min-h-screen w-screen flex flex-col">
|
||||
<!-- 顶部栏 -->
|
||||
<view class="header flex items-center gap-4 px-3 py-1.5">
|
||||
<image class="logo h-[60rpx] w-[60rpx] rounded-3xl" :src="appInfo.logo" mode="scaleToFill" @click="handleOnLogoToPage" />
|
||||
<view class="search-input h-[64rpx] flex flex-1 items-center rounded-3xl bg-[#f5f5f5] px-3" @click="handleToSearch">
|
||||
<view class="search-icon flex items-center">
|
||||
<wd-icon name="search" size="16px" color="#999" />
|
||||
</view>
|
||||
<text class="search-text text-grey ml-3 text-[26rpx] text-[#999]">搜索内容...</text>
|
||||
</view>
|
||||
<!-- #ifdef APP-PLUS || H5 -->
|
||||
<view class="app-name max-w-[140rpx] overflow-hidden text-ellipsis whitespace-nowrap text-[26rpx] text-[#666]">
|
||||
{{ appInfo.name }}
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading !== 'success' && articleList.length === 0" class="loading-wrap px-3">
|
||||
<wd-skeleton :row="3" :animated="true" />
|
||||
</view>
|
||||
|
||||
<block v-else>
|
||||
<!-- 轮播 Banner -->
|
||||
<view v-if="bannerConfig?.enabled" class="bg-white pb-6">
|
||||
<view v-if="bannerList.length !== 0" class="banner mx-3 mt-3 overflow-hidden rounded-xl">
|
||||
<uh-swiper
|
||||
:height="bannerConfig.height"
|
||||
:dot-position="bannerConfig.dotPosition"
|
||||
:autoplay="true"
|
||||
:use-dot="bannerConfig.showIndicator"
|
||||
:show-title="bannerConfig.showTitle"
|
||||
:type="bannerConfig.type"
|
||||
:list="bannerList"
|
||||
@on-click="handleOnBannerClick"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷导航 -->
|
||||
<view v-if="calcIsShowQuickNavigationEnabled && navList.filter(x => x.show).length" class="nav-box mx-6 mb-6 mt-4 overflow-hidden rounded-xl bg-white p-3">
|
||||
<view class="page-item-title font-bold">
|
||||
快捷导航
|
||||
</view>
|
||||
<view class="nav-list grid grid-cols-4 mt-6 gap-6">
|
||||
<template v-for="item in navList.filter(x => x.show)" :key="item.key">
|
||||
<view class="nav-item flex flex-col items-center gap-3" @click="handleClickNav(item)">
|
||||
<view class="nav-item-icon h-[88rpx] w-[88rpx] flex items-center justify-center rounded-3xl" :style="{ backgroundColor: item.bgColor }">
|
||||
<wd-icon :name="item.icon" size="24px" color="#fff" />
|
||||
</view>
|
||||
<view class="nav-item-text text-[24rpx] text-[#303133]">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 精选分类 -->
|
||||
<block v-if="calcIsShowCategory">
|
||||
<view class="mb-6 mt-6 flex items-center justify-between px-3">
|
||||
<view class="page-item-title font-bold">
|
||||
精选分类
|
||||
</view>
|
||||
<view class="show-more flex items-center justify-center rounded-xl bg-white" @click="handleToCategoryPage">
|
||||
<wd-icon name="arrow-right" size="12px" color="#909399" />
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="category mx-6 h-[200rpx] whitespace-nowrap" :scroll-x="true">
|
||||
<view v-if="categoryList.length === 0" class="cate-empty text-grey h-[180rpx] w-full flex items-center justify-center">
|
||||
还没有任何分类~
|
||||
</view>
|
||||
<view
|
||||
v-for="category in categoryList"
|
||||
v-else
|
||||
:key="category.metadata.name"
|
||||
class="category-item mr-4 inline-block"
|
||||
@click="handleToCategoryBy(category)"
|
||||
>
|
||||
<uh-category-mini-card :category="category" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</block>
|
||||
|
||||
<!-- 最新文章 -->
|
||||
<view class="mb-6 mt-6 flex items-center justify-between px-3">
|
||||
<view class="page-item-title font-bold">
|
||||
最新列表
|
||||
</view>
|
||||
<view class="show-more flex items-center justify-center rounded-xl bg-white" @click="handleToArticlesPage">
|
||||
<wd-icon name="arrow-right" size="12px" color="#909399" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="articleList.length === 0" class="article-empty py-10">
|
||||
<wd-empty description="博主还没有发表任何内容~" />
|
||||
</view>
|
||||
<block v-else>
|
||||
<view :class="globalAppSettings.layout.home">
|
||||
<uh-article-card
|
||||
v-for="(article, index) in articleList"
|
||||
:key="index"
|
||||
from="home"
|
||||
:article="article"
|
||||
@on-click="handleToArticleDetail"
|
||||
/>
|
||||
</view>
|
||||
<view class="load-text mt-3 pb-5 text-center text-[24rpx] text-[#999]">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
<view v-if="articleList.length > 10" class="to-top-btn" @click="handleToTopPage()">
|
||||
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
|
||||
<!-- 通知弹窗 -->
|
||||
<uh-notify-dialog
|
||||
v-if="notify.show"
|
||||
:show="notify.show"
|
||||
:title="notify.data.title || ''"
|
||||
:content="notify.data.content || ''"
|
||||
:url="notify.data.url || ''"
|
||||
@on-change="handleOnNotifyChange"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
.search-text {
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-item-title {
|
||||
position: relative;
|
||||
padding-left: 24rpx;
|
||||
font-size: 32rpx;
|
||||
color: #303133;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8rpx;
|
||||
width: 8rpx;
|
||||
height: 30rpx;
|
||||
background-color: rgb(33 150 243);
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.show-more {
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
box-shadow: 0 0 24rpx rgb(0 0 0 / 3%);
|
||||
}
|
||||
|
||||
.to-top-btn {
|
||||
position: fixed;
|
||||
right: 24rpx;
|
||||
bottom: 120rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 16rpx rgb(0 0 0 / 10%);
|
||||
z-index: 6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,379 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 瞬间页(源自旧项目 pages/tabbar/moments/moments.vue,新建复刻)
|
||||
* 功能:瞬间卡片列表(头像/内容/图片/音频/视频/标签) + 分页加载
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import dayjs from 'dayjs'
|
||||
import { getMomentList } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url'
|
||||
import { generateUUID } from '@/utils/uuid'
|
||||
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
||||
import { randomTagColor } from '@/utils/random'
|
||||
import { t } from '@/locale'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import { markdownConfig } from '@/config/markdown'
|
||||
import type { IMoment } from '@/api/types/halo'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '瞬间',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const mockJson = computed(() => appConfigStore.mockJson)
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
const calcUseTagRandomColor = computed(() => !!haloConfigs.value.pageConfig?.momentConfig?.useTagRandomColor)
|
||||
|
||||
const bloggerInfo = computed(() => {
|
||||
const blogger = haloConfigs.value.authorConfig?.blogger as { nickname?: string, avatar?: string } | undefined
|
||||
return {
|
||||
nickname: blogger?.nickname || '',
|
||||
avatar: checkAvatarUrl(blogger?.avatar),
|
||||
}
|
||||
})
|
||||
|
||||
const startConfig = computed(() => haloConfigs.value.appConfig?.startConfig as { title?: string } | undefined)
|
||||
|
||||
/** 依赖插件(plugin-moments) */
|
||||
const uniHaloPluginId = 'plugin-moments'
|
||||
const uniHaloPluginAvailable = ref(true)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const queryParams = ref({ size: 10, page: 1 })
|
||||
const hasNext = ref(false)
|
||||
const dataList = ref<(IMoment & { images?: { type?: string, url: string }[], videos?: { id?: string, url: string }[], audios?: { type?: string, url: string }[], spec: { newHtml?: string } })[]>([])
|
||||
const isLoadMore = ref(false)
|
||||
const loadMoreText = ref(t('common.loading'))
|
||||
const videoContexts = ref<Record<string, UniApp.VideoContext | undefined>>({})
|
||||
const currentVideoId = ref<string | null>(null)
|
||||
|
||||
/** 移除内容中的 tag 链接 */
|
||||
function removeTagLinksCompletely(htmlString: string): string {
|
||||
const regex = /<a\b[^>]+class=(['"])[^'"]*\btag\b[^'"]*\1[^>]*>[\s\S]*?<\/a>/gi
|
||||
return htmlString.replace(regex, '')
|
||||
}
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetData() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
const momentsMock = mockJson.value.moments as { list?: { content?: string, time?: string, images?: string[] }[] } | undefined
|
||||
dataList.value = (momentsMock?.list || []).map(item => ({
|
||||
metadata: { name: String(Date.now() * Math.random()) },
|
||||
spec: {
|
||||
content: item.content || '',
|
||||
owner: {
|
||||
displayName: bloggerInfo.value.nickname,
|
||||
avatar: bloggerInfo.value.avatar,
|
||||
},
|
||||
visible: 'PUBLIC',
|
||||
allowComment: true,
|
||||
approved: true,
|
||||
releaseTime: item.time,
|
||||
},
|
||||
images: (item.images || []).map(img => ({ type: 'PHOTO', url: checkThumbnailUrl(img) })),
|
||||
videos: [],
|
||||
}))
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ mask: true, title: t('common.loading') })
|
||||
if (!isLoadMore.value) {
|
||||
loading.value = 'loading'
|
||||
}
|
||||
loadMoreText.value = t('common.loading')
|
||||
|
||||
try {
|
||||
const res = await getMomentList({ ...queryParams.value })
|
||||
console.log('获取瞬间数据成功', res)
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
hasNext.value = res.data.hasNext
|
||||
|
||||
const tempItems = res.data.items
|
||||
.filter(x => x.spec.visible === 'PUBLIC')
|
||||
.map((item) => {
|
||||
const medium = (item.spec as unknown as { medium?: { type?: string, url: string }[] }).medium || []
|
||||
const newItem = {
|
||||
...item,
|
||||
spec: {
|
||||
...item.spec,
|
||||
owner: {
|
||||
displayName: bloggerInfo.value.nickname,
|
||||
avatar: bloggerInfo.value.avatar,
|
||||
},
|
||||
newHtml: removeTagLinksCompletely((item.spec as unknown as { content?: { html?: string } }).content?.html || ''),
|
||||
},
|
||||
images: medium.filter(x => x.type === 'PHOTO').map(x => ({ ...x, url: checkThumbnailUrl(x.url, true) })),
|
||||
videos: medium.filter(x => x.type === 'VIDEO').map(x => ({ ...x, id: generateUUID() })),
|
||||
audios: medium.filter(x => x.type === 'AUDIO'),
|
||||
}
|
||||
return newItem
|
||||
})
|
||||
|
||||
dataList.value = isLoadMore.value
|
||||
? dataList.value.concat(tempItems)
|
||||
: tempItems
|
||||
|
||||
nextTick(() => {
|
||||
createVideoContexts(tempItems)
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
}
|
||||
finally {
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 视频互斥 ---------------- */
|
||||
function createVideoContexts(list: { videos?: { id?: string }[] }[]) {
|
||||
stopAllVideos()
|
||||
list.map(item => item.videos || []).flat().forEach((item) => {
|
||||
if (item.id) {
|
||||
videoContexts.value[item.id] = uni.createVideoContext(`video_${item.id}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function stopAllVideos(excludesVideoId: string | null = null) {
|
||||
Object.keys(videoContexts.value).forEach((videoId) => {
|
||||
if (!excludesVideoId || excludesVideoId !== videoId) {
|
||||
videoContexts.value[videoId]?.pause()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onVideoPlay(videoId: string) {
|
||||
currentVideoId.value = videoId
|
||||
stopAllVideos(videoId)
|
||||
}
|
||||
|
||||
function onVideoPause(videoId: string) {
|
||||
if (currentVideoId.value === videoId) {
|
||||
currentVideoId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function onVideoEnded() {
|
||||
currentVideoId.value = null
|
||||
}
|
||||
|
||||
/* ---------------- 交互 ---------------- */
|
||||
function handlePreview(index: number, list: { url: string }[]) {
|
||||
uni.previewImage({
|
||||
current: index,
|
||||
urls: list.map(item => item.url),
|
||||
})
|
||||
}
|
||||
|
||||
function handleToMomentDetail(moment: IMoment) {
|
||||
if (calcAuditModeEnabled.value)
|
||||
return
|
||||
uni.navigateTo({
|
||||
url: `/pages-blog/moment-detail/moment-detail?name=${moment.metadata.name}`,
|
||||
animationType: 'slide-in-right',
|
||||
})
|
||||
}
|
||||
|
||||
function handleToTopPage(duration = 500) {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration,
|
||||
fail: (err) => {
|
||||
console.error('回顶失败', err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 格式化瞬间时间 */
|
||||
function formatMomentTime(time?: string): string {
|
||||
// 与旧项目一致:yyyy年MM月dd日 星期w
|
||||
return time ? formatTimeUtil({ d: time, f: 'yyyy年MM月dd日 星期w' }) : ''
|
||||
}
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
onLoad(async () => {
|
||||
uni.setNavigationBarTitle({ title: t('page.moments.title') })
|
||||
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
|
||||
if (!uniHaloPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
handleGetData()
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
if (!uniHaloPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
isLoadMore.value = false
|
||||
queryParams.value.page = 1
|
||||
videoContexts.value = {}
|
||||
currentVideoId.value = null
|
||||
handleGetData()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!uniHaloPluginAvailable.value)
|
||||
return
|
||||
if (calcAuditModeEnabled.value) {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
return
|
||||
}
|
||||
if (hasNext.value) {
|
||||
queryParams.value.page += 1
|
||||
isLoadMore.value = true
|
||||
handleGetData()
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: t('common.noMoreData') })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="mt-10 text-center text-green-500">
|
||||
瞬间
|
||||
<view class="app-page box-border min-h-screen w-screen flex flex-col py-6">
|
||||
<uh-plugin-unavailable
|
||||
v-if="!uniHaloPluginAvailable"
|
||||
:plugin-id="uniHaloPluginId"
|
||||
error-text="检测到当前插件没有安装或者启用,无法使用瞬间功能哦,请联系管理员"
|
||||
@on-refresh="handleGetData"
|
||||
/>
|
||||
<template v-else>
|
||||
<view v-if="loading !== 'success'" class="loading-wrap p-3">
|
||||
<wd-skeleton :row="3" :animated="true" />
|
||||
</view>
|
||||
|
||||
<view v-else class="app-page-content">
|
||||
<view v-if="dataList.length === 0" class="min-h-[70vh] w-full flex items-center justify-center content-empty">
|
||||
<wd-empty :description="t('common.empty')" />
|
||||
</view>
|
||||
|
||||
<block v-else>
|
||||
<!-- 瞬间卡片 -->
|
||||
<view v-for="moment in dataList" :key="moment.metadata.name" class="moment-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white shadow-sm">
|
||||
<view class="head flex items-center p-3 pb-0">
|
||||
<image class="avatar h-[66rpx] w-[66rpx] shrink-0 rounded-full" :src="moment.spec.owner?.avatar || bloggerInfo.avatar" mode="aspectFill" />
|
||||
<view class="nickname ml-3">
|
||||
<view class="nickname-text text-[30rpx] text-[#333] font-bold">
|
||||
{{ moment.spec.owner?.displayName || bloggerInfo.nickname }}
|
||||
</view>
|
||||
<view class="release-time mt-1 text-[24rpx] text-[#666]">
|
||||
{{ formatMomentTime(moment.spec.releaseTime) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="moment-content px-3 py-2" @click.stop="handleToMomentDetail(moment)">
|
||||
<mp-html
|
||||
class="evan-markdown"
|
||||
lazy-load
|
||||
:domain="markdownConfig.domain ?? ''"
|
||||
:loading-img="markdownConfig.loadingGif"
|
||||
scroll-table
|
||||
selectable
|
||||
:tag-style="markdownConfig.tagStyle"
|
||||
:container-style="markdownConfig.containStyle"
|
||||
:content="moment.spec.newHtml || ''"
|
||||
:markdown="true"
|
||||
:show-line-number="true"
|
||||
:show-language-name="true"
|
||||
copy-by-long-press
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 图片 -->
|
||||
<view v-if="moment.images && moment.images.length !== 0" class="images flex flex-wrap items-start px-3 pb-6" :class="`images-${moment.images.length}`">
|
||||
<view v-for="(image, mediumIndex) in moment.images" :key="mediumIndex" class="image-item box-border p-1" :class="moment.images && moment.images.length === 1 ? 'h-[350rpx] w-full' : (moment.images && moment.images.length === 2 ? 'h-[250rpx] w-1/2' : 'h-[200rpx] w-1/3')">
|
||||
<image
|
||||
mode="aspectFill"
|
||||
class="image-src h-full w-full rounded-lg"
|
||||
:src="image.url"
|
||||
@click="handlePreview(mediumIndex, moment.images || [])"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 音频 -->
|
||||
<view v-if="moment.audios && moment.audios.length !== 0" class="audio-list mb-3 flex flex-col gap-3 px-3">
|
||||
<uh-audio-player
|
||||
v-for="audio in moment.audios"
|
||||
:key="audio.url"
|
||||
:src="audio.url"
|
||||
:poster="bloggerInfo.avatar"
|
||||
:name="`来自${startConfig?.title || bloggerInfo.nickname}的声音`"
|
||||
:author="bloggerInfo.nickname"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 视频 -->
|
||||
<view v-if="moment.videos && moment.videos.length !== 0" class="video-list mb-3 flex flex-col gap-3 px-3">
|
||||
<video
|
||||
v-for="(video, index) in moment.videos"
|
||||
:id="`video_${video.id}`"
|
||||
:key="index"
|
||||
class="video-src h-[400rpx] w-full rounded-xl"
|
||||
:src="video.url"
|
||||
:show-mute-btn="true"
|
||||
:controls="true"
|
||||
:show-center-play-btn="true"
|
||||
:enable-progress-gesture="true"
|
||||
@play="onVideoPlay(video.id || '')"
|
||||
@pause="onVideoPause(video.id || '')"
|
||||
@ended="onVideoEnded"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 标签 -->
|
||||
<view v-if="moment.spec.tags && moment.spec.tags.length !== 0" class="tags flex flex-wrap gap-4 px-3 pb-6">
|
||||
<view v-for="(tag, tagIndex) in moment.spec.tags" :key="tagIndex" class="tag text-[24rpx]" :style="{ color: randomTagColor() }">
|
||||
{{ tag }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="to-top-btn fixed bottom-[120rpx] right-6 z-6 h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToTopPage()">
|
||||
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
||||
</view>
|
||||
<view class="load-text pb-5 text-center text-[24rpx] text-[#999]">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-page {
|
||||
/* 布局全部由 UnoCSS 原子类实现 */
|
||||
}
|
||||
|
||||
.moment-card {
|
||||
.head {
|
||||
.nickname {
|
||||
.nickname-text {
|
||||
/* 无额外样式 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user