1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00

refactor: 整体项目样式与组件优化升级

主要变更:
1. 新增uhemoji2图标字体库并全局引入
2. 重构导航组件与页面布局,统一使用自定义导航栏
3. 替换旧图标为emoji图标,统一视觉风格
4. 清理冗余调试代码与注释
5. 优化快捷导航、关于页等页面样式与交互
6. 修复滚动到顶部组件黑名单配置
This commit is contained in:
小莫唐尼
2026-09-04 18:54:34 +08:00
parent 62b604644c
commit 56ce29e69e
15 changed files with 1534 additions and 1440 deletions
+81 -3
View File
@@ -295,7 +295,85 @@ onLoad(() => {
- 页面根节点用 `app-page` 类 + 主题底色:`<view class="app-page min-h-screen w-screen flex flex-col bg-page">` - 页面根节点用 `app-page` 类 + 主题底色:`<view class="app-page min-h-screen w-screen flex flex-col bg-page">`
- 页面标题 `navigationBarTitleText` 写中文;下拉刷新 `enablePullDownRefresh: true` - 页面标题 `navigationBarTitleText` 写中文;下拉刷新 `enablePullDownRefresh: true`
### 5.4 页面数据请求(统一 updateLoadingStatus + uh-data-loading 组件) ### 5.4 页面自定义导航(uh-navbar 组件)
**用途**:子页面(非 tabbar 页,如文章详情、设置、投票详情等)的顶部自定义导航栏。
配合页面 `navigationStyle: 'custom'` 使用,**新页面一律使用它,不要用默认导航栏**。
**前提(definePage 里必须声明)**
```ts
definePage({
style: {
navigationStyle: 'custom', // 关闭系统默认导航栏,让位给 uh-navbar
},
})
```
**Props 一览**
| Prop | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `use-back` | `boolean` | `true` | 是否显示左侧返回按钮 |
| `use-title` | `boolean` | `true` | 是否显示标题 |
| `default-title` | `string` | - | 默认标题(顶部文案) |
| `title-color` | `string` | - | 标题颜色类名(如 `text-gray-900`),**带此属性时固定颜色、不随滚动变色** |
| `scroll-title` | `string` | - | 滚动后标题(配合 default-title:顶部显示默认标题,滚过 50% 换 scroll-title |
| `need-placeholder` | `boolean` | `true` | 是否生成占位(为页面内容让出导航高度) |
**页面结构模板**(参考 `src/pages-blog/test/test.vue`):
```vue
<script lang="ts" setup>
definePage({
style: {
navigationBarTitleText: '测试页面',
navigationStyle: 'custom', // 使用 uh-navbar 必须声明
},
})
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
</script>
<template>
<!-- 最外层仅作容器(bg-page 底色),不设 padding -->
<view class="w-full min-h-screen bg-page">
<!-- 导航栏:置于页面最顶部,内置占位/安全区处理 -->
<uh-navbar default-title="测试页面" :need-placeholder="true" title-color="text-gray-900" />
<!-- 内容区:从这开始写,因为 uh-navbar 已内置占位(need-placeholder=true) -->
<view class="box-border px-3">
<uh-data-loading v-if="loadingStatus !== DataLoadingStatusEnum.Success" :loading-status="loadingStatus" min-height="80vh" />
<view v-else>
请求成功啦
</view>
</view>
</view>
</template>
```
**使用要点**
- **`need-placeholder` 必须按场景传对**
- 页面内容直接从导航下开始(普通子页面)→ `:need-placeholder="true"`(默认即可,如 test.vue / setting.vue
- 页面顶部有全屏封面/背景图,内容要盖到导航下面 → `:need-placeholder="false"`(如 article-detail.vue,封面 `pt-72` 上移)
- **`title-color` 与 `scroll-title` 二选一**
- 页面底色非深色 → 传 `title-color="text-gray-900"`(固定深色,如 test.vue / setting.vue
- 不传时标题会随滚动变白→深灰(顶部透明、滚后加深),适合顶部是深色大图的场景(如 article-detail.vue 只传 `default-title` + `scroll-title`,让滚动变色)
- 中间标题可用**默认插槽覆盖**(不传则显示 `default-title`/`scroll-title`),右侧扩展用 **`#right` 插槽**
```vue
<uh-navbar default-title="偏好设置" :need-placeholder="true">
<template #right>
<view @click="handleSave">保存</view>
</template>
</uh-navbar>
```
- 返回按钮内置(`uni.navigateBack`),无需自写
- easycom 已配置 `uh-` 前缀,直接用 `<uh-navbar />`**无需 import**
### 5.5 页面数据请求(统一 updateLoadingStatus + uh-data-loading 组件)
**数据加载四态**`loading / error / empty / success`,统一用 **数据加载四态**`loading / error / empty / success`,统一用
`useDataLoadingStatus``src/hooks/useDataLoadingStatus.ts`)的 `useDataLoadingStatus``src/hooks/useDataLoadingStatus.ts`)的
@@ -419,7 +497,7 @@ onReachBottom(() => {
列表页/四态展示优先 `updateLoadingStatus` 写法 列表页/四态展示优先 `updateLoadingStatus` 写法
- 简单场景也可用 `useRequest(fn, { immediate })`:返回 `{ loading, error, data, run }` - 简单场景也可用 `useRequest(fn, { immediate })`:返回 `{ loading, error, data, run }`
### 5.5 列表页(分页加载) ### 5.6 列表页(分页加载)
列表页可用 z-pagingeasycom 已配置 `<z-paging>` 直接用)或手写分页。 列表页可用 z-pagingeasycom 已配置 `<z-paging>` 直接用)或手写分页。
手写分页的既有模式(参考 `src/pages/tabbar/home/home.vue`、`pages-blog/votes/votes.vue`): 手写分页的既有模式(参考 `src/pages/tabbar/home/home.vue`、`pages-blog/votes/votes.vue`):
@@ -457,7 +535,7 @@ onReachBottom(() => {
}) })
``` ```
### 5.6 依赖插件的页面(插件可用性 + 维护拦截) ### 5.7 依赖插件的页面(插件可用性 + 维护拦截)
Halo 是插件化 CMS,页面可能依赖插件(投票 plugin-vote、瞬间 PluginMoments 等): Halo 是插件化 CMS,页面可能依赖插件(投票 plugin-vote、瞬间 PluginMoments 等):
@@ -1,100 +1,79 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { getNotices } from '@/api/uni-halo' import { getNotices } from '@/api/uni-halo'
import type { INoticeListVo } from '@/api/types/uni-halo' import type { INoticeListVo } from '@/api/types/uni-halo'
/** 轮播展示条数上限(垂直循环) */ /** 轮播展示条数上限(垂直循环) */
const MAX_SHOW = 6 const MAX_SHOW = 6
const list = ref<INoticeListVo[]>([]) const list = ref<INoticeListVo[]>([])
const showList = computed(() => list.value.slice(0, MAX_SHOW)) const showList = computed(() => list.value.slice(0, MAX_SHOW))
async function fetchNotices() { async function fetchNotices() {
try { try {
const res = await getNotices({ page: 1, size: MAX_SHOW }) const res = await getNotices({ page: 1, size: MAX_SHOW })
list.value = res.data?.items || [] list.value = res.data?.items || []
} }
catch (err) { catch (err) {
console.error('首页公告获取失败', err) console.error('首页公告获取失败', err)
list.value = [] list.value = []
} }
} }
/** 点击单条公告标题 → 详情页 */ /** 点击单条公告标题 → 详情页 */
function handleTap(item: INoticeListVo) { function handleTap(item : INoticeListVo) {
if (!item.name) if (!item.name) { return }
return uni.navigateTo({ url: `/pages-blog/notice/detail?name=${item.name}` })
uni.navigateTo({ url: `/pages-blog/notice/detail?name=${item.name}` }) }
}
/** 点击「公告/更多」→ 公告列表页 */ /** 点击「公告/更多」→ 公告列表页 */
function handleGoList() { function handleGoList() {
uni.navigateTo({ url: '/pages-blog/notice/notice' }) uni.navigateTo({ url: '/pages-blog/notice/notice' })
} }
onMounted(() => { onMounted(() => {
fetchNotices() fetchNotices()
}) })
</script> </script>
<template> <template>
<view <view v-if="showList.length > 0"
v-if="showList.length > 0" class="uh-global-card-glass box-border mx-3 mt-3 mb-2 flex items-center rounded-xl px-3">
class="uh-global-card-glass box-border mx-3 mt-3 mb-2 flex items-center rounded-xl px-3" <!-- 左侧公告入口 -->
> <view class="flex shrink-0 items-center gap-2 py-2 pr-3" @click="handleGoList">
<!-- 左侧公告入口 --> <wd-icon class-prefix="uhemoji2-icon" name="-happy-" size="36rpx" />
<view class="flex shrink-0 items-center gap-1 py-2 pr-3" @click="handleGoList"> <text class="text-sm font-bold text-red-400">
<text class="text-sm"> 公告
📢 </text>
</text> </view>
<text class="text-xs font-bold text-red-400">
公告
</text>
</view>
<!-- 标题轮播(仅一条时静态展示) --> <!-- 标题轮播(仅一条时静态展示) -->
<view class="h-[60rpx] min-w-0 flex-1 overflow-hidden"> <view class="h-[60rpx] min-w-0 flex-1 overflow-hidden">
<swiper <swiper v-if="showList.length > 1" class="h-full w-full" vertical circular autoplay :interval="3500"
v-if="showList.length > 1" :duration="400">
class="h-full w-full" <swiper-item v-for="(item, index) in showList" :key="item.name || index" class="h-full w-full">
vertical <view class="flex h-full w-full items-center truncate text-xs text-gray-500"
circular @click="handleTap(item)">
autoplay {{ item.title }}
:interval="3500" </view>
:duration="400" </swiper-item>
> </swiper>
<swiper-item <view v-else class="flex h-full w-full items-center truncate text-[24rpx] text-[#555]"
v-for="(item, index) in showList" @click="handleTap(showList[0])">
:key="item.name || index" {{ showList[0]?.title }}
class="h-full w-full" </view>
> </view>
<view
class="flex h-full w-full items-center truncate text-xs text-gray-500"
@click="handleTap(item)"
>
{{ item.title }}
</view>
</swiper-item>
</swiper>
<view
v-else
class="flex h-full w-full items-center truncate text-[24rpx] text-[#555]"
@click="handleTap(showList[0])"
>
{{ showList[0]?.title }}
</view>
</view>
<!-- 右侧更多入口 --> <!-- 右侧更多入口 -->
<view class="flex shrink-0 items-center gap-0.5 py-2 pl-2" @click="handleGoList"> <view class="flex shrink-0 items-center gap-0.5 py-2 pl-2" @click="handleGoList">
<text class="text-[22rpx] text-[#bbb]"> <text class="text-[22rpx] text-[#bbb]">
更多 更多
</text> </text>
<wd-icon name="arrow-right" size="12px" color="#bbb" /> <wd-icon name="arrow-right" size="12px" color="#bbb" />
</view> </view>
</view> </view>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
/* 布局由 UnoCSS 原子类实现 */ /* 布局由 UnoCSS 原子类实现 */
</style> </style>
@@ -14,20 +14,27 @@
const navList = computed(() => { const navList = computed(() => {
const loveEnabled = !!(haloConfigs.value.loveConfig as { loveEnabled ?: boolean })?.loveEnabled const loveEnabled = !!(haloConfigs.value.loveConfig as { loveEnabled ?: boolean })?.loveEnabled
const socialEnabled = !!(haloConfigs.value.authorConfig?.social as { enabled ?: boolean } | undefined)?.enabled const socialEnabled = !!(haloConfigs.value.authorConfig?.social as { enabled ?: boolean } | undefined)?.enabled
// <wd-icon class-prefix="uhemoji-icon" name="-smile-" size="32rpx" />
return [ return [
{ {
key: 'archives', key: 'archives',
title: calcAuditModeEnabled.value ? '内容归档' : '文章归档', title: calcAuditModeEnabled.value ? '内容归档' : '文章归档',
bgColor: 'rgba(3, 169, 244, 0.95)', color: '#03A9F4',
icon: 'news', bgGlass: 'rgba(3, 169, 244, 0.14)',
borderColor: 'rgba(3, 169, 244, 0.35)',
iconPrefix: 'uhemoji2-icon',
icon: '-mask',
path: '/pages-blog/archives/archives', path: '/pages-blog/archives/archives',
show: true, show: true,
}, },
{ {
key: 'vote', key: 'vote',
title: '投票中心', title: '投票中心',
bgColor: 'rgba(0, 188, 212, 0.95)', color: '#00BCD4',
icon: 'box', bgGlass: 'rgba(0, 188, 212, 0.14)',
borderColor: 'rgba(0, 188, 212, 0.35)',
iconPrefix: 'uhemoji2-icon',
icon: '-confused',
path: '/pages-blog/votes/votes', path: '/pages-blog/votes/votes',
// show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value, // show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value,
show: true, show: true,
@@ -35,8 +42,11 @@
{ {
key: 'disclaimers', key: 'disclaimers',
title: '友情链接', title: '友情链接',
bgColor: 'rgba(0, 150, 136, 0.95)', color: '#009688',
icon: 'link', bgGlass: 'rgba(0, 150, 136, 0.14)',
borderColor: 'rgba(0, 150, 136, 0.35)',
iconPrefix: 'uhemoji2-icon',
icon: '-wink',
path: '/pages-blog/friend-links/friend-links', path: '/pages-blog/friend-links/friend-links',
// show: calcLinksPluginEnabled.value, // show: calcLinksPluginEnabled.value,
show: true, show: true,
@@ -44,8 +54,11 @@
{ {
key: 'love', key: 'love',
title: '恋爱日记', title: '恋爱日记',
bgColor: 'rgba(255, 76, 103, 0.95)', color: '#FF4C67',
icon: 'heart', bgGlass: 'rgba(255, 76, 103, 0.14)',
borderColor: 'rgba(255, 76, 103, 0.075)',
iconPrefix: 'uhemoji2-icon',
icon: '-in-love',
path: '/pages-blog/love/love', path: '/pages-blog/love/love',
// show: loveEnabled, // show: loveEnabled,
show: true, show: true,
@@ -53,8 +66,11 @@
{ {
key: 'contact-blogger', key: 'contact-blogger',
title: '联系博主', title: '联系博主',
bgColor: 'rgba(255, 152, 0, 0.95)', color: '#FF9800',
icon: 'message', bgGlass: 'rgba(255, 152, 0, 0.14)',
borderColor: 'rgba(255, 152, 0, 0.35)',
iconPrefix: 'uhemoji2-icon',
icon: '-cool',
path: '/pages-blog/contact/contact', path: '/pages-blog/contact/contact',
show: socialEnabled, show: socialEnabled,
}, },
@@ -68,21 +84,37 @@
</script> </script>
<template> <template>
<view v-if="navList.length" class="overflow-hidden rounded-xl p-3 px-4 mb-3"> <view v-if="navList.length" class="box-border overflow-hidden rounded-xl p-3 px-4 mb-3">
<uh-section-title class="mb-4"> <uh-section-title class="mb-4">
快捷导航 快捷导航
</uh-section-title> </uh-section-title>
<view class="grid grid-cols-5 gap-4"> <view class="grid grid-cols-5 gap-4">
<view v-for="item in navList" :key="item.key" class="flex flex-col items-center gap-2" <view v-for="item in navList" :key="item.key" class="flex flex-col items-center gap-2"
@click="handleClickNav(item)"> @click="handleClickNav(item)">
<view class="uh-global-card-glass border h-12 w-12 flex items-center justify-center rounded-2xl" <view
:style="{ backgroundColor: item.bgColor }"> class="uh-global-card-glass uh-shadow-xs h-13 w-13 flex items-center justify-center rounded-2xl border transition-transform active:scale-90"
<wd-icon :name="item.icon" size="24px" color="#fff" /> :style="{
backgroundColor: item.bgGlass
}">
<wd-icon :class-prefix="item.iconPrefix" :name="item.icon" size="64rpx" :color="item.color" />
</view> </view>
<view class="text-xs text-gray-900 font-bold"> <view class="text-xs text-gray-900">
{{ item.title }} {{ item.title }}
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<style scoped lang="scss">
/* 彩色玻璃图标:低透明度同色底 + 同色描边 + 柔和同色投影,图标用实体色保证通透感 */
.quick-nav-icon {
backdrop-filter: blur(16rpx) saturate(160%);
-webkit-backdrop-filter: blur(16rpx) saturate(160%);
/* 低端安卓 WebView 不支持 backdrop-filter 的兜底:提高底色不透明度保证可读性 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
background-color: rgb(255 255 255 / 60%) !important;
}
}
</style>
+31 -19
View File
@@ -6,12 +6,15 @@
useBack : boolean; useBack : boolean;
useTitle : boolean; useTitle : boolean;
defaultTitle ?: string; defaultTitle ?: string;
titleColor ?: string;
scrollTitle ?: string; scrollTitle ?: string;
needPlaceholder ?: boolean;
} }
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
useBack: true, useBack: true,
useTitle: true, useTitle: true,
needPlaceholder: true,
}) })
const slots = useSlots() const slots = useSlots()
@@ -31,6 +34,10 @@
const customCalss = computed(() => { const customCalss = computed(() => {
const _class = [] const _class = []
if (props.titleColor) {
_class.push(props.titleColor)
return
}
if (scrollThreshold.value) { if (scrollThreshold.value) {
_class.push('text-white') _class.push('text-white')
} }
@@ -49,9 +56,9 @@
} }
return props.scrollTitle; return props.scrollTitle;
}) })
// todo:注意:如果是从分享进来的,我们需要处理为返回 home页面 // todo:注意:如果是从分享进来的,我们需要处理为返回 home页面
function handleBack(){ function handleBack() {
uni.navigateBack({ delta: 1 }) uni.navigateBack({ delta: 1 })
} }
@@ -61,25 +68,30 @@
</script> </script>
<template> <template>
<view class="box-border pt-safe w-full fixed left-0 top-0 z-50" :class="customCalss" :style="[customStyle]"> <view class="w-full box-border">
<view class="w-full h-[46px] flex items-center gap-x-4 box-border px-3 backdrop-blur-[2rpx]"> <view class="box-border pt-safe w-full fixed left-0 top-0 z-50" :class="customCalss" :style="[customStyle]">
<!-- 左边 --> <view class="w-full h-[46px] flex items-center gap-x-4 box-border px-3 backdrop-blur-[2rpx]">
<view class="shrink-0" @click="handleBack()"> <!-- 左边 -->
<view <view class="shrink-0" @click="handleBack()">
class="uh-global-card-glass h-7 px-3 rounded-full border flex items-center gap-x-2 text-gray-900 text-sm"> <view
<wd-icon name="arrow-left" size="32rpx"></wd-icon> class="uh-global-card-glass h-7 px-3 rounded-full border flex items-center gap-x-2 text-gray-900 text-sm">
<view class="w-[1px] h-4 bg-white/60" /> <wd-icon name="arrow-left" size="32rpx"></wd-icon>
<text class="text-xs font-bold">返回</text> <view class="w-[1px] h-4 bg-white/60" />
<text class="text-xs font-bold">返回</text>
</view>
</view>
<!-- 中间 -->
<view class="flex-1 truncate text-center font-bold transition-colors duration-300">
<slot> {{visibleTitle}} </slot>
</view>
<!-- 右边 -->
<view class="shrink-0 min-w-18">
<slot name="right"></slot>
</view> </view>
</view> </view>
<!-- 中间 --> </view>
<view class="flex-1 truncate text-center font-bold transition-colors duration-300"> <view v-if="props.needPlaceholder" class="box-border w-full pt-safe">
<slot> {{visibleTitle}} </slot> <view class="w-full h-[46px]"></view>
</view>
<!-- 右边 -->
<view class="shrink-0 min-w-18">
<slot name="right"></slot>
</view>
</view> </view>
</view> </view>
</template> </template>
@@ -81,9 +81,7 @@
<!-- 头部:标题 + 关闭 --> <!-- 头部:标题 + 关闭 -->
<view class="flex items-center justify-between"> <view class="flex items-center justify-between">
<view class="flex items-center gap-2"> <view class="flex items-center gap-2">
<text class="text-sm"> <wd-icon class-prefix="uhemoji2-icon" name="-happy-" size="36rpx" />
📢
</text>
<text class="text-md font-bold text-gray-900"> <text class="text-md font-bold text-gray-900">
最新公告 最新公告
</text> </text>
@@ -101,7 +99,8 @@
<!-- 内容 --> <!-- 内容 -->
<view class="mt-4"> <view class="mt-4">
<image v-if="notice.cover" :src="checkImageUrl(notice.cover)" class="w-full h-34 rounded-lg mb-2"></image> <image v-if="notice.cover" :src="checkImageUrl(notice.cover)" class="w-full h-34 rounded-lg mb-2">
</image>
<view class="text-md font-bold leading-snug text-gray-900"> <view class="text-md font-bold leading-snug text-gray-900">
{{ notice.title }} {{ notice.title }}
</view> </view>
@@ -1,8 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
/**
* 插件不可用提示(源自旧项目 components/plugin-unavailable,新建复刻)
* 当依赖的 Halo 插件未安装/未启用时展示:插件 logo、名称、错误标签、描述、插件地址、复制/反馈按钮
*/
import { computed } from 'vue' import { computed } from 'vue'
import { NeedPlugins } from '@/hooks/usePluginAvailable' import { NeedPlugins } from '@/hooks/usePluginAvailable'
@@ -45,18 +41,7 @@
...defaultStyle, ...defaultStyle,
...props.customStyle, ...props.customStyle,
})) }))
function copy() {
if (!pluginInfo.value.url)
return
uni.setClipboardData({
data: pluginInfo.value.url,
showToast: false,
success: () => {
uni.showToast({ icon: 'none', title: '插件地址已复制' })
},
})
}
</script> </script>
<template> <template>
@@ -13,7 +13,7 @@
} }
// 获取当前页面,并且设置黑名单模式,因为有的页面可能不需要滚动到顶部 // 获取当前页面,并且设置黑名单模式,因为有的页面可能不需要滚动到顶部
const balckList = ['pages/maintenance/maintenance'] const balckList = ['pages/maintenance/maintenance','pages-blog/setting/setting']
const pages = getCurrentPages() const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] const currentPage = pages[pages.length - 1]
const visible = computed(() => { const visible = computed(() => {
File diff suppressed because it is too large Load Diff
+4 -7
View File
@@ -1,10 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
/**
* 收藏页(纯本地,文章 × 瞬间)
* 功能:双 Tab(仿图库顶部吸顶胶囊 chip)本地收藏列表 + 跳详情 + 删除 + 状态舞台
* 数据:useFavoritesStore(persist),快照自包含,无需网络加载
* 状态:useDataLoadingStatus + uh-data-loading(与 tabbar 页同构);本地同步数据,状态直接推导
*/
import { computed, ref, watchEffect } from 'vue' import { computed, ref, watchEffect } from 'vue'
import { formatTime } from '@/utils/formatTime' import { formatTime } from '@/utils/formatTime'
import { useFavoritesStore } from '@/store/favorites' import { useFavoritesStore } from '@/store/favorites'
@@ -14,7 +8,7 @@ import type { FavoriteKind, IFavoriteItem } from '@/utils/favorite'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '我的收藏', navigationBarTitleText: '我的收藏',
backgroundColor: '#f6f3ee', navigationStyle: 'custom',
}, },
}) })
@@ -80,6 +74,9 @@ const emptyText = computed(() => (activeKind.value === 'post' ? '还没有收藏
<template> <template>
<view class="box-border min-h-screen w-screen bg-page pb-10"> <view class="box-border min-h-screen w-screen bg-page pb-10">
<!-- 自定义导航 -->
<uh-navbar default-title="我的收藏" title-color="text-gray-900"></uh-navbar>
<!-- 顶部类型 Tab(与图库页同款:吸顶玻璃胶囊 chip) --> <!-- 顶部类型 Tab(与图库页同款:吸顶玻璃胶囊 chip) -->
<wd-sticky> <wd-sticky>
<scroll-view scroll-x class="w-full whitespace-nowrap"> <scroll-view scroll-x class="w-full whitespace-nowrap">
+283 -323
View File
@@ -1,349 +1,309 @@
<script lang="ts" setup> <script lang="ts" setup>
/** import { ref } from 'vue'
* 偏好设置页(两层:站点默认 L0 + 本地差异 L1-L,设计见 .docs/config-system-v2-redesign §3-4) import { onLoad } from '@dcloudio/uni-app'
* - 每项可「跟随站点默认」(差异为空)或覆盖为具体值;改动即写本地差异(uh_pref_local_v1)即时生效; import { DefaultAppSettings } from '@/config/appSettings'
* - 底部「重置为站点默认」= 清空全部本地差异,回退站长在插件后台配置的默认(未配置则为内置默认)。 import { useAppConfigStore } from '@/store/appConfig'
* - 风格:对齐全站设计语言(bg-page + uh-global-card-glass + uh-section-title) import { useSettingStore } from '@/store/setting'
*/ import { collectSiteDefaults, isLocalOverride, readLocalPrefs } from '@/utils/preference'
import { ref } from 'vue' import type { LocalPrefs } from '@/utils/preference'
import { onLoad } from '@dcloudio/uni-app'
import { DefaultAppSettings } from '@/config/appSettings'
import { useAppConfigStore } from '@/store/appConfig'
import { useSettingStore } from '@/store/setting'
import { collectSiteDefaults, isLocalOverride, readLocalPrefs } from '@/utils/preference'
import type { LocalPrefs } from '@/utils/preference'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '偏好设置', navigationBarTitleText: '偏好设置',
}, navigationStyle: 'custom'
}) },
})
const settingStore = useSettingStore() const settingStore = useSettingStore()
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore()
/** 确保启动合并已执行(入口页未跑或 H5 直达时兜底) */ /** 确保启动合并已执行(入口页未跑或 H5 直达时兜底) */
onLoad(() => { onLoad(() => {
if (!settingStore.siteDefaults) { if (!settingStore.siteDefaults) {
settingStore.applySiteDefaults(collectSiteDefaults(appConfigStore.configs)) settingStore.applySiteDefaults(collectSiteDefaults(appConfigStore.configs))
} }
uni.setNavigationBarTitle({ title: '偏好设置' }) uni.setNavigationBarTitle({ title: '偏好设置' })
}) })
/* ---------------- 路径取值工具 ---------------- */ /* ---------------- 路径取值工具 ---------------- */
type Path = string[] type Path = string[]
function getByPath(obj: unknown, path: Path): unknown { function getByPath(obj : unknown, path : Path) : unknown {
let cursor: unknown = obj let cursor : unknown = obj
for (const key of path) { for (const key of path) {
if (cursor === null || cursor === undefined) if (cursor === null || cursor === undefined)
return undefined return undefined
cursor = (cursor as Record<string, unknown>)[key] cursor = (cursor as Record<string, unknown>)[key]
} }
return cursor return cursor
} }
/** 按路径构造差异 patch(null 表示删除该键=跟随站点默认) */ /** 按路径构造差异 patch(null 表示删除该键=跟随站点默认) */
function buildPatch(path: Path, value: unknown): LocalPrefs { function buildPatch(path : Path, value : unknown) : LocalPrefs {
const [head, ...rest] = path const [head, ...rest] = path
if (rest.length === 0) if (rest.length === 0)
return { [head]: value } as LocalPrefs return { [head]: value } as LocalPrefs
return { [head]: buildPatch(rest, value) } as LocalPrefs return { [head]: buildPatch(rest, value) } as LocalPrefs
} }
/** 偏好字段定义(现页已有项;弹幕已下线、友链分组二期再开) */ /** 偏好字段定义(现页已有项;弹幕已下线、友链分组二期再开) */
interface PrefDef { interface PrefDef {
key: string key : string
label: string label : string
kind: 'bool' | 'enum' kind : 'bool' | 'enum'
path: Path path : Path
options?: { label: string, value: string }[] options ?: { label : string, value : string }[]
siteLabelOf?: (value: string) => string siteLabelOf ?: (value : string) => string
} }
const layoutPrefs: PrefDef[] = [ const layoutPrefs : PrefDef[] = [
{ {
key: 'home', key: 'home',
label: '首页文章布局', label: '首页文章布局',
kind: 'enum', kind: 'enum',
path: ['layout', 'home'], path: ['layout', 'home'],
options: [ options: [
{ label: '一行一列', value: 'h_row_col1' }, { label: '一行一列', value: 'h_row_col1' },
{ label: '一行两列', value: 'h_row_col2' }, { label: '一行两列', value: 'h_row_col2' },
], ],
}, },
{ {
key: 'cardType', key: 'cardType',
label: '文章卡片样式', label: '文章卡片样式',
kind: 'enum', kind: 'enum',
path: ['layout', 'cardType'], path: ['layout', 'cardType'],
options: [ options: [
{ label: '左图右文', value: 'lr_image_text' }, { label: '左图右文', value: 'lr_image_text' },
{ label: '左文右图', value: 'lr_text_image' }, { label: '左文右图', value: 'lr_text_image' },
{ label: '上图下文', value: 'tb_image_text' }, { label: '上图下文', value: 'tb_image_text' },
{ label: '上文下图', value: 'tb_text_image' }, { label: '上文下图', value: 'tb_text_image' },
{ label: '只有文字', value: 'only_text' }, { label: '只有文字', value: 'only_text' },
], ],
}, },
] ]
const featurePrefs: PrefDef[] = [ const featurePrefs : PrefDef[] = [
{ key: 'useWaterfull', label: '图库瀑布流模式', kind: 'bool', path: ['gallery', 'useWaterfull'] }, { key: 'isAvatarRadius', label: '是否圆形头像', kind: 'bool', path: ['isAvatarRadius'] },
{ key: 'useSimple', label: '友链简洁模式', kind: 'bool', path: ['links', 'useSimple'] }, ]
{ key: 'isAvatarRadius', label: '是否圆形头像', kind: 'bool', path: ['isAvatarRadius'] },
{ key: 'useDot', label: '轮播图指示器', kind: 'bool', path: ['banner', 'useDot'] },
{
key: 'dotPosition',
label: '指示器位置',
kind: 'enum',
path: ['banner', 'dotPosition'],
options: [
{ label: '右边', value: 'right' },
{ label: '下边', value: 'bottom' },
],
siteLabelOf: (value: string) => {
const map: Record<string, string> = { right: '右边', bottom: '下边', left: '左边', top: '上边' }
return map[value] || value
},
},
]
/* ---------------- 状态读取 ---------------- */ /* ---------------- 状态读取 ---------------- */
function valueOf(path: Path): unknown { function valueOf(path : Path) : unknown {
return getByPath(settingStore.settings, path) return getByPath(settingStore.settings, path)
} }
/** 站点默认值(未配置时回退内置默认) */ /** 站点默认值(未配置时回退内置默认) */
function siteDefaultOf(path: Path): unknown { function siteDefaultOf(path : Path) : unknown {
const site = getByPath(settingStore.siteDefaults, path) const site = getByPath(settingStore.siteDefaults, path)
if (site !== undefined && site !== null) if (site !== undefined && site !== null)
return site return site
return getByPath(DefaultAppSettings, path) return getByPath(DefaultAppSettings, path)
} }
function isOverridden(path: Path): boolean { function isOverridden(path : Path) : boolean {
return isLocalOverride(readLocalPrefs(), path) return isLocalOverride(readLocalPrefs(), path)
} }
function enumLabelOf(def: PrefDef, value: unknown): string { function enumLabelOf(def : PrefDef, value : unknown) : string {
const hit = def.options?.find(opt => opt.value === value) const hit = def.options?.find(opt => opt.value === value)
if (hit) if (hit)
return hit.label return hit.label
if (def.siteLabelOf && typeof value === 'string') if (def.siteLabelOf && typeof value === 'string')
return def.siteLabelOf(value) return def.siteLabelOf(value)
return value === undefined || value === null ? '—' : String(value) return value === undefined || value === null ? '—' : String(value)
} }
/* ---------------- 交互 ---------------- */ /* ---------------- 交互 ---------------- */
/** 开关事件(模板透传 $event) */ /** 开关事件(模板透传 $event) */
function handleSwitchChange(def: PrefDef, detail: { value?: unknown }) { function handleSwitchChange(def : PrefDef, detail : { value ?: unknown }) {
handleBoolChange(def.path, detail.value === true) handleBoolChange(def.path, detail.value === true)
} }
/** 开关类:选值等于站点默认则还原为跟随(只存差异) */ /** 开关类:选值等于站点默认则还原为跟随(只存差异) */
function handleBoolChange(path: Path, next: boolean) { function handleBoolChange(path : Path, next : boolean) {
if (next === siteDefaultOf(path)) { if (next === siteDefaultOf(path)) {
settingStore.savePreference(buildPatch(path, null)) settingStore.savePreference(buildPatch(path, null))
} }
else { else {
settingStore.savePreference(buildPatch(path, next)) settingStore.savePreference(buildPatch(path, next))
} }
} }
/** 单项还原为跟随站点默认 */ /** 单项还原为跟随站点默认 */
function handleRevert(path: Path) { function handleRevert(path : Path) {
settingStore.savePreference(buildPatch(path, null)) settingStore.savePreference(buildPatch(path, null))
} }
/* ---------------- 枚举底部弹层 ---------------- */ /* ---------------- 枚举底部弹层 ---------------- */
const enumSheet = ref<{ show: boolean, def: PrefDef | null }>({ show: false, def: null }) const enumSheet = ref<{ show : boolean, def : PrefDef | null }>({ show: false, def: null })
function handleOpenEnum(def: PrefDef) { function handleOpenEnum(def : PrefDef) {
enumSheet.value = { show: true, def } enumSheet.value = { show: true, def }
} }
function handleCloseEnum() { function handleCloseEnum() {
enumSheet.value.show = false enumSheet.value.show = false
} }
function handleChooseEnum(value: string | null) { function handleChooseEnum(value : string | null) {
const def = enumSheet.value.def const def = enumSheet.value.def
if (def) { if (def) {
if (value === null || value === siteDefaultOf(def.path)) { if (value === null || value === siteDefaultOf(def.path)) {
handleRevert(def.path) handleRevert(def.path)
} }
else { else {
settingStore.savePreference(buildPatch(def.path, value)) settingStore.savePreference(buildPatch(def.path, value))
} }
} }
handleCloseEnum() handleCloseEnum()
} }
/** 当前枚举项是否处于「跟随站点默认」 */ /** 当前枚举项是否处于「跟随站点默认」 */
function isFollowing(def: PrefDef): boolean { function isFollowing(def : PrefDef) : boolean {
return !isOverridden(def.path) return !isOverridden(def.path)
} }
/* ---------------- 重置全部 ---------------- */ /* ---------------- 重置全部 ---------------- */
function handleResetAll() { function handleResetAll() {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定将所有偏好恢复为站点默认吗?本地自定义的偏好将被清除,未配置站点默认的项将恢复为内置默认。', content: '确定将所有偏好恢复为站点默认吗?本地自定义的偏好将被清除,未配置站点默认的项将恢复为内置默认。',
showCancel: true, showCancel: true,
cancelText: '取消', cancelText: '取消',
confirmText: '确定', confirmText: '确定',
confirmColor: '#03a9f4', confirmColor: '#03a9f4',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
settingStore.resetPreferences() settingStore.resetPreferences()
enumSheet.value.show = false enumSheet.value.show = false
uni.showToast({ icon: 'none', title: '已恢复为站点默认' }) uni.showToast({ icon: 'none', title: '已恢复为站点默认' })
} }
}, },
}) })
} }
</script> </script>
<template> <template>
<view class="app-page box-border min-h-screen bg-page pb-[200rpx]"> <view class="box-border min-h-screen bg-page">
<!-- 说明 --> <!-- 自定义标题 -->
<view class="pref-tip uh-global-card-glass mx-4 mt-4 flex items-start gap-2 rounded-xl px-4 py-3"> <uh-navbar default-title="偏好设置" title-color="text-gray-900" :need-placeholder="true"></uh-navbar>
<wd-icon name="info" size="28rpx" color="#a8a294" class="mt-0.5 shrink-0" />
<text class="tip-text flex-1 text-2xs text-gray-400 leading-[1.6]"> <!-- 内容区域 -->
你的偏好仅保存在本机未自定义的项自动跟随站长在插件后台配置的站点默认点击底部重置为站点默认可清空全部本地偏好 <view class="box-border p-3 flex flex-col gap-y-6">
</text> <!-- 布局设置 -->
</view> <view class="flex flex-col gap-y-3">
<uh-section-title>
<!-- 布局设置 --> 布局
<uh-section-title class="mx-4 mb-3 mt-6 text-[30rpx]"> <template #right>
布局 <text class="text-2xs text-gray-400">应用以及文章列表布局设置</text>
<template #right> </template>
<text class="text-2xs text-gray-400">应用以及文章列表布局设置</text> </uh-section-title>
</template> <view class="uh-global-card-glass overflow-hidden rounded-2xl">
</uh-section-title> <view v-for="(def, index) in layoutPrefs" :key="def.key"
<view class="setting-sheet uh-global-card-glass mx-4 overflow-hidden rounded-2xl"> class="pick-row flex items-center justify-between px-4 py-4"
<view :class="index < layoutPrefs.length - 1 ? 'border-b border-black/5' : ''"
v-for="(def, index) in layoutPrefs" @click="handleOpenEnum(def)">
:key="def.key" <view class="row-left flex flex-col gap-1">
class="pick-row flex items-center justify-between px-4 py-4" <text class="row-label text-[28rpx] text-gray-900 font-bold">{{ def.label }}</text>
:class="index < layoutPrefs.length - 1 ? 'border-b border-black/5' : ''" <view class="flex items-center gap-2">
@click="handleOpenEnum(def)" <text v-if="isFollowing(def)" class="row-sub text-2xs text-gray-400">跟随站点默认</text>
> <view v-else
<view class="row-left flex flex-col gap-1"> class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none">
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ def.label }}</text> 已自定义
<view class="flex items-center gap-2"> </view>
<text v-if="isFollowing(def)" class="row-sub text-2xs text-gray-400">跟随站点默认</text> </view>
<view v-else class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none"> </view>
已自定义 <view class="row-value flex items-center gap-2">
</view> <text
</view> class="value-text text-[26rpx] text-gray-400">{{ enumLabelOf(def, valueOf(def.path)) }}</text>
</view> <wd-icon name="arrow-right" size="12px" color="#c8c2b4" />
<view class="row-value flex items-center gap-2"> </view>
<text class="value-text text-[26rpx] text-gray-400">{{ enumLabelOf(def, valueOf(def.path)) }}</text> </view>
<wd-icon name="arrow-right" size="12px" color="#c8c2b4" /> </view>
</view> </view>
</view> <!-- 功能设置 -->
</view> <view class="flex flex-col gap-y-3">
<uh-section-title>
<!-- 功能设置 --> 功能
<uh-section-title class="mx-4 mb-3 mt-6 text-[30rpx]"> <template #right>
功能 <text class="text-2xs text-gray-400">一些常用的功能性设置</text>
<template #right> </template>
<text class="text-2xs text-gray-400">一些常用的功能性设置</text> </uh-section-title>
</template> <view class="setting-sheet uh-global-card-glass overflow-hidden rounded-2xl">
</uh-section-title> <template v-for="(def, index) in featurePrefs" :key="def.key">
<view class="setting-sheet uh-global-card-glass mx-4 overflow-hidden rounded-2xl"> <!-- 布尔开关 -->
<template v-for="(def, index) in featurePrefs" :key="def.key"> <view v-if="def.kind === 'bool'" class="switch-row flex items-center justify-between px-4 py-4"
<!-- 布尔开关 --> :class="index < featurePrefs.length - 1 ? 'border-b border-black/5' : ''">
<view <view class="row-left flex flex-col gap-1">
v-if="def.kind === 'bool'" <text class="row-label text-[28rpx] text-gray-900 font-bold">{{ def.label }}</text>
class="switch-row flex items-center justify-between px-4 py-4" <view class="flex items-center gap-2">
:class="index < featurePrefs.length - 1 ? 'border-b border-black/5' : ''" <text v-if="isFollowing(def)" class="row-sub text-2xs text-gray-400">跟随站点默认</text>
> <template v-else>
<view class="row-left flex flex-col gap-1"> <view
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ def.label }}</text> class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none">
<view class="flex items-center gap-2"> 已自定义
<text v-if="isFollowing(def)" class="row-sub text-2xs text-gray-400">跟随站点默认</text> </view>
<template v-else> <text class="revert-text text-2xs text-gray-400 underline"
<view class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none"> @click.stop="handleRevert(def.path)">恢复默认</text>
已自定义 </template>
</view> </view>
<text class="revert-text text-2xs text-gray-400 underline" @click.stop="handleRevert(def.path)">恢复默认</text> </view>
</template> <wd-switch :model-value="def.path" @change="handleSwitchChange(def, $event)" />
</view> </view>
</view> <!-- 枚举选择(指示器位置) -->
<wd-switch :model-value="!!valueOf(def.path)" @change="handleSwitchChange(def, $event)" /> <view v-else class="pick-row flex items-center justify-between px-4 py-4"
</view> :class="index < featurePrefs.length - 1 ? 'border-b border-black/5' : ''"
<!-- 枚举选择(指示器位置) --> @click="handleOpenEnum(def)">
<view <view class="row-left flex flex-col gap-1">
v-else <text class="row-label text-[28rpx] text-gray-900 font-bold">{{ def.label }}</text>
class="pick-row flex items-center justify-between px-4 py-4" <view class="flex items-center gap-2">
:class="index < featurePrefs.length - 1 ? 'border-b border-black/5' : ''" <text v-if="isFollowing(def)" class="row-sub text-2xs text-gray-400">跟随站点默认</text>
@click="handleOpenEnum(def)" <template v-else>
> <view
<view class="row-left flex flex-col gap-1"> class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none">
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ def.label }}</text> 已自定义
<view class="flex items-center gap-2"> </view>
<text v-if="isFollowing(def)" class="row-sub text-2xs text-gray-400">跟随站点默认</text> <text class="revert-text text-2xs text-gray-400 underline"
<template v-else> @click.stop="handleRevert(def.path)">恢复默认</text>
<view class="rounded-full bg-secondary px-2 py-0.5 text-[20rpx] text-[#4d7c0f] leading-none"> </template>
已自定义 </view>
</view> </view>
<text class="revert-text text-2xs text-gray-400 underline" @click.stop="handleRevert(def.path)">恢复默认</text> <view class="row-value flex items-center gap-2">
</template> <text
</view> class="value-text text-[26rpx] text-gray-400">{{ enumLabelOf(def, valueOf(def.path)) }}</text>
</view> <wd-icon name="arrow-right" size="12px" color="#c8c2b4" />
<view class="row-value flex items-center gap-2"> </view>
<text class="value-text text-[26rpx] text-gray-400">{{ enumLabelOf(def, valueOf(def.path)) }}</text> </view>
<wd-icon name="arrow-right" size="12px" color="#c8c2b4" /> </template>
</view> </view>
</view> </view>
</template> <!-- 底部操作栏(玻璃悬浮-->
</view> <view class="box-border w-full px-2">
<uh-button custom-class="uh-global-card-glass py-2 !rounded-full"
<!-- 底部操作栏(玻璃悬浮 + 底部安全区适配) --> @click="handleResetAll">恢复默认</uh-button>
<view class="btn-bar uh-global-card-glass fixed bottom-0 left-0 box-border w-screen px-4 pt-3 pb-safe"> </view>
<view class="reset-btn h-[84rpx] w-full flex items-center justify-center rounded-full bg-gray-900 active:opacity-80" @click="handleResetAll"> </view>
<text class="text-[28rpx] text-white font-bold">重置为站点默认</text> <!-- 枚举选择底部弹层 -->
</view> <wd-popup v-model="enumSheet.show" position="bottom" closable custom-style="border-radius: 24rpx 24rpx 0 0;"
</view> @close="handleCloseEnum">
<view v-if="enumSheet.def" class="enum-sheet box-border w-full pb-[env(safe-area-inset-bottom)]">
<!-- 枚举选择底部弹层 --> <view class="enum-title py-6 text-center text-[30rpx] text-gray-900 font-bold">
<wd-popup {{ enumSheet.def.label }}
v-model="enumSheet.show" </view>
position="bottom" <view class="enum-item flex items-center justify-between px-6 py-5"
closable :class="isFollowing(enumSheet.def) ? 'bg-secondary text-[#4d7c0f]' : 'text-gray-900'"
custom-style="border-radius: 24rpx 24rpx 0 0;" @click="handleChooseEnum(null)">
@close="handleCloseEnum" <text class="text-[28rpx]">跟随站点默认</text>
> <wd-icon v-if="isFollowing(enumSheet.def)" name="check" size="16px" color="#4d7c0f" />
<view v-if="enumSheet.def" class="enum-sheet box-border w-full pb-[env(safe-area-inset-bottom)]"> </view>
<view class="enum-title py-6 text-center text-[30rpx] text-gray-900 font-bold"> <view v-for="opt in enumSheet.def.options" :key="opt.value"
{{ enumSheet.def.label }} class="enum-item flex items-center justify-between border-t border-[#f0ece2] px-6 py-5"
</view> :class="valueOf(enumSheet.def.path) === opt.value && !isFollowing(enumSheet.def) ? 'bg-secondary text-[#4d7c0f]' : 'text-gray-900'"
<view @click="handleChooseEnum(opt.value)">
class="enum-item flex items-center justify-between px-6 py-5" <text class="text-[28rpx]">{{ opt.label }}</text>
:class="isFollowing(enumSheet.def) ? 'bg-secondary text-[#4d7c0f]' : 'text-gray-900'" <wd-icon v-if="valueOf(enumSheet.def.path) === opt.value && !isFollowing(enumSheet.def)"
@click="handleChooseEnum(null)" name="check" size="16px" color="#4d7c0f" />
> </view>
<text class="text-[28rpx]">跟随站点默认</text> </view>
<wd-icon v-if="isFollowing(enumSheet.def)" name="check" size="16px" color="#4d7c0f" /> </wd-popup>
</view> </view>
<view </template>
v-for="opt in enumSheet.def.options"
:key="opt.value"
class="enum-item flex items-center justify-between border-t border-[#f0ece2] px-6 py-5"
:class="valueOf(enumSheet.def.path) === opt.value && !isFollowing(enumSheet.def) ? 'bg-secondary text-[#4d7c0f]' : 'text-gray-900'"
@click="handleChooseEnum(opt.value)"
>
<text class="text-[28rpx]">{{ opt.label }}</text>
<wd-icon
v-if="valueOf(enumSheet.def.path) === opt.value && !isFollowing(enumSheet.def)"
name="check"
size="16px"
color="#4d7c0f"
/>
</view>
</view>
</wd-popup>
</view>
</template>
+22 -3
View File
@@ -1,14 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { DataLoadingStatusEnum } from '@/hooks/useDataLoadingStatus' import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '测试页面', navigationBarTitleText: '测试页面',
navigationStyle: 'custom',
}, },
}) })
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
setTimeout(() => {
updateLoadingStatus(DataLoadingStatusEnum.Success)
}, 3000)
</script> </script>
<template> <template>
<view class="w-full h-screen"> <view class="w-full min-h-screen bg-page">
<uh-data-loading :loading-status="DataLoadingStatusEnum.Loading"></uh-data-loading> <uh-navbar default-title="测试页面" :need-placeholder="true" title-color="text-gray-900"></uh-navbar>
<!-- 内容区由于 uh-navbar 内置有占位所以我们的页面的主要内容应该从这里开始比如这里就可以设置内边距或者其他样式最外层的 <view class="w-full min-h-screen bg-page"> 仅作为容器-->
<view class="box-border px-3">
<!-- 加载状态 -->
<uh-data-loading v-if="loadingStatus!==DataLoadingStatusEnum.Success" :loading-status="loadingStatus"
min-height="80vh"></uh-data-loading>
<!-- 实际内容 -->
<view v-else>
请求成功啦
</view>
</view>
</view> </view>
</template> </template>
+343 -337
View File
@@ -1,375 +1,381 @@
<script lang="ts" setup> <script lang="ts" setup>
/** /**
* 关于页(源自旧项目 pages/tabbar/about/about.vue,新建复刻) * 关于页(源自旧项目 pages/tabbar/about/about.vue,新建复刻)
* 功能:博主信息 + 站点统计 + 功能导航 + 版权 * 功能:博主信息 + 站点统计 + 功能导航 + 版权
* 风格:对齐全站设计语言(bg-page + uh-global-card-glass + uh-section-title + 彩色图标块) * 风格:对齐全站设计语言(bg-page + uh-global-card-glass + uh-section-title + 彩色图标块)
*/ */
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { onPullDownRefresh, onShow } from '@dcloudio/uni-app' import { onPullDownRefresh, onShow } from '@dcloudio/uni-app'
import { getBlogStatistics } from '@/api/halo' import { getBlogStatistics } from '@/api/halo'
import { useAppConfigStore } from '@/store/appConfig' import { useAppConfigStore } from '@/store/appConfig'
import { useFavoritesStore } from '@/store/favorites' import { useFavoritesStore } from '@/store/favorites'
import { checkAvatarUrl, checkImageUrl } from '@/utils/url' import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
import { checkHasAdminLogin } from '@/utils/auth' import { checkHasAdminLogin } from '@/utils/auth'
import { t } from '@/locale' import { t } from '@/locale'
import type { IBlogStats } from '@/api/types/halo' import type { IBlogStats } from '@/api/types/halo'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '关于', navigationBarTitleText: '关于',
enablePullDownRefresh: true, enablePullDownRefresh: true,
navigationStyle: 'custom', navigationStyle: 'custom',
}, },
}) })
const appConfigStore = useAppConfigStore() const appConfigStore = useAppConfigStore()
const favoritesStore = useFavoritesStore() const favoritesStore = useFavoritesStore()
const haloConfigs = computed(() => appConfigStore.configs) const haloConfigs = computed(() => appConfigStore.configs)
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled) const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled) const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled) const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
/** 数据看板插件可用性(供导航项显隐判断) */ /** 数据看板插件可用性(供导航项显隐判断) */
const { check: checkDataVisualPlugin } = usePluginAvailable('plugin-data-statistics') const { check: checkDataVisualPlugin } = usePluginAvailable('plugin-data-statistics')
/* ---------------- 计算属性 ---------------- */ /* ---------------- 计算属性 ---------------- */
const bloggerInfo = computed(() => { const bloggerInfo = computed(() => {
const blogger = haloConfigs.value.authorConfig?.blogger as const blogger = haloConfigs.value.authorConfig?.blogger as
| { nickname?: string, avatar?: string, description?: string } | { nickname ?: string, avatar ?: string, description ?: string }
| undefined | undefined
return { return {
nickname: blogger?.nickname || '', nickname: blogger?.nickname || '',
avatar: checkAvatarUrl(blogger?.avatar), avatar: checkAvatarUrl(blogger?.avatar),
description: blogger?.description || '', description: blogger?.description || '',
} }
}) })
const pageConfig = computed(() => haloConfigs.value.pageConfig?.aboutConfig as const pageConfig = computed(() => haloConfigs.value.pageConfig?.aboutConfig as
| { bgImageUrl?: string, waveImageUrl?: string } | { bgImageUrl ?: string, waveImageUrl ?: string }
| undefined) | undefined)
const calcProfileStyle = computed(() => ({ const calcProfileStyle = computed(() => ({
backgroundImage: `url(${checkImageUrl(pageConfig.value?.bgImageUrl)})`, backgroundImage: `url(${checkImageUrl(pageConfig.value?.bgImageUrl)})`,
})) }))
const calcWaveUrl = computed(() => checkImageUrl(pageConfig.value?.waveImageUrl)) const calcWaveUrl = computed(() => checkImageUrl(pageConfig.value?.waveImageUrl))
const basicConfig = computed(() => haloConfigs.value.basicConfig as const basicConfig = computed(() => haloConfigs.value.basicConfig as
| { | {
copyrightConfig?: { enabled?: boolean, content?: string } copyrightConfig ?: { enabled ?: boolean, content ?: string }
disclaimers?: { enabled?: boolean } disclaimers ?: { enabled ?: boolean }
showAboutSystem?: boolean showAboutSystem ?: boolean
} }
| undefined) | undefined)
const copyrightConfig = computed(() => basicConfig.value?.copyrightConfig) const copyrightConfig = computed(() => basicConfig.value?.copyrightConfig)
const loveEnabled = computed(() => !!(haloConfigs.value.loveConfig as { loveEnabled?: boolean } | undefined)?.loveEnabled) const loveEnabled = computed(() => !!(haloConfigs.value.loveConfig as { loveEnabled ?: boolean } | undefined)?.loveEnabled)
const socialEnabled = computed(() => !!(haloConfigs.value.authorConfig?.social as { enabled?: boolean } | undefined)?.enabled) const socialEnabled = computed(() => !!(haloConfigs.value.authorConfig?.social as { enabled ?: boolean } | undefined)?.enabled)
/* ---------------- 状态 ---------------- */ /* ---------------- 状态 ---------------- */
const statisticsShowMore = ref(false) const statisticsShowMore = ref(false)
const statistics = ref<IBlogStats>({ post: 0, comment: 0, category: 0, visit: 0, upvote: 0 }) const statistics = ref<IBlogStats>({ post: 0, comment: 0, category: 0, visit: 0, upvote: 0 })
/** 主行统计(常驻展示) */ /** 主行统计(常驻展示) */
const allStats = computed(() => [ const allStats = computed(() => [
{ key: 'post', label: '内容', value: statistics.value.post }, { key: 'post', label: '内容', value: statistics.value.post },
{ key: 'visit', label: '访客', value: statistics.value.visit }, { key: 'visit', label: '访客', value: statistics.value.visit },
{ key: 'category', label: '分类', value: statistics.value.category }, { key: 'category', label: '分类', value: statistics.value.category },
{ key: 'comment', label: '评论', value: statistics.value.comment }, { key: 'comment', label: '评论', value: statistics.value.comment },
{ key: 'upvote', label: '点赞', value: statistics.value.upvote }, { key: 'upvote', label: '点赞', value: statistics.value.upvote },
]) ])
interface INavItem { interface INavItem {
key: string key : string
title: string title : string
icon: string iconPrefix ?: string
/** 图标块背景色(与首页快捷导航同色板,同一功能同色) */ icon : string
bgColor: string /** 图标块背景色(与首页快捷导航同色板,同一功能同色) */
rightText: string bgColor : string
path: string | null rightText : string
isAdmin?: boolean path : string | null
openType?: string isAdmin ?: boolean
show: boolean openType ?: string
/** 分组:blog=博客功能 more=更多信息 */ show : boolean
group: 'blog' | 'more' /** 分组:blog=博客功能 more=更多信息 */
} group : 'blog' | 'more'
}
const navList = ref<INavItem[]>([]) const navList = ref<INavItem[]>([])
/** 分组渲染(过滤后空组整组隐藏) */ /** 分组渲染(过滤后空组整组隐藏) */
const calcNavGroups = computed(() => { const calcNavGroups = computed(() => {
const visible = navList.value.filter(n => n.show) const visible = navList.value.filter(n => n.show)
const groupDefs: { key: 'blog' | 'more', title: string }[] = [ const groupDefs : { key : 'blog' | 'more', title : string }[] = [
{ key: 'blog', title: '博客功能' }, { key: 'blog', title: '博客功能' },
{ key: 'more', title: '其他功能' }, { key: 'more', title: '其他功能' },
] ]
return groupDefs return groupDefs
.map(def => ({ ...def, items: visible.filter(n => n.group === def.key) })) .map(def => ({ ...def, items: visible.filter(n => n.group === def.key) }))
.filter(group => group.items.length > 0) .filter(group => group.items.length > 0)
}) })
/* ---------------- 功能导航 ---------------- */ /* ---------------- 功能导航 ---------------- */
/** 图标块浅色背景:品牌深色 rgba 降透明度 → 轻量底色 */ /** 图标块浅色背景:品牌深色 rgba 降透明度 → 轻量底色 */
function toLightBg(rgba: string) { function toLightBg(rgba : string) {
return rgba.replace('0.95)', '0.15)') return rgba.replace('0.95)', '0.15)')
} }
/** 图标颜色:品牌深色实色 */ /** 图标颜色:品牌深色实色 */
function toSolidColor(rgba: string) { function toSolidColor(rgba : string) {
return rgba.replace('0.95)', '1)') return rgba.replace('0.95)', '1)')
} }
/** 收藏导航项右侧文案跟随收藏总数(收藏页返回/切回时刷新) */ /** 收藏导航项右侧文案跟随收藏总数(收藏页返回/切回时刷新) */
function syncFavoritesNavText() { function syncFavoritesNavText() {
const nav = navList.value.find(n => n.key === 'favorites') const nav = navList.value.find(n => n.key === 'favorites')
if (nav) { if (nav) {
nav.rightText = `${favoritesStore.counts.total} 条收藏` nav.rightText = `${favoritesStore.counts.total} 条收藏`
} }
} }
async function handleGetNavList() { async function handleGetNavList() {
const dataVisualAvailable = await checkDataVisualPlugin() const dataVisualAvailable = await checkDataVisualPlugin()
navList.value = [ navList.value = [
{ {
key: 'favorites', key: 'favorites',
title: '我的收藏', title: '我的收藏',
icon: 'star', iconPrefix: 'uhemoji2-icon',
bgColor: 'rgba(255, 179, 0, 0.95)', icon: '-smiling',
rightText: '', bgColor: 'rgba(255, 179, 0, 0.95)',
path: '/pages-blog/favorites/favorites', rightText: '',
show: true, path: '/pages-blog/favorites/favorites',
group: 'blog', show: true,
}, group: 'blog',
{ },
key: 'data-visual', {
title: '数据看板', key: 'data-visual',
icon: 'chart', title: '数据看板',
bgColor: 'rgba(102, 60, 201, 0.95)', iconPrefix: 'uhemoji2-icon',
rightText: '站点数据可视化', icon: '-surprised',
path: '/pages-blog/data-visual/data-visual', bgColor: 'rgba(102, 60, 201, 0.95)',
show: dataVisualAvailable, rightText: '站点数据可视化',
group: 'blog', path: '/pages-blog/data-visual/data-visual',
}, show: dataVisualAvailable,
{ group: 'blog',
key: 'archives', },
title: calcAuditModeEnabled.value ? '内容归档' : '文章归档', {
icon: 'folder', key: 'archives',
bgColor: 'rgba(3, 169, 244, 0.95)', title: calcAuditModeEnabled.value ? '内容归档' : '文章归档',
rightText: calcAuditModeEnabled.value ? '全部已归档内容' : '全部已归档文章', iconPrefix: 'uhemoji2-icon',
path: '/pages-blog/archives/archives', icon: '-mask',
show: true, bgColor: 'rgba(3, 169, 244, 0.95)',
group: 'blog', rightText: calcAuditModeEnabled.value ? '全部已归档内容' : '全部已归档文章',
}, path: '/pages-blog/archives/archives',
{ show: true,
key: 'love', group: 'blog',
title: '恋爱日记', },
icon: 'heart', {
bgColor: 'rgba(255, 76, 103, 0.95)', key: 'love',
rightText: '博主的恋爱日记', title: '恋爱日记',
path: '/pages-blog/love/love', iconPrefix: 'uhemoji2-icon',
show: loveEnabled.value, icon: '-in-love',
// show: true, bgColor: 'rgba(255, 76, 103, 0.95)',
group: 'blog', rightText: '博主的恋爱日记',
}, path: '/pages-blog/love/love',
{ show: loveEnabled.value,
key: 'vote', // show: true,
title: '投票中心', group: 'blog',
icon: 'box', },
bgColor: 'rgba(0, 188, 212, 0.95)', {
rightText: '查看和进行投票', key: 'vote',
path: '/pages-blog/votes/votes', title: '投票中心',
show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value, iconPrefix: 'uhemoji2-icon',
// show: true, icon: '-confused',
group: 'blog', bgColor: 'rgba(0, 188, 212, 0.95)',
}, rightText: '查看和进行投票',
{ path: '/pages-blog/votes/votes',
key: 'friend-links', show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value,
title: '友情链接', // show: true,
icon: 'link', group: 'blog',
bgColor: 'rgba(0, 150, 136, 0.95)', },
rightText: '看看博主朋友们吧', {
path: '/pages-blog/friend-links/friend-links', key: 'friend-links',
show: calcLinksPluginEnabled.value, title: '友情链接',
// show: true, iconPrefix: 'uhemoji2-icon',
group: 'blog', icon: '-cool',
}, bgColor: 'rgba(0, 150, 136, 0.95)',
{ rightText: '看看博主朋友们吧',
key: 'disclaimers', path: '/pages-blog/friend-links/friend-links',
title: '免责声明', show: calcLinksPluginEnabled.value,
icon: 'map', // show: true,
bgColor: 'rgba(121, 85, 72, 0.95)', group: 'blog',
rightText: '博客内容免责声明', },
path: '/pages-blog/disclaimers/disclaimers', {
show: !!basicConfig.value?.disclaimers?.enabled, key: 'disclaimers',
// show: true, title: '免责声明',
group: 'more', iconPrefix: 'uhemoji2-icon',
}, icon: '-smirking',
{ bgColor: 'rgba(121, 85, 72, 0.95)',
key: 'contact-blogger', rightText: '博客内容免责声明',
title: '联系博主', path: '/pages-blog/disclaimers/disclaimers',
icon: 'message', show: !!basicConfig.value?.disclaimers?.enabled,
bgColor: 'rgba(255, 152, 0, 0.95)', // show: true,
rightText: '博主常用联系方式', group: 'more',
path: '/pages-blog/contact/contact', },
show: socialEnabled.value, {
// show: true, key: 'contact-blogger',
group: 'more', title: '联系博主',
}, iconPrefix: 'uhemoji2-icon',
{ icon: '-wink',
key: 'about', bgColor: 'rgba(255, 152, 0, 0.95)',
title: '关于项目', rightText: '博主常用联系方式',
icon: 'info', path: '/pages-blog/contact/contact',
bgColor: 'rgba(96, 125, 139, 0.95)', show: socialEnabled.value,
rightText: '小莫唐尼开源项目', // show: true,
path: '/pages-blog/about/about', group: 'more',
show: !!basicConfig.value?.showAboutSystem, },
// show: true, {
group: 'more', key: 'about',
}, title: '关于项目',
{ iconPrefix: 'uhemoji2-icon',
key: 'setting', icon: '-happy-',
title: '偏好设置', bgColor: 'rgba(96, 125, 139, 0.95)',
icon: 'settings', rightText: '小莫唐尼开源项目',
bgColor: 'rgba(121, 134, 203, 0.95)', path: '/pages-blog/about/about',
rightText: '首页布局、卡片样式等本地偏好', show: !!basicConfig.value?.showAboutSystem,
path: '/pages-blog/setting/setting', // show: true,
show: true, group: 'more',
group: 'more', },
}, {
] key: 'setting',
syncFavoritesNavText() title: '偏好设置',
} iconPrefix: 'uhemoji2-icon',
icon: '-tired',
bgColor: 'rgba(121, 134, 203, 0.95)',
rightText: '首页布局、卡片样式等本地偏好',
path: '/pages-blog/setting/setting',
show: true,
group: 'more',
},
]
syncFavoritesNavText()
}
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
async function handleGetData() { async function handleGetData() {
try { try {
const res = await getBlogStatistics() const res = await getBlogStatistics()
statistics.value = res.data statistics.value = res.data
} }
catch (err) { catch (err) {
console.error('获取统计失败', err) console.error('获取统计失败', err)
uni.showToast({ icon: 'none', title: t('common.loadFailedRetry') }) uni.showToast({ icon: 'none', title: t('common.loadFailedRetry') })
} }
finally { finally {
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
} }
} }
/* ---------------- 交互 ---------------- */ /* ---------------- 交互 ---------------- */
function handleOnNav(data: { path: string | null, isAdmin?: boolean }) { function handleOnNav(data : { path : string | null, isAdmin ?: boolean }) {
const { path, isAdmin } = data const { path, isAdmin } = data
if (!path) if (!path)
return return
// 拦截后台管理页面(需超管登录) // 拦截后台管理页面(需超管登录)
if (isAdmin && !checkHasAdminLogin()) { if (isAdmin && !checkHasAdminLogin()) {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '未登录超管账号或登录状态已过期,是否立即登录?', content: '未登录超管账号或登录状态已过期,是否立即登录?',
showCancel: true, showCancel: true,
cancelText: '否', cancelText: '否',
cancelColor: '#999999', cancelColor: '#999999',
confirmText: '是', confirmText: '是',
confirmColor: '#03a9f4', confirmColor: '#03a9f4',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
uni.navigateTo({ url: '/pages/auth/login' }) uni.navigateTo({ url: '/pages/auth/login' })
} }
}, },
}) })
return return
} }
uni.navigateTo({ url: path }) uni.navigateTo({ url: path })
} }
/* ---------------- 生命周期 ---------------- */ /* ---------------- 生命周期 ---------------- */
watch(haloConfigs, () => { watch(haloConfigs, () => {
handleGetNavList() handleGetNavList()
}, { deep: true, immediate: true }) }, { deep: true, immediate: true })
handleGetData() handleGetData()
// 从收藏页返回/切回时刷新收藏数文案 // 从收藏页返回/切回时刷新收藏数文案
onShow(() => { onShow(() => {
syncFavoritesNavText() syncFavoritesNavText()
}) })
onPullDownRefresh(() => { onPullDownRefresh(() => {
handleGetData() handleGetData()
}) })
</script> </script>
<template> <template>
<view class="box-border min-h-screen w-screen bg-page pb-8"> <view class="box-border min-h-screen w-screen bg-page pb-8">
<!-- 头部:博主信息(背景图 + 遮罩 + wave,内容区做状态栏适配) --> <!-- 头部:博主信息(背景图 + 遮罩 + wave,内容区做状态栏适配) -->
<view class="blogger-info relative h-76 w-full bg-cover bg-no-repeat" :style="[calcProfileStyle]"> <view class="blogger-info relative h-76 w-full bg-cover bg-no-repeat" :style="[calcProfileStyle]">
<!-- 背景遮罩 --> <!-- 背景遮罩 -->
<view class="absolute left-0 top-0 z-0 h-full w-full bg-black/30 backdrop-blur-[2rpx]" /> <view class="absolute left-0 top-0 z-0 h-full w-full bg-black/30 backdrop-blur-[2rpx]" />
<view class="relative z-6 h-full flex flex-col items-center justify-center pb-[140rpx] pt-safe"> <view class="relative z-6 h-full flex flex-col items-center justify-center pb-[140rpx] pt-safe">
<image <image class="uh-global-card-glass h-20 w-20 rounded-full" :src="bloggerInfo.avatar"
class="uh-global-card-glass h-20 w-20 rounded-full" :src="bloggerInfo.avatar" mode="aspectFill" />
mode="aspectFill" <view class="mt-4 text-lg text-white font-bold text-shadow-[0_2rpx_8rpx_rgba(0,0,0,0.4)]">
/> {{ bloggerInfo.nickname }}
<view class="mt-4 text-lg text-white font-bold text-shadow-[0_2rpx_8rpx_rgba(0,0,0,0.4)]"> </view>
{{ bloggerInfo.nickname }} <view
</view> class="desc mt-2 px-10 text-center text-[26rpx] text-white/90 leading-relaxed text-shadow-[0_2rpx_8rpx_rgba(0,0,0,0.4)]">
<view {{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
class="desc mt-2 px-10 text-center text-[26rpx] text-white/90 leading-relaxed text-shadow-[0_2rpx_8rpx_rgba(0,0,0,0.4)]" </view>
> </view>
{{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }} <image v-if="calcWaveUrl" :src="calcWaveUrl" mode="scaleToFill"
</view> class="gif-wave absolute bottom-0 left-0 z-99 h-[100rpx] w-full" style="mix-blend-mode: screen;" />
</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="uh-global-card-glass relative z-100 mx-4 flex border rounded-2xl -mt-12"> <view class="uh-global-card-glass relative z-100 mx-4 flex border rounded-2xl -mt-12">
<view v-for="item in allStats" :key="item.key" class="flex-1 py-6 text-center"> <view v-for="item in allStats" :key="item.key" class="flex-1 py-6 text-center">
<view class="text-lg text-gray-900 font-bold"> <wd-count-to
{{ item.value }} :key="`${item.key}-${item.value}`" :start-val="0" :end-val="item.value"
</view> :duration="900" separator="" color="#111827" custom-class="text-lg font-bold"
<view class="mt-1 text-xs text-gray-500"> />
{{ item.label }} <view class="mt-1 text-xs text-gray-500">
</view> {{ item.label }}
</view> </view>
</view> </view>
</view>
<!-- 功能导航(分组玻璃卡) --> <!-- 功能导航(分组玻璃卡) -->
<template v-for="group in calcNavGroups" :key="group.key"> <template v-for="group in calcNavGroups" :key="group.key">
<uh-section-title class="mx-4 mb-3 mt-8"> <uh-section-title class="mx-4 mb-3 mt-8">
{{ group.title }} {{ group.title }}
</uh-section-title> </uh-section-title>
<view class="nav-wrap uh-global-card-glass mx-4 overflow-hidden rounded-2xl"> <view class="uh-global-card-glass mx-4 overflow-hidden rounded-2xl">
<view <view v-for="(nav, index) in group.items" :key="nav.key"
v-for="(nav, index) in group.items" :key="nav.key" class="nav-item flex items-center justify-between px-4"
class="nav-item flex items-center justify-between px-4" :class="index < group.items.length - 1 ? 'border-b border-b-solid border-black/5' : ''"
:class="index < group.items.length - 1 ? 'border-b border-b-solid border-black/5' : ''" @click="handleOnNav(nav)" @click="handleOnNav(nav)">
> <view class="nav-left flex items-center gap-3 py-3">
<view class="nav-left flex items-center gap-3 py-3"> <view
<view class="uh-global-card-glass border uh-shadow-xs h-8 w-8 flex items-center justify-center rounded-xl"
class="h-9 w-9 flex items-center justify-center border border-black/5 rounded-xl" :style="{ backgroundColor: toLightBg(nav.bgColor) }">
:style="{ backgroundColor: toLightBg(nav.bgColor) }" <wd-icon :class-prefix="nav.iconPrefix" :name="nav.icon" size="36rpx"
> :color="toSolidColor(nav.bgColor)" />
<wd-icon :name="nav.icon" size="20px" :color="toSolidColor(nav.bgColor)" /> </view>
</view> <text class="nav-title text-sm text-gray-900 font-bold">{{ nav.title }}</text>
<text class="nav-title text-sm text-gray-900 font-bold">{{ nav.title }}</text> </view>
</view> <view class="nav-right flex items-center gap-2">
<view class="nav-right flex items-center gap-2"> <text class="nav-right-text text-xs text-gray-400">{{ nav.rightText }}</text>
<text class="nav-right-text text-xs text-gray-400">{{ nav.rightText }}</text> <wd-icon name="arrow-right" size="12px" color="#c8c2b4" />
<wd-icon name="arrow-right" size="12px" color="#c8c2b4" /> </view>
</view> </view>
</view> </view>
</view> </template>
</template>
<!-- 版权 --> <!-- 版权 -->
<view v-if="copyrightConfig?.enabled" class="mt-6 px-6 text-center text-xs text-gray-400"> <view v-if="copyrightConfig?.enabled" class="mt-6 px-6 text-center text-xs text-gray-400">
<view>{{ copyrightConfig.content }}</view> <view>{{ copyrightConfig.content }}</view>
</view> </view>
</view> </view>
</template> </template>
-2
View File
@@ -148,13 +148,11 @@
onLoad(async () => { onLoad(async () => {
// 检查插件可用性 // 检查插件可用性
await checkPluginAvailable() await checkPluginAvailable()
console.log('uniHaloPluginAvailable',uniHaloPluginAvailable.value)
if (!uniHaloPluginAvailable.value) { if (!uniHaloPluginAvailable.value) {
uni.stopPullDownRefresh() uni.stopPullDownRefresh()
return return
} }
// 开始正常数据请求 // 开始正常数据请求
handleGetCategory() handleGetCategory()
}) })
+1
View File
@@ -1,6 +1,7 @@
// 测试用的 iconfont,可生效 // 测试用的 iconfont,可生效
@import './iconfont.css'; @import './iconfont.css';
@import './uhemoji-iconfont.css'; @import './uhemoji-iconfont.css';
@import './uhemoji2-iconfont.css';
:root, :root,
page { page {
File diff suppressed because one or more lines are too long