mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-07-27 04:20:43 +08:00
feat: 新增友链页面,添加友链tabbar项并调整配置
1. 新增友链页面组件,实现分组展示、滚动导航功能 2. 多语言中添加友链相关翻译 3. 修改默认首页跳转至友链页 4. 调整API请求配置,添加halo请求标记 5. 优化剪贴板提示、布局样式和工具函数 6. 更新开发环境请求地址
This commit is contained in:
Vendored
+2
-1
@@ -6,4 +6,5 @@ VITE_DELETE_CONSOLE=false
|
|||||||
VITE_SHOW_SOURCEMAP=false
|
VITE_SHOW_SOURCEMAP=false
|
||||||
|
|
||||||
# development mode 后台请求地址
|
# development mode 后台请求地址
|
||||||
VITE_UNI_HALO_BASEURL='https://www.yijunzhao.cn'
|
# VITE_UNI_HALO_BASEURL='https://www.yijunzhao.cn'
|
||||||
|
VITE_UNI_HALO_BASEURL='https://www.xhhao.com'
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { http } from '@/http/alova'
|
|||||||
* @returns 获取链接分分组组列表响应参数
|
* @returns 获取链接分分组组列表响应参数
|
||||||
*/
|
*/
|
||||||
export function queryLinkGroups(params: any) {
|
export function queryLinkGroups(params: any) {
|
||||||
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/linkgroups`, { params })
|
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/linkgroups`, {
|
||||||
|
params,
|
||||||
|
meta: { isHalo: true },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,5 +18,8 @@ export function queryLinkGroups(params: any) {
|
|||||||
* @returns 获取链接列表响应参数
|
* @returns 获取链接列表响应参数
|
||||||
*/
|
*/
|
||||||
export function queryLinks(params: any) {
|
export function queryLinks(params: any) {
|
||||||
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/links`, { params })
|
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/links`, {
|
||||||
|
params,
|
||||||
|
meta: { isHalo: true },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<view class="w-full pb-safe">
|
||||||
|
<!-- 60px -->
|
||||||
|
<view class="h-62px w-full" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
interface IPaginationParams {
|
export interface IPaginationParams {
|
||||||
page: number
|
page: number
|
||||||
size: number
|
size: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"tabbar.home": "Home",
|
"tabbar.home": "Home",
|
||||||
"tabbar.gallery": "Gallery",
|
"tabbar.gallery": "Gallery",
|
||||||
"tabbar.moments": "Moments",
|
"tabbar.moments": "Moments",
|
||||||
|
"tabbar.links": "Links",
|
||||||
"tabbar.about": "About",
|
"tabbar.about": "About",
|
||||||
"tabbar.me": "Me",
|
"tabbar.me": "Me",
|
||||||
"tabbar.i18n": "I18n",
|
"tabbar.i18n": "I18n",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"tabbar.gallery": "相册",
|
"tabbar.gallery": "相册",
|
||||||
"tabbar.moments": "瞬间",
|
"tabbar.moments": "瞬间",
|
||||||
"tabbar.about": "关于",
|
"tabbar.about": "关于",
|
||||||
|
"tabbar.links": "友链",
|
||||||
"tabbar.me": "我的",
|
"tabbar.me": "我的",
|
||||||
"tabbar.i18n": "语言",
|
"tabbar.i18n": "语言",
|
||||||
"i18n.title": "中文标题",
|
"i18n.title": "中文标题",
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ onPageScroll(() => { })
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="min-h-screen bg-page pb-safe">
|
<view class="min-h-screen bg-page2">
|
||||||
<!-- 顶部banner -->
|
<!-- 顶部banner -->
|
||||||
<view v-if="false" class="relative h-72 w-full">
|
<view v-if="false" class="relative h-72 w-full">
|
||||||
<image class="h-72 w-full object-cover" :src="exampleImageUrls.banner" />
|
<image class="h-72 w-full object-cover" :src="exampleImageUrls.banner" />
|
||||||
@@ -217,7 +217,7 @@ onPageScroll(() => { })
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部导航占位 -->
|
<!-- 底部导航占位 -->
|
||||||
<view class="h-60px w-full" />
|
<uh-tabbar-page-placeholder />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ definePage({
|
|||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
console.log('index onLoad')
|
console.log('index onLoad')
|
||||||
// 默认跳转首页
|
// 默认跳转首页
|
||||||
uni.switchTab({ url: '/pages/home/home' })
|
// uni.switchTab({ url: '/pages/home/home' })
|
||||||
|
uni.switchTab({ url: '/pages/links/links' })
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { getCurrentInstance } from 'vue'
|
||||||
|
import useRequest from '@/hooks/useRequest'
|
||||||
|
import { queryLinkGroups, queryLinks } from '@/api/halo-plugin/link'
|
||||||
|
import { cover } from '@/utils/imageHelper'
|
||||||
|
import { copy } from '@/utils/base/clipboard'
|
||||||
|
import { getRect } from '@/utils/base/getRect'
|
||||||
|
import { useListData } from '@/hooks/useListData'
|
||||||
|
import type { IPaginationParams } from '@/hooks/useListData'
|
||||||
|
import { debounce } from '@/utils/base/debounce'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Links',
|
||||||
|
})
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '友情链接',
|
||||||
|
navigationBarBackgroundColor: '#ffffff',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { loading: loadingGroups, error: errorGroups, data: dataGroups, run: runGroups } = useRequest<any>(queryLinkGroups, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||||
|
const { loading, error, data, run } = useRequest<any, IPaginationParams>(queryLinks, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||||
|
|
||||||
|
const { list: links, refresh } = useListData<any>({
|
||||||
|
params: {
|
||||||
|
page: 1,
|
||||||
|
size: 0,
|
||||||
|
},
|
||||||
|
loading: toRef(loading),
|
||||||
|
onPullDownRefresh: async (params, updateData) => {
|
||||||
|
await run({ ...params })
|
||||||
|
updateData({
|
||||||
|
error: error.value,
|
||||||
|
hasNext: data.value?.hasNext,
|
||||||
|
items: data.value?.items ?? [],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onReachBottom: async (params, updateData) => {
|
||||||
|
await run({ ...params })
|
||||||
|
updateData({
|
||||||
|
error: error.value,
|
||||||
|
hasNext: data.value?.hasNext,
|
||||||
|
items: data.value?.items ?? [],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分组列表,处理排序,分组显示
|
||||||
|
interface ILinkGroupData {
|
||||||
|
name: string
|
||||||
|
index: string
|
||||||
|
priority: number
|
||||||
|
links: any[]
|
||||||
|
}
|
||||||
|
const linkGroups = computed(() => {
|
||||||
|
const groupedLinks = links.value.reduce((acc, link) => {
|
||||||
|
const groupName = link.spec.groupName
|
||||||
|
|
||||||
|
const group = dataGroups.value?.find(item => item.metadata.name === groupName)
|
||||||
|
let _groupName = '未分组'
|
||||||
|
if (group) {
|
||||||
|
_groupName = group.spec.displayName
|
||||||
|
}
|
||||||
|
if (!acc[_groupName]) {
|
||||||
|
acc[_groupName] = {
|
||||||
|
name: _groupName,
|
||||||
|
index: _groupName,
|
||||||
|
priority: group?.spec.priority ?? (dataGroups.value ? dataGroups.value.length + 1 : 99),
|
||||||
|
links: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acc[_groupName].links.push(toRaw(link))
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, any[]>)
|
||||||
|
// // 分组还需要根据 分组中的 spec.priority 字段进行排序
|
||||||
|
return Object.values(groupedLinks)
|
||||||
|
.map((item) => {
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
.sort((a: ILinkGroupData, b: ILinkGroupData) => a.priority - b.priority)
|
||||||
|
.map((item: ILinkGroupData) => {
|
||||||
|
item.links.sort((a, b) => a.spec.priority - b.spec.priority)
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const useBottomGroupNav = ref(false)
|
||||||
|
const allGroupRects = ref([])
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
|
||||||
|
async function getAllGroupPosition() {
|
||||||
|
await nextTick()
|
||||||
|
allGroupRects.value = []
|
||||||
|
linkGroups.value.forEach(async (item, index) => {
|
||||||
|
const groupRects = await getRect({ selector: `.group-start-${index},.group-end-${index}`, instance, all: true })
|
||||||
|
allGroupRects.value.push({
|
||||||
|
name: item.name,
|
||||||
|
index,
|
||||||
|
active: false,
|
||||||
|
startRect: groupRects[0],
|
||||||
|
endRect: groupRects[1],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToGroup(item: any) {
|
||||||
|
const startRect = item.startRect
|
||||||
|
if (startRect) {
|
||||||
|
uni.pageScrollTo({
|
||||||
|
scrollTop: startRect.top - 12,
|
||||||
|
duration: 300,
|
||||||
|
})
|
||||||
|
uni.showToast({
|
||||||
|
title: item.name,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 600,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleActiveNavGroup = debounce((scrollTop: number) => {
|
||||||
|
allGroupRects.value.forEach((item) => {
|
||||||
|
if (scrollTop >= item.startRect.top - 24 && scrollTop <= item.endRect.top + 60) {
|
||||||
|
item.active = true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item.active = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 30)
|
||||||
|
|
||||||
|
watch(() => linkGroups.value, async (newVal) => {
|
||||||
|
if (newVal.length !== 0) {
|
||||||
|
await getAllGroupPosition()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onPageScroll(({ scrollTop }) => {
|
||||||
|
handleActiveNavGroup(scrollTop)
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad(async () => {
|
||||||
|
await runGroups()
|
||||||
|
await refresh()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="min-h-screen w-full bg-page2">
|
||||||
|
<!-- <view>
|
||||||
|
<view>分组数据</view>
|
||||||
|
<view>{{ dataGroups }}</view>
|
||||||
|
</view> -->
|
||||||
|
|
||||||
|
<!-- 分组列表 -->
|
||||||
|
<view v-if="linkGroups.length !== 0" class="box-border w-full flex flex-col gap-y-4 overflow-hidden p-4 pb-0">
|
||||||
|
<template v-for="(item, index) in linkGroups" :key="item.name">
|
||||||
|
<view
|
||||||
|
v-if="linkGroups.length !== 1" class="flex items-center justify-between text-sm"
|
||||||
|
:class="[`group-start-${index}`]"
|
||||||
|
>
|
||||||
|
<text class="font-bold">{{ item.name }}</text>
|
||||||
|
<text class="text-xs text-gray-600"> {{ item.links.length }} 条记录 </text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分组卡片 -->
|
||||||
|
<view
|
||||||
|
v-for="(link, linkIndex) in item.links" :key="link.metadata.name"
|
||||||
|
class="uh-shadow-card box-border w-full flex flex-col items-center justify-center gap-y-2 overflow-hidden border-2 border-white rounded-xl border-solid bg-white p-4"
|
||||||
|
:class="[
|
||||||
|
linkIndex === item.links.length - 1 ? `group-end-${index}` : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- 顶部信息 -->
|
||||||
|
<view class="box-border w-full flex items-center justify-between gap-x-4">
|
||||||
|
<view
|
||||||
|
class="uh-shadow-md box-border h-11 w-11 shrink-0 overflow-hidden border-2 border-white rounded-xl border-solid bg-white"
|
||||||
|
>
|
||||||
|
<image :src="cover(link.spec.logo)" mode="aspectFill" class="h-full w-full" />
|
||||||
|
</view>
|
||||||
|
<view class="flex flex-1 flex-col gap-y-1">
|
||||||
|
<view class="line-clamp-1 text-sm text-gray-900 font-bold">
|
||||||
|
{{ link.spec.displayName }}
|
||||||
|
</view>
|
||||||
|
<view class="line-clamp-1 text-xs text-gray-400">
|
||||||
|
{{ link.spec.url }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="shrink-0">
|
||||||
|
<view
|
||||||
|
class="uh-shadow-md rounded-3xl bg-[#1A1A1A] px-4 py-2 text-xs text-white"
|
||||||
|
@click="copy(link.spec.url, true)"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 底部信息 -->
|
||||||
|
<view class="line-clamp-2 w-full text-xs text-gray-500 leading-relaxed">
|
||||||
|
{{ link.spec.description }}
|
||||||
|
<!-- {{ '你好,我是 UNI HALO v3.0 新版本,开源免费 Halo 博客小程序,支持多端编译。' }} -->
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
<view v-else class="w-full flex flex-1 items-center justify-center">
|
||||||
|
<wd-empty icon="no-content" tip="暂无友链">
|
||||||
|
<template #image>
|
||||||
|
<wd-icon name="empty" color="#1A1A1A" :size="52" />
|
||||||
|
</template>
|
||||||
|
</wd-empty>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 导航 -->
|
||||||
|
<view
|
||||||
|
v-if="!useBottomGroupNav && allGroupRects.length !== 0" class="fixed right-2 top-1/2 z-50" :style="{
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<view class="uh-shadow-md h-full flex flex-col items-center justify-center gap-y-1 rounded-full bg-white p-1">
|
||||||
|
<view
|
||||||
|
v-for="(item) in allGroupRects" :key="item.index"
|
||||||
|
class="flex flex-col items-center justify-center rounded-full px-1 py-1.5 text-gray-600"
|
||||||
|
:class="{ 'bg-gray-900 text-white py-1.5!': item.active }" @click="scrollToGroup(item)"
|
||||||
|
>
|
||||||
|
<text v-if="item.active" class="text-xs">
|
||||||
|
#
|
||||||
|
</text>
|
||||||
|
<text class="text-[10px] -mt-1">
|
||||||
|
{{ item.name.slice(0, 1) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-if="useBottomGroupNav && allGroupRects.length !== 0"
|
||||||
|
class="fixed bottom-0 left-4 right-4 z-50 box-border pb-4"
|
||||||
|
>
|
||||||
|
<view class="uh-shadow-md box-border w-full flex-1 overflow-hidden rounded-full bg-white p-2">
|
||||||
|
<scroll-view :scroll-x="true" class="uh-shadow-md w-full whitespace-nowrap rounded-full">
|
||||||
|
<view
|
||||||
|
v-for="(item) in allGroupRects" :key="item.index"
|
||||||
|
class="mr-2 inline-block border border-gray-900 rounded-2xl border-solid bg-gray-900 px-3 py-1.5 text-[11px] text-white"
|
||||||
|
@click="scrollToGroup(item.name)"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
<!-- 底部导航占位 -->
|
||||||
|
<uh-tabbar-page-placeholder />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部导航占位 -->
|
||||||
|
<view
|
||||||
|
class="w-full shrink-0" :class="{
|
||||||
|
'pb-10': useBottomGroupNav,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<uh-tabbar-page-placeholder />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.uh-shadow-xs {
|
||||||
|
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.035);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uh-shadow-md {
|
||||||
|
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uh-shadow-card {
|
||||||
|
box-shadow: 0 16rpx 60rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -58,6 +58,12 @@ export const customTabbarList: CustomTabBarItem[] = [
|
|||||||
iconType: 'uiLib',
|
iconType: 'uiLib',
|
||||||
icon: 'send',
|
icon: 'send',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pagePath: 'pages/links/links',
|
||||||
|
text: '%tabbar.links%',
|
||||||
|
iconType: 'uiLib',
|
||||||
|
icon: 'link',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pagePath: 'pages/me/me',
|
pagePath: 'pages/me/me',
|
||||||
text: '%tabbar.me%',
|
text: '%tabbar.me%',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function copy(content: string, showToast: boolean = false) {
|
|||||||
if (showToast) {
|
if (showToast) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '复制成功',
|
title: '复制成功',
|
||||||
icon: 'success',
|
icon: 'none',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
|
import { getCurrentInstance } from 'vue'
|
||||||
|
import type { ComponentInternalInstance } from 'vue'
|
||||||
|
|
||||||
|
interface IOptions {
|
||||||
|
/** 选择器 */
|
||||||
|
selector: string
|
||||||
|
/** 实例 */
|
||||||
|
instance?: ComponentInternalInstance
|
||||||
|
/** 是否获取所有匹配元素 */
|
||||||
|
all?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取元素的位置信息
|
* 获取元素的位置信息
|
||||||
* @param {any} selector 选择器
|
* @param {any} selector 选择器
|
||||||
* @param {boolean} all 是否获取所有匹配元素
|
* @param {boolean} all 是否获取所有匹配元素
|
||||||
* @returns {Promise<any>} 返回一个 Promise,解析为元素的位置信息
|
* @returns {Promise<any>} 返回一个 Promise,解析为元素的位置信息
|
||||||
*/
|
*/
|
||||||
|
export function getRect(options: IOptions): Promise<any> {
|
||||||
import { getCurrentInstance } from 'vue'
|
const { selector, instance = getCurrentInstance(), all = false } = options
|
||||||
|
|
||||||
export function getRect(selector: any, _instance: any = null, all: boolean = false): Promise<any> {
|
|
||||||
const instance = _instance || getCurrentInstance()
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const query = uni.createSelectorQuery()
|
const query = uni.createSelectorQuery()
|
||||||
.in(instance?.proxy)
|
.in(instance?.proxy)
|
||||||
|
|||||||
@@ -97,12 +97,17 @@ export default defineConfig({
|
|||||||
/** 主题色,用法如: text-primary */
|
/** 主题色,用法如: text-primary */
|
||||||
primary: 'var(--wot-color-theme,#0957DE)',
|
primary: 'var(--wot-color-theme,#0957DE)',
|
||||||
page: '#F8F8F8',
|
page: '#F8F8F8',
|
||||||
|
page2: '#F9FAFB',
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
/** 提供更小号的字体,用法如:text-2xs */
|
/** 提供更小号的字体,用法如:text-2xs */
|
||||||
'2xs': ['20rpx', '28rpx'],
|
'2xs': ['20rpx', '28rpx'],
|
||||||
'3xs': ['18rpx', '26rpx'],
|
'3xs': ['18rpx', '26rpx'],
|
||||||
},
|
},
|
||||||
|
shadow: {
|
||||||
|
xs: '0 0 24px rgba(0, 0, 0, 0.075)',
|
||||||
|
card: '0 16rpx 60rpx rgba(0, 0, 0, 0.04)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// windows 系统会报错:[plugin:unocss:transformers:pre] Cannot overwrite a zero-length range - use append Left or prependRight instead.
|
// windows 系统会报错:[plugin:unocss:transformers:pre] Cannot overwrite a zero-length range - use append Left or prependRight instead.
|
||||||
// 去掉下面的就正常了
|
// 去掉下面的就正常了
|
||||||
|
|||||||
Reference in New Issue
Block a user