1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-07-27 04:20:43 +08:00

feat: 完成uni-halo小程序基础功能迭代

本次提交完成多项核心功能升级与优化:
1. 新增页面背景色变量page3并替换部分页面背景样式
2. 新增通用列表分页请求基础接口类型
3. 调整首页默认跳转路径为首页
4. 重构配置结构,新增插件配置模块
5. 新增瞬间、友链、相册相关API接口与类型定义
6. 重构useRequest导出方式,修复hook导出问题
7. 移除部分调试日志代码
8. 新增padStart兼容polyfill工具函数
9. 新增文章卡片多种布局样式支持
10. 优化友链页面UI展示与交互逻辑
11. 新增瞬间页面时间轴展示模式与时间解析工具
12. 新增相册页面多种浏览模式切换功能
This commit is contained in:
小莫唐尼
2026-06-14 22:00:54 +08:00
parent a60def91a0
commit 21eeb912fd
17 changed files with 717 additions and 150 deletions
+68 -6
View File
@@ -1,13 +1,52 @@
import { http } from '@/http/alova' import { http } from '@/http/alova'
import type { IHaloListPaginationRequestBase, IHaloListResponseBase } from '@/api/types/halo'
const EndpointBase = '/apis/api.link.halo.run/v1alpha1'
export type LinkTypes = 'website' | 'mpweixin' | 'android' | 'ios'
export interface ILink {
metadata: {
name: string
version: string
creationTimestamp: string
}
spec: {
displayName: string
url: string
logo: string
groupName: string
priority: number
description: string
}
// uni halo 独有配置
uh: {
// 站点类型
type: LinkTypes
}
}
export interface ILinkGroup {
metadata: {
name: string
creationTimestamp: string
version: string
}
spec: {
name?: string
displayName: string
priority: number
links: ILink[]
}
links: ILink[]
}
/** /**
* 获取链接组分组列表 * 获取链接组分组列表
* @param params 获取链接分分组列表请求参数
* @returns 获取链接分分组组列表响应参数 * @returns 获取链接分分组组列表响应参数
*/ */
export function queryLinkGroups(params: any) { export function queryLinkGroups() {
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/linkgroups`, { return http.Get<ILinkGroup[]>(`${EndpointBase}/linkgroups`, {
params,
meta: { isHalo: true }, meta: { isHalo: true },
}) })
} }
@@ -17,9 +56,32 @@ export function queryLinkGroups(params: any) {
* @param params 获取链接列表请求参数 * @param params 获取链接列表请求参数
* @returns 获取链接列表响应参数 * @returns 获取链接列表响应参数
*/ */
export function queryLinks(params: any) { export function queryLinks(params: IHaloListPaginationRequestBase) {
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/links`, { return http.Get<IHaloListResponseBase<ILink>>(`${EndpointBase}/links`, {
params, params,
meta: { isHalo: true }, meta: { isHalo: true },
}) })
} }
/**
* 随机返回一组链接
* @param params 随机返回一组链接请求参数
* @param params.maxSize 随机返回的链接数量 取值范围为 1 到 100
* @returns 获取随机一组链接响应参数
*/
export function queryRandomLinks(params: { maxSize: number }) {
return http.Get<ILink[]>(`${EndpointBase}/-/random`, {
params,
meta: { isHalo: true },
})
}
/**
* 获取链接总数
* @returns 获取链接总数响应参数
*/
export function queryTotalLinks() {
return http.Get<number>(`${EndpointBase}/-/total`, {
meta: { isHalo: true },
})
}
+53 -3
View File
@@ -1,14 +1,64 @@
import { http } from '@/http/alova' import { http } from '@/http/alova'
import type { TimeParseResult } from '@/utils/base/time'
import type { IHaloListResponseBase } from '@/api/types/halo'
const EndpointBase = '/apis/api.moment.halo.run/v1alpha1/moments' const EndpointBase = '/apis/api.moment.halo.run/v1alpha1/moments'
export interface IMomentListRequest {
page: number // 分页页码,从 1 开始
size: number // 分页条数
tag?: string // 标签
ownerName?: string // 创建者用户名 name
startDate?: string // 开始时间 通过时间区间筛选发布时间
endDate?: string // 结束时间
sort?: string[] // 排序字段,格式为 字段名,排序方式,排序方式可选值为 asc 或 desc,如 spec.releaseTime,desc
}
export type MomentMediaType = 'PHOTO' | 'VIDEO' | 'POST' | 'AUDIO'
export interface IMoment {
metadata: {
name: string // 唯一标识
creationTimestamp: string // 创建时间
}
spec: {
content: {
raw: string // 原始内容,一般用于编辑器使用
html: string // HTML 内容,用于主题端进行最终渲染的内容
medium: [
// 媒体内容
{
type: MomentMediaType // 类型
url: string // 链接
originType: string // 原始类型,例如:image/jpeg
},
]
}
_parseReleaseTime: TimeParseResult // 用于后续存储发布时间的结构化时间对象
releaseTime: string // 发布时间
visible: 'PUBLIC' | 'PRIVATE' // 可见性
owner: string // 所属用户
tags: string[] // 所拥有的标签
}
owner: {
name: string // 用户名
avatar: string // 头像
bio: string // 描述
displayName: string // 显示名称
}
stats: {
upvote: number // 点赞数
totalComment: number // 评论数
approvedComment: number // 审核通过的评论数
}
}
/** /**
* 获取瞬间列表 * 获取瞬间列表
* @param params 获取瞬间列表请求参数 * @param params 获取瞬间列表请求参数
* @returns 获取瞬间列表响应参数 * @returns 获取瞬间列表响应参数
*/ */
export function queryMoments(params: any) { export function queryMoments(params: IMomentListRequest) {
return http.Get<any>(`${EndpointBase}`, { return http.Get<IHaloListResponseBase<IMoment>>(`${EndpointBase}`, {
params, params,
meta: { meta: {
isHalo: true, isHalo: true,
@@ -22,7 +72,7 @@ export function queryMoments(params: any) {
* @returns 获取瞬间详情响应参数 * @returns 获取瞬间详情响应参数
*/ */
export function queryMomentByName(name: string) { export function queryMomentByName(name: string) {
return http.Get<any>(`${EndpointBase}/${name}`, { return http.Get<IMoment>(`${EndpointBase}/${name}`, {
meta: { meta: {
isHalo: true, isHalo: true,
}, },
+19 -1
View File
@@ -1,7 +1,25 @@
import { http } from '@/http/alova' import { http } from '@/http/alova'
export interface IUniHaloConfig { export interface IUniHaloConfig {
baseURL: string base: {
// 博客域名
domain: string
// 是否为个人资质的小程序
isIndividual: boolean
}
home: {
// 是否显示金刚区
isShowShortcutDock: boolean
}
moment: {
}
link: {
}
gallery: {
}
} }
/** /**
+10
View File
@@ -21,3 +21,13 @@ export interface IHaloListRequestBase {
page?: number page?: number
size?: number size?: number
} }
/**
* Halo 列表分页请求基础结构
*/
export interface IHaloListPaginationRequestBase extends IHaloListRequestBase {
keyword?: string
sort?: string
labelSelector?: string
fieldSelector?: string
}
+36 -5
View File
@@ -5,11 +5,11 @@ import type { ListedPostVo } from '@halo-dev/api-client'
interface IProps { interface IProps {
post?: ListedPostVo post?: ListedPostVo
type?: 'default' | 'topCover' | 'rightCover' type?: 'noCover' | 'leftCover' | 'bottomCover' | 'rightCover' | 'topCover'
} }
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
type: 'default', type: 'leftCover',
post: undefined, post: undefined,
}) })
@@ -20,6 +20,15 @@ const wrapClass = computed(() => {
if (props.type === 'rightCover') { if (props.type === 'rightCover') {
return 'relative h-28 p-0! border-2 border-solid border-white' return 'relative h-28 p-0! border-2 border-solid border-white'
} }
if (props.type === 'noCover') {
return 'pl-0'
}
if (props.type === 'leftCover') {
return ' '
}
if (props.type === 'bottomCover') {
return 'flex-col gap-y-2'
}
return '' return ''
}) })
@@ -30,7 +39,16 @@ const coverClass = computed(() => {
if (props.type === 'rightCover') { if (props.type === 'rightCover') {
return 'absolute right-0 top-0 bottom-0 w-32 rounded-lt-0 rounded-lb-0' return 'absolute right-0 top-0 bottom-0 w-32 rounded-lt-0 rounded-lb-0'
} }
if (props.type === 'noCover') {
return 'hidden!'
}
if (props.type === 'leftCover') {
return 'h-22 w-22' return 'h-22 w-22'
}
if (props.type === 'bottomCover') {
return 'w-full h-32 order-2'
}
return ''
}) })
const textWrapClass = computed(() => { const textWrapClass = computed(() => {
@@ -40,6 +58,10 @@ const textWrapClass = computed(() => {
if (props.type === 'rightCover') { if (props.type === 'rightCover') {
return 'p-4 pr-32 relative z-10' return 'p-4 pr-32 relative z-10'
} }
if (props.type === 'bottomCover') {
return 'order-1'
}
return 'pl-4' return 'pl-4'
}) })
@@ -47,9 +69,15 @@ const textContainerClass = computed(() => {
if (props.type === 'topCover') { if (props.type === 'topCover') {
return 'gap-y-2' return 'gap-y-2'
} }
if (props.type === 'noCover') {
return 'gap-y-2'
}
if (props.type === 'rightCover') { if (props.type === 'rightCover') {
return '' return ''
} }
if (props.type === 'bottomCover') {
return 'gap-y-2'
}
return '' return ''
}) })
@@ -60,12 +88,15 @@ const textFooterClass = computed(() => {
if (props.type === 'rightCover') { if (props.type === 'rightCover') {
return 'gap-x-2' return 'gap-x-2'
} }
if (props.type === 'bottomCover') {
return ''
}
return 'gap-x-2' return 'gap-x-2'
}) })
</script> </script>
<template> <template>
<view v-if="post" class="uh-shadow-xs flex justify-center overflow-hidden rounded-2xl bg-white p-4" :class="wrapClass"> <view v-if="post" class="uh-shadow-xs box-border w-full flex justify-center overflow-hidden rounded-2xl bg-white p-4" :class="wrapClass">
<view class="shrink-0 overflow-hidden rounded-lg" :class="coverClass"> <view class="shrink-0 overflow-hidden rounded-lg" :class="coverClass">
<image :src="completeUrl(post.spec.cover)" mode="aspectFill" class="h-full w-full" /> <image :src="completeUrl(post.spec.cover)" mode="aspectFill" class="h-full w-full" />
<view v-if="props.type === 'rightCover'" class="absolute bottom-0 left-0 top-0 z-10 h-full w-full from-white/0 via-white/10 to-white bg-gradient-to-l" /> <view v-if="props.type === 'rightCover'" class="absolute bottom-0 left-0 top-0 z-10 h-full w-full from-white/0 via-white/10 to-white bg-gradient-to-l" />
@@ -79,7 +110,7 @@ const textFooterClass = computed(() => {
{{ post.status.excerpt }} {{ post.status.excerpt }}
</view> </view>
<view class="w-full flex items-center" :class="textFooterClass"> <view class="w-full flex items-center" :class="textFooterClass">
<view class="text-[11px] text-gray-600"> <view class="line-clamp-1 text-[11px] text-gray-600">
{{ timeFrom(post.spec.publishTime) }} {{ timeFrom(post.spec.publishTime) }}
</view> </view>
<view class="shrink-0 text-xs text-gray-600"> <view class="shrink-0 text-xs text-gray-600">
@@ -99,7 +130,7 @@ const textFooterClass = computed(() => {
<view class="shrink-0 text-xs text-gray-600"> <view class="shrink-0 text-xs text-gray-600">
· ·
</view> </view>
<view class="text-[11px] text-gray-600"> <view class="line-clamp-1 text-[11px] text-gray-600">
浏览 {{ post.stats.visit }} 浏览 {{ post.stats.visit }}
</view> </view>
</view> </view>
-4
View File
@@ -57,7 +57,6 @@ export function useListData<T>(options: IUseListDataOptions<T>) {
} }
function refresh() { function refresh() {
console.log('刷新数据', options)
if (options.loading.value) { if (options.loading.value) {
return return
} }
@@ -68,7 +67,6 @@ export function useListData<T>(options: IUseListDataOptions<T>) {
/** 下拉刷新 */ /** 下拉刷新 */
onPullDownRefresh(() => { onPullDownRefresh(() => {
console.log('下拉刷新', options)
refresh() refresh()
}) })
@@ -87,8 +85,6 @@ export function useListData<T>(options: IUseListDataOptions<T>) {
/** 监听items变化 */ /** 监听items变化 */
watch(() => _items.value, (newVal) => { watch(() => _items.value, (newVal) => {
console.log('监听数据变化', newVal)
console.log('监听数据变化', options)
if (_error.value) { if (_error.value) {
return return
} }
+3 -1
View File
@@ -32,7 +32,7 @@ interface IUseRequestReturn<T, P = undefined> {
* @param options.loadingToastMask 加载提示是否显示mask,默认为false。 * @param options.loadingToastMask 加载提示是否显示mask,默认为false。
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。 * @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
*/ */
export default function useRequest<T, P = undefined>( export function useRequest<T, P = undefined>(
func: (args?: P) => Promise<T>, func: (args?: P) => Promise<T>,
options: IUseRequestOptions<T> = { immediate: false, loadingToast: false, loadingToastContent: '加载中', loadingToastMask: false }, options: IUseRequestOptions<T> = { immediate: false, loadingToast: false, loadingToastContent: '加载中', loadingToastMask: false },
): IUseRequestReturn<T, P> { ): IUseRequestReturn<T, P> {
@@ -70,3 +70,5 @@ export default function useRequest<T, P = undefined>(
} }
return { loading, error, data, run } return { loading, error, data, run }
} }
export default useRequest
+119 -13
View File
@@ -3,6 +3,7 @@ import useRequest from '@/hooks/useRequest'
import { queryPhotoGroups, queryPhotos } from '@/api/halo-plugin/photo' import { queryPhotoGroups, queryPhotos } from '@/api/halo-plugin/photo'
import type { IPhoto, IPhotoGroup } from '@/api/halo-plugin/photo' import type { IPhoto, IPhotoGroup } from '@/api/halo-plugin/photo'
import { completeUrl } from '@/utils/url' import { completeUrl } from '@/utils/url'
import random from '@/utils/base/random'
defineOptions({ defineOptions({
name: 'Gallery', name: 'Gallery',
@@ -28,7 +29,7 @@ interface IGalleryGroup {
const groupBanner = reactive({ const groupBanner = reactive({
current: 0, current: 0,
}) })
const galleryStyle = ref('list') const galleryStyle = ref('mix')
const showListPanel = reactive({ const showListPanel = reactive({
show: false, show: false,
metadataName: '', metadataName: '',
@@ -46,7 +47,7 @@ function onGroupBannerChange(e: any) {
function getGroupImageRotateClass(index: number) { function getGroupImageRotateClass(index: number) {
if (index === 0) { if (index === 0) {
return 'z-30 border-2 border-solid border-white uh-shadow-card group-hover:scale-95' return 'z-30 border-2 border-solid border-white uh-shadow-md group-hover:scale-95'
} }
if (index === 1) { if (index === 1) {
return 'z-20 -rotate-6 group-hover:-rotate-22 border-2 border-solid border-white uh-shadow-card' return 'z-20 -rotate-6 group-hover:-rotate-22 border-2 border-solid border-white uh-shadow-card'
@@ -107,11 +108,12 @@ function getGalleryPhotosByGroups() {
} }
// 执行对每个分组查询图片 // 执行对每个分组查询图片
galleryGroups.value.forEach(async (group) => { galleryGroups.value.forEach(async (group) => {
const randomSize = random(1, 4)
const queryParams = { const queryParams = {
group: undefined, group: undefined,
fieldSelector: undefined, fieldSelector: undefined,
page: 1, page: 1,
size: 4, size: randomSize,
} }
if (group.metadataName.trim()) { if (group.metadataName.trim()) {
queryParams.group = group.metadataName queryParams.group = group.metadataName
@@ -178,7 +180,28 @@ onPageScroll(() => { })
</script> </script>
<template> <template>
<view class="min-h-screen w-full flex flex-col bg-page"> <view class="bg-page3 min-h-screen w-full flex flex-col">
<!-- 测试 -->
<view class="box-border w-full p-4 pb-0">
<view class="uh-shadow-card box-border w-full flex items-center gap-x-4 rounded-xl bg-white p-4 text-sm">
<view class="shrink-0">
{{ galleryStyle }}
</view>
<view class="box-border shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="galleryStyle = 'card'">
卡片
</view>
<view class="box-border shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="galleryStyle = 'list'">
列表
</view>
<view class="box-border shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="galleryStyle = 'mix'">
混排
</view>
<view class="box-border shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="galleryStyle = 'timeline'">
时间轴
</view>
</view>
</view>
<!-- 分组列表常态显示 --> <!-- 分组列表常态显示 -->
<view v-if="galleryGroups.length !== 0" class="box-border w-full"> <view v-if="galleryGroups.length !== 0" class="box-border w-full">
<!-- 顶部区域固定区域 --> <!-- 顶部区域固定区域 -->
@@ -196,7 +219,10 @@ onPageScroll(() => { })
class="absolute bottom-0 left-0 right-0 z-10 h-2/5 from-transparent via-page/30 to-page bg-gradient-to-b" class="absolute bottom-0 left-0 right-0 z-10 h-2/5 from-transparent via-page/30 to-page bg-gradient-to-b"
/> />
</view> </view>
<view v-if="galleryGroups.length > 1 && galleryGroups[0].photos.length >= 4" class="relative z-10 box-border w-full flex items-center justify-center gap-x-2 overflow-hidden p-4 -mt-12"> <view
v-if="galleryGroups.length > 1 && galleryGroups[0].photos.length >= 4"
class="relative z-10 box-border w-full flex items-center justify-center gap-x-2 overflow-hidden p-4 -mt-12"
>
<view class="uh-shadow-card box-border w-full flex items-center justify-center gap-x-4 rounded-xl bg-white p-4"> <view class="uh-shadow-card box-border w-full flex items-center justify-center gap-x-4 rounded-xl bg-white p-4">
<image <image
v-for="(photo, index) in galleryGroups[0].photos.slice(0, 5)" :key="photo.metadata.name" v-for="(photo, index) in galleryGroups[0].photos.slice(0, 5)" :key="photo.metadata.name"
@@ -216,11 +242,40 @@ onPageScroll(() => { })
<text class="text-sm text-gray-900 font-bold">我的相册</text> <text class="text-sm text-gray-900 font-bold">我的相册</text>
<text class="text-[10px] text-gray-600"> 30 组记录</text> <text class="text-[10px] text-gray-600"> 30 组记录</text>
</view> </view>
<!-- 卡片模式 -->
<view v-if="galleryStyle === 'card'" class="grid grid-cols-2 mt-4 w-full gap-4">
<view
v-for="group in galleryGroups" :key="group.metadataName"
class="group relative box-border h-36 w-full flex items-center justify-center p-1"
@click="handleShowListPanel(group)"
>
<!-- 预览图区域 -->
<view class="relative h-full w-full">
<view
v-for="(photo, index) in group.photos.slice(0, 1)" :key="photo.metadata.name"
class="uh-shadow-md relative left-0 top-0 z-30 box-border h-full w-full origin-center overflow-hidden border-2 border-white rounded-2xl border-solid"
>
<image :src="completeUrl(photo.spec.url)" mode="scaleToFill" class="h-full w-full transition-all duration-300 group-hover:scale-110" />
<!-- 文字区域 -->
<view
v-if="index === 0"
class="absolute bottom-0 left-0 right-0 z-40 w-full flex flex-col gap-y-0.5 from-transparent to-black/90 bg-gradient-to-b px-2 py-1.5"
>
<text class="text-xs text-white font-bold">{{ group.displayName }}</text>
<text class="text-xs text-white/80">{{ group.photoCount }} 张图片</text>
</view>
</view>
</view>
</view>
</view>
<!-- 混合模式 --> <!-- 混合模式 -->
<view v-if="galleryStyle === 'mix'" class="grid grid-cols-2 mt-4 w-full gap-4"> <view v-if="galleryStyle === 'mix'" class="grid grid-cols-2 mt-4 w-full gap-4">
<view <view
v-for="group in galleryGroups" :key="group.metadataName" v-for="group in galleryGroups" :key="group.metadataName"
class="group relative box-border h-36 w-full flex items-center justify-center p-1" class="group relative box-border h-36 w-full flex items-center justify-center p-1"
@click="handleShowListPanel(group)"
> >
<!-- 预览图区域 --> <!-- 预览图区域 -->
<view class="relative h-full w-full"> <view class="relative h-full w-full">
@@ -229,14 +284,14 @@ onPageScroll(() => { })
class="absolute left-0 top-0 box-border h-full w-full origin-center overflow-hidden rounded-2xl transition-all duration-300" class="absolute left-0 top-0 box-border h-full w-full origin-center overflow-hidden rounded-2xl transition-all duration-300"
:class="[getGroupImageRotateClass(index)]" :class="[getGroupImageRotateClass(index)]"
> >
<image :src="photo.spec.url" mode="scaleToFill" class="h-full w-full" /> <image :src="completeUrl(photo.spec.url)" mode="scaleToFill" class="h-full w-full" />
<!-- 文字区域 --> <!-- 文字区域 -->
<view <view
v-if="index === 0" v-if="index === 0"
class="absolute bottom-0 left-0 right-0 z-40 w-full flex flex-col gap-y-0.5 from-transparent to-black/90 bg-gradient-to-b px-2 py-1.5" class="absolute bottom-0 left-0 right-0 z-40 w-full flex flex-col gap-y-0.5 from-transparent to-black/90 bg-gradient-to-b px-2 py-1.5"
> >
<text class="text-xs text-white font-bold">{{ group.displayName }}</text> <text class="text-xs text-white font-bold">{{ group.displayName }}</text>
<text class="text-[8px] text-white/80">{{ group.photoCount }} 张图片</text> <text class="text-xs text-white/80">{{ group.photoCount }} 张图片</text>
</view> </view>
</view> </view>
</view> </view>
@@ -248,7 +303,7 @@ onPageScroll(() => { })
<template v-for="group in galleryGroups" :key="group.metadataName"> <template v-for="group in galleryGroups" :key="group.metadataName">
<view <view
v-if="group.photoCount > 0" v-if="group.photoCount > 0"
class="box-border flex flex-col gap-y-2 overflow-hidden rounded-xl bg-white p-4 pt-3" class="uh-shadow-card box-border flex flex-col gap-y-2 overflow-hidden rounded-xl bg-white p-4 pt-3"
@click="handleShowListPanel(group)" @click="handleShowListPanel(group)"
> >
<!-- 顶部区域 --> <!-- 顶部区域 -->
@@ -257,7 +312,7 @@ onPageScroll(() => { })
<view class="hidden h-2 w-2 rounded-full bg-gray-900" /> {{ group.displayName }} <view class="hidden h-2 w-2 rounded-full bg-gray-900" /> {{ group.displayName }}
</view> </view>
<view class="shrink-0"> <view class="shrink-0">
<text class="text-[10px] text-gray-400">{{ group.photoCount }} </text> <text class="text-xs text-gray-400">{{ group.photoCount }} </text>
</view> </view>
</view> </view>
@@ -279,6 +334,59 @@ onPageScroll(() => { })
</view> </view>
</template> </template>
</view> </view>
<!-- 时间轴卡片模式 -->
<view v-else-if="galleryStyle === 'timeline'" class="mt-2 w-full flex flex-col gap-y-4">
<template v-for="(group, groupIndex) in galleryGroups" :key="group.metadataName">
<view v-if="group.photoCount > 0" class="flex">
<!-- 左边区域 -->
<view class="relative h-auto w-4 flex shrink-0 items-center justify-center">
<view
class="uh-shadow-md h-full w-px bg-gray-300" :class="{
'mt-6 h-[110%]!': groupIndex !== galleryGroups.length - 1,
}"
/>
<view
class="uh-shadow-md absolute left-1/2 top-0 box-border h-4 w-4 border-2 border-white rounded-full border-solid bg-gray-900"
:style="{
transform: 'translateX(-50%)',
}"
/>
</view>
<view
class="box-border flex flex-1 flex-col gap-y-2 overflow-hidden pl-4"
@click="handleShowListPanel(group)"
>
<!-- 顶部区域 -->
<view class="flex items-center justify-between">
<view class="flex items-center gap-x-2 text-sm font-bold">
<view class="hidden h-2 w-2 rounded-full bg-gray-900" /> {{ group.displayName }}
</view>
<view class="shrink-0">
<text class="text-[10px] text-gray-400">{{ group.photoCount }} </text>
</view>
</view>
<!-- 图片列表 -->
<view
class="uh-shadow-card grid gap-0.5 overflow-hidden rounded-lg bg-white p-4"
:class="[getGalleryListContainerClass(group.photos.slice(0, 4).length)]"
>
<view
v-for="(photo, index) in group.photos.slice(0, 4)" :key="photo.metadata.name"
class="overflow-hidden border-2 border-white rounded-lg border-solid"
:class="getGalleryListItemClass(index, group.photos.slice(0, 4).length)"
>
<image
:src="completeUrl(photo.spec.url)" mode="aspectFill"
class="h-full w-full object-cover transition-transform duration-500 hover:scale-105"
/>
</view>
</view>
</view>
</view>
</template>
</view>
</view> </view>
</view> </view>
<!-- 分组列表无数据显示 --> <!-- 分组列表无数据显示 -->
@@ -292,10 +400,8 @@ onPageScroll(() => { })
<!-- 相册列表根据分组显示 --> <!-- 相册列表根据分组显示 -->
<uh-gallery-panel <uh-gallery-panel
v-if="showListPanel.show" v-if="showListPanel.show" :group-metadata-name="showListPanel.metadataName"
:group-metadata-name="showListPanel.metadataName" :group-display-name="showListPanel.displayName" @close="showListPanel.show = false"
:group-display-name="showListPanel.displayName"
@close="showListPanel.show = false"
/> />
<!-- 底部导航占位 --> <!-- 底部导航占位 -->
+30 -4
View File
@@ -23,6 +23,11 @@ definePage({
}, },
}) })
type PostCardStyle = 'noCover' | 'leftCover' | 'bottomCover' | 'rightCover' | 'topCover'
const postCardStyle = ref<PostCardStyle>('leftCover')
const showPostCardModeSelector = ref(false)
const exampleImageUrls = reactive({ const exampleImageUrls = reactive({
banner: 'https://fastly.picsum.photos/id/353/800/800.jpg?hmac=RaDuQ92sSXj4q5vOYna9G00J7KrBUC3eBS0slCWYZXA', banner: 'https://fastly.picsum.photos/id/353/800/800.jpg?hmac=RaDuQ92sSXj4q5vOYna9G00J7KrBUC3eBS0slCWYZXA',
avatar: 'https://www.xiaoxiaomo.cn/logo.jpg', avatar: 'https://www.xiaoxiaomo.cn/logo.jpg',
@@ -122,7 +127,7 @@ onPageScroll(() => { })
</script> </script>
<template> <template>
<view class="min-h-screen bg-page2"> <view class="bg-page3 min-h-screen">
<!-- 顶部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" />
@@ -164,7 +169,28 @@ onPageScroll(() => { })
<!-- 作者卡片 --> <!-- 作者卡片 -->
<view class="relative z-10 box-border px-4 -mt-12"> <view class="relative z-10 box-border px-4 -mt-12">
<profile-card /> <profile-card @click="showPostCardModeSelector = !showPostCardModeSelector" />
</view>
<!-- 测试 -->
<view v-if="showPostCardModeSelector" class="box-border w-full p-4 pb-0">
<view class="uh-shadow-card box-border w-full flex items-center gap-x-2 rounded-xl bg-white p-4 text-sm">
<view class="box-border flex-1 shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="postCardStyle = 'noCover'">
无封面
</view>
<view class="box-border flex-1 shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="postCardStyle = 'leftCover'">
左封面
</view>
<view class="box-border flex-1 shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="postCardStyle = 'topCover'">
上封面
</view>
<view class="box-border flex-1 shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="postCardStyle = 'rightCover'">
右封面
</view>
<view class="box-border flex-1 shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="postCardStyle = 'bottomCover'">
下封面
</view>
</view>
</view> </view>
<!-- 金刚区 --> <!-- 金刚区 -->
@@ -172,7 +198,7 @@ onPageScroll(() => { })
<view class="flex items-center justify-around gap-x-4"> <view class="flex items-center justify-around gap-x-4">
<view <view
v-for="item in quickList" :key="item.pagePath" v-for="item in quickList" :key="item.pagePath"
class="w-1/5 flex flex-col items-center justify-center gap-y-1" class="flex flex-1 flex-col items-center justify-center gap-y-1"
> >
<view class="uh-shadow-xs flex items-center justify-center rounded-2xl bg-white p-3.5"> <view class="uh-shadow-xs flex items-center justify-center rounded-2xl bg-white p-3.5">
<wd-icon :name="item.icon" :size="24" /> <wd-icon :name="item.icon" :size="24" />
@@ -211,7 +237,7 @@ onPageScroll(() => { })
<wd-empty :icon-size="60" icon="no-result" tip="暂无数据" /> <wd-empty :icon-size="60" icon="no-result" tip="暂无数据" />
</view> </view>
<template v-else> <template v-else>
<post-card v-for="post in postList" :key="post.metadata.name" :post="post" type="topCover" /> <post-card v-for="post in postList" :key="post.metadata.name" :post="post" :type="postCardStyle" />
</template> </template>
</view> </view>
</view> </view>
+3 -2
View File
@@ -14,8 +14,9 @@ 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/gallery/gallery' }) // uni.switchTab({ url: '/pages/links/links' })
// uni.switchTab({ url: '/pages/moments/moments' })
}) })
</script> </script>
+96 -25
View File
@@ -1,13 +1,18 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getCurrentInstance } from 'vue' import { getCurrentInstance } from 'vue'
import useRequest from '@/hooks/useRequest' import { storeToRefs } from 'pinia'
import { useRequest } from '@/hooks/useRequest'
import { random } from '@/utils/base/random'
import { queryLinkGroups, queryLinks } from '@/api/halo-plugin/link' import { queryLinkGroups, queryLinks } from '@/api/halo-plugin/link'
import { cover } from '@/utils/imageHelper' import { cover } from '@/utils/imageHelper'
import { copy } from '@/utils/base/clipboard' import { copy } from '@/utils/base/clipboard'
import { getRect } from '@/utils/base/getRect' import { getRect } from '@/utils/base/getRect'
import { useListData } from '@/hooks/useListData' import { useListData } from '@/hooks/useListData'
import type { IPaginationParams } from '@/hooks/useListData'
import { debounce } from '@/utils/base/debounce' import { debounce } from '@/utils/base/debounce'
import { useUniHaloConfigStore } from '@/store/config'
import type { IPaginationParams } from '@/hooks/useListData'
import type { ILink, ILinkGroup, LinkTypes } from '@/api/halo-plugin/link'
import type { IHaloListResponseBase } from '@/api/types/halo'
defineOptions({ defineOptions({
name: 'Links', name: 'Links',
@@ -21,10 +26,13 @@ definePage({
}, },
}) })
const { loading: loadingGroups, error: errorGroups, data: dataGroups, run: runGroups } = useRequest<any>(queryLinkGroups, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true }) const { config } = storeToRefs(useUniHaloConfigStore())
const { loading, error, data, run } = useRequest<any, IPaginationParams>(queryLinks, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true }) const { reset } = useUniHaloConfigStore()
const { list: links, refresh } = useListData<any>({ const { loading: loadingGroups, error: errorGroups, data: dataGroups, run: runGroups } = useRequest<ILinkGroup[]>(queryLinkGroups, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
const { loading, error, data, run } = useRequest<IHaloListResponseBase<ILink>, IPaginationParams>(queryLinks, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
const { list: links, refresh } = useListData<ILink>({
params: { params: {
page: 1, page: 1,
size: 0, size: 0,
@@ -53,12 +61,20 @@ interface ILinkGroupData {
name: string name: string
index: string index: string
priority: number priority: number
links: any[] links: ILink[]
} }
const linkTypes: LinkTypes[] = ['mpweixin', 'website', 'android', 'ios']
const linkGroups = computed(() => { const linkGroups = computed(() => {
const groupedLinks = links.value.reduce((acc, link) => { const groupedLinks = links.value.reduce((acc, link) => {
const groupName = link.spec.groupName const groupName = link.spec.groupName
// 处理站点类型
link.uh = {
type: linkTypes[random(0, linkTypes.length - 1)],
}
const group = dataGroups.value?.find(item => item.metadata.name === groupName) const group = dataGroups.value?.find(item => item.metadata.name === groupName)
let _groupName = '未分组' let _groupName = '未分组'
if (group) { if (group) {
@@ -74,7 +90,7 @@ const linkGroups = computed(() => {
} }
acc[_groupName].links.push(toRaw(link)) acc[_groupName].links.push(toRaw(link))
return acc return acc
}, {} as Record<string, any[]>) }, {} as Record<string, ILinkGroupData>)
// // 分组还需要根据 分组中的 spec.priority 字段进行排序 // // 分组还需要根据 分组中的 spec.priority 字段进行排序
return Object.values(groupedLinks) return Object.values(groupedLinks)
.map((item) => { .map((item) => {
@@ -149,39 +165,54 @@ onLoad(async () => {
</script> </script>
<template> <template>
<view class="min-h-screen w-full flex flex-col bg-page2"> <view class="bg-page3 min-h-screen w-full flex flex-col">
<!-- <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"> <view v-if="linkGroups.length !== 0" class="box-border w-full flex flex-col gap-y-6 overflow-hidden p-4 pb-0">
<template v-for="(item, index) in linkGroups" :key="item.name"> <view v-for="(item, index) in linkGroups" :key="item.name" class="flex flex-col gap-y-4">
<view <view
v-if="linkGroups.length !== 1" class="flex items-center justify-between text-sm" v-if="linkGroups.length !== 1" class="flex items-center justify-between text-sm"
:class="[`group-start-${index}`]" :class="[`group-start-${index}`]"
> >
<view class="flex items-center gap-x-1 font-bold">
<wd-icon name="folder" :size="16" />
<text class="font-bold">{{ item.name }}</text> <text class="font-bold">{{ item.name }}</text>
</view>
<text class="text-xs text-gray-600"> {{ item.links.length }} 条记录 </text> <text class="text-xs text-gray-600"> {{ item.links.length }} 条记录 </text>
</view> </view>
<!-- 分组卡片 --> <!-- 分组卡片 -->
<view <view
v-for="(link, linkIndex) in item.links" :key="link.metadata.name" 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="uh-shadow-card relative box-border w-full flex flex-col items-center justify-center gap-y-2 overflow-hidden rounded-xl bg-white p-4"
:class="[ :class="[
linkIndex === item.links.length - 1 ? `group-end-${index}` : '', linkIndex === item.links.length - 1 ? `group-end-${index}` : '',
]" ]" :style="{
boxShadow: '0 16rpx 60rpx rgba(0, 0, 0, 0.06)',
}"
> >
<!-- 右上角装饰 -->
<view
class="uh-blur-3xl absolute right-0 top-0 h-12 w-12 rounded-full" :class="[
{
'bg-[#10B981]/30': link.uh.type === 'mpweixin',
'bg-[#007AFF]/30': link.uh.type === 'website',
'bg-[#FF9800]/30': link.uh.type === 'android',
'bg-[#FF5722]/30': link.uh.type === 'ios',
},
]"
/>
<!-- 顶部信息 --> <!-- 顶部信息 -->
<view class="box-border w-full flex items-center justify-between gap-x-3"> <view class="box-border w-full flex items-center justify-between gap-x-3">
<view <view
class="uh-shadow-md box-border h-10 w-10 shrink-0 overflow-hidden border-2 border-white rounded-xl border-solid bg-white" class="uh-shadow-md box-border h-11 w-11 shrink-0 overflow-hidden border-2 border-white rounded-lg border-solid bg-white"
:style="{
boxShadow: '0 16rpx 40rpx rgba(0, 0, 0, 0.055)',
}"
> >
<image :src="cover(link.spec.logo)" mode="aspectFill" class="h-full w-full" /> <image :src="cover(link.spec.logo)" mode="aspectFill" class="h-full w-full" />
</view> </view>
<view class="flex flex-1 flex-col gap-y-0.5"> <view class="box-border flex flex-1 flex-col gap-y-0.5 pl-1">
<view class="line-clamp-1 text-sm text-gray-900 font-bold"> <view class="line-clamp-1 text-sm text-gray-900 font-bold">
{{ link.spec.displayName }} {{ link.spec.displayName }}
</view> </view>
@@ -189,22 +220,58 @@ onLoad(async () => {
{{ link.spec.url }} {{ link.spec.url }}
</view> </view>
</view> </view>
<view class="shrink-0"> <view v-if="false" class="shrink-0">
<view <view
class="uh-shadow-md rounded-3xl bg-[#1A1A1A] px-4 py-1.5 text-xs text-white" class="uh-shadow-md rounded-xl bg-gray-900 px-4 py-1.5 text-xs text-white"
@click="copy(link.spec.url, true)" @click="copy(link.spec.url, true)"
> >
复制 复制
</view> </view>
</view> </view>
</view> </view>
<!-- 底部信息 --> <!-- 站点描述 -->
<view class="line-clamp-2 w-full text-xs text-gray-500 leading-relaxed"> <view v-if="link.spec.description" class="line-clamp-2 mt-1 w-full text-xs text-gray-500 leading-relaxed">
{{ link.spec.description }} {{ link.spec.description }}
<!-- {{ '你好,我是 UNI HALO v3.0 新版本,开源免费 Halo 博客小程序支持多端编译' }} --> <!-- {{ '你好,我是 UNI HALO v3.0 新版本,开源免费 Halo 博客小程序支持多端编译' }} -->
</view> </view>
<!-- 底部操作区域 -->
<view class="w-full flex items-center justify-between">
<view class="flex shrink-0 items-center gap-x-2">
<view
class="box-border flex items-center justify-center border border-gray-200 rounded-lg border-solid p-2 text-gray-600"
>
<wd-icon name="share-alt" :size="14" />
</view>
<view
class="box-border flex items-center justify-center border border-gray-200 rounded-lg border-solid p-2 text-gray-600"
>
<wd-icon name="copy" :size="14" />
</view>
<view
v-if="false"
class="box-border flex items-center justify-center border border-gray-200 rounded-lg border-solid p-2 py-1.5 text-xs text-gray-600"
>
网站
</view>
</view>
<view class="mt-2 flex shrink-0 items-center justify-end gap-x-2">
<button
v-if="config.base.isIndividual"
class="uh-shadow-md rounded-lg bg-gray-900 px-4 py-2 text-xs text-white outline-0 border-none!"
@click="copy(link.spec.url, true)"
>
<wd-icon name="copy" :size="14" /> 复制
</button>
<button
v-if="!config.base.isIndividual"
class="uh-shadow-md rounded-lg bg-gray-900 px-4 py-2 text-xs text-white outline-0 border-none!"
>
<wd-icon name="Launch" :size="14" /> 访问
</button>
</view>
</view>
</view>
</view> </view>
</template>
</view> </view>
<view v-else class="w-full flex flex-1 items-center justify-center"> <view v-else class="w-full flex flex-1 items-center justify-center">
<wd-empty icon="no-content" tip="暂无友链"> <wd-empty icon="no-content" tip="暂无友链">
@@ -216,7 +283,7 @@ onLoad(async () => {
<!-- 导航 --> <!-- 导航 -->
<view <view
v-if="!useBottomGroupNav && allGroupRects.length !== 0" class="fixed right-2 top-1/2 z-50" :style="{ v-if="!useBottomGroupNav && allGroupRects.length !== 0" class="fixed right-2 top-1/2 z-50 hidden!" :style="{
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
}" }"
> >
@@ -278,4 +345,8 @@ onLoad(async () => {
.uh-shadow-card { .uh-shadow-card {
box-shadow: 0 16rpx 60rpx rgba(0, 0, 0, 0.04); box-shadow: 0 16rpx 60rpx rgba(0, 0, 0, 0.04);
} }
.uh-blur-3xl {
filter: blur(64px);
}
</style> </style>
+1 -1
View File
@@ -93,7 +93,7 @@ function radioChange(evt) {
</script> </script>
<template> <template>
<view class="box-border min-h-screen w-full flex flex-col items-center justify-center gap-y-6 bg-page2 px-4"> <view class="bg-page3 box-border min-h-screen w-full flex flex-col items-center justify-center gap-y-6 px-4">
<view class="mt-3 break-all px-3 text-center text-green-500"> <view class="mt-3 break-all px-3 text-center text-green-500">
{{ userInfo.username ? '已登录' : '未登录' }} {{ userInfo.username ? '已登录' : '未登录' }}
</view> </view>
+99 -52
View File
@@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cover } from '@/utils/imageHelper' import { cover } from '@/utils/imageHelper'
import { copy } from '@/utils/base/clipboard'
import { queryMoments } from '@/api/halo-plugin/moment' import { queryMoments } from '@/api/halo-plugin/moment'
import { timeFrom } from '@/utils/base/timeFrom' import { parseDateTime } from '@/utils/base/time'
import { timeFormat } from '@/utils/base/timeFormat' import type { IMoment, IMomentListRequest } from '@/api/halo-plugin/moment'
import type { IHaloListResponseBase } from '@/api/types/halo'
defineOptions({ defineOptions({
name: 'Moments', name: 'Moments',
@@ -17,9 +17,18 @@ definePage({
}, },
}) })
const { loading, error, data, run } = useRequest<any, IPaginationParams>(queryMoments, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true }) const useTimeline = ref(true)
const { list: moments, refresh } = useListData<any>({ const { loading, error, data, run } = useRequest<IHaloListResponseBase<IMoment>, IMomentListRequest>(queryMoments, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
function formatDataItems(items: IMoment[]) {
return items.map((item) => {
item.spec._parseReleaseTime = parseDateTime(item.spec.releaseTime)
return item
})
}
const { list: moments, refresh } = useListData<IMoment>({
params: { params: {
page: 1, page: 1,
size: 10, size: 10,
@@ -30,7 +39,7 @@ const { list: moments, refresh } = useListData<any>({
updateData({ updateData({
error: error.value, error: error.value,
hasNext: data.value?.hasNext, hasNext: data.value?.hasNext,
items: data.value?.items ?? [], items: formatDataItems(data.value?.items ?? []),
}) })
}, },
onReachBottom: async (params, updateData) => { onReachBottom: async (params, updateData) => {
@@ -38,7 +47,7 @@ const { list: moments, refresh } = useListData<any>({
updateData({ updateData({
error: error.value, error: error.value,
hasNext: data.value?.hasNext, hasNext: data.value?.hasNext,
items: data.value?.items ?? [], items: formatDataItems(data.value?.items ?? []),
}) })
}, },
}) })
@@ -54,86 +63,115 @@ onPageScroll(() => { })
</script> </script>
<template> <template>
<view class="min-h-screen w-full flex flex-col bg-page2"> <view class="bg-page3 min-h-screen w-full flex flex-col">
<!-- 测试 -->
<view class="box-border w-full p-4 pb-0">
<view class="uh-shadow-card box-border w-full flex items-center gap-x-4 rounded-xl bg-white p-4 text-sm">
<view class="shrink-0">
模式{{ useTimeline }}
</view>
<view class="box-border shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="useTimeline = false">
纯列表
</view>
<view class="box-border shrink-0 rounded-lg bg-gray-900 px-2 py-1 text-xs text-white" @click="useTimeline = true">
时间轴
</view>
</view>
</view>
<!-- 瞬间列表 --> <!-- 瞬间列表 -->
<view v-if="moments.length !== 0" class="box-border w-full flex flex-col gap-y-4 overflow-hidden p-4 pb-0"> <view v-if="moments.length !== 0" class="box-border w-full flex flex-col gap-y-4 overflow-hidden p-4 pb-0">
<view v-for="moment in moments" :key="moment.metadata.name" class="flex"> <view v-for="(moment, index) in moments" :key="moment.metadata.name" class="flex">
<!-- 左边区域 --> <!-- 左边区域 -->
<view class="relative h-auto w-4 flex shrink-0 items-center justify-center"> <view v-if="useTimeline" class="relative h-auto w-4 flex shrink-0 items-center justify-center">
<view class="uh-shadow-md h-full w-2px bg-white" />
<view <view
class="uh-shadow-md absolute left-1/2 top-0 box-border h-4 w-4 border-2 border-white rounded-full border-solid bg-[#1A1A1A]" class="uh-shadow-md h-full w-px bg-gray-300" :class="{
'mt-6 h-[110%]!': index !== moments.length - 1,
}"
/>
<view
class="uh-shadow-md absolute left-1/2 top-0 box-border h-4 w-4 border-2 border-white rounded-full border-solid bg-gray-900"
:style="{ :style="{
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
}" }"
/> />
</view> </view>
<!-- 瞬间卡片 --> <!-- 瞬间卡片 -->
<view class="box-border flex-1 pl-2">
<view class="mb-2 text-sm text-gray-900 font-semibold">
{{ timeFormat(moment.spec.releaseTime) }}
</view>
<view <view
class="uh-shadow-card box-border w-full flex flex-1 flex-col items-center justify-center gap-y-2 overflow-hidden border-2 border-white rounded-xl border-solid bg-white p-4" class="box-border flex-1"
:class="{ 'pl-2': useTimeline }"
> >
<!-- 用户信息 -->
<view class="box-border w-full flex items-center justify-between gap-x-4">
<view <view
class="uh-shadow-md box-border h-8 w-8 shrink-0 overflow-hidden border-2 border-white rounded-xl border-solid bg-white" class="uh-shadow-card group relative box-border w-full flex flex-1 flex-col items-center justify-center gap-y-2 overflow-hidden border-2 border-white rounded-xl border-solid bg-white p-4"
> >
<image :src="cover(moment.owner.avatar)" mode="aspectFill" class="h-full w-full" /> <view class="w-full flex items-center justify-between">
<!-- 时间信息 -->
<view class="box-border w-full flex flex-col gap-y-1">
<view class="text-xs text-gray-400 uppercase">
{{ moment.spec._parseReleaseTime.year }}
{{ moment.spec._parseReleaseTime.week.en }}
</view> </view>
<view class="flex flex-1 flex-col gap-y-1"> <view class="flex items-center gap-x-2 text-sm text-gray-900 font-bold">
<view class="text-sm text-gray-900 font-bold"> <text>{{ moment.spec._parseReleaseTime.month }}{{ moment.spec._parseReleaseTime.day }}</text>
{{ moment.owner.displayName }} <view class="h-1.5 w-1.5 rounded-full bg-gray-900" />
</view> <text> {{ moment.spec._parseReleaseTime.week.zh }} </text>
<view v-if="false" class="text-xs text-gray-400">
{{ timeFrom(moment.spec.releaseTime) }}
</view> </view>
</view> </view>
<view class="shrink-0"> <!-- 右侧操作 -->
<view <view
class="uh-shadow-md rounded-3xl bg-[#1A1A1A] px-4 py-1.5 text-xs text-white" class="uh-shadow-md shrink-0 rounded-lg bg-[#1A1A1A] px-4 py-1.5 text-xs text-white opacity-0 group-hover:opacity-100!"
> >
详情 复制
</view> </view>
</view> </view>
<!-- 右侧年份装饰 -->
<view v-if="false" class="absolute right-4 top-1.5">
<text class="outline-text text-11 text-gray-900 font-bold opacity-0 transition-all duration-300 group-hover:opacity-100!">
{{ moment.spec._parseReleaseTime.year }}
</text>
</view> </view>
<!-- 瞬间内容 --> <!-- 瞬间内容 -->
<view class="line-clamp-6 w-full text-xs text-gray-900 leading-relaxed"> <view class="line-clamp-6 mt-2 w-full text-sm text-gray-900 leading-5 tracking-wide">
<rich-text :nodes="moment.spec.content.raw" /> <rich-text :nodes="moment.spec.content.raw" />
</view> </view>
<!-- 瞬间附件 --> <!-- 瞬间附件 -->
<view class="w-full"> <view class="mt-2 hidden w-full">
<!-- 如果有附件 --> <!-- 如果有附件 -->
</view> </view>
<!-- 分割线 -->
<view class="h-1px w-full bg-gray-50" /> <!-- 标签 -->
<!-- 底部信息标签时间点赞数评论数 --> <view v-if="false && moment.spec.tags.length !== 0" class="mt-2 w-full flex items-center gap-x-2">
<view class="w-full flex items-center justify-between gap-x-2 text-xs text-gray-500 leading-relaxed"> <view v-for="tag in moment.spec.tags" :key="tag" class="text-xs text-gray-600">
<template v-if="moment.spec.tags.length !== 0">
<view class="shrink-0">
<view
v-for="tag in moment.spec.tags" :key="tag"
class="text-xs text-gray-600"
>
{{ tag }} {{ tag }}
</view> </view>
</view> </view>
<view class="shrink-0 text-xs text-gray-600">
·
</view>
</template>
<view class="text-[11px] text-gray-600"> <!-- 分割线 -->
点赞 {{ moment.stats.upvote }} <view class="mb-1 mt-2 h-1px w-full bg-gray-50" />
<!-- 底部信息标签时间点赞数评论数 -->
<view class="w-full flex items-center justify-between gap-x-2 text-xs text-gray-500 leading-relaxed">
<!-- 用户信息 -->
<view class="box-border w-full flex items-center justify-between gap-x-2">
<view
class="uh-shadow-md box-border h-6 w-6 shrink-0 overflow-hidden border-2 border-white rounded-lg border-solid bg-white"
>
<image :src="cover(moment.owner.avatar)" mode="aspectFill" class="h-full w-full" />
</view> </view>
<view class="shrink-0 text-xs text-gray-600"> <view class="flex-1 text-sm text-gray-600">
· {{ moment.owner.displayName }}
</view>
</view>
<view class="flex shrink-0 items-center justify-end gap-x-4">
<view class="flex items-center gap-x-1 text-sm text-gray-600">
<wd-icon name="heart" :size="16" /> {{ moment.stats.upvote }}
</view>
<view class="flex items-center gap-x-1 text-sm text-gray-600">
<wd-icon name="message" :size="16" /> {{ moment.stats.totalComment }}
</view> </view>
<view class="text-[11px] text-gray-600">
评论 {{ moment.stats.totalComment }}
</view> </view>
</view> </view>
</view> </view>
@@ -154,3 +192,12 @@ onPageScroll(() => { })
</view> </view>
</view> </view>
</template> </template>
<style scoped>
.outline-text {
color: transparent;
/* 填充透明 */
-webkit-text-stroke: 2rpx #1a1a1a;
/* 描边宽度+颜色 */
}
</style>
+25 -2
View File
@@ -4,7 +4,22 @@ import type { IUniHaloConfig } from '@/api/halo-plugin/uni-halo'
import { deepMerge } from '@/utils/base/deepMerge' import { deepMerge } from '@/utils/base/deepMerge'
const defaultConfig: IUniHaloConfig = { const defaultConfig: IUniHaloConfig = {
baseURL: import.meta.env.VITE_UNI_HALO_BASEURL, base: {
domain: import.meta.env.VITE_UNI_HALO_DOMAIN,
isIndividual: false,
},
home: {
isShowShortcutDock: true,
},
moment: {
},
link: {
},
gallery: {
},
} }
const ConfigsStoreKey = 'UNI_HALO_CONFIG' const ConfigsStoreKey = 'UNI_HALO_CONFIG'
@@ -21,7 +36,8 @@ export const useUniHaloConfigStore = defineStore('uniHaloConfig', () => {
}, defaultConfig)) }, defaultConfig))
/** /**
* 获取插件配置 * 初始化配置,从服务器获取配置并合并到默认配置
* 一般只需要在全局调用一次即可
*/ */
async function init() { async function init() {
try { try {
@@ -35,9 +51,16 @@ export const useUniHaloConfigStore = defineStore('uniHaloConfig', () => {
} }
} }
function reset() {
config.value = defaultConfig
console.log('reset', config.value)
}
return { return {
defaultConfig,
config, config,
init, init,
reset,
} }
}, { }, {
persist: true, persist: true,
+142
View File
@@ -0,0 +1,142 @@
/**
* padStart 的 polyfill,因为某些机型或情况,还无法支持es7的padStart,比如电脑版的微信小程序
* 所以这里做一个兼容polyfill的兼容处理
*/
export function padStartPolyfill() {
if (!String.prototype.padStart) {
// 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解
String.prototype.padStart = function (this: string, maxLength: number, fillString: string = ' '): string {
if (Object.prototype.toString.call(fillString) !== '[object String]')
throw new TypeError('fillString must be String')
if (this.length >= maxLength) {
return String(this)
}
const fillLength = maxLength - this.length
let times = Math.ceil(fillLength / fillString.length)
while (times) {
times >>= 1
fillString += fillString
if (times === 1) {
fillString += fillString
}
}
return fillString.slice(0, fillLength) + this
}
}
}
padStartPolyfill()
/**
* 时间解析结果类型
*/
export interface TimeParseResult {
year: number
month: string
day: string
hour: string
minute: string
second: string
quarter: {
en: string
zh: string
value: number
}
week: {
en: string
zh: string
value: number
}
}
/**
* 解析时间字符串,返回年月日时分秒、季度、周信息
* @param timeStr 时间字符串(标准时间格式 / 时间戳字符串 / 时间戳)
* @returns 结构化时间对象
*/
export function parseDateTime(timeStr: string | number): TimeParseResult {
const date = new Date(timeStr)
// 校验时间合法性
if (Number.isNaN(date.getTime())) {
throw new TypeError('传入的时间字符串格式不合法')
}
// 基础时间
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
const second = String(date.getSeconds()).padStart(2, '0')
// ========== 季度处理 ==========
const monthNum = date.getMonth() + 1
let quarterValue: number
let quarterEn: string
let quarterZh: string
if (monthNum >= 1 && monthNum <= 3) {
quarterValue = 1
quarterEn = 'Q1'
quarterZh = '第一季度'
}
else if (monthNum >= 4 && monthNum <= 6) {
quarterValue = 2
quarterEn = 'Q2'
quarterZh = '第二季度'
}
else if (monthNum >= 7 && monthNum <= 9) {
quarterValue = 3
quarterEn = 'Q3'
quarterZh = '第三季度'
}
else {
quarterValue = 4
quarterEn = 'Q4'
quarterZh = '第四季度'
}
// ========== 周处理(国内规则:周一=1,周日=7 ==========
// getDay(): 0=周日,1=周一...6=周六
const originWeek = date.getDay()
let weekValue: number
if (originWeek === 0) {
weekValue = 7
}
else {
weekValue = originWeek
}
const weekMap = [
{ en: 'Sunday', zh: '星期日' },
{ en: 'Monday', zh: '星期一' },
{ en: 'Tuesday', zh: '星期二' },
{ en: 'Wednesday', zh: '星期三' },
{ en: 'Thursday', zh: '星期四' },
{ en: 'Friday', zh: '星期五' },
{ en: 'Saturday', zh: '星期六' },
]
const weekItem = weekMap[originWeek]
return {
year,
month,
day,
hour,
minute,
second,
quarter: {
en: quarterEn,
zh: quarterZh,
value: quarterValue,
},
week: {
en: weekItem.en,
zh: weekItem.zh,
value: weekValue,
},
}
}
export default parseDateTime
+3 -22
View File
@@ -1,25 +1,6 @@
// padStart 的 polyfill,因为某些机型或情况,还无法支持es7的padStart,比如电脑版的微信小程序 import { padStartPolyfill } from './time'
// 所以这里做一个兼容polyfill的兼容处理
if (!String.prototype.padStart) { padStartPolyfill()
// 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解
String.prototype.padStart = function (this: string, maxLength: number, fillString: string = ' '): string {
if (Object.prototype.toString.call(fillString) !== '[object String]')
throw new TypeError('fillString must be String')
if (this.length >= maxLength) {
return String(this)
}
const fillLength = maxLength - this.length
let times = Math.ceil(fillLength / fillString.length)
while (times) {
times >>= 1
fillString += fillString
if (times === 1) {
fillString += fillString
}
}
return fillString.slice(0, fillLength) + this
}
}
/** /**
* 时间格式化 * 时间格式化
+1
View File
@@ -98,6 +98,7 @@ export default defineConfig({
primary: 'var(--wot-color-theme,#0957DE)', primary: 'var(--wot-color-theme,#0957DE)',
page: '#F8F8F8', page: '#F8F8F8',
page2: '#F9FAFB', page2: '#F9FAFB',
page3: '#F8FAFC',
}, },
fontSize: { fontSize: {
/** 提供更小号的字体,用法如:text-2xs */ /** 提供更小号的字体,用法如:text-2xs */