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

feat: 新增点赞、游客评论、分享等核心功能,重构页面逻辑

1. 新增全局点赞状态管理,使用metadata.name做key并持久化存储
2. 新增游客信息存储与评论面板,支持游客评论互动
3. 新增全局分享hook,为多个页面配置分享功能
4. 重构登录页面,支持登录/注册切换与游客访问模式
5. 合并注册页到登录页,简化路由结构
6. 优化剪贴板工具函数,支持自定义提示文本
7. 重构个人中心页面,支持游客信息管理
8. 修复相册页面标题与样式问题
9. 移除无用的备份文件与测试代码
This commit is contained in:
小莫唐尼
2026-06-16 00:43:48 +08:00
parent 1ba71ff43f
commit a9e649e110
19 changed files with 1077 additions and 597 deletions
+2
View File
@@ -0,0 +1,2 @@
我们需要全局缓存一个点赞的列表,使用 metadata.name 做key,存储到store并且开启持久化存储,如果已经点过赞,直接显示已
经点赞的样式。同时我们的点赞接口是不会返回内容的,所以再捕获异常的时候不要提示异常信息。
+4
View File
@@ -1,6 +1,10 @@
import { http } from '@/http/alova' import { http } from '@/http/alova'
export interface IUniHaloConfig { export interface IUniHaloConfig {
// 应用信息配置
app: {
name: string
}
base: { base: {
// 博客域名 // 博客域名
domain: string domain: string
@@ -0,0 +1,137 @@
<script setup lang="ts">
import { useGuestStore } from '@/store/guest'
import type { IGuestInfo } from '@/store/guest'
const emits = defineEmits<{
(e: 'close'): void
(e: 'saved'): void
}>()
const guestStore = useGuestStore()
const formData = reactive<IGuestInfo>({
displayName: guestStore.info.displayName || '',
email: guestStore.info.email || '',
website: guestStore.info.website || '',
})
const rules = reactive({
displayName: '',
email: '',
})
function validate(): boolean {
let valid = true
rules.displayName = ''
rules.email = ''
if (!formData.displayName.trim()) {
rules.displayName = '请输入昵称'
valid = false
}
if (!formData.email.trim()) {
rules.email = '请输入邮箱'
valid = false
}
else if (!/^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/.test(formData.email)) {
rules.email = '邮箱格式不正确'
valid = false
}
return valid
}
function handleSubmit() {
if (!validate())
return
guestStore.save({
displayName: formData.displayName.trim(),
email: formData.email.trim(),
website: formData.website.trim(),
})
uni.showToast({ title: '保存成功', icon: 'none' })
emits('saved')
emits('close')
}
function handleClose() {
emits('close')
}
</script>
<template>
<view class="fixed inset-0 z-120 flex items-end">
<!-- 遮罩 -->
<view class="absolute inset-0 bg-black/40" @click="handleClose" />
<!-- 面板 -->
<wd-transition :show="true" :duration="300" name="slide-up" :lazy-render="false">
<view
class="relative box-border w-screen flex flex-col rounded-t-3xl bg-white pb-safe"
@click.stop
>
<!-- 拖拽指示条 -->
<view class="w-full center py-2" @click.stop="handleClose">
<view class="h-1 w-10 rounded-full bg-gray-300" />
</view>
<!-- 头部 -->
<view class="box-border flex items-center justify-between border-b border-gray-100 px-4 pb-3">
<text class="text-base text-gray-900 font-bold">游客信息</text>
<view class="h-8 w-8 center rounded-full bg-gray-50" @click="handleClose">
<wd-icon name="close" color="#6B7280" :size="18" />
</view>
</view>
<!-- 表单 -->
<view class="box-border flex flex-col gap-y-4 px-4 pt-4">
<!-- 昵称 -->
<view class="flex flex-col gap-y-1">
<text class="text-xs text-gray-500 font-medium">昵称 <text class="text-red-500">*</text></text>
<input
v-model="formData.displayName"
class="rounded-xl bg-gray-50 px-4 py-2.5 text-sm"
placeholder="请输入昵称"
:maxlength="30"
>
<text v-if="rules.displayName" class="text-[11px] text-red-500">{{ rules.displayName }}</text>
</view>
<!-- 邮箱 -->
<view class="flex flex-col gap-y-1">
<text class="text-xs text-gray-500 font-medium">邮箱 <text class="text-red-500">*</text></text>
<input
v-model="formData.email"
class="rounded-xl bg-gray-50 px-4 py-2.5 text-sm"
placeholder="请输入邮箱"
type="text"
:maxlength="100"
>
<text v-if="rules.email" class="text-[11px] text-red-500">{{ rules.email }}</text>
</view>
<!-- 网站 -->
<view class="flex flex-col gap-y-1">
<text class="text-xs text-gray-500 font-medium">网站</text>
<input
v-model="formData.website"
class="rounded-xl bg-gray-50 px-4 py-2.5 text-sm"
placeholder="请输入网站(选填)"
type="text"
:maxlength="200"
>
</view>
<!-- 提交按钮 -->
<view
class="h-11 w-full center rounded-xl bg-gray-900 text-sm text-white font-medium"
@click="handleSubmit"
>
保存
</view>
<view class="h-2 w-full" />
</view>
</view>
</wd-transition>
</view>
</template>
@@ -2,8 +2,11 @@
import { createComment, createReply, listCommentReplies, listComments } from '@/api/halo-base/comment' import { createComment, createReply, listCommentReplies, listComments } from '@/api/halo-base/comment'
import { avatar } from '@/utils/imageHelper' import { avatar } from '@/utils/imageHelper'
import { timeFrom } from '@/utils/base/timeFrom' import { timeFrom } from '@/utils/base/timeFrom'
import { useGuestStore } from '@/store/guest'
import { useUpvoteStore } from '@/store/upvote'
import type { CommentV1alpha1PublicApiListCommentRepliesRequest, CommentV1alpha1PublicApiListComments1Request, CommentWithReplyVo, ReplyVo } from '@halo-dev/api-client' import type { CommentV1alpha1PublicApiListCommentRepliesRequest, CommentV1alpha1PublicApiListComments1Request, CommentWithReplyVo, ReplyVo } from '@halo-dev/api-client'
import type { IHaloListResponseBase } from '@/api/types/halo' import type { IHaloListResponseBase } from '@/api/types/halo'
import UhGuestInfoPanel from './uh-guest-info-panel.vue'
interface IProps { interface IProps {
postName: string postName: string
@@ -20,6 +23,9 @@ const emits = defineEmits<{
(e: 'commented'): void (e: 'commented'): void
}>() }>()
const guestStore = useGuestStore()
const upvoteStore = useUpvoteStore()
// --- 评论列表 --- // --- 评论列表 ---
const commentList = ref<CommentWithReplyVo[]>([]) const commentList = ref<CommentWithReplyVo[]>([])
const commentPage = ref(1) const commentPage = ref(1)
@@ -44,6 +50,26 @@ const replyLoadingMap = ref<Record<string, boolean>>({})
const replyHasNextMap = ref<Record<string, boolean>>({}) const replyHasNextMap = ref<Record<string, boolean>>({})
const replyPageMap = ref<Record<string, number>>({}) const replyPageMap = ref<Record<string, number>>({})
// --- 游客信息面板 ---
const showGuestPanel = ref(false)
// --- 点赞(切换) ---
async function handleUpvote(name: string) {
const nowUpvoted = await upvoteStore.toggle(name, 'comments')
// 更新本地计数
for (const comment of commentList.value) {
if (comment.metadata.name === name) {
comment.stats.upvote = Math.max(0, (comment.stats.upvote ?? 0) + (nowUpvoted ? 1 : -1))
break
}
const reply = comment.replies?.items?.find(r => r.metadata.name === name)
if (reply) {
reply.stats.upvote = Math.max(0, (reply.stats.upvote ?? 0) + (nowUpvoted ? 1 : -1))
break
}
}
}
// --- 获取评论列表 --- // --- 获取评论列表 ---
async function fetchComments(page = 1, append = false) { async function fetchComments(page = 1, append = false) {
commentLoading.value = true commentLoading.value = true
@@ -152,13 +178,29 @@ function handleCancelReply() {
} }
// --- 提交评论/回复 --- // --- 提交评论/回复 ---
async function handleSubmit() { function handleSubmit() {
const text = inputText.value.trim() const text = inputText.value.trim()
if (!text || submitLoading.value) if (!text || submitLoading.value)
return return
// 检查游客信息,未设置直接弹出面板
if (!guestStore.isComplete) {
showGuestPanel.value = true
return
}
doSubmit(text)
}
async function doSubmit(text: string) {
submitLoading.value = true submitLoading.value = true
try { try {
const owner = {
displayName: guestStore.info.displayName,
email: guestStore.info.email,
website: guestStore.info.website || undefined,
}
if (replyTarget.value) { if (replyTarget.value) {
await createReply({ await createReply({
name: replyTarget.value.commentName, name: replyTarget.value.commentName,
@@ -167,6 +209,7 @@ async function handleSubmit() {
raw: text, raw: text,
quoteReply: replyTarget.value.replyName || undefined, quoteReply: replyTarget.value.replyName || undefined,
allowNotification: true, allowNotification: true,
owner,
}, },
} as any) } as any)
} }
@@ -182,6 +225,8 @@ async function handleSubmit() {
version: props.commentSubjectVersion, version: props.commentSubjectVersion,
}, },
allowNotification: true, allowNotification: true,
hidden: false,
owner,
}, },
} as any) } as any)
} }
@@ -201,6 +246,16 @@ async function handleSubmit() {
} }
} }
// --- 游客信息保存后自动提交 ---
function handleGuestSaved() {
showGuestPanel.value = false
// 保存后如果有输入内容,自动提交
const text = inputText.value.trim()
if (text) {
doSubmit(text)
}
}
// --- 查找被引用的回复 --- // --- 查找被引用的回复 ---
function findQuotedReply(comment: CommentWithReplyVo, quoteReplyName: string): ReplyVo | undefined { function findQuotedReply(comment: CommentWithReplyVo, quoteReplyName: string): ReplyVo | undefined {
return comment.replies?.items?.find(r => r.metadata.name === quoteReplyName) return comment.replies?.items?.find(r => r.metadata.name === quoteReplyName)
@@ -291,6 +346,24 @@ onMounted(() => {
<!-- 操作栏 --> <!-- 操作栏 -->
<view class="flex items-center gap-x-4"> <view class="flex items-center gap-x-4">
<!-- 点赞 -->
<view
class="flex items-center gap-x-0.5"
@click="handleUpvote(comment.metadata.name)"
>
<wd-icon
name="thumb-up"
:color="upvoteStore.isUpvoted(comment.metadata.name) ? '#EF4444' : '#9CA3AF'"
:size="14"
/>
<text
class="text-[11px]"
:class="upvoteStore.isUpvoted(comment.metadata.name) ? 'text-red-500' : 'text-gray-400'"
>
{{ comment.stats.upvote || '' }}
</text>
</view>
<!-- 回复 -->
<view <view
class="flex items-center gap-x-1" class="flex items-center gap-x-1"
@click="handleReply(comment.metadata.name, undefined, comment.owner.displayName || '匿名用户', comment.spec.raw)" @click="handleReply(comment.metadata.name, undefined, comment.owner.displayName || '匿名用户', comment.spec.raw)"
@@ -298,6 +371,7 @@ onMounted(() => {
<wd-icon name="message" color="#9CA3AF" :size="14" /> <wd-icon name="message" color="#9CA3AF" :size="14" />
<text class="text-[11px] text-gray-400">回复</text> <text class="text-[11px] text-gray-400">回复</text>
</view> </view>
<!-- 展开回复 -->
<view <view
v-if="(comment.status?.replyCount ?? 0) > 0" v-if="(comment.status?.replyCount ?? 0) > 0"
class="flex items-center gap-x-1" class="flex items-center gap-x-1"
@@ -345,12 +419,33 @@ onMounted(() => {
<rich-text :nodes="reply.spec.raw" /> <rich-text :nodes="reply.spec.raw" />
</text> </text>
<view <!-- 回复操作栏 -->
class="flex items-center gap-x-1" <view class="flex items-center gap-x-3">
@click="handleReply(comment.metadata.name, reply.metadata.name, reply.owner.displayName || '匿名用户', reply.spec.raw)" <!-- 点赞 -->
> <view
<wd-icon name="message" color="#9CA3AF" :size="12" /> class="flex items-center gap-x-0.5"
<text class="text-[10px] text-gray-400">回复</text> @click="handleUpvote(reply.metadata.name)"
>
<wd-icon
name="thumb-up"
:color="upvoteStore.isUpvoted(reply.metadata.name) ? '#EF4444' : '#9CA3AF'"
:size="12"
/>
<text
class="text-[10px]"
:class="upvoteStore.isUpvoted(reply.metadata.name) ? 'text-red-500' : 'text-gray-400'"
>
{{ reply.stats.upvote || '' }}
</text>
</view>
<!-- 回复 -->
<view
class="flex items-center gap-x-1"
@click="handleReply(comment.metadata.name, reply.metadata.name, reply.owner.displayName || '匿名用户', reply.spec.raw)"
>
<wd-icon name="message" color="#9CA3AF" :size="12" />
<text class="text-[10px] text-gray-400">回复</text>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -422,4 +517,11 @@ onMounted(() => {
</view> </view>
</wd-transition> </wd-transition>
</view> </view>
<!-- 游客信息面板 -->
<uh-guest-info-panel
v-if="showGuestPanel"
@close="showGuestPanel = false"
@saved="handleGuestSaved"
/>
</template> </template>
+47
View File
@@ -0,0 +1,47 @@
import { storeToRefs } from 'pinia'
import { useUniHaloConfigStore } from '@/store/config'
export interface IShareAppMessageOption {
title?: string
path: string
query?: string
imageUrl?: string
onlyAppNameTitle?: boolean
}
/**
* 应用分享
*/
export function useAppShare() {
const { config } = storeToRefs(useUniHaloConfigStore())
const setShareOptions = (options: IShareAppMessageOption) => {
const { onlyAppNameTitle = false } = options
if (!options.path) {
return
}
const title = onlyAppNameTitle ? config.value.app.name : options.title
onShareAppMessage(() => {
return {
title,
path: options.path + (options.query ? `?${options.query}` : ''),
imageUrl: options.imageUrl,
}
})
onShareTimeline(() => {
return {
title,
path: options.path,
imageUrl: options.imageUrl,
query: options.query,
}
})
}
return {
setShareOptions,
}
}
+354 -16
View File
@@ -1,40 +1,378 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useTokenStore } from '@/store/token' import { useTokenStore } from '@/store/token'
import { useGuestStore } from '@/store/guest'
import GuestInfoPanel from '@/components/post-detail/uh-guest-info-panel.vue'
defineOptions({ name: 'Login' })
definePage({ definePage({
style: { style: {
navigationBarTitleText: '登录', navigationBarTitleText: '登录',
navigationBarBackgroundColor: '#ffffff',
}, },
}) })
const tokenStore = useTokenStore() const tokenStore = useTokenStore()
async function doLogin() { const guestStore = useGuestStore()
if (tokenStore.hasLogin) {
uni.navigateBack() // ---- Tab 切换 ----
return type AuthMode = 'login' | 'register'
const activeMode = ref<AuthMode>('login')
function switchMode(mode: AuthMode) {
activeMode.value = mode
// 切换时清空错误提示
clearErrors()
}
// ---- 登录表单 ----
const loginForm = reactive({
username: '',
password: '',
})
const loginErrors = reactive({
username: '',
password: '',
})
function validateLogin(): boolean {
let valid = true
loginErrors.username = ''
loginErrors.password = ''
if (!loginForm.username.trim()) {
loginErrors.username = '请输入用户名'
valid = false
} }
if (!loginForm.password.trim()) {
loginErrors.password = '请输入密码'
valid = false
}
return valid
}
async function handleLogin() {
if (!validateLogin()) return
try { try {
// 调用登录接口
await tokenStore.login({ await tokenStore.login({
username: '小莫唐尼', username: loginForm.username.trim(),
password: '123456', password: loginForm.password.trim(),
}) })
uni.navigateBack() handleLoginSuccess()
} }
catch (error) { catch (error) {
console.log('登录失败', error) console.error('登录失败:', error)
} }
} }
// ---- 注册表单 ----
const registerForm = reactive({
username: '',
password: '',
confirmPassword: '',
})
const registerErrors = reactive({
username: '',
password: '',
confirmPassword: '',
})
function validateRegister(): boolean {
let valid = true
registerErrors.username = ''
registerErrors.password = ''
registerErrors.confirmPassword = ''
if (!registerForm.username.trim()) {
registerErrors.username = '请输入用户名'
valid = false
}
if (!registerForm.password.trim()) {
registerErrors.password = '请输入密码'
valid = false
}
else if (registerForm.password.length < 6) {
registerErrors.password = '密码至少6位'
valid = false
}
if (!registerForm.confirmPassword.trim()) {
registerErrors.confirmPassword = '请确认密码'
valid = false
}
else if (registerForm.password !== registerForm.confirmPassword) {
registerErrors.confirmPassword = '两次密码不一致'
valid = false
}
return valid
}
async function handleRegister() {
if (!validateRegister()) return
try {
// TODO: 调用注册 API
// await register({
// username: registerForm.username.trim(),
// password: registerForm.password.trim(),
// })
uni.showToast({ title: '注册成功', icon: 'success' })
// 注册成功后切换到登录
switchMode('login')
loginForm.username = registerForm.username
loginForm.password = ''
}
catch (error) {
console.error('注册失败:', error)
}
}
function clearErrors() {
loginErrors.username = ''
loginErrors.password = ''
registerErrors.username = ''
registerErrors.password = ''
registerErrors.confirmPassword = ''
}
// ---- 密码可见性 ----
const showLoginPwd = ref(false)
const showRegisterPwd = ref(false)
const showConfirmPwd = ref(false)
// ---- 微信一键登录 ----
// #ifdef MP-WEIXIN
async function handleWxLogin() {
try {
await tokenStore.wxLogin()
handleLoginSuccess()
}
catch (error) {
console.error('微信登录失败:', error)
}
}
// #endif
// ---- 游客身份访问 ----
const showGuestPanel = ref(false)
function handleGuestAccess() {
showGuestPanel.value = true
}
function handleGuestSaved() {
showGuestPanel.value = false
uni.navigateBack()
}
// ---- 登录成功处理 ----
function handleLoginSuccess() {
// 检查是否有重定向地址(从路由拦截器传入)
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] as any
const redirect = currentPage?.$page?.options?.redirect || currentPage?.options?.redirect
if (redirect) {
uni.redirectTo({ url: decodeURIComponent(redirect) })
}
else {
uni.navigateBack()
}
}
// ---- 密码输入框切换可见性 ----
function togglePwdVisibility(target: 'login' | 'register' | 'confirm') {
if (target === 'login') showLoginPwd.value = !showLoginPwd.value
else if (target === 'register') showRegisterPwd.value = !showRegisterPwd.value
else showConfirmPwd.value = !showConfirmPwd.value
}
</script> </script>
<template> <template>
<view class="min-h-screen w-full flex flex-col items-center justify-center gap-y-6 bg-page"> <view class="bg-page3 box-border min-h-screen w-full flex flex-col px-6 pt-12">
<!-- 本页面是非MP的登录页主要用于 h5 APP --> <!-- 头部 Logo -->
<view class="text-center"> <view class="mb-8 flex flex-col items-center gap-y-2">
登录页 <image
class="h-16 w-16 rounded-2xl"
src="/static/logo.png"
mode="scaleToFill"
/>
<text class="text-xl text-gray-900 font-bold">欢迎回来</text>
<text class="text-xs text-gray-400">登录你的账号探索更多精彩</text>
</view> </view>
<button class="mt-4 w-40 text-center" @click="doLogin">
点击模拟登录 <!-- Tab 切换 -->
</button> <view class="mb-6 flex items-center rounded-xl bg-gray-100 p-1">
<view
class="flex-1 center rounded-lg py-2 text-sm font-medium transition-all"
:class="activeMode === 'login' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500'"
@click="switchMode('login')"
>
登录
</view>
<view
class="flex-1 center rounded-lg py-2 text-sm font-medium transition-all"
:class="activeMode === 'register' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500'"
@click="switchMode('register')"
>
注册
</view>
</view>
<!-- 登录表单 -->
<view v-if="activeMode === 'login'" class="flex flex-col gap-y-4">
<!-- 用户名 -->
<view class="flex flex-col gap-y-1">
<text class="text-xs text-gray-500 font-medium">用户名</text>
<view class="flex items-center rounded-xl bg-gray-50 px-4">
<wd-icon name="user" color="#9CA3AF" :size="16" />
<input
v-model="loginForm.username"
class="flex-1 py-2.5 pl-2 text-sm"
placeholder="请输入用户名"
:maxlength="30"
>
</view>
<text v-if="loginErrors.username" class="text-[11px] text-red-500">{{ loginErrors.username }}</text>
</view>
<!-- 密码 -->
<view class="flex flex-col gap-y-1">
<text class="text-xs text-gray-500 font-medium">密码</text>
<view class="flex items-center rounded-xl bg-gray-50 px-4">
<wd-icon name="lock" color="#9CA3AF" :size="16" />
<input
v-model="loginForm.password"
class="flex-1 py-2.5 pl-2 text-sm"
placeholder="请输入密码"
:password="!showLoginPwd"
:maxlength="30"
>
<view @click="togglePwdVisibility('login')">
<wd-icon :name="showLoginPwd ? 'view' : 'eye-close'" color="#9CA3AF" :size="16" />
</view>
</view>
<text v-if="loginErrors.password" class="text-[11px] text-red-500">{{ loginErrors.password }}</text>
</view>
<!-- 登录按钮 -->
<view
class="mt-2 h-11 w-full center rounded-xl bg-gray-900 text-sm text-white font-medium"
@click="handleLogin"
>
登录
</view>
</view>
<!-- 注册表单 -->
<view v-if="activeMode === 'register'" class="flex flex-col gap-y-4">
<!-- 用户名 -->
<view class="flex flex-col gap-y-1">
<text class="text-xs text-gray-500 font-medium">用户名</text>
<view class="flex items-center rounded-xl bg-gray-50 px-4">
<wd-icon name="user" color="#9CA3AF" :size="16" />
<input
v-model="registerForm.username"
class="flex-1 py-2.5 pl-2 text-sm"
placeholder="请输入用户名"
:maxlength="30"
>
</view>
<text v-if="registerErrors.username" class="text-[11px] text-red-500">{{ registerErrors.username }}</text>
</view>
<!-- 密码 -->
<view class="flex flex-col gap-y-1">
<text class="text-xs text-gray-500 font-medium">密码</text>
<view class="flex items-center rounded-xl bg-gray-50 px-4">
<wd-icon name="lock" color="#9CA3AF" :size="16" />
<input
v-model="registerForm.password"
class="flex-1 py-2.5 pl-2 text-sm"
placeholder="请输入密码(至少6位)"
:password="!showRegisterPwd"
:maxlength="30"
>
<view @click="togglePwdVisibility('register')">
<wd-icon :name="showRegisterPwd ? 'view' : 'eye-close'" color="#9CA3AF" :size="16" />
</view>
</view>
<text v-if="registerErrors.password" class="text-[11px] text-red-500">{{ registerErrors.password }}</text>
</view>
<!-- 确认密码 -->
<view class="flex flex-col gap-y-1">
<text class="text-xs text-gray-500 font-medium">确认密码</text>
<view class="flex items-center rounded-xl bg-gray-50 px-4">
<wd-icon name="lock" color="#9CA3AF" :size="16" />
<input
v-model="registerForm.confirmPassword"
class="flex-1 py-2.5 pl-2 text-sm"
placeholder="请再次输入密码"
:password="!showConfirmPwd"
:maxlength="30"
>
<view @click="togglePwdVisibility('confirm')">
<wd-icon :name="showConfirmPwd ? 'view' : 'eye-close'" color="#9CA3AF" :size="16" />
</view>
</view>
<text v-if="registerErrors.confirmPassword" class="text-[11px] text-red-500">{{ registerErrors.confirmPassword }}</text>
</view>
<!-- 注册按钮 -->
<view
class="mt-2 h-11 w-full center rounded-xl bg-gray-900 text-sm text-white font-medium"
@click="handleRegister"
>
注册
</view>
</view>
<!-- 分隔线 -->
<view class="my-6 flex items-center gap-x-3">
<view class="h-px flex-1 bg-gray-200" />
<text class="text-xs text-gray-400">其他方式</text>
<view class="h-px flex-1 bg-gray-200" />
</view>
<!-- 第三方登录 & 游客访问 -->
<view class="flex flex-col items-center gap-y-3">
<!-- #ifdef MP-WEIXIN -->
<view
class="h-11 w-full flex items-center justify-center gap-x-2 rounded-xl border border-green-500 text-sm text-green-600 font-medium"
@click="handleWxLogin"
>
<wd-icon name="wechat" color="#16A34A" :size="18" />
<text>微信一键登录</text>
</view>
<!-- #endif -->
<view
class="h-11 w-full flex items-center justify-center gap-x-2 rounded-xl border border-gray-200 text-sm text-gray-600"
@click="handleGuestAccess"
>
<wd-icon name="user" color="#6B7280" :size="16" />
<text>游客身份访问</text>
</view>
</view>
<!-- 底部协议提示 -->
<view class="mt-auto mb-8 flex flex-col items-center gap-y-1 pt-6">
<text class="text-[11px] text-gray-400">登录或注册即代表同意</text>
<view class="flex items-center gap-x-1">
<text class="text-[11px] text-gray-500 font-medium">用户协议</text>
<text class="text-[11px] text-gray-400"></text>
<text class="text-[11px] text-gray-500 font-medium">隐私政策</text>
</view>
</view>
<!-- 游客信息编辑面板 -->
<GuestInfoPanel
v-if="showGuestPanel"
@close="showGuestPanel = false"
@saved="handleGuestSaved"
/>
</view> </view>
</template> </template>
-30
View File
@@ -1,30 +0,0 @@
<script lang="ts" setup>
import { LOGIN_PAGE } from '@/router/config'
definePage({
style: {
navigationBarTitleText: '注册',
},
})
function doRegister() {
uni.showToast({
title: '注册成功',
})
// 注册成功后跳转到登录页
uni.navigateTo({
url: LOGIN_PAGE,
})
}
</script>
<template>
<view class="min-h-screen w-full flex flex-col items-center justify-center gap-y-6 bg-page">
<view class="text-center">
注册页
</view>
<button class="mt-4 w-40 text-center" @click="doRegister">
点击模拟注册
</button>
</view>
</template>
-348
View File
@@ -1,348 +0,0 @@
<script lang="ts" setup>
import { getPicSumImages } from '@/utils/randomResources'
import { queryLinks } from '@/api/halo-plugin/link'
defineOptions({
name: 'Gallery',
})
definePage({
style: {
navigationBarTitleText: '相册',
navigationBarBackgroundColor: '#ffffff',
enablePullDownRefresh: true,
},
})
interface GalleryItem {
metadataName: string
displayName: string
url: string
}
interface GalleryGroup {
metadataName: string
displayName: string
images: GalleryItem[]
}
const groupBanner = reactive({
current: 0,
})
const galleryStyle = ref('list')
const showListPanel = reactive({
show: false,
metadataName: '',
})
// 相册分组预览
const galleryGroups = ref<GalleryGroup[]>([
{
metadataName: 'photo-group-HGaGs',
displayName: '我们的故事',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称3',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称4',
url: getPicSumImages(800, 600),
}],
},
{
metadataName: '履行记录',
displayName: '履行记录',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称3',
url: getPicSumImages(800, 600),
}],
},
{
metadataName: '家庭时光',
displayName: '家庭时光',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}, {
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
}],
},
{
metadataName: '其他',
displayName: '其他',
images: [{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(800, 600),
}],
},
])
// 相册图片
const galleryItems = ref<GalleryItem[]>([
{
metadataName: '',
displayName: '图片名称1',
url: getPicSumImages(400, 400),
},
{
metadataName: '',
displayName: '图片名称2',
url: getPicSumImages(800, 600),
},
{
metadataName: '',
displayName: '图片名称3',
url: getPicSumImages(400, 800),
},
{
metadataName: '',
displayName: '图片名称4',
url: getPicSumImages(1920, 1080),
},
{
metadataName: '',
displayName: '图片名称5',
url: getPicSumImages(800, 1920),
},
])
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 ?? [],
})
},
})
function onGroupBannerChange(e: any) {
groupBanner.current = e.detail.current
}
function getGroupImageRotateClass(index: number) {
if (index === 0) {
return 'z-30 border-2 border-solid border-white uh-shadow-card group-hover:scale-95'
}
if (index === 1) {
return 'z-20 -rotate-6 group-hover:-rotate-22 border-2 border-solid border-white uh-shadow-card'
}
if (index === 2) {
return 'z-10 rotate-6 group-hover:rotate-22 border-2 border-solid border-white uh-shadow-card'
}
}
// -- 图片列表模式
// 图片列表容器样式
function getGalleryListContainerClass(total: number) {
if (total === 1) {
return 'h-36'
}
if (total === 2) {
return 'grid-cols-2 h-32'
}
if (total === 3) {
return 'grid-cols-2 grid-rows-2 h-36'
}
if (total === 4) {
return 'grid-cols-2 grid-rows-2 h-42'
}
return ''
}
// 图片项样式
function getGalleryListItemClass(index: number, total: number) {
if (total === 1) {
return ''
}
if (total === 2) {
return ''
}
if (total === 3) {
if (index === 0) {
return 'row-span-2'
}
return ''
}
if (total === 4) {
return ''
}
return ''
}
function handleShowListPanel(metadataName: string) {
showListPanel.metadataName = metadataName
showListPanel.show = true
}
onLoad(() => {
refresh()
})
onMounted(() => {
})
onPageScroll(() => { })
</script>
<template>
<view class="min-h-screen w-full flex flex-col bg-page">
<!-- 分组列表常态显示 -->
<view class="box-border w-full px-4 py-4">
<!-- 顶部区域固定区域 -->
<view class="relative w-full">
<swiper
autoplay circular class="h-46 w-full overflow-hidden rounded-xl" :current="groupBanner.current"
@change="onGroupBannerChange"
>
<swiper-item v-for="img in galleryGroups[0].images" :key="img.metadataName">
<image :src="img.url" class="h-full w-full object-cover" />
</swiper-item>
</swiper>
<view
class="absolute left-4 top-4 flex flex-col items-center justify-center rounded-2xl bg-orange-500 px-2 py-1.5 text-xs text-white font-bold"
>
最新精选
</view>
<view class="absolute bottom-4 left-4 flex flex-col gap-y-1">
<view class="text-sm text-white font-bold">
显示图片的名称
</view>
<view class="text-xs text-white/50">
显示描述/日期
</view>
<view class="w-full flex items-center justify-center gap-x-2">
<image
v-for="(item, index) in galleryGroups[0].images" :key="item.metadataName" :src="item.url"
mode="scaleToFill"
class="box-border h-8 w-8 border-2 border-white rounded-lg border-solid transition-all duration-300"
:class="[
{
'scale-110': groupBanner.current === index,
},
index % 2 === 0 ? '-rotate-4' : 'rotate-4',
]" @click="groupBanner.current = index"
/>
</view>
</view>
</view>
<!-- 分组列表区域 -->
<view class="mt-6 w-full flex items-center justify-between">
<text class="text-sm text-gray-900 font-bold">我的相册</text>
<text class="text-[10px] text-gray-600"> 30 组记录</text>
</view>
<!-- 混合模式 -->
<view v-if="galleryStyle === 'mix'" 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"
>
<!-- 预览图区域 -->
<view class="relative h-full w-full">
<view
v-for="(img, index) in group.images.slice(0, 3)" :key="img.metadataName"
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)]"
>
<image :src="img.url" mode="scaleToFill" class="h-full w-full" />
<!-- 文字区域 -->
<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-[8px] text-white/80">{{ group.images.length }} 张图片</text>
</view>
</view>
</view>
</view>
</view>
<!-- 列表卡片模式 -->
<view v-else-if="galleryStyle === 'list'" class="mt-2 w-full flex flex-col gap-y-4">
<view
v-for="group in galleryGroups" :key="group.metadataName"
class="box-border flex flex-col gap-y-2 overflow-hidden rounded-xl bg-white p-4 pt-3" @click="handleShowListPanel(group.metadataName)"
>
<!-- 顶部区域 -->
<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.images.length }} </text>
</view>
</view>
<!-- 图片列表 -->
<view
class="grid gap-1 overflow-hidden rounded-lg"
:class="[getGalleryListContainerClass(group.images.slice(0, 4).length)]"
>
<view
v-for="(img, index) in group.images.slice(0, 4)" :key="img.metadataName"
class="overflow-hidden"
:class="getGalleryListItemClass(index, group.images.slice(0, 4).length)"
>
<image
:src="img.url" mode="scaleToFill"
class="h-full w-full object-cover transition-transform duration-500 hover:scale-105"
/>
</view>
</view>
</view>
</view>
</view>
<!-- 分组列表无数据显示 -->
<view class="hidden">
无数据
</view>
<!-- 相册列表根据分组显示 -->
<uh-gallery-panel v-if="showListPanel.show" :group-name="showListPanel.metadataName" @close="showListPanel.show = false" />
<!-- 底部导航占位 -->
<view class="w-full shrink-0">
<uh-tabbar-page-placeholder />
</view>
</view>
</template>
+13 -5
View File
@@ -1,9 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import useRequest from '@/hooks/useRequest' import { useAppShare } from '@/hooks/useShare'
import { useRequest } from '@/hooks/useRequest'
import { completeUrl } from '@/utils/url'
import { random } from '@/utils/base/random'
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 random from '@/utils/base/random'
defineOptions({ defineOptions({
name: 'Gallery', name: 'Gallery',
@@ -11,7 +12,7 @@ defineOptions({
definePage({ definePage({
style: { style: {
navigationBarTitleText: '相册', navigationBarTitleText: '图库',
navigationBarBackgroundColor: '#ffffff', navigationBarBackgroundColor: '#ffffff',
enablePullDownRefresh: true, enablePullDownRefresh: true,
}, },
@@ -26,6 +27,13 @@ interface IGalleryGroup {
photos: IPhoto[] photos: IPhoto[]
} }
const { setShareOptions } = useAppShare()
setShareOptions({
title: '图库',
path: '/pages/gallery/gallery',
})
const groupBanner = reactive({ const groupBanner = reactive({
current: 0, current: 0,
}) })
@@ -180,7 +188,7 @@ onPageScroll(() => { })
</script> </script>
<template> <template>
<view class="bg-page3 min-h-screen w-full flex flex-col"> <view class="min-h-screen w-full flex flex-col bg-page3">
<!-- 测试 --> <!-- 测试 -->
<view v-if="false" class="box-border w-full p-4 pb-0"> <view v-if="false" 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="uh-shadow-card box-border w-full flex items-center gap-x-4 rounded-xl bg-white p-4 text-sm">
+9 -11
View File
@@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import useRequest from '@/hooks/useRequest' import { useAppShare } from '@/hooks/useShare'
import { useRequest } from '@/hooks/useRequest'
import { queryPostByName, queryPosts } from '@/api/halo-base/post' import { queryPostByName, queryPosts } from '@/api/halo-base/post'
import { cover } from '@/utils/imageHelper' import { cover } from '@/utils/imageHelper'
import { timeFrom } from '@/utils/base/timeFrom' import { timeFrom } from '@/utils/base/timeFrom'
@@ -23,6 +24,12 @@ definePage({
}, },
}) })
const { setShareOptions } = useAppShare()
setShareOptions({
path: '/pages/home/home',
})
type PostCardStyle = 'noCover' | 'leftCover' | 'bottomCover' | 'rightCover' | 'topCover' type PostCardStyle = 'noCover' | 'leftCover' | 'bottomCover' | 'rightCover' | 'topCover'
const postCardStyle = ref<PostCardStyle>('leftCover') const postCardStyle = ref<PostCardStyle>('leftCover')
@@ -129,16 +136,7 @@ onPageScroll(() => { })
<template> <template>
<view class="min-h-screen bg-page3"> <view class="min-h-screen bg-page3">
<!-- 顶部banner --> <!-- 顶部banner -->
<view v-if="false" class="relative h-72 w-full"> <view class="box-border w-full">
<image class="h-72 w-full object-cover" :src="exampleImageUrls.banner" />
<view class="absolute bottom-4 left-4 flex flex-col gap-y-2">
<text class="text-xs text-white">welcome to new world</text>
<text class="text-2xl text-white font-bold">UNI HALO V3.0 </text>
<text class="text-xs text-white">新全新探索简约的艺术风格</text>
</view>
</view>
<view>
<wd-swiper <wd-swiper
v-model:current="banner.current" :list="banner.items" indicator-position="right" autoplay v-model:current="banner.current" :list="banner.items" indicator-position="right" autoplay
:height="340" :height="340"
+11 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getCurrentInstance } from 'vue' import { getCurrentInstance } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useAppShare } from '@/hooks/useShare'
import { useRequest } from '@/hooks/useRequest' import { useRequest } from '@/hooks/useRequest'
import { random } from '@/utils/base/random' import { random } from '@/utils/base/random'
import { queryLinkGroups, queryLinks } from '@/api/halo-plugin/link' import { queryLinkGroups, queryLinks } from '@/api/halo-plugin/link'
@@ -26,9 +27,16 @@ definePage({
}, },
}) })
const { setShareOptions } = useAppShare()
const { config } = storeToRefs(useUniHaloConfigStore()) const { config } = storeToRefs(useUniHaloConfigStore())
const { reset } = useUniHaloConfigStore() const { reset } = useUniHaloConfigStore()
setShareOptions({
title: '友情链接',
path: '/pages/links/links',
})
const { loading: loadingGroups, error: errorGroups, data: dataGroups, run: runGroups } = useRequest<ILinkGroup[]>(queryLinkGroups, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true }) 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 { loading, error, data, run } = useRequest<IHaloListResponseBase<ILink>, IPaginationParams>(queryLinks, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
@@ -165,7 +173,7 @@ onLoad(async () => {
</script> </script>
<template> <template>
<view class="bg-page3 min-h-screen w-full flex flex-col"> <view class="min-h-screen w-full flex flex-col bg-page3">
<!-- 分组列表 --> <!-- 分组列表 -->
<view v-if="linkGroups.length !== 0" class="box-border w-full flex flex-col gap-y-6 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">
<view v-for="(item, index) in linkGroups" :key="item.name" class="flex flex-col gap-y-4"> <view v-for="(item, index) in linkGroups" :key="item.name" class="flex flex-col gap-y-4">
@@ -223,7 +231,7 @@ onLoad(async () => {
<view v-if="false" class="shrink-0"> <view v-if="false" class="shrink-0">
<view <view
class="uh-shadow-md rounded-xl bg-gray-900 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, '链接已复制')"
> >
复制 复制
</view> </view>
@@ -258,7 +266,7 @@ onLoad(async () => {
<button <button
v-if="config.base.isIndividual" 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!" 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)" @click="copy(link.spec.url, '链接已复制')"
> >
<wd-icon name="copy" :size="14" /> 复制 <wd-icon name="copy" :size="14" /> 复制
</button> </button>
+176 -87
View File
@@ -2,38 +2,92 @@
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { LOGIN_PAGE } from '@/router/config' import { LOGIN_PAGE } from '@/router/config'
import { useUserStore } from '@/store' import { useUserStore } from '@/store'
import { useGuestStore } from '@/store/guest'
import { useTokenStore } from '@/store/token' import { useTokenStore } from '@/store/token'
import i18n, { t } from '@/locale/index' import GuestInfoPanel from '@/components/post-detail/uh-guest-info-panel.vue'
import { setTabbarItem } from '@/tabbar/i18n'
defineOptions({ defineOptions({ name: 'Mine' })
name: 'Mine',
})
definePage({ definePage({
style: { style: {
navigationBarTitleText: '我的', navigationBarTitleText: '我的',
navigationBarBackgroundColor: '#ffffff', navigationBarBackgroundColor: '#ffffff',
enablePullDownRefresh: true,
}, },
}) })
const userStore = useUserStore() const userStore = useUserStore()
const tokenStore = useTokenStore() const tokenStore = useTokenStore()
// 使用storeToRefs解构userInfo const guestStore = useGuestStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
// 微信小程序下登录 /** 是否已登录 */
async function handleLogin() { const hasLogin = computed(() => tokenStore.hasLogin)
// #ifdef MP-WEIXIN /** 是否已填写游客信息 */
// 微信登录 const hasGuestInfo = computed(() => guestStore.isComplete)
await tokenStore.wxLogin()
/** 显示的头像 */
const displayAvatar = computed(() => {
if (hasLogin.value && userInfo.value.avatar) {
return userInfo.value.avatar
}
return '/static/images/default-avatar.png'
})
/** 显示的名称 */
const displayName = computed(() => {
if (hasLogin.value) {
return userInfo.value.nickname || userInfo.value.username
}
if (hasGuestInfo.value) {
return guestStore.info.displayName
}
return '未登录'
})
/** 显示的副标题 */
const displaySubtitle = computed(() => {
if (hasLogin.value) {
return userInfo.value.role || userInfo.value.username
}
if (hasGuestInfo.value) {
return guestStore.info.email
}
return '登录后解锁更多功能'
})
/** 用户状态标签 */
const statusLabel = computed(() => {
if (hasLogin.value) return '已登录'
if (hasGuestInfo.value) return '游客'
return '未登录'
})
const statusColor = computed(() => {
if (hasLogin.value) return 'bg-green-50 text-green-600'
if (hasGuestInfo.value) return 'bg-amber-50 text-amber-600'
return 'bg-gray-100 text-gray-500'
})
// ---- 游客信息编辑 ----
const showGuestPanel = ref(false)
function handleEditGuest() {
showGuestPanel.value = true
}
function handleGuestSaved() {
showGuestPanel.value = false
uni.showToast({ title: '保存成功', icon: 'success' })
}
// ---- 登录/退出 ----
function handleLogin() {
// #ifdef MP-WEIXIN
tokenStore.wxLogin()
// #endif // #endif
// #ifndef MP-WEIXIN // #ifndef MP-WEIXIN
uni.navigateTo({ uni.navigateTo({ url: LOGIN_PAGE })
url: `${LOGIN_PAGE}`,
})
// #endif // #endif
} }
@@ -43,93 +97,121 @@ function handleLogout() {
content: '确定要退出登录吗?', content: '确定要退出登录吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
// 清空用户信息 tokenStore.logout()
useTokenStore().logout() uni.showToast({ title: '已退出登录', icon: 'success' })
// 执行退出登录逻辑
uni.showToast({
title: '退出登录成功',
icon: 'success',
})
// #ifdef MP-WEIXIN
// 微信小程序,去首页
// uni.reLaunch({ url: '/pages/index/index' })
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序,去登录页
// uni.navigateTo({ url: LOGIN_PAGE })
// #endif
} }
}, },
}) })
} }
const current = ref(uni.getLocale()) // ---- 功能导航 ----
const languages = [ interface NavItem {
{ icon: string
value: 'zh-Hans', label: string
name: '中文', path?: string
checked: 'true', showArrow?: boolean
}, }
{
value: 'en', const navList: NavItem[] = [
name: '英文', { icon: 'file', label: '免责声明', path: '', showArrow: true },
}, { icon: 'chat', label: '在线客服', path: '', showArrow: true },
{ icon: 'edit', label: '意见反馈', path: '', showArrow: true },
{ icon: 'info-circle', label: '关于项目', path: '/subpkg-blog/about/about', showArrow: true },
] ]
function radioChange(evt) { function handleNavClick(item: NavItem) {
// console.log(evt) if (item.path) {
current.value = evt.detail.value uni.navigateTo({ url: item.path })
// 下面2句缺一不可!!! }
uni.setLocale(evt.detail.value) else {
i18n.global.locale = evt.detail.value uni.showToast({ title: '即将开放,敬请期待', icon: 'none' })
}
// 底部tabbar需要重新设置一下
setTabbarItem()
// 本页的标题也需要重新设置一下
uni.setNavigationBarTitle({
title: t('i18n.title'),
})
} }
</script> </script>
<template> <template>
<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="bg-page3 box-border min-h-screen w-full flex flex-col gap-y-4 px-4 pt-4">
<view class="mt-3 break-all px-3 text-center text-green-500"> <!-- 用户信息卡片 -->
{{ userInfo.username ? '已登录' : '未登录' }} <view class="uh-shadow-xs box-border flex flex-col rounded-2xl bg-white p-4">
</view> <view class="w-full flex items-center gap-x-3">
<view class="mt-3 break-all px-3"> <!-- 头像 -->
{{ JSON.stringify(userInfo, null, 2) }} <image
</view> class="uh-shadow-xs h-16 w-16 shrink-0 border-2 border-white rounded-full border-solid"
:src="displayAvatar"
<!-- 切换语言 --> mode="scaleToFill"
<view class="mt-6 w-full flex flex-col items-center justify-center"> />
<view class="mb-2 text-gray-900 font-bold"> <!-- 信息区 -->
切换语言 <view class="flex flex-1 flex-col gap-y-1">
</view> <view class="flex items-center gap-x-2">
<view class="w-full flex items-center justify-center gap-4"> <text class="text-base text-gray-900 font-bold">{{ displayName }}</text>
<radio-group class="flex flex-col items-center justify-center gap-2" @change="radioChange"> <view class="rounded-full px-2 py-0.5" :class="statusColor">
<label <text class="text-[10px] font-medium">{{ statusLabel }}</text>
v-for="item in languages"
:key="item.value"
class="flex items-center gap-x-2"
>
<view>
<radio :value="item.value" :checked="item.value === current" />
</view> </view>
<view>{{ item.name }}</view> </view>
</label> <text class="line-clamp-1 text-xs text-gray-500">{{ displaySubtitle }}</text>
</radio-group> </view>
<!-- 游客编辑入口 -->
<view
v-if="!hasLogin && hasGuestInfo"
class="h-8 w-8 center shrink-0 rounded-full bg-gray-50"
@click="handleEditGuest"
>
<wd-icon name="edit" color="#6B7280" :size="16" />
</view>
</view>
<!-- 游客信息快捷编辑入口未登录且未填写游客信息时 -->
<view
v-if="!hasLogin && !hasGuestInfo"
class="mt-3 box-border flex items-center gap-x-2 rounded-xl bg-gray-50 px-3 py-2.5"
@click="handleEditGuest"
>
<wd-icon name="edit" color="#9CA3AF" :size="14" />
<text class="text-xs text-gray-400">填写游客信息用于评论互动</text>
<wd-icon name="arrow-right" color="#D1D5DB" :size="14" />
</view>
<!-- 已登录时显示的额外信息 -->
<view v-if="hasLogin" class="mt-3 my-1 h-px w-full bg-gray-50" />
<view v-if="hasLogin" class="flex items-center justify-between">
<text class="text-xs text-gray-400">账号{{ userInfo.username }}</text>
</view> </view>
</view> </view>
<view class="mt-6"> <!-- 功能导航 -->
<view class="m-auto w-160px text-center"> <view class="uh-shadow-xs box-border flex flex-col rounded-2xl bg-white">
<button v-if="tokenStore.hasLogin" type="warn" class="w-full rounded-xl text-xs" @click="handleLogout"> <view
退出登录 v-for="(item, index) in navList"
</button> :key="item.label"
<button v-else class="w-full rounded-xl bg-gray-900 py-2.5 text-xs text-white" @click="handleLogin"> class="box-border flex items-center justify-between px-4 py-3.5"
立即登录 :class="{ 'border-b border-gray-50': index < navList.length - 1 }"
</button> @click="handleNavClick(item)"
>
<view class="flex items-center gap-x-3">
<view class="h-8 w-8 center rounded-xl bg-gray-50">
<wd-icon :name="item.icon" color="#6B7280" :size="16" />
</view>
<text class="text-sm text-gray-800">{{ item.label }}</text>
</view>
<wd-icon v-if="item.showArrow" name="arrow-right" color="#D1D5DB" :size="14" />
</view>
</view>
<!-- 登录/退出按钮 -->
<view class="mt-2 w-full">
<view
v-if="hasLogin"
class="uh-shadow-xs h-11 w-full center rounded-2xl bg-white text-sm text-red-500 font-medium"
@click="handleLogout"
>
退出登录
</view>
<view
v-else
class="h-11 w-full center rounded-2xl bg-gray-900 text-sm text-white font-medium"
@click="handleLogin"
>
立即登录
</view> </view>
</view> </view>
@@ -137,5 +219,12 @@ function radioChange(evt) {
<view class="w-full shrink-0"> <view class="w-full shrink-0">
<uh-tabbar-page-placeholder /> <uh-tabbar-page-placeholder />
</view> </view>
<!-- 游客信息编辑面板 -->
<GuestInfoPanel
v-if="showGuestPanel"
@close="showGuestPanel = false"
@saved="handleGuestSaved"
/>
</view> </view>
</template> </template>
+9 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useAppShare } from '@/hooks/useShare'
import { cover } from '@/utils/imageHelper' import { cover } from '@/utils/imageHelper'
import { queryMoments } from '@/api/halo-plugin/moment' import { queryMoments } from '@/api/halo-plugin/moment'
import { parseDateTime } from '@/utils/base/time' import { parseDateTime } from '@/utils/base/time'
@@ -17,6 +18,13 @@ definePage({
}, },
}) })
const { setShareOptions } = useAppShare()
setShareOptions({
title: '瞬间',
path: '/pages/moments/moments',
})
const useTimeline = ref(true) const useTimeline = ref(true)
const { loading, error, data, run } = useRequest<IHaloListResponseBase<IMoment>, IMomentListRequest>(queryMoments, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true }) const { loading, error, data, run } = useRequest<IHaloListResponseBase<IMoment>, IMomentListRequest>(queryMoments, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
@@ -63,7 +71,7 @@ onPageScroll(() => { })
</script> </script>
<template> <template>
<view class="bg-page3 min-h-screen w-full flex flex-col"> <view class="min-h-screen w-full flex flex-col bg-page3">
<!-- 测试 --> <!-- 测试 -->
<view v-if="false" class="box-border w-full p-4 pb-0"> <view v-if="false" 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="uh-shadow-card box-border w-full flex items-center gap-x-4 rounded-xl bg-white p-4 text-sm">
+2 -2
View File
@@ -9,9 +9,9 @@ export const LOGIN_STRATEGY = LOGIN_STRATEGY_MAP.DEFAULT_NO_NEED_LOGIN
export const isNeedLoginMode = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN export const isNeedLoginMode = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN
export const LOGIN_PAGE = '/pages/auth/login' export const LOGIN_PAGE = '/pages/auth/login'
export const REGISTER_PAGE = '/pages/auth/register' export const REGISTER_PAGE = '/pages/auth/login'
export const LOGIN_PAGE_LIST = [LOGIN_PAGE, REGISTER_PAGE] export const LOGIN_PAGE_LIST = [LOGIN_PAGE]
// 在 definePage 里面配置了 excludeLoginPath 的页面,功能与 EXCLUDE_LOGIN_PATH_LIST 相同 // 在 definePage 里面配置了 excludeLoginPath 的页面,功能与 EXCLUDE_LOGIN_PATH_LIST 相同
export const excludeLoginPathList = getAllPages('excludeLoginPath').map(page => page.path) export const excludeLoginPathList = getAllPages('excludeLoginPath').map(page => page.path)
+3
View File
@@ -4,6 +4,9 @@ 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 = {
app: {
name: 'uni-halo',
},
base: { base: {
domain: import.meta.env.VITE_UNI_HALO_DOMAIN, domain: import.meta.env.VITE_UNI_HALO_DOMAIN,
token: import.meta.env.VITE_UNI_HALO_TOKEN, token: import.meta.env.VITE_UNI_HALO_TOKEN,
+48
View File
@@ -0,0 +1,48 @@
import { defineStore } from 'pinia'
export interface IGuestInfo {
displayName: string
email: string
website: string
}
const GuestStoreKey = 'UNI_HALO_GUEST_INFO'
const defaultGuestInfo: IGuestInfo = {
displayName: '',
email: '',
website: '',
}
/**
* 游客信息状态管理
*/
export const useGuestStore = defineStore('guest', () => {
const info = ref<IGuestInfo>({ ...defaultGuestInfo, ...(uni.getStorageSync(GuestStoreKey) ?? {}) })
/** 是否已填写游客信息 */
const isComplete = computed(() => {
return !!(info.value.displayName.trim() && info.value.email.trim())
})
/** 保存游客信息 */
function save(data: IGuestInfo) {
info.value = { ...data }
uni.setStorageSync(GuestStoreKey, info.value)
}
/** 清除游客信息 */
function clear() {
info.value = { ...defaultGuestInfo }
uni.removeStorageSync(GuestStoreKey)
}
return {
info,
isComplete,
save,
clear,
}
}, {
persist: false, // 手动管理持久化
})
+47
View File
@@ -0,0 +1,47 @@
import { defineStore } from 'pinia'
import { downvote, upvote } from '@/api/halo-base/trackers'
const UpvoteStoreKey = 'UNI_HALO_UPVOTED_SET'
/**
* 全局点赞状态管理
* 使用 metadata.name 作为 key,持久化存储
*/
export const useUpvoteStore = defineStore('upvote', () => {
const upvotedSet = ref<Set<string>>(new Set(uni.getStorageSync(UpvoteStoreKey) ?? []))
/** 是否已点赞 */
function isUpvoted(name: string): boolean {
return upvotedSet.value.has(name)
}
/** 切换点赞状态 */
async function toggle(name: string, plural: 'comments' | 'posts' = 'comments'): Promise<boolean> {
const alreadyUpvoted = upvotedSet.value.has(name)
try {
if (alreadyUpvoted) {
await downvote({ group: 'content.halo.run', plural, name })
upvotedSet.value.delete(name)
}
else {
await upvote({ group: 'content.halo.run', plural, name })
upvotedSet.value.add(name)
}
// 持久化
uni.setStorageSync(UpvoteStoreKey, Array.from(upvotedSet.value))
return !alreadyUpvoted
}
catch {
// 接口不返回内容,静默处理
return alreadyUpvoted
}
}
return {
upvotedSet,
isUpvoted,
toggle,
}
}, {
persist: false, // 手动管理持久化
})
+102 -83
View File
@@ -3,15 +3,15 @@ import { useRequest } from '@/hooks/useRequest'
import { markdownConfig } from '@/config/markdown/config' import { markdownConfig } from '@/config/markdown/config'
import { useUniHaloConfigStore } from '@/store/config' import { useUniHaloConfigStore } from '@/store/config'
import { queryPostByName, queryPostNavigationByName } from '@/api/halo-base/post' import { queryPostByName, queryPostNavigationByName } from '@/api/halo-base/post'
import { upvote } from '@/api/halo-base/trackers'
import { completeUrl } from '@/utils/url' import { completeUrl } from '@/utils/url'
import { cover } from '@/utils/imageHelper' import { cover } from '@/utils/imageHelper'
import { timeFrom } from '@/utils/base/timeFrom' import { timeFrom } from '@/utils/base/timeFrom'
import { formatNumberUnit } from '@/utils/base/digital' import { formatNumberUnit } from '@/utils/base/digital'
import { useUpvoteStore } from '@/store/upvote'
import type { NavigationPostVo, PostV1alpha1PublicApiQueryPostByNameRequest, PostVo } from '@halo-dev/api-client' import type { NavigationPostVo, PostV1alpha1PublicApiQueryPostByNameRequest, PostVo } from '@halo-dev/api-client'
import type { VoteRequest } from '@/api/halo-base/trackers'
import mpHtml from '@/components/mp-html/components/mp-html/mp-html.vue' import mpHtml from '@/components/mp-html/components/mp-html/mp-html.vue'
import UhPostCommentPanel from '@/components/post-detail/uh-post-comment-panel.vue' import UhPostCommentPanel from '@/components/post-detail/uh-post-comment-panel.vue'
import copy from '@/utils/base/clipboard'
interface IPostDetailLoadOptions { interface IPostDetailLoadOptions {
metadataName: string metadataName: string
@@ -37,14 +37,17 @@ const { data: navigation, run: fetchNavigation } = useRequest<NavigationPostVo,
let postName = '' let postName = ''
// --- 点赞状态 --- const upvoteStore = useUpvoteStore()
const isUpvoted = ref(false)
// --- 点赞计数 ---
const upvoteCount = ref(0) const upvoteCount = ref(0)
const isUpvoted = computed(() => upvoteStore.isUpvoted(postName))
// --- 计算属性 --- // --- 计算属性 ---
const coverUrl = computed(() => { const coverUrl = computed(() => {
const raw = post.value?.spec?.cover const raw = post.value?.spec?.cover
return raw ? completeUrl(raw) : '' return raw ? cover(raw) : ''
}) })
const categories = computed(() => { const categories = computed(() => {
@@ -80,42 +83,28 @@ function handleBack() {
} }
async function handleUpvote() { async function handleUpvote() {
if (isUpvoted.value) const nowUpvoted = await upvoteStore.toggle(postName, 'posts')
return upvoteCount.value = Math.max(0, (post.value?.stats?.upvote ?? 0) + (nowUpvoted ? 1 : -1))
try {
await upvote({
group: 'content.halo.run',
plural: 'posts',
name: postName,
} as VoteRequest)
isUpvoted.value = true
upvoteCount.value = (post.value?.stats?.upvote ?? 0) + 1
uni.showToast({ title: '点赞成功', icon: 'none' })
}
catch {
uni.showToast({ title: '点赞失败', icon: 'none' })
}
} }
function handleShare() { function handleShare() {
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
// 微信小程序中由页面右上角菜单处理分享 // 微信小程序中由页面右上角菜单处理分享
// #endif // #endif
// #ifdef H5 // #ifndef MP-WEIXIN
uni.setClipboardData({ copy(post.value?.status?.permalink ?? '', '链接已复制')
data: post.value?.status?.permalink ?? '',
success: () => {
uni.showToast({ title: '链接已复制', icon: 'none' })
},
})
// #endif // #endif
} }
function handleScrollToTop() {
uni.pageScrollTo({ scrollTop: 0, duration: 300 })
}
function handleNavigatePost(name: string) { function handleNavigatePost(name: string) {
postName = name postName = name
fetchPost({ name }) fetchPost({ name })
fetchNavigation({ name }) fetchNavigation({ name })
uni.pageScrollTo({ scrollTop: 0, duration: 300 }) handleScrollToTop()
} }
async function loadData() { async function loadData() {
@@ -133,10 +122,6 @@ onPageScroll(({ scrollTop }) => {
beforeScrollTop = scrollTop beforeScrollTop = scrollTop
}) })
function handleScrollToTop() {
uni.pageScrollTo({ scrollTop: 0, duration: 300 })
}
// --- 生命周期 --- // --- 生命周期 ---
onLoad((options: IPostDetailLoadOptions) => { onLoad((options: IPostDetailLoadOptions) => {
postName = options.metadataName postName = options.metadataName
@@ -153,6 +138,16 @@ onShareAppMessage(() => {
return { return {
title: post.value?.spec?.title ?? '文章详情', title: post.value?.spec?.title ?? '文章详情',
path: `/subpkg-blog/post-detail/post-detail?metadataName=${postName}`, path: `/subpkg-blog/post-detail/post-detail?metadataName=${postName}`,
imageUrl: coverUrl.value,
}
})
onShareTimeline(() => {
return {
title: post.value?.spec?.title ?? '文章详情',
path: `/subpkg-blog/post-detail/post-detail`,
query: `metadataName=${postName}`,
imageUrl: coverUrl.value,
} }
}) })
@@ -162,26 +157,15 @@ const showCommentPanel = ref(false)
function handleOpenCommentPanel() { function handleOpenCommentPanel() {
showCommentPanel.value = true showCommentPanel.value = true
} }
function handleScrollContentToTop() {
uni.pageScrollTo({ scrollTop: 236, duration: 300 })
}
</script> </script>
<template> <template>
<view class="min-h-screen w-full flex flex-col bg-page3"> <view class="min-h-screen w-full flex flex-col bg-page3">
<!-- 顶部导航栏 -->
<view v-if="false" class="fixed left-0 z-100 box-border w-full flex items-center justify-between"
:style="{ paddingTop: 'calc(var(--status-bar-height, 44px) ' }">
<view class="box-border h-44px w-full flex items-center justify-between px-2">
<!-- 返回按钮 -->
<view class="h-8 w-9 center rounded-lg bg-gray-900 text-white" @click="handleBack">
<wd-icon name="arrow-left" color="#fff" :size="16" />
</view>
<view v-if="false" class="line-clamp-1 box-border pl-4 pr-6 text-white font-bold">
{{ post?.spec?.title ?? '笔记详情' }}
</view>
</view>
</view>
<!-- 顶部封面区域 --> <!-- 顶部封面区域 -->
<view class="fixed left-0 right-0 top-0 z-10 h-52 w-full overflow-hidden"> <view class="fixed left-0 right-0 top-0 z-10 h-62 w-full overflow-hidden">
<image v-if="coverUrl" class="h-full w-full" mode="aspectFill" :src="coverUrl" /> <image v-if="coverUrl" class="h-full w-full" mode="aspectFill" :src="coverUrl" />
<view v-else class="h-full w-full bg-gray-200" /> <view v-else class="h-full w-full bg-gray-200" />
@@ -190,24 +174,34 @@ function handleOpenCommentPanel() {
</view> </view>
<!-- 文章信息卡片覆盖封面底部 --> <!-- 文章信息卡片覆盖封面底部 -->
<view class="relative z-20 box-border w-full flex flex-col gap-y-4 pt-52 -mt-8"> <view class="relative z-20 box-border w-full flex flex-col gap-y-4 pt-62 -mt-8">
<!-- 主信息卡片 --> <!-- 主信息卡片 -->
<view v-if="htmlContent" <view
class="uh-shadow-xs box-border w-full flex flex-col gap-y-3 overflow-hidden rounded-3xl bg-white"> v-if="htmlContent"
class="uh-shadow-xs box-border w-full flex flex-col gap-y-3 overflow-hidden rounded-3xl bg-white"
>
<!-- 操作条 -->
<view class="w-full flex items-center justify-center pt-3" @click="handleScrollContentToTop">
<view class="box-border h-4px w-8 rounded-full bg-gray-200 transition-colors duration-300 hover:bg-gray-600" />
</view>
<!-- 标题 --> <!-- 标题 -->
<text class="box-border p-4 pb-0 text-lg text-gray-900 font-bold leading-6"> <text class="box-border p-4 pb-0 pt-0 text-lg text-gray-900 font-bold leading-6">
{{ post?.spec?.title ?? '加载中...' {{ post?.spec?.title ?? '加载中...' }}
}}
</text> </text>
<!-- 分类 & 标签 --> <!-- 分类 & 标签 -->
<view v-if="categories.length > 0 || tags.length > 0" class="box-border flex flex-wrap items-center gap-2 px-4"> <view v-if="categories.length > 0 || tags.length > 0" class="box-border flex flex-wrap items-center gap-2 px-4">
<view v-for="cat in categories" :key="cat.metadata.name" <view
class="rounded-full bg-gray-900 px-2.5 py-1 text-xs text-white font-medium"> v-for="cat in categories" :key="cat.metadata.name"
class="rounded-full bg-gray-900 px-2.5 py-1 text-xs text-white font-medium"
>
{{ cat.spec?.displayName }} {{ cat.spec?.displayName }}
</view> </view>
<view v-for="tag in tags" :key="tag.metadata.name" <view
class="border border-gray-200 rounded-full bg-white px-2.5 py-1 text-xs text-gray-500"> v-for="tag in tags" :key="tag.metadata.name"
class="border border-gray-200 rounded-full bg-white px-2.5 py-1 text-xs text-gray-500"
>
# {{ tag.spec?.displayName }} # {{ tag.spec?.displayName }}
</view> </view>
</view> </view>
@@ -230,8 +224,10 @@ function handleOpenCommentPanel() {
<!-- 作者信息 --> <!-- 作者信息 -->
<view v-if="post?.owner" class="box-border flex items-center gap-x-2 hidden!"> <view v-if="post?.owner" class="box-border flex items-center gap-x-2 hidden!">
<image v-if="post.owner.avatar" class="h-8 w-8 border-2 border-white rounded-full border-solid" <image
:src="completeUrl(post.owner.avatar)" mode="aspectFill" /> v-if="post.owner.avatar" class="h-8 w-8 border-2 border-white rounded-full border-solid"
:src="completeUrl(post.owner.avatar)" mode="aspectFill"
/>
<view class="flex flex-col"> <view class="flex flex-col">
<text class="text-xs text-gray-900 font-medium">{{ post.owner.displayName }}</text> <text class="text-xs text-gray-900 font-medium">{{ post.owner.displayName }}</text>
<text v-if="post.owner.bio" class="line-clamp-1 text-[11px] text-gray-400">{{ post.owner.bio }}</text> <text v-if="post.owner.bio" class="line-clamp-1 text-[11px] text-gray-400">{{ post.owner.bio }}</text>
@@ -240,21 +236,28 @@ function handleOpenCommentPanel() {
<!-- 文章正文 --> <!-- 文章正文 -->
<view class="box-border px-1"> <view class="box-border px-1">
<mp-html :content="htmlContent" :tag-style="markdownConfig.tagStyle" <mp-html
:content="htmlContent" :tag-style="markdownConfig.tagStyle"
:container-style="markdownConfig.containStyle" :domain="markdownConfig.domain" lazy-load selectable :container-style="markdownConfig.containStyle" :domain="markdownConfig.domain" lazy-load selectable
:scroll-table="true" :use-anchor="true" /> :scroll-table="true" :use-anchor="true"
/>
</view> </view>
<!-- 上一篇/下一篇导航 --> <!-- 上一篇/下一篇导航 -->
<view v-if="prevPost || nextPost" class="box-border w-full"> <view v-if="prevPost || nextPost" class="box-border w-full">
<view <view
class="box-border w-full flex flex-col gap-y-2 border-0 border-gray-100 rounded-xl border-solid p-4 pt-0"> class="box-border w-full flex flex-col gap-y-2 border-0 border-gray-100 rounded-xl border-solid p-4 pt-0"
>
<!-- 上一篇 --> <!-- 上一篇 -->
<view v-if="prevPost" class="flex items-center gap-x-3 rounded-xl bg-gray-50 p-3" <view
@click="handleNavigatePost(prevPost.metadata.name)"> v-if="prevPost" class="flex items-center gap-x-3 rounded-xl bg-gray-50 p-3"
@click="handleNavigatePost(prevPost.metadata.name)"
>
<view class="h-10 w-10 shrink-0 overflow-hidden rounded-lg"> <view class="h-10 w-10 shrink-0 overflow-hidden rounded-lg">
<image class="box-border h-full w-full border-2 border-gray-50 rounded-lg border-solid bg-gray-200" <image
mode="aspectFill" :src="cover(prevPost.spec?.cover)" /> class="box-border h-full w-full border-2 border-gray-50 rounded-lg border-solid bg-gray-200"
mode="aspectFill" :src="cover(prevPost.spec?.cover)"
/>
</view> </view>
<view class="flex flex-1 flex-col gap-y-0.5 overflow-hidden"> <view class="flex flex-1 flex-col gap-y-0.5 overflow-hidden">
<text class="text-[11px] text-gray-400">上一篇</text> <text class="text-[11px] text-gray-400">上一篇</text>
@@ -264,11 +267,15 @@ function handleOpenCommentPanel() {
</view> </view>
<!-- 下一篇 --> <!-- 下一篇 -->
<view v-if="nextPost" class="flex items-center gap-x-3 rounded-xl bg-gray-50 p-3" <view
@click="handleNavigatePost(nextPost.metadata.name)"> v-if="nextPost" class="flex items-center gap-x-3 rounded-xl bg-gray-50 p-3"
@click="handleNavigatePost(nextPost.metadata.name)"
>
<view class="h-10 w-10 shrink-0 overflow-hidden rounded-lg"> <view class="h-10 w-10 shrink-0 overflow-hidden rounded-lg">
<image class="box-border h-full w-full border-2 border-gray-50 rounded-lg border-solid bg-gray-200" <image
mode="aspectFill" :src="cover(nextPost.spec?.cover)" /> class="box-border h-full w-full border-2 border-gray-50 rounded-lg border-solid bg-gray-200"
mode="aspectFill" :src="cover(nextPost.spec?.cover)"
/>
</view> </view>
<view class="flex flex-1 flex-col gap-y-0.5 overflow-hidden"> <view class="flex flex-1 flex-col gap-y-0.5 overflow-hidden">
<text class="text-[11px] text-gray-400">下一篇</text> <text class="text-[11px] text-gray-400">下一篇</text>
@@ -285,19 +292,25 @@ function handleOpenCommentPanel() {
<!-- 加载中占位 --> <!-- 加载中占位 -->
<view v-else-if="loading" class="box-border h-full w-full center py-26"> <view v-else-if="loading" class="box-border h-full w-full center py-26">
<wd-loading :size="32" color="#1a1a1a">加载中...</wd-loading> <wd-loading :size="32" color="#1a1a1a">
加载中...
</wd-loading>
</view> </view>
<!-- 上一篇/下一篇导航 --> <!-- 上一篇/下一篇导航 -->
<view v-if="prevPost || nextPost" <view
class="uh-shadow-xs box-border w-full flex flex-col gap-y-2 rounded-2xl bg-white p-4 hidden!"> v-if="prevPost || nextPost"
class="uh-shadow-xs box-border w-full flex flex-col gap-y-2 rounded-2xl bg-white p-4 hidden!"
>
<view class="mb-1 text-sm text-gray-900 font-bold"> <view class="mb-1 text-sm text-gray-900 font-bold">
文章导航 文章导航
</view> </view>
<!-- 上一篇 --> <!-- 上一篇 -->
<view v-if="prevPost" class="flex items-center gap-x-3 rounded-xl bg-gray-50 p-3" <view
@click="handleNavigatePost(prevPost.metadata.name)"> v-if="prevPost" class="flex items-center gap-x-3 rounded-xl bg-gray-50 p-3"
@click="handleNavigatePost(prevPost.metadata.name)"
>
<view class="h-10 w-10 shrink-0 overflow-hidden rounded-lg"> <view class="h-10 w-10 shrink-0 overflow-hidden rounded-lg">
<image class="h-full w-full" mode="aspectFill" :src="cover(prevPost.spec?.cover)" /> <image class="h-full w-full" mode="aspectFill" :src="cover(prevPost.spec?.cover)" />
</view> </view>
@@ -309,8 +322,10 @@ function handleOpenCommentPanel() {
</view> </view>
<!-- 下一篇 --> <!-- 下一篇 -->
<view v-if="nextPost" class="flex items-center gap-x-3 rounded-xl bg-gray-50 p-3" <view
@click="handleNavigatePost(nextPost.metadata.name)"> v-if="nextPost" class="flex items-center gap-x-3 rounded-xl bg-gray-50 p-3"
@click="handleNavigatePost(nextPost.metadata.name)"
>
<view class="h-10 w-10 shrink-0 overflow-hidden rounded-lg"> <view class="h-10 w-10 shrink-0 overflow-hidden rounded-lg">
<image class="h-full w-full" mode="aspectFill" :src="cover(nextPost.spec?.cover)" /> <image class="h-full w-full" mode="aspectFill" :src="cover(nextPost.spec?.cover)" />
</view> </view>
@@ -327,7 +342,8 @@ function handleOpenCommentPanel() {
<view class="fixed bottom-6 left-4 right-4 z-100 flex items-center gap-x-4" @touchmove.stop.prevent> <view class="fixed bottom-6 left-4 right-4 z-100 flex items-center gap-x-4" @touchmove.stop.prevent>
<!-- 左侧操作栏 --> <!-- 左侧操作栏 -->
<view <view
class="uh-shadow-md uh-backdrop-blur box-border h-46px flex flex-1 items-center justify-around border border-white rounded-full border-solid bg-white/95 p-1"> class="uh-shadow-md uh-backdrop-blur box-border h-46px flex flex-1 items-center justify-around border border-white rounded-full border-solid bg-white/95 p-1"
>
<!-- 返回 --> <!-- 返回 -->
<view class="flex flex-1 items-center justify-center gap-x-0.5" @click="handleBack"> <view class="flex flex-1 items-center justify-center gap-x-0.5" @click="handleBack">
<view class="center rounded-full transition-colors"> <view class="center rounded-full transition-colors">
@@ -357,25 +373,28 @@ function handleOpenCommentPanel() {
</view> </view>
<!-- 分享 --> <!-- 分享 -->
<view class="flex flex-1 items-center justify-center gap-x-1" @click="handleShare"> <button open-type="share" class="flex flex-1 items-center justify-center gap-x-1" @click="handleShare">
<view class="center rounded-full bg-transparent"> <view class="center rounded-full bg-transparent">
<wd-icon name="share-alt" color="#6B7280" :size="18" /> <wd-icon name="share-alt" color="#6B7280" :size="18" />
</view> </view>
<text class="text-xs text-gray-900 font-medium">分享</text> <text class="text-xs text-gray-900 font-medium">分享</text>
</view> </button>
</view> </view>
<!-- 右侧返回顶部 --> <!-- 右侧返回顶部 -->
<view <view
class="uh-shadow-md uh-backdrop-blur box-border h-46px w-46px center flex shrink-0 border border-white rounded-full border-solid bg-white/95" class="uh-shadow-md uh-backdrop-blur box-border h-46px w-46px center flex shrink-0 border border-white rounded-full border-solid bg-white/95"
@click="handleScrollToTop"> @click="handleScrollToTop"
>
<wd-icon name="arrow-up" color="#1A1A1A" :size="18" /> <wd-icon name="arrow-up" color="#1A1A1A" :size="18" />
</view> </view>
</view> </view>
</view> </view>
<!-- 评论面板 --> <!-- 评论面板 -->
<uh-post-comment-panel v-if="showCommentPanel" :post-name="postName" <uh-post-comment-panel
v-if="showCommentPanel" :post-name="postName"
:comment-subject-version="post?.spec?.releaseSnapshot ?? ''" :comment-count="post?.stats?.comment ?? 0" :comment-subject-version="post?.spec?.releaseSnapshot ?? ''" :comment-count="post?.stats?.comment ?? 0"
@close="showCommentPanel = false" @commented="loadData" /> @close="showCommentPanel = false" @commented="loadData"
/>
</template> </template>
+4 -4
View File
@@ -1,16 +1,16 @@
/** /**
* 复制文本到剪贴板 * 复制文本到剪贴板
* @param {string} content 要复制的文本 * @param {string} content 要复制的文本
* @param {boolean} showToast 是否显示复制成功提示 * @param {string} toastText 复制成功提示内容
*/ */
export function copy(content: string, showToast: boolean = false) { export function copy(content: string, toastText?: string) {
uni.setClipboardData({ uni.setClipboardData({
data: content, data: content,
showToast: false, showToast: false,
success: () => { success: () => {
if (showToast) { if (toastText) {
uni.showToast({ uni.showToast({
title: '复制成功', title: toastText,
icon: 'none', icon: 'none',
}) })
} }