mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-12 16:40: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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user