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:
@@ -0,0 +1,2 @@
|
|||||||
|
我们需要全局缓存一个点赞的列表,使用 metadata.name 做key,存储到store并且开启持久化存储,如果已经点过赞,直接显示已
|
||||||
|
经点赞的样式。同时我们的点赞接口是不会返回内容的,所以再捕获异常的时候不要提示异常信息。
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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, // 手动管理持久化
|
||||||
|
})
|
||||||
@@ -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, // 手动管理持久化
|
||||||
|
})
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user