mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-12 16:40:40 +08:00
feat: 重构评论交互与请求错误处理,新增搜索页
- 请求层:非 200 响应统一抛 UniHaloError,Halo 来源响应归一化 - 评论弹窗:新增 allowNotification、验证码错误自动刷新与错误详情提示 - 评论回复结构调整为分页结果(replies.items) - 首页:快捷导航改 5 列、最新列表跳转搜索页,移除旧文章列表页 - 图库:分类切换改用 wd-tabs/wd-tab 组件 - 新增搜索页、uh-data-loading 组件、useDataLoadingStatus 与 exception 工具 Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>
This commit is contained in:
@@ -49,3 +49,5 @@ src/pages.json
|
|||||||
|
|
||||||
|
|
||||||
.docs/
|
.docs/
|
||||||
|
.screenshots/
|
||||||
|
.design/
|
||||||
@@ -4,6 +4,7 @@ import { getCurrentInstance, onMounted, onUnmounted } from 'vue'
|
|||||||
import { navigateToInterceptor } from '@/router/interceptor'
|
import { navigateToInterceptor } from '@/router/interceptor'
|
||||||
import { tabbarStore } from '@/tabbar/store'
|
import { tabbarStore } from '@/tabbar/store'
|
||||||
import { permission } from '@/router/permission'
|
import { permission } from '@/router/permission'
|
||||||
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
|
|
||||||
const { proxy } = (getCurrentInstance() || {}) as any
|
const { proxy } = (getCurrentInstance() || {}) as any
|
||||||
const router = proxy?.$router
|
const router = proxy?.$router
|
||||||
@@ -11,6 +12,10 @@ const router = proxy?.$router
|
|||||||
router && permission.install(router)
|
router && permission.install(router)
|
||||||
|
|
||||||
onLaunch((options) => {
|
onLaunch((options) => {
|
||||||
|
|
||||||
|
// 初始化获取配置
|
||||||
|
useAppConfigStore()
|
||||||
|
|
||||||
console.log('App.vue onLaunch', options)
|
console.log('App.vue onLaunch', options)
|
||||||
})
|
})
|
||||||
onShow((options) => {
|
onShow((options) => {
|
||||||
|
|||||||
+1
-4
@@ -1,9 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Halo 官方 API 接口定义
|
* Halo 官方 API 接口定义
|
||||||
* 覆盖官方扩展点:api.content.halo.run / api.halo.run / api.moment.halo.run /
|
|
||||||
* api.photo.halo.run / api.link.halo.run / api.plugin.halo.run
|
|
||||||
* 风格参考 src/api/foo-alova.ts:http.Get<IResponse<T>>(url, { params, header, meta })
|
|
||||||
* 源自旧项目 api/v2/all.api.js(官方部分),按需命名导出
|
|
||||||
*/
|
*/
|
||||||
import { http } from '@/http/alova';
|
import { http } from '@/http/alova';
|
||||||
import { RequestFrom } from '@/http/tools/enum';
|
import { RequestFrom } from '@/http/tools/enum';
|
||||||
@@ -144,6 +140,7 @@ export function getPostCommentReplyList(commentName: string, params: ICommentLis
|
|||||||
|
|
||||||
/** 新增评论(带验证码,captchaCode 转入请求头) */
|
/** 新增评论(带验证码,captchaCode 转入请求头) */
|
||||||
export interface IAddCommentReq {
|
export interface IAddCommentReq {
|
||||||
|
allowNotification: boolean;
|
||||||
raw: string;
|
raw: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
owner?: Record<string, unknown>;
|
owner?: Record<string, unknown>;
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ export type IPostListRes = IListResult<IPost>
|
|||||||
/** 文章搜索请求参数(关键字) */
|
/** 文章搜索请求参数(关键字) */
|
||||||
export interface ISearchReq {
|
export interface ISearchReq {
|
||||||
keyword?: string
|
keyword?: string
|
||||||
|
/** 返回条数上限(旧版默认 50) */
|
||||||
|
limit?: number
|
||||||
page?: number
|
page?: number
|
||||||
size?: number
|
size?: number
|
||||||
highlightPreTag?: string
|
highlightPreTag?: string
|
||||||
@@ -226,7 +228,7 @@ export interface IComment {
|
|||||||
replyCount?: number
|
replyCount?: number
|
||||||
visibleTime?: string
|
visibleTime?: string
|
||||||
}
|
}
|
||||||
replies?: ICommentReply[]
|
replies?: IListResult<ICommentReply>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICommentReply {
|
export interface ICommentReply {
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ export function getDoubanDetail(url: string) {
|
|||||||
*/
|
*/
|
||||||
export function getCommentWidgetCaptcha() {
|
export function getCommentWidgetCaptcha() {
|
||||||
return http.Get<IResponse<string>>('/apis/api.commentwidget.halo.run/v1alpha1/captcha/-/generate', {
|
return http.Get<IResponse<string>>('/apis/api.commentwidget.halo.run/v1alpha1/captcha/-/generate', {
|
||||||
|
cacheFor:0,
|
||||||
meta: { requestFrom: RequestFrom.Halo },
|
meta: { requestFrom: RequestFrom.Halo },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,9 +155,9 @@ handleGetData()
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 二级评论 -->
|
<!-- 二级评论 -->
|
||||||
<template v-if="comment.replies && comment.replies.length !== 0">
|
<template v-if="comment.replies && comment.replies.items.length !== 0">
|
||||||
<uh-comment-item
|
<uh-comment-item
|
||||||
v-for="childComment in comment.replies"
|
v-for="childComment in comment.replies.items"
|
||||||
:key="childComment.metadata.name"
|
:key="childComment.metadata.name"
|
||||||
:use-content-bg="false"
|
:use-content-bg="false"
|
||||||
:is-child="true"
|
:is-child="true"
|
||||||
|
|||||||
@@ -1,55 +1,51 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
/**
|
import { computed, ref, watch } from 'vue'
|
||||||
* 评论弹窗(源自旧项目 components/comment-modal,新建复刻)
|
import { addPostComment, addPostCommentReply } from '@/api/halo'
|
||||||
* 支持新增评论 / 回复评论,含匿名评论验证码(cookie 链路)
|
import { getCommentWidgetCaptcha, getCommentWidgetConfig } from '@/api/uni-halo'
|
||||||
*/
|
import { setCache } from '@/utils/storage'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { deepMerge } from '@/utils/merge'
|
||||||
import { addPostComment, addPostCommentReply } from '@/api/halo'
|
import { UniHaloError } from '@/http/tools/exception'
|
||||||
import { getCommentWidgetCaptcha, getCommentWidgetConfig } from '@/api/uni-halo'
|
const props = withDefaults(defineProps<{
|
||||||
import { setCache } from '@/utils/storage'
|
show : boolean
|
||||||
import { deepMerge } from '@/utils/merge'
|
isComment ?: boolean
|
||||||
|
title ?: string
|
||||||
const props = withDefaults(defineProps<{
|
postName : string
|
||||||
show: boolean
|
}>(), {
|
||||||
isComment?: boolean
|
|
||||||
title?: string
|
|
||||||
postName: string
|
|
||||||
}>(), {
|
|
||||||
isComment: false,
|
isComment: false,
|
||||||
title: '',
|
title: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'on-close', data: { isSubmit: boolean, refresh: boolean }): void
|
(e : 'on-close', data : { isSubmit : boolean, refresh : boolean }) : void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isShow = ref(false)
|
const isShow = ref(false)
|
||||||
|
|
||||||
interface ICaptchaConfig {
|
interface ICaptchaConfig {
|
||||||
security?: {
|
security ?: {
|
||||||
captcha?: {
|
captcha ?: {
|
||||||
anonymousCommentCaptcha?: boolean
|
anonymousCommentCaptcha ?: boolean
|
||||||
[key: string]: unknown
|
[key : string] : unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
editor?: {
|
editor ?: {
|
||||||
placeholder?: string
|
placeholder ?: string
|
||||||
|
}
|
||||||
|
[key : string] : unknown
|
||||||
}
|
}
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ICommentForm {
|
interface ICommentForm {
|
||||||
allowNotification: boolean
|
allowNotification : boolean
|
||||||
author: string
|
author : string
|
||||||
avatar: string
|
avatar : string
|
||||||
authorUrl: string
|
authorUrl : string
|
||||||
content: string
|
content : string
|
||||||
email: string
|
email : string
|
||||||
postName: string
|
postName : string
|
||||||
captchaCode?: string
|
captchaCode ?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = ref<ICaptchaConfig>({
|
const config = ref<ICaptchaConfig>({
|
||||||
security: {
|
security: {
|
||||||
captcha: {
|
captcha: {
|
||||||
anonymousCommentCaptcha: false,
|
anonymousCommentCaptcha: false,
|
||||||
@@ -58,14 +54,14 @@ const config = ref<ICaptchaConfig>({
|
|||||||
editor: {
|
editor: {
|
||||||
placeholder: '请输入内容,不超过200字符...',
|
placeholder: '请输入内容,不超过200字符...',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const captchaData = ref<{ image: string, status: 'loading' | 'success' | 'fail' }>({
|
const captchaData = ref<{ image : string, status : 'loading' | 'success' | 'fail' }>({
|
||||||
image: '',
|
image: '',
|
||||||
status: 'loading',
|
status: 'loading',
|
||||||
})
|
})
|
||||||
|
|
||||||
const form = ref<ICommentForm>({
|
const form = ref<ICommentForm>({
|
||||||
allowNotification: true,
|
allowNotification: true,
|
||||||
author: '',
|
author: '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
@@ -74,15 +70,15 @@ const form = ref<ICommentForm>({
|
|||||||
email: '',
|
email: '',
|
||||||
postName: props.postName,
|
postName: props.postName,
|
||||||
captchaCode: undefined,
|
captchaCode: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const calcTitle = computed(() => {
|
const calcTitle = computed(() => {
|
||||||
if (props.isComment)
|
if (props.isComment)
|
||||||
return props.title || '新增评论'
|
return props.title || '新增评论'
|
||||||
return `回复用户:${props.title}`
|
return `回复用户:${props.title}`
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleResetForm() {
|
function handleResetForm() {
|
||||||
form.value = {
|
form.value = {
|
||||||
allowNotification: true,
|
allowNotification: true,
|
||||||
author: '',
|
author: '',
|
||||||
@@ -93,10 +89,10 @@ function handleResetForm() {
|
|||||||
postName: props.postName,
|
postName: props.postName,
|
||||||
captchaCode: undefined,
|
captchaCode: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取评论组件配置(验证码开关) */
|
/** 获取评论组件配置(验证码开关) */
|
||||||
async function handleGetConfig() {
|
async function handleGetConfig() {
|
||||||
try {
|
try {
|
||||||
const res = await getCommentWidgetConfig()
|
const res = await getCommentWidgetConfig()
|
||||||
config.value = deepMerge(config.value as Record<string, unknown>, res.data as Record<string, unknown>) as ICaptchaConfig
|
config.value = deepMerge(config.value as Record<string, unknown>, res.data as Record<string, unknown>) as ICaptchaConfig
|
||||||
@@ -107,10 +103,10 @@ async function handleGetConfig() {
|
|||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('获取验证码配置失败', err)
|
console.error('获取验证码配置失败', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取评论验证码图片 */
|
/** 获取评论验证码图片 */
|
||||||
async function handleGetCaptchaImage() {
|
async function handleGetCaptchaImage() {
|
||||||
captchaData.value.status = 'loading'
|
captchaData.value.status = 'loading'
|
||||||
try {
|
try {
|
||||||
const res = await getCommentWidgetCaptcha()
|
const res = await getCommentWidgetCaptcha()
|
||||||
@@ -123,10 +119,10 @@ async function handleGetCaptchaImage() {
|
|||||||
captchaData.value.status = 'fail'
|
captchaData.value.status = 'fail'
|
||||||
captchaData.value.image = ''
|
captchaData.value.image = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化访客信息 */
|
/** 初始化访客信息 */
|
||||||
function handleInitVisitor() {
|
function handleInitVisitor() {
|
||||||
const visitor = uni.getStorageSync('Visitor')
|
const visitor = uni.getStorageSync('Visitor')
|
||||||
if (!visitor)
|
if (!visitor)
|
||||||
return
|
return
|
||||||
@@ -140,20 +136,20 @@ function handleInitVisitor() {
|
|||||||
catch {
|
catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存访客信息 */
|
/** 保存访客信息 */
|
||||||
function handleSetVisitor() {
|
function handleSetVisitor() {
|
||||||
setCache('Visitor', {
|
setCache('Visitor', {
|
||||||
author: form.value.author,
|
author: form.value.author,
|
||||||
avatar: form.value.avatar,
|
avatar: form.value.avatar,
|
||||||
email: form.value.email,
|
email: form.value.email,
|
||||||
authorUrl: form.value.authorUrl,
|
authorUrl: form.value.authorUrl,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 提交校验 */
|
/** 提交校验 */
|
||||||
function validateForm(): boolean {
|
function validateForm() : boolean {
|
||||||
if (!form.value.content.trim()) {
|
if (!form.value.content.trim()) {
|
||||||
uni.showToast({ icon: 'none', title: '请填写评论内容' })
|
uni.showToast({ icon: 'none', title: '请填写评论内容' })
|
||||||
return false
|
return false
|
||||||
@@ -171,9 +167,9 @@ function validateForm(): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleHandle() {
|
async function handleHandle() {
|
||||||
if (!validateForm())
|
if (!validateForm())
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -183,6 +179,7 @@ async function handleHandle() {
|
|||||||
if (props.isComment) {
|
if (props.isComment) {
|
||||||
// 新增评论
|
// 新增评论
|
||||||
await addPostComment({
|
await addPostComment({
|
||||||
|
allowNotification: form.value.allowNotification,
|
||||||
raw: form.value.content,
|
raw: form.value.content,
|
||||||
content: form.value.content,
|
content: form.value.content,
|
||||||
owner: {
|
owner: {
|
||||||
@@ -203,6 +200,7 @@ async function handleHandle() {
|
|||||||
else {
|
else {
|
||||||
// 回复评论
|
// 回复评论
|
||||||
await addPostCommentReply(form.value.postName, {
|
await addPostCommentReply(form.value.postName, {
|
||||||
|
allowNotification: form.value.allowNotification,
|
||||||
raw: form.value.content,
|
raw: form.value.content,
|
||||||
content: form.value.content,
|
content: form.value.content,
|
||||||
owner: {
|
owner: {
|
||||||
@@ -219,85 +217,102 @@ async function handleHandle() {
|
|||||||
handleClose(true)
|
handleClose(true)
|
||||||
handleResetForm()
|
handleResetForm()
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err : any) {
|
||||||
console.error('评论提交失败', err)
|
const error = err as UniHaloError
|
||||||
if (config.value?.security?.captcha?.anonymousCommentCaptcha) {
|
if (config.value?.security?.captcha?.anonymousCommentCaptcha) {
|
||||||
captchaData.value.status = 'success'
|
captchaData.value.status = 'success'
|
||||||
form.value.captchaCode = undefined
|
form.value.captchaCode = undefined
|
||||||
|
if(error?.data?.captcha){
|
||||||
|
captchaData.value.image = error?.data?.captcha
|
||||||
|
}else{
|
||||||
handleGetCaptchaImage()
|
handleGetCaptchaImage()
|
||||||
}
|
}
|
||||||
uni.showToast({ icon: 'none', title: '提交失败' })
|
}
|
||||||
|
uni.showToast({ icon: 'none', title: error?.data?.detail ?? '提交失败' })
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOnChange(isOpen: boolean) {
|
function handleOnChange(isOpen : boolean) {
|
||||||
isShow.value = isOpen
|
isShow.value = isOpen
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
emit('on-close', { isSubmit: false, refresh: false })
|
emit('on-close', { isSubmit: false, refresh: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClose(refresh = false) {
|
function handleClose(refresh = false) {
|
||||||
isShow.value = false
|
isShow.value = false
|
||||||
emit('on-close', { isSubmit: true, refresh })
|
emit('on-close', { isSubmit: true, refresh })
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.show, (newVal) => {
|
watch(() => props.show, (newVal) => {
|
||||||
if (!newVal)
|
if (!newVal) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
isShow.value = true
|
isShow.value = true
|
||||||
handleResetForm()
|
handleResetForm()
|
||||||
form.value.postName = props.postName
|
form.value.postName = props.postName
|
||||||
handleGetConfig()
|
handleGetConfig()
|
||||||
handleInitVisitor()
|
handleInitVisitor()
|
||||||
})
|
}, {
|
||||||
|
immediate: true
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<wd-popup v-model="isShow" position="bottom" custom-style="border-radius:12rpx 12rpx 0 0;" @close="handleOnChange(false)">
|
<wd-popup v-model="isShow" position="bottom" :z-index="100" closable custom-style="border-radius:12rpx 12rpx 0 0;"
|
||||||
<view class="uh-comment-modal box-border max-h-[71vh] overflow-y-auto p-3">
|
@close="handleOnChange(false)">
|
||||||
<view class="title my-6 text-center text-[32rpx] font-bold">
|
<view class="uh-comment-modal box-border p-3">
|
||||||
|
<view class="title text-center text-md font-bold">
|
||||||
{{ calcTitle }}
|
{{ calcTitle }}
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form">
|
<view class="form mt-6 max-h-[70vh] overflow-y-auto">
|
||||||
<view class="form-item mb-5 flex items-center">
|
<view class="form-item mb-5 flex items-center">
|
||||||
<textarea
|
<textarea v-model="form.content"
|
||||||
v-model="form.content"
|
|
||||||
class="content-input box-border w-full rounded-xl bg-[#f5f5f5] p-5 text-[26rpx]"
|
class="content-input box-border w-full rounded-xl bg-[#f5f5f5] p-5 text-[26rpx]"
|
||||||
:placeholder="config.editor?.placeholder || '请输入内容,不超过200字符...'"
|
:placeholder="config.editor?.placeholder || '请输入内容,不超过200字符...'" :maxlength="200"
|
||||||
:maxlength="200"
|
style="height: 200rpx;" />
|
||||||
style="height: 200rpx;"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item mb-5 flex items-center">
|
<view class="form-item mb-5 flex items-center">
|
||||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的昵称</text>
|
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的昵称</text>
|
||||||
<input v-model="form.author" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="请输入您的昵称...">
|
<input v-model="form.author"
|
||||||
|
class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]"
|
||||||
|
placeholder="请输入您的昵称...">
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item mb-5 flex items-center">
|
<view class="form-item mb-5 flex items-center">
|
||||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的邮箱</text>
|
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的邮箱</text>
|
||||||
<input v-model="form.email" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="请输入您的邮箱...">
|
<input v-model="form.email" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]"
|
||||||
|
placeholder="请输入您的邮箱...">
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-item mb-5 flex items-center">
|
<view class="form-item mb-5 flex items-center">
|
||||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的网站</text>
|
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">我的网站</text>
|
||||||
<input v-model="form.authorUrl" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="请输入您的网址...">
|
<input v-model="form.authorUrl"
|
||||||
|
class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]"
|
||||||
|
placeholder="请输入您的网址...">
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 匿名评论验证码 -->
|
<!-- 匿名评论验证码 -->
|
||||||
<view v-if="config?.security?.captcha?.anonymousCommentCaptcha" class="form-item mb-5 flex items-center">
|
<view v-if="config?.security?.captcha?.anonymousCommentCaptcha"
|
||||||
|
class="form-item mb-5 flex items-center">
|
||||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">验证码</text>
|
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">验证码</text>
|
||||||
<view class="captcha-row flex flex-1 items-center gap-4">
|
<view class="captcha-row flex flex-1 items-center gap-4">
|
||||||
<input v-model="form.captchaCode" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="请输入验证码结果">
|
<input v-model="form.captchaCode"
|
||||||
|
class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]"
|
||||||
|
placeholder="请输入验证码结果">
|
||||||
<view class="captcha-wrapper h-[68rpx] w-[200rpx] flex shrink-0 items-center justify-center">
|
<view class="captcha-wrapper h-[68rpx] w-[200rpx] flex shrink-0 items-center justify-center">
|
||||||
<text v-if="captchaData.status === 'loading'" class="captcha-tip text-[24rpx] text-[#999]">获取中...</text>
|
<text v-if="captchaData.status === 'loading'"
|
||||||
<text v-else-if="captchaData.status === 'fail'" class="captcha-tip text-[24rpx] text-[#f56c6c]" @click="handleGetCaptchaImage()">请重试</text>
|
class="captcha-tip text-[24rpx] text-[#999]">获取中...</text>
|
||||||
<image v-else :src="captchaData.image" class="captcha-img h-full w-full" mode="aspectFit" @click="handleGetCaptchaImage()" />
|
<text v-else-if="captchaData.status === 'fail'"
|
||||||
|
class="captcha-tip text-[24rpx] text-[#f56c6c]"
|
||||||
|
@click="handleGetCaptchaImage()">请重试</text>
|
||||||
|
<image v-else :src="captchaData.image" class="captcha-img h-full w-full" mode="aspectFit"
|
||||||
|
@click="handleGetCaptchaImage()" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -313,9 +328,9 @@ watch(() => props.show, (newVal) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.uh-comment-modal {
|
.uh-comment-modal {
|
||||||
.content-input {
|
.content-input {
|
||||||
min-height: 200rpx;
|
min-height: 200rpx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DataLoadingStatusEnum } from '@/hooks/useDataLoadingStatus'
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
loadingStatus?: DataLoadingStatusEnum
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<IProps>(), {
|
||||||
|
loadingStatus: DataLoadingStatusEnum.Loading,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
function handleRefresh() {
|
||||||
|
emit('refresh')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="min-h-screen w-full flex items-center justify-center">
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<wd-loading v-if="props.loadingStatus === DataLoadingStatusEnum.Loading" :size="60">
|
||||||
|
<view>加载中...</view>
|
||||||
|
</wd-loading>
|
||||||
|
|
||||||
|
<!-- 加载错误 -->
|
||||||
|
<wd-empty v-else-if="props.loadingStatus === DataLoadingStatusEnum.Error" :icon-size="60" icon="no-result">
|
||||||
|
<view>加载失败</view>
|
||||||
|
<wd-button @click="handleRefresh">
|
||||||
|
刷新试试
|
||||||
|
</wd-button>
|
||||||
|
</wd-empty>
|
||||||
|
|
||||||
|
<!-- 加载成功 -->
|
||||||
|
<wd-empty v-else-if="props.loadingStatus === DataLoadingStatusEnum.Empty" :icon-size="60" icon="success">
|
||||||
|
<view>无数据</view>
|
||||||
|
<wd-button @click="handleRefresh">
|
||||||
|
刷新试试
|
||||||
|
</wd-button>
|
||||||
|
</wd-empty>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export enum DataLoadingStatusEnum {
|
||||||
|
Loading = 'loading',
|
||||||
|
Error = 'error',
|
||||||
|
Empty = 'empty',
|
||||||
|
Success = 'success',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDataLoadingStatus() {
|
||||||
|
const loadingStatus = ref<DataLoadingStatusEnum>(DataLoadingStatusEnum.Loading)
|
||||||
|
|
||||||
|
function resetLoadingStatus() {
|
||||||
|
loadingStatus.value = DataLoadingStatusEnum.Loading
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLoadingStatus(newStatus: DataLoadingStatusEnum) {
|
||||||
|
loadingStatus.value = newStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadingStatus,
|
||||||
|
resetLoadingStatus,
|
||||||
|
updateLoadingStatus,
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
-62
@@ -1,19 +1,20 @@
|
|||||||
import type { uniappRequestAdapter } from '@alova/adapter-uniapp';
|
import type { uniappRequestAdapter } from '@alova/adapter-uniapp'
|
||||||
import type { IResponse } from './types';
|
import type { IResponse } from './types'
|
||||||
import AdapterUniapp from '@alova/adapter-uniapp';
|
import AdapterUniapp from '@alova/adapter-uniapp'
|
||||||
import { createAlova } from 'alova';
|
import { createAlova } from 'alova'
|
||||||
import { createServerTokenAuthentication } from 'alova/client';
|
import { createServerTokenAuthentication } from 'alova/client'
|
||||||
import VueHook from 'alova/vue';
|
import VueHook from 'alova/vue'
|
||||||
import { toLoginPage } from '@/utils/toLoginPage';
|
import { toLoginPage } from '@/utils/toLoginPage'
|
||||||
import { ContentTypeEnum, RequestFrom, ResultEnum, ShowMessage } from './tools/enum';
|
import { ContentTypeEnum, RequestFrom, ResultEnum, ShowMessage } from './tools/enum'
|
||||||
import { saveCommentCookies } from './tools/commentCookies';
|
import { saveCommentCookies } from './tools/commentCookies'
|
||||||
import { handleCategoryPasswordError } from './tools/categoryPassword';
|
import { handleCategoryPasswordError } from './tools/categoryPassword'
|
||||||
|
import { UniHaloError } from './tools/exception'
|
||||||
|
|
||||||
// 配置动态Tag
|
// 配置动态Tag
|
||||||
export const API_DOMAINS = {
|
export const API_DOMAINS = {
|
||||||
DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
|
DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
|
||||||
SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY
|
SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY,
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建请求实例
|
* 创建请求实例
|
||||||
@@ -22,19 +23,20 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
|
|||||||
// 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛
|
// 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛
|
||||||
refreshTokenOnError: {
|
refreshTokenOnError: {
|
||||||
isExpired: (error) => {
|
isExpired: (error) => {
|
||||||
return error.response?.status === ResultEnum.Unauthorized;
|
return error.response?.status === ResultEnum.Unauthorized
|
||||||
},
|
},
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
try {
|
try {
|
||||||
// await authLogin();
|
// await authLogin();
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
// 切换到登录页
|
// 切换到登录页
|
||||||
toLoginPage({ mode: 'reLaunch' });
|
toLoginPage({ mode: 'reLaunch' })
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* alova 请求实例
|
* alova 请求实例
|
||||||
@@ -50,79 +52,82 @@ const alovaInstance = createAlova({
|
|||||||
method.config.headers = {
|
method.config.headers = {
|
||||||
ContentType: ContentTypeEnum.JSON,
|
ContentType: ContentTypeEnum.JSON,
|
||||||
Accept: 'application/json, text/plain, */*',
|
Accept: 'application/json, text/plain, */*',
|
||||||
...method.config.headers
|
...method.config.headers,
|
||||||
};
|
}
|
||||||
|
|
||||||
const { config } = method;
|
const { config } = method
|
||||||
const ignoreAuth = !config.meta?.ignoreAuth;
|
const ignoreAuth = !config.meta?.ignoreAuth
|
||||||
console.log('ignoreAuth===>', ignoreAuth);
|
console.log('ignoreAuth===>', ignoreAuth)
|
||||||
// 处理认证信息 自行处理认证问题
|
// 处理认证信息 自行处理认证问题
|
||||||
if (ignoreAuth) {
|
if (ignoreAuth) {
|
||||||
const token = 'getToken()';
|
const token = 'getToken()'
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error('[请求错误]:未登录');
|
throw new Error('[请求错误]:未登录')
|
||||||
}
|
}
|
||||||
// method.config.headers.token = token;
|
// method.config.headers.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.meta?.personalToken) {
|
if (config.meta?.personalToken) {
|
||||||
config.headers['Authorization'] = `Bearer ${config.meta.personalToken}`;
|
config.headers.Authorization = `Bearer ${config.meta.personalToken}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理动态域名
|
// 处理动态域名
|
||||||
if (config.meta?.domain) {
|
if (config.meta?.domain) {
|
||||||
method.baseURL = config.meta.domain;
|
method.baseURL = config.meta.domain
|
||||||
console.log('当前域名', method.baseURL);
|
console.log('当前域名', method.baseURL)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
responded: onResponseRefreshToken({
|
||||||
responded: onResponseRefreshToken((response, method) => {
|
onSuccess: (response, method) => {
|
||||||
console.log('response===>', response);
|
console.log('onResponseRefreshToken response===>', response)
|
||||||
console.log('method===>', method);
|
console.log('onResponseRefreshToken method===>', method)
|
||||||
const { config } = method;
|
const { config } = method
|
||||||
const { requestType } = config;
|
const { requestType } = config
|
||||||
const { statusCode, data: rawData, errMsg, header } = response as UniNamespace.RequestSuccessCallbackResult;
|
const { statusCode, data: rawData, header } = response as UniNamespace.RequestSuccessCallbackResult
|
||||||
|
|
||||||
// 处理特殊请求类型(上传/下载)
|
// 处理特殊请求类型(上传/下载)
|
||||||
if (requestType === 'upload' || requestType === 'download') {
|
if (requestType === 'upload' || requestType === 'download') {
|
||||||
return response;
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存评论验证码 cookie
|
// 保存评论验证码 cookie
|
||||||
saveCommentCookies(method.url, header as Record<string, string> | undefined);
|
saveCommentCookies(method.url, header as Record<string, string> | undefined)
|
||||||
|
|
||||||
// 分类加密密码处理(Halo 私密分类 401/403)
|
// 分类加密密码处理(Halo 私密分类 401/403)
|
||||||
// 注意:此处需在 alova 统一 401 刷新之前消费,避免分类密码请求被误判为登录过期
|
// 注意:此处需在 alova 统一 401 刷新之前消费,避免分类密码请求被误判为登录过期
|
||||||
const isCategoryConsumed = handleCategoryPasswordError(statusCode, method.url, rawData as { status?: number });
|
// const isCategoryConsumed = handleCategoryPasswordError(statusCode, method.url, rawData as { status?: number });
|
||||||
if (isCategoryConsumed) {
|
// if (isCategoryConsumed) {
|
||||||
throw new Error('分类访问受限');
|
// throw new Error('分类访问受限');
|
||||||
|
// }
|
||||||
|
if (statusCode !== ResultEnum.Success200) {
|
||||||
|
const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
|
||||||
|
throw new UniHaloError({
|
||||||
|
message: errorMessage,
|
||||||
|
data: rawData,
|
||||||
|
code: statusCode,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 HTTP 状态码错误
|
|
||||||
if (statusCode !== 200) {
|
|
||||||
const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`;
|
|
||||||
console.error('errorMessage===>', errorMessage);
|
|
||||||
uni.showToast({
|
|
||||||
title: errorMessage,
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
throw new Error(`${errorMessage}:${errMsg}`);
|
|
||||||
}
|
|
||||||
// 归一化响应结构:Halo 来源的原始数据统一封装为 { code, data, message },
|
// 归一化响应结构:Halo 来源的原始数据统一封装为 { code, data, message },
|
||||||
// 与标准接口走同一条解析路径,保证响应模型一致
|
// 与标准接口走同一条解析路径,保证响应模型一致
|
||||||
let responseData: IResponse;
|
let responseData: IResponse
|
||||||
if (config.meta?.requestFrom === RequestFrom.Halo) {
|
if (config.meta?.requestFrom === RequestFrom.Halo) {
|
||||||
responseData = {
|
responseData = {
|
||||||
code: ResultEnum.Success200,
|
code: statusCode,
|
||||||
data: rawData,
|
data: rawData,
|
||||||
message: '请求成功'
|
message: '请求成功',
|
||||||
};
|
|
||||||
} else {
|
|
||||||
responseData = rawData as IResponse;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
responseData = rawData as IResponse
|
||||||
|
}
|
||||||
|
return responseData
|
||||||
|
},
|
||||||
|
onError: (error, method) => {
|
||||||
|
console.error('onResponseRefreshToken error===>', error)
|
||||||
|
console.error('onResponseRefreshToken method===>', method)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
return responseData;
|
export const http = alovaInstance
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
export const http = alovaInstance;
|
|
||||||
|
|||||||
@@ -19,19 +19,13 @@ const httpInterceptor = {
|
|||||||
// 接口请求支持通过 query 参数配置 queryString
|
// 接口请求支持通过 query 参数配置 queryString
|
||||||
if (options.query) {
|
if (options.query) {
|
||||||
const queryStr = stringifyQuery(options.query)
|
const queryStr = stringifyQuery(options.query)
|
||||||
// const queryStr = qs.stringify(options.query, {
|
|
||||||
// allowDots: true,
|
|
||||||
// encodeValuesOnly: true,
|
|
||||||
// skipNulls: true,
|
|
||||||
// encode: true,
|
|
||||||
// arrayFormat: 'repeat'
|
|
||||||
// });
|
|
||||||
if (options.url.includes('?')) {
|
if (options.url.includes('?')) {
|
||||||
options.url += `&${queryStr}`;
|
options.url += `&${queryStr}`;
|
||||||
} else {
|
} else {
|
||||||
options.url += `?${queryStr}`;
|
options.url += `?${queryStr}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非 http 开头需拼接地址
|
// 非 http 开头需拼接地址
|
||||||
if (!options.url.startsWith('http')) {
|
if (!options.url.startsWith('http')) {
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { IResponse } from '../types';
|
||||||
|
|
||||||
|
export class UniHaloError extends Error {
|
||||||
|
public readonly code: number;
|
||||||
|
public readonly data?: any;
|
||||||
|
/**
|
||||||
|
* @param message 错误提示
|
||||||
|
* @param code 业务码
|
||||||
|
* @param data 附加对象数据
|
||||||
|
*/
|
||||||
|
constructor(errData: IResponse) {
|
||||||
|
super(errData.message);
|
||||||
|
// 必须设置name,instanceof、日志打印才正常
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
this.code = errData.code;
|
||||||
|
this.data = errData.data;
|
||||||
|
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, UniHaloError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
export type CustomRequestOptions = UniApp.RequestOptions & {
|
export type CustomRequestOptions = UniApp.RequestOptions & {
|
||||||
query?: Record<string, any>
|
query?: Record<string, any>
|
||||||
|
params?: Record<string, any>
|
||||||
/** 出错时是否隐藏错误提示 */
|
/** 出错时是否隐藏错误提示 */
|
||||||
hideErrorToast?: boolean
|
hideErrorToast?: boolean
|
||||||
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
|
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
/**
|
|
||||||
* 文章详情页(源自旧项目 pagesA/article-detail/article-detail.vue,新建复刻)
|
|
||||||
* 功能:文章头部(标题/作者/封面/统计) + 分类标签 + mp-html 内容渲染 + 受限阅读 + 点赞 + 评论
|
|
||||||
* TODO: 投票(article-vote)、豆瓣(article-douban)、分享海报(liu-poster)待阶段2/3 补充
|
|
||||||
*/
|
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
import { onLoad, onPullDownRefresh, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { getPostByName, getPostCommentReplyList, postTrackersCounter, submitUpvote } from '@/api/halo'
|
import { getPostByName, getPostCommentReplyList, postTrackersCounter, submitUpvote } from '@/api/halo'
|
||||||
import { createVerificationCode, requestRestrictReadCheck } from '@/api/uni-halo'
|
import { createVerificationCode, requestRestrictReadCheck } from '@/api/uni-halo'
|
||||||
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
||||||
@@ -18,6 +12,7 @@ import { checkPostRestrictRead, copyToClipboard, getRestrictReadTypeName, getSho
|
|||||||
import { getDomainOnly } from '@/utils/urlParams'
|
import { getDomainOnly } from '@/utils/urlParams'
|
||||||
import { markdownConfig } from '@/config/markdown'
|
import { markdownConfig } from '@/config/markdown'
|
||||||
import type { IComment, IPost } from '@/api/types/halo'
|
import type { IComment, IPost } from '@/api/types/halo'
|
||||||
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
@@ -28,11 +23,11 @@ definePage({
|
|||||||
|
|
||||||
const appConfigStore = useAppConfigStore()
|
const appConfigStore = useAppConfigStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
|
|
||||||
const haloConfigs = computed(() => appConfigStore.configs)
|
const haloConfigs = computed(() => appConfigStore.configs)
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
|
||||||
const queryName = ref('')
|
const queryName = ref('')
|
||||||
const result = ref<IPost & {
|
const result = ref<IPost & {
|
||||||
_voteIds?: string[]
|
_voteIds?: string[]
|
||||||
@@ -137,7 +132,7 @@ function handleGetOpenid() {
|
|||||||
|
|
||||||
/* ---------------- 数据加载 ---------------- */
|
/* ---------------- 数据加载 ---------------- */
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
loading.value = 'loading'
|
updateLoadingStatus(DataLoadingStatusEnum.Loading)
|
||||||
try {
|
try {
|
||||||
const res = await getPostByName(queryName.value)
|
const res = await getPostByName(queryName.value)
|
||||||
const tempResult = res.data as typeof result.value
|
const tempResult = res.data as typeof result.value
|
||||||
@@ -160,12 +155,12 @@ async function handleGetData() {
|
|||||||
}
|
}
|
||||||
result.value = tempResult
|
result.value = tempResult
|
||||||
uni.setNavigationBarTitle({ title: '文章详情' })
|
uni.setNavigationBarTitle({ title: '文章详情' })
|
||||||
loading.value = 'success'
|
updateLoadingStatus(DataLoadingStatusEnum.Success)
|
||||||
handleTrackersCounter()
|
handleTrackersCounter()
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('获取文章失败', err)
|
console.error('获取文章失败', err)
|
||||||
loading.value = 'error'
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
@@ -175,8 +170,9 @@ async function handleGetData() {
|
|||||||
|
|
||||||
/** 访问计数埋点 */
|
/** 访问计数埋点 */
|
||||||
async function handleTrackersCounter() {
|
async function handleTrackersCounter() {
|
||||||
if (!result.value)
|
if (!result.value) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
const winInfo = uni.getWindowInfo()
|
const winInfo = uni.getWindowInfo()
|
||||||
const appBaseInfo = uni.getAppBaseInfo()
|
const appBaseInfo = uni.getAppBaseInfo()
|
||||||
const baseUrl = import.meta.env.VITE_SERVER_BASEURL || ''
|
const baseUrl = import.meta.env.VITE_SERVER_BASEURL || ''
|
||||||
@@ -310,10 +306,14 @@ async function getVerificationCode() {
|
|||||||
|
|
||||||
/* ---------------- 评论 ---------------- */
|
/* ---------------- 评论 ---------------- */
|
||||||
function handleToComment() {
|
function handleToComment() {
|
||||||
if (!result.value)
|
console.log('calcIsShowComment.value',calcIsShowComment.value)
|
||||||
|
console.log('result.value',result.value)
|
||||||
|
if (!result.value){
|
||||||
return
|
return
|
||||||
if (!calcIsShowComment.value)
|
}
|
||||||
|
if (!calcIsShowComment.value){
|
||||||
return
|
return
|
||||||
|
}
|
||||||
if (!result.value.spec.allowComment) {
|
if (!result.value.spec.allowComment) {
|
||||||
uni.showToast({ icon: 'none', title: '文章已开启禁止评论!' })
|
uni.showToast({ icon: 'none', title: '文章已开启禁止评论!' })
|
||||||
return
|
return
|
||||||
@@ -461,15 +461,15 @@ const globalAppSettings = computed(() => settingStore.settings)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page box-border min-h-screen w-screen flex flex-col pb-[120rpx]" style="background-color: #fafafd;">
|
<view class="app-page box-border min-h-screen w-screen flex flex-col pb-safe" style="background-color: #fafafd;">
|
||||||
<!-- 骨架屏 -->
|
<!-- 骨架屏 -->
|
||||||
<view v-if="loading !== 'success'" class="loading-wrap bg-white p-3">
|
<view v-if="loadingStatus !== 'success'" class="box-border p-4">
|
||||||
<wd-skeleton :row="4" :animated="true" />
|
<uh-data-loading :loading-status="loadingStatus" @refresh="handleGetData()" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<!-- 顶部信息 -->
|
<!-- 顶部信息 -->
|
||||||
<view class="head mx-6 mt-6 flex flex-col items-center rounded-xl bg-white px-6 py-9 shadow-sm">
|
<view class="head flex flex-col items-center rounded-xl bg-white p-4 shadow-sm">
|
||||||
<view class="title text-center text-[36rpx] font-semibold">
|
<view class="title text-center text-[36rpx] font-semibold">
|
||||||
{{ result?.spec.title }}
|
{{ result?.spec.title }}
|
||||||
</view>
|
</view>
|
||||||
@@ -481,14 +481,16 @@ const globalAppSettings = computed(() => settingStore.settings)
|
|||||||
|
|
||||||
<view v-if="result?.spec.cover" class="cover mt-6 h-[280rpx] w-full">
|
<view v-if="result?.spec.cover" class="cover mt-6 h-[280rpx] w-full">
|
||||||
<image
|
<image
|
||||||
class="cover-img h-full w-full rounded-xl"
|
class="cover-img h-full w-full rounded-xl" mode="aspectFill"
|
||||||
mode="aspectFill"
|
|
||||||
:src="calcUrl(result.spec.cover)"
|
:src="calcUrl(result.spec.cover)"
|
||||||
@click="handlePreview(0, [{ url: calcUrl(result.spec.cover) }])"
|
@click="handlePreview(0, [{ url: calcUrl(result.spec.cover) }])"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="count mt-6 flex justify-between" :class="{ 'no-thumbnail border-t-2 border-[#f2f2f2] pt-3': !result?.spec.cover }">
|
<view
|
||||||
|
class="count mt-6 flex justify-between"
|
||||||
|
:class="{ 'no-thumbnail border-t-2 border-[#f2f2f2] pt-3': !result?.spec.cover }"
|
||||||
|
>
|
||||||
<view class="count-item flex flex-1 items-end justify-center text-[#666]">
|
<view class="count-item flex flex-1 items-end justify-center text-[#666]">
|
||||||
<text class="value text-[32rpx]">{{ result?.stats?.visit ?? 0 }}</text>
|
<text class="value text-[32rpx]">{{ result?.stats?.visit ?? 0 }}</text>
|
||||||
<text class="label pl-2 text-[24rpx]">阅读</text>
|
<text class="label pl-2 text-[24rpx]">阅读</text>
|
||||||
@@ -497,7 +499,10 @@ const globalAppSettings = computed(() => settingStore.settings)
|
|||||||
<text class="value text-[32rpx]">{{ result?.stats?.upvote ?? 0 }}</text>
|
<text class="value text-[32rpx]">{{ result?.stats?.upvote ?? 0 }}</text>
|
||||||
<text class="label pl-2 text-[24rpx]">喜欢</text>
|
<text class="label pl-2 text-[24rpx]">喜欢</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="calcIsShowComment" class="count-item flex flex-1 items-end justify-center text-[#666]">
|
<view
|
||||||
|
v-if="calcIsShowComment"
|
||||||
|
class="count-item flex flex-1 items-end justify-center text-[#666]"
|
||||||
|
>
|
||||||
<text class="value text-[32rpx]">{{ result?.stats?.comment ?? 0 }}</text>
|
<text class="value text-[32rpx]">{{ result?.stats?.comment ?? 0 }}</text>
|
||||||
<text class="label pl-2 text-[24rpx]">评论</text>
|
<text class="label pl-2 text-[24rpx]">评论</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -510,76 +515,89 @@ const globalAppSettings = computed(() => settingStore.settings)
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 分类标签 -->
|
<!-- 分类标签 -->
|
||||||
<view class="category mx-6 mt-6 rounded-xl bg-white p-6 text-[28rpx] shadow-sm">
|
<view class="category rounded-xl bg-white p-4 text-xs">
|
||||||
<view class="category-type leading-[55rpx]">
|
<view class="category-type leading-[55rpx]">
|
||||||
<text class="category-label font-bold">分类:</text>
|
<text class="category-label font-bold">分类:</text>
|
||||||
<text v-if="!result?.categories?.length" class="category-tag is-empty rounded-md bg-[#607d8b] px-1.5 py-0.5 text-[24rpx] text-white">未选择分类</text>
|
<text
|
||||||
<text v-for="(item, index) in result?.categories" v-else :key="index" class="category-tag mr-3 rounded-md bg-[#5bb8fa] px-1.5 py-0.5 text-[24rpx] text-white" @click="handleToCate(item)">
|
v-if="!result?.categories?.length"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
未选择分类
|
||||||
|
</text>
|
||||||
|
<template v-else>
|
||||||
|
<text
|
||||||
|
v-for="(item, index) in result?.categories" :key="index"
|
||||||
|
class="text-xs"
|
||||||
|
@click="handleToCate(item)"
|
||||||
|
>
|
||||||
{{ item.spec.displayName }}
|
{{ item.spec.displayName }}
|
||||||
</text>
|
</text>
|
||||||
|
</template>
|
||||||
</view>
|
</view>
|
||||||
<view class="category-type leading-[55rpx]">
|
<view class="category-type leading-[55rpx]">
|
||||||
<text class="category-label font-bold">标签:</text>
|
<text class="category-label font-bold">标签:</text>
|
||||||
<text v-if="!result?.tags?.length" class="category-tag is-empty rounded-md bg-[#607d8b] px-1.5 py-0.5 text-[24rpx] text-white">未选择标签</text>
|
|
||||||
<text
|
<text
|
||||||
v-for="(item, index) in result?.tags"
|
v-if="!result?.tags?.length"
|
||||||
v-else
|
class="text-xs"
|
||||||
:key="index"
|
>
|
||||||
class="category-tag mr-3 rounded-md px-1.5 py-0.5 text-[24rpx] text-white"
|
未选择标签
|
||||||
:style="{ backgroundColor: item.spec.color || '#5bb8fa' }"
|
</text>
|
||||||
|
<template v-else>
|
||||||
|
<text
|
||||||
|
v-for="(item, index) in result?.tags" :key="index"
|
||||||
|
class="text-xs"
|
||||||
@click="handleToTag(item)"
|
@click="handleToTag(item)"
|
||||||
>
|
>
|
||||||
{{ item.spec.displayName }}
|
{{ item.spec.displayName }}
|
||||||
</text>
|
</text>
|
||||||
|
</template>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="originalURL" class="category-type flex leading-[55rpx]">
|
<view v-if="originalURL" class="category-type flex leading-[55rpx]">
|
||||||
<view class="original-url-left w-[84rpx] shrink-0 font-bold">
|
<view class="original-url-left w-[84rpx] shrink-0 font-bold">
|
||||||
原文:
|
原文:
|
||||||
</view>
|
</view>
|
||||||
<view class="original-url-right inline-flex flex-1 items-center">
|
<view class="original-url-right inline-flex flex-1 items-center">
|
||||||
<text class="original-url-link inline-block w-[410rpx] overflow-hidden text-ellipsis whitespace-nowrap text-[#909399]" @click.stop="handleToOriginal(originalURL)">{{ originalURL }}</text>
|
<text
|
||||||
<text class="original-url-btn flex-1 text-right text-[#03a9f4]" @click.stop="handleToOriginal(originalURL)">阅读原文</text>
|
class="original-url-link inline-block w-[410rpx] overflow-hidden text-ellipsis whitespace-nowrap text-[#909399]"
|
||||||
|
@click.stop="handleToOriginal(originalURL)"
|
||||||
|
>
|
||||||
|
{{ originalURL }}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
class="original-url-btn flex-1 text-right text-[#03a9f4]"
|
||||||
|
@click.stop="handleToOriginal(originalURL)"
|
||||||
|
>
|
||||||
|
阅读原文
|
||||||
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<view class="content mx-6 mt-6">
|
<view class="content">
|
||||||
<view class="markdown-wrap overflow-hidden rounded-xl bg-white p-1.5 shadow-sm">
|
<view class="markdown-wrap overflow-hidden rounded-xl bg-white p-1.5">
|
||||||
<!-- 受限阅读 -->
|
<!-- 受限阅读 -->
|
||||||
<template v-if="checkPostRestrictRead(result!)">
|
<template v-if="checkPostRestrictRead(result!)">
|
||||||
<view v-if="showContentArr.length === 0">
|
<view v-if="showContentArr.length === 0">
|
||||||
<uh-restrict-read-skeleton
|
<uh-restrict-read-skeleton
|
||||||
:loading="true"
|
:loading="true" :lines="3"
|
||||||
:lines="3"
|
|
||||||
:tip-text="`此处内容已隐藏,「${getRestrictReadTypeName(result!)}可见」`"
|
:tip-text="`此处内容已隐藏,「${getRestrictReadTypeName(result!)}可见」`"
|
||||||
:button-text="getRestrictReadTypeName(result!)"
|
:button-text="getRestrictReadTypeName(result!)" button-color="#1890ff"
|
||||||
button-color="#1890ff"
|
|
||||||
@refresh="readMore"
|
@refresh="readMore"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view v-for="(showContent, showContentIndex) in showContentArr" v-else :key="showContentIndex">
|
<view v-for="(showContent, showContentIndex) in showContentArr" v-else :key="showContentIndex">
|
||||||
<mp-html
|
<mp-html
|
||||||
class="evan-markdown"
|
class="evan-markdown" lazy-load :domain="markdownConfig.domain ?? ''"
|
||||||
lazy-load
|
:loading-img="markdownConfig.loadingGif" scroll-table selectable
|
||||||
:domain="markdownConfig.domain ?? ''"
|
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
|
||||||
:loading-img="markdownConfig.loadingGif"
|
:content="showContent" :markdown="true" :show-line-number="true"
|
||||||
scroll-table
|
:show-language-name="true" copy-by-long-press
|
||||||
selectable
|
|
||||||
:tag-style="markdownConfig.tagStyle"
|
|
||||||
:container-style="markdownConfig.containStyle"
|
|
||||||
:content="showContent"
|
|
||||||
:markdown="true"
|
|
||||||
:show-line-number="true"
|
|
||||||
:show-language-name="true"
|
|
||||||
copy-by-long-press
|
|
||||||
/>
|
/>
|
||||||
<uh-restrict-read-skeleton
|
<uh-restrict-read-skeleton
|
||||||
:loading="true"
|
:loading="true" :lines="3"
|
||||||
:lines="3"
|
|
||||||
:tip-text="`此处内容已隐藏,「${getRestrictReadTypeName(result!)}可见」`"
|
:tip-text="`此处内容已隐藏,「${getRestrictReadTypeName(result!)}可见」`"
|
||||||
:button-text="getRestrictReadTypeName(result!)"
|
:button-text="getRestrictReadTypeName(result!)" button-color="#1890ff"
|
||||||
button-color="#1890ff"
|
|
||||||
@refresh="readMore"
|
@refresh="readMore"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@@ -588,75 +606,85 @@ const globalAppSettings = computed(() => settingStore.settings)
|
|||||||
<!-- 正常渲染 -->
|
<!-- 正常渲染 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<mp-html
|
<mp-html
|
||||||
class="evan-markdown"
|
class="evan-markdown" lazy-load :domain="markdownConfig.domain ?? ''"
|
||||||
lazy-load
|
:loading-img="markdownConfig.loadingGif" scroll-table selectable
|
||||||
:domain="markdownConfig.domain ?? ''"
|
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
|
||||||
:loading-img="markdownConfig.loadingGif"
|
:content="result?.content?.raw || ''" :markdown="true" :show-line-number="true"
|
||||||
scroll-table
|
:show-language-name="true" copy-by-long-press
|
||||||
selectable
|
|
||||||
:tag-style="markdownConfig.tagStyle"
|
|
||||||
:container-style="markdownConfig.containStyle"
|
|
||||||
:content="result?.content?.raw || ''"
|
|
||||||
:markdown="true"
|
|
||||||
:show-line-number="true"
|
|
||||||
:show-language-name="true"
|
|
||||||
copy-by-long-press
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 版权声明 -->
|
<!-- 版权声明 -->
|
||||||
<view v-if="postDetailConfig?.copyrightEnabled" class="card-wrap mt-6 rounded-xl bg-white p-6 shadow-sm">
|
<view
|
||||||
|
v-if="postDetailConfig?.copyrightEnabled"
|
||||||
|
class="card-wrap rounded-xl bg-white p-4"
|
||||||
|
>
|
||||||
<view class="card-title relative box-border pl-6 text-[30rpx] font-bold">
|
<view class="card-title relative box-border pl-6 text-[30rpx] font-bold">
|
||||||
<text class="absolute left-0 top-2 h-[26rpx] w-2 rounded-lg bg-[#03aefc]" />
|
<text class="absolute left-0 top-1 h-[26rpx] w-1 rounded-lg bg-[#03aefc]" />
|
||||||
版权声明
|
版权声明
|
||||||
</view>
|
</view>
|
||||||
<view class="copyright-content mt-3 rounded-xl bg-[#fafafa] px-6 py-1.5">
|
<view class="copyright-content mt-3 rounded-xl bg-[#fafafa] px-6 py-1.5">
|
||||||
<view v-if="postDetailConfig.copyrightAuthor" class="copyright-text text-[26rpx] text-[#606266] leading-[1.7]">
|
<view
|
||||||
|
v-if="postDetailConfig.copyrightAuthor"
|
||||||
|
class="copyright-text text-[26rpx] text-[#606266] leading-[1.7]"
|
||||||
|
>
|
||||||
版权归属:{{ postDetailConfig.copyrightAuthor }}
|
版权归属:{{ postDetailConfig.copyrightAuthor }}
|
||||||
</view>
|
</view>
|
||||||
<view v-if="postDetailConfig.copyrightDesc" class="copyright-text text-[26rpx] text-[#606266] leading-[1.7]">
|
<view
|
||||||
|
v-if="postDetailConfig.copyrightDesc"
|
||||||
|
class="copyright-text text-[26rpx] text-[#606266] leading-[1.7]"
|
||||||
|
>
|
||||||
版权说明:{{ postDetailConfig.copyrightDesc }}
|
版权说明:{{ postDetailConfig.copyrightDesc }}
|
||||||
</view>
|
</view>
|
||||||
<view v-if="postDetailConfig.copyrightViolation" class="copyright-text text-[26rpx] text-[#f56c6c] leading-[1.7]">
|
<view
|
||||||
|
v-if="postDetailConfig.copyrightViolation"
|
||||||
|
class="copyright-text text-[26rpx] text-[#f56c6c] leading-[1.7]"
|
||||||
|
>
|
||||||
侵权处理:{{ postDetailConfig.copyrightViolation }}
|
侵权处理:{{ postDetailConfig.copyrightViolation }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 评论区域 -->
|
<!-- 评论区域 -->
|
||||||
<view v-if="calcIsShowComment && result" class="card-wrap mt-6 rounded-xl bg-white p-6 shadow-sm">
|
<view v-if="calcIsShowComment && result" class="card-wrap rounded-xl bg-white p-4">
|
||||||
<uh-comment-list
|
<uh-comment-list
|
||||||
:disallow-comment="!result.spec.allowComment"
|
:disallow-comment="!result.spec.allowComment" :post-name="result.metadata.name"
|
||||||
:post-name="result.metadata.name"
|
:post="result" @on-comment="handleOnComment" @on-comment-detail="handleOnShowCommentDetail"
|
||||||
:post="result"
|
|
||||||
@on-comment="handleOnComment"
|
|
||||||
@on-comment-detail="handleOnShowCommentDetail"
|
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 悬浮操作 -->
|
<!-- 悬浮操作 -->
|
||||||
<view class="flot-buttons fixed bottom-[100rpx] right-8 z-999 flex flex-col gap-1.5">
|
<view class="flot-buttons fixed bottom-[100rpx] right-4 z-99 flex flex-col gap-1.5">
|
||||||
<view class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToTopPage()">
|
<view
|
||||||
|
class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm"
|
||||||
|
@click="handleToTopPage()"
|
||||||
|
>
|
||||||
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
||||||
</view>
|
</view>
|
||||||
<view class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" :class="{ active: hasUpvoted() }" @click="handleDoLikes">
|
<view
|
||||||
<wd-icon :name="hasUpvoted() ? 'heart' : 'heart-outline'" size="20px" :color="hasUpvoted() ? '#f44336' : '#03a9f4'" />
|
class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm"
|
||||||
|
:class="{ active: hasUpvoted() }" @click="handleDoLikes"
|
||||||
|
>
|
||||||
|
<wd-icon
|
||||||
|
name="heart" size="20px"
|
||||||
|
:color="hasUpvoted() ? '#f44336' : '#03a9f4'"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="calcIsShowComment" class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToComment">
|
<view
|
||||||
<wd-icon name="chat" size="20px" color="#4caf50" />
|
v-if="calcIsShowComment"
|
||||||
|
class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm"
|
||||||
|
@click="handleToComment()"
|
||||||
|
>
|
||||||
|
<wd-icon name="message" size="20px" color="#4caf50" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</block>
|
</block>
|
||||||
|
|
||||||
<!-- 密码弹窗 -->
|
<!-- 密码弹窗 -->
|
||||||
<wd-dialog
|
<wd-dialog
|
||||||
v-model="passwordModal.show"
|
v-model="passwordModal.show" title="验证提示" :show-cancel="true" show-confirm-button confirm-text="确定"
|
||||||
title="验证提示"
|
|
||||||
:show-cancel="true"
|
|
||||||
show-confirm-button
|
|
||||||
confirm-text="确定"
|
|
||||||
@confirm="restrictReadCheck"
|
@confirm="restrictReadCheck"
|
||||||
>
|
>
|
||||||
<view class="modal-body py-4">
|
<view class="modal-body py-4">
|
||||||
@@ -666,32 +694,28 @@ const globalAppSettings = computed(() => settingStore.settings)
|
|||||||
|
|
||||||
<!-- 验证码弹窗 -->
|
<!-- 验证码弹窗 -->
|
||||||
<wd-dialog
|
<wd-dialog
|
||||||
v-model="verificationCodeModal.show"
|
v-model="verificationCodeModal.show" title="验证提示" :show-cancel="true" confirm-text="确定"
|
||||||
title="验证提示"
|
|
||||||
:show-cancel="true"
|
|
||||||
confirm-text="确定"
|
|
||||||
@confirm="restrictReadCheck"
|
@confirm="restrictReadCheck"
|
||||||
>
|
>
|
||||||
<view class="modal-body py-4">
|
<view class="modal-body py-4">
|
||||||
<image v-if="verificationCodeModal.imgUrl" :src="verificationCodeModal.imgUrl" class="modal-code-img mb-4 h-[200rpx] w-full" mode="aspectFit" />
|
<image
|
||||||
|
v-if="verificationCodeModal.imgUrl" :src="verificationCodeModal.imgUrl"
|
||||||
|
class="modal-code-img mb-4 h-[200rpx] w-full" mode="aspectFit"
|
||||||
|
/>
|
||||||
<wd-input v-model="restrictReadInputCode" placeholder="请输入验证码" class="mt-2" />
|
<wd-input v-model="restrictReadInputCode" placeholder="请输入验证码" class="mt-2" />
|
||||||
</view>
|
</view>
|
||||||
</wd-dialog>
|
</wd-dialog>
|
||||||
|
|
||||||
<!-- 评论弹窗 -->
|
<!-- 评论弹窗 -->
|
||||||
<uh-comment-modal
|
<uh-comment-modal
|
||||||
v-if="commentModal.show"
|
v-if="commentModal.show" :show="commentModal.show" :is-comment="commentModal.isComment"
|
||||||
:show="commentModal.show"
|
:title="commentModal.title" :post-name="commentModal.postName" @on-close="handleOnCommentModalClose"
|
||||||
:is-comment="commentModal.isComment"
|
|
||||||
:title="commentModal.title"
|
|
||||||
:post-name="commentModal.postName"
|
|
||||||
@on-close="handleOnCommentModalClose"
|
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.app-page {
|
.app-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
* 功能:关键词搜索文章/瞬间,结果列表展示
|
* 功能:关键词搜索文章/瞬间,结果列表展示
|
||||||
*/
|
*/
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
import { onLoad, onPullDownRefresh, onShow } from '@dcloudio/uni-app'
|
||||||
import { getPostListByKeyword } from '@/api/halo'
|
import { getPostListByKeyword } from '@/api/halo'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import { usePluginAvailable } from '@/utils/plugin'
|
import { usePluginAvailable } from '@/utils/plugin'
|
||||||
import { markdownConfig } from '@/config/markdown'
|
import { markdownConfig } from '@/config/markdown'
|
||||||
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
import { formatTime as formatTimeUtil } from '@/utils/formatTime'
|
||||||
|
import { debounce } from '@/utils/debounce'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
@@ -42,6 +43,24 @@ const dataList = ref<{
|
|||||||
updateTimestamp?: string
|
updateTimestamp?: string
|
||||||
}[]>([])
|
}[]>([])
|
||||||
|
|
||||||
|
/* ---------------- 动画(对应旧版 mixin calcAniWait/fnResetSetAniWaitIndex) ---------------- */
|
||||||
|
const aniWaitIndex = ref(0)
|
||||||
|
|
||||||
|
function resetAniWaitIndex() {
|
||||||
|
aniWaitIndex.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算列表项动画等待(每 10 项重置一轮,每项递增 50ms) */
|
||||||
|
function calcAniWait(index: number): number {
|
||||||
|
if ((index + 1) % 10 === 0) {
|
||||||
|
aniWaitIndex.value = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
aniWaitIndex.value += 1
|
||||||
|
}
|
||||||
|
return aniWaitIndex.value * 50
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- 搜索 ---------------- */
|
/* ---------------- 搜索 ---------------- */
|
||||||
async function handleGetData() {
|
async function handleGetData() {
|
||||||
if (calcAuditModeEnabled.value)
|
if (calcAuditModeEnabled.value)
|
||||||
@@ -64,6 +83,7 @@ async function handleGetData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleOnSearch() {
|
function handleOnSearch() {
|
||||||
|
resetAniWaitIndex()
|
||||||
if (!queryParams.value.keyword) {
|
if (!queryParams.value.keyword) {
|
||||||
dataList.value = []
|
dataList.value = []
|
||||||
loading.value = 'success'
|
loading.value = 'success'
|
||||||
@@ -73,6 +93,11 @@ function handleOnSearch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 实时搜索:输入防抖 400ms 后触发(对应旧版 tm-search 的 @input) */
|
||||||
|
const handleOnInput = debounce(() => {
|
||||||
|
handleOnSearch()
|
||||||
|
}, 400)
|
||||||
|
|
||||||
function isArticle(item: { type?: string }): boolean {
|
function isArticle(item: { type?: string }): boolean {
|
||||||
return item.type === 'post.content.halo.run'
|
return item.type === 'post.content.halo.run'
|
||||||
}
|
}
|
||||||
@@ -106,14 +131,23 @@ function handleToTopPage(duration = 500) {
|
|||||||
|
|
||||||
/* ---------------- 生命周期 ---------------- */
|
/* ---------------- 生命周期 ---------------- */
|
||||||
onLoad(async () => {
|
onLoad(async () => {
|
||||||
|
resetAniWaitIndex()
|
||||||
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
|
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
|
||||||
if (!uniHaloPluginAvailable.value) {
|
if (!uniHaloPluginAvailable.value) {
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 关键词非空(如带参进入)时自动搜索,否则展示空态
|
||||||
if (!queryParams.value.keyword) {
|
if (!queryParams.value.keyword) {
|
||||||
loading.value = 'success'
|
loading.value = 'success'
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
handleGetData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
resetAniWaitIndex()
|
||||||
})
|
})
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
onPullDownRefresh(() => {
|
||||||
@@ -143,6 +177,7 @@ onPullDownRefresh(() => {
|
|||||||
class="search-field flex-1 text-[26rpx]"
|
class="search-field flex-1 text-[26rpx]"
|
||||||
placeholder="搜索内容..."
|
placeholder="搜索内容..."
|
||||||
confirm-type="search"
|
confirm-type="search"
|
||||||
|
@input="handleOnInput"
|
||||||
@confirm="handleOnSearch"
|
@confirm="handleOnSearch"
|
||||||
>
|
>
|
||||||
<view v-if="queryParams.keyword" class="clear-btn flex items-center" @click="queryParams.keyword = ''; handleOnSearch()">
|
<view v-if="queryParams.keyword" class="clear-btn flex items-center" @click="queryParams.keyword = ''; handleOnSearch()">
|
||||||
@@ -168,7 +203,13 @@ onPullDownRefresh(() => {
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<view v-for="(item, index) in dataList" :key="index" class="article-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm" @click="handleToDetail(item)">
|
<view
|
||||||
|
v-for="(item, index) in dataList"
|
||||||
|
:key="index"
|
||||||
|
class="article-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm fade-up"
|
||||||
|
:style="{ animationDelay: `${calcAniWait(index)}ms` }"
|
||||||
|
@click="handleToDetail(item)"
|
||||||
|
>
|
||||||
<view class="card-head mb-3 flex items-center">
|
<view class="card-head mb-3 flex items-center">
|
||||||
<view class="type-tag mr-3 shrink-0 rounded-md px-1.5 py-0.5 text-[22rpx] text-white" :class="isArticle(item) ? 'bg-[#2196f3]' : 'bg-[#4caf50]'">
|
<view class="type-tag mr-3 shrink-0 rounded-md px-1.5 py-0.5 text-[22rpx] text-white" :class="isArticle(item) ? 'bg-[#2196f3]' : 'bg-[#4caf50]'">
|
||||||
{{ isArticle(item) ? '文章' : '瞬间' }}
|
{{ isArticle(item) ? '文章' : '瞬间' }}
|
||||||
@@ -208,4 +249,20 @@ onPullDownRefresh(() => {
|
|||||||
.app-page {
|
.app-page {
|
||||||
/* 布局全部由 UnoCSS 原子类实现 */
|
/* 布局全部由 UnoCSS 原子类实现 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 列表项入场动画(对应旧版 tm-translate fadeUp) */
|
||||||
|
.fade-up {
|
||||||
|
animation: fade-up 0.4s ease-out both;
|
||||||
|
|
||||||
|
@keyframes fade-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(24rpx);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -28,7 +28,7 @@ const articleDetailPath = '/pages-blog/article-detail/article-detail'
|
|||||||
// 本地开发快速跳转页面,发布请置为 false
|
// 本地开发快速跳转页面,发布请置为 false
|
||||||
const DEV_MODE = false
|
const DEV_MODE = false
|
||||||
const DEV_TO_TYPE = 'page' as 'page' | 'tabbar'
|
const DEV_TO_TYPE = 'page' as 'page' | 'tabbar'
|
||||||
const DEV_TO_PATH = '/pages-blog/data-visual/data-visual'
|
const DEV_TO_PATH = articleDetailPath + '?name=01a057b2-3200-74af-8afe-28a054092e82'
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const appConfigStore = useAppConfigStore()
|
const appConfigStore = useAppConfigStore()
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ async function handleGetCategory() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
||||||
category.value.list = (res.data.items || [])
|
console.log('分类数据',res.data)
|
||||||
|
category.value.list = (res.data || [])
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
name: item.metadata.name,
|
name: item.metadata.name,
|
||||||
displayName: item.spec.displayName,
|
displayName: item.spec.displayName,
|
||||||
@@ -134,10 +135,11 @@ function handleGetDataByCategory(index: number) {
|
|||||||
handleGetData(true)
|
handleGetData(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOnCategoryChange(e: { detail: { current: number } }) {
|
function handleOnCategoryChange(e:{index:number,name:number}) {
|
||||||
|
console.log('切换分类', e)
|
||||||
if (lock.value)
|
if (lock.value)
|
||||||
return
|
return
|
||||||
handleGetDataByCategory(e.detail.current)
|
handleGetDataByCategory(e.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 图片预览 ---------------- */
|
/* ---------------- 图片预览 ---------------- */
|
||||||
@@ -207,16 +209,16 @@ onReachBottom(() => {
|
|||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- 顶部切换 -->
|
<!-- 顶部切换 -->
|
||||||
<view v-if="category.list.length > 0" class="category-tabs fixed inset-x-0 top-0 z-6 bg-white">
|
|
||||||
<wd-tabs
|
<wd-tabs
|
||||||
|
v-if="category.list.length > 0"
|
||||||
v-model="category.activeIndex"
|
v-model="category.activeIndex"
|
||||||
:tabs="category.list.map(item => ({ title: item.displayName }))"
|
|
||||||
align="left"
|
align="left"
|
||||||
|
sticky
|
||||||
|
:offset-top="0"
|
||||||
@change="handleOnCategoryChange"
|
@change="handleOnCategoryChange"
|
||||||
/>
|
>
|
||||||
</view>
|
<wd-tab v-for="cate in category.list" :key="cate.displayName" :title="cate.displayName"></wd-tab>
|
||||||
<!-- 占位区域 -->
|
</wd-tabs>
|
||||||
<view v-if="category.list.length > 0" class="h-[90rpx] w-screen" />
|
|
||||||
|
|
||||||
<!-- 骨架屏 -->
|
<!-- 骨架屏 -->
|
||||||
<view v-if="loading === 'loading'" class="loading-wrap box-border p-3">
|
<view v-if="loading === 'loading'" class="loading-wrap box-border p-3">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ definePage({
|
|||||||
navigationBarTitleText: '首页',
|
navigationBarTitleText: '首页',
|
||||||
enablePullDownRefresh: true,
|
enablePullDownRefresh: true,
|
||||||
navigationStyle: 'custom',
|
navigationStyle: 'custom',
|
||||||
|
backgroundColor:'#F8F8F8'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ const bloggerInfo = computed(() => {
|
|||||||
|
|
||||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||||
|
|
||||||
const calcIsShowQuickNavigationEnabled = computed(() => !!haloConfigs.value.pageConfig?.homeConfig?.useQuickNavigation)
|
const calcIsShowQuickNavigationEnabled = computed(() => haloConfigs.value.pageConfig?.homeConfig?.useQuickNavigation)
|
||||||
|
|
||||||
const calcIsShowCategory = computed(() => {
|
const calcIsShowCategory = computed(() => {
|
||||||
if (calcAuditModeEnabled.value)
|
if (calcAuditModeEnabled.value)
|
||||||
@@ -241,7 +242,7 @@ async function handleGetArticleList() {
|
|||||||
loadMoreText.value = t('common.loading')
|
loadMoreText.value = t('common.loading')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getPostList({ ...queryParams.value })
|
const res = await getPostList({ ...toRaw(queryParams.value) })
|
||||||
result.value.hasNext = res.data.hasNext
|
result.value.hasNext = res.data.hasNext
|
||||||
articleList.value = isLoadMore.value
|
articleList.value = isLoadMore.value
|
||||||
? articleList.value.concat(res.data.items)
|
? articleList.value.concat(res.data.items)
|
||||||
@@ -278,10 +279,6 @@ function handleToCategoryPage() {
|
|||||||
uni.switchTab({ url: '/pages/tabbar/category/category' })
|
uni.switchTab({ url: '/pages/tabbar/category/category' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToArticlesPage() {
|
|
||||||
uni.navigateTo({ url: '/pages-blog/articles/articles' })
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleToCategoryBy(category: ICategory) {
|
function handleToCategoryBy(category: ICategory) {
|
||||||
if (calcAuditModeEnabled.value)
|
if (calcAuditModeEnabled.value)
|
||||||
return
|
return
|
||||||
@@ -291,7 +288,7 @@ function handleToCategoryBy(category: ICategory) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleToSearch() {
|
function handleToSearch() {
|
||||||
uni.navigateTo({ url: '/pages-blog/articles/articles' })
|
uni.navigateTo({ url: '/pages-blog/search/search' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOnLogoToPage() {
|
function handleOnLogoToPage() {
|
||||||
@@ -374,7 +371,7 @@ handleQuery()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page min-h-screen w-screen flex flex-col">
|
<view class="min-h-screen w-screen flex flex-col">
|
||||||
<!-- 顶部栏 -->
|
<!-- 顶部栏 -->
|
||||||
<view class="header flex items-center gap-4 px-3 py-1.5">
|
<view class="header flex items-center gap-4 px-3 py-1.5">
|
||||||
<image class="logo h-[60rpx] w-[60rpx] rounded-3xl" :src="appInfo.logo" mode="scaleToFill" @click="handleOnLogoToPage" />
|
<image class="logo h-[60rpx] w-[60rpx] rounded-3xl" :src="appInfo.logo" mode="scaleToFill" @click="handleOnLogoToPage" />
|
||||||
@@ -398,7 +395,7 @@ handleQuery()
|
|||||||
|
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<!-- 轮播 Banner -->
|
<!-- 轮播 Banner -->
|
||||||
<view v-if="bannerConfig?.enabled" class="bg-white pb-6">
|
<view v-if="bannerConfig?.enabled" class="bg-white mb-4">
|
||||||
<view v-if="bannerList.length !== 0" class="banner mx-3 mt-3 overflow-hidden rounded-xl">
|
<view v-if="bannerList.length !== 0" class="banner mx-3 mt-3 overflow-hidden rounded-xl">
|
||||||
<uh-swiper
|
<uh-swiper
|
||||||
:height="bannerConfig.height"
|
:height="bannerConfig.height"
|
||||||
@@ -414,11 +411,11 @@ handleQuery()
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 快捷导航 -->
|
<!-- 快捷导航 -->
|
||||||
<view v-if="calcIsShowQuickNavigationEnabled && navList.filter(x => x.show).length" class="nav-box mx-6 mb-6 mt-4 overflow-hidden rounded-xl bg-white p-3">
|
<view v-if="navList.filter(x => x.show).length" class="nav-box px-4 overflow-hidden rounded-xl bg-white p-3">
|
||||||
<view class="page-item-title font-bold">
|
<view class="page-item-title font-bold">
|
||||||
快捷导航
|
快捷导航
|
||||||
</view>
|
</view>
|
||||||
<view class="nav-list grid grid-cols-4 mt-6 gap-6">
|
<view class="nav-list grid grid-cols-5 mt-6 gap-6">
|
||||||
<template v-for="item in navList.filter(x => x.show)" :key="item.key">
|
<template v-for="item in navList.filter(x => x.show)" :key="item.key">
|
||||||
<view class="nav-item flex flex-col items-center gap-3" @click="handleClickNav(item)">
|
<view class="nav-item flex flex-col items-center gap-3" @click="handleClickNav(item)">
|
||||||
<view class="nav-item-icon h-[88rpx] w-[88rpx] flex items-center justify-center rounded-3xl" :style="{ backgroundColor: item.bgColor }">
|
<view class="nav-item-icon h-[88rpx] w-[88rpx] flex items-center justify-center rounded-3xl" :style="{ backgroundColor: item.bgColor }">
|
||||||
@@ -463,7 +460,7 @@ handleQuery()
|
|||||||
<view class="page-item-title font-bold">
|
<view class="page-item-title font-bold">
|
||||||
最新列表
|
最新列表
|
||||||
</view>
|
</view>
|
||||||
<view class="show-more flex items-center justify-center rounded-xl bg-white" @click="handleToArticlesPage">
|
<view class="show-more flex items-center justify-center rounded-xl bg-white" @click="handleToSearch">
|
||||||
<wd-icon name="arrow-right" size="12px" color="#909399" />
|
<wd-icon name="arrow-right" size="12px" color="#909399" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -501,60 +498,3 @@ handleQuery()
|
|||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.app-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
.logo {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
.search-text {
|
|
||||||
margin-left: 12rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-item-title {
|
|
||||||
position: relative;
|
|
||||||
padding-left: 24rpx;
|
|
||||||
font-size: 32rpx;
|
|
||||||
color: #303133;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 8rpx;
|
|
||||||
width: 8rpx;
|
|
||||||
height: 30rpx;
|
|
||||||
background-color: rgb(33 150 243);
|
|
||||||
border-radius: 6rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-more {
|
|
||||||
width: 42rpx;
|
|
||||||
height: 42rpx;
|
|
||||||
box-shadow: 0 0 24rpx rgb(0 0 0 / 3%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.to-top-btn {
|
|
||||||
position: fixed;
|
|
||||||
right: 24rpx;
|
|
||||||
bottom: 120rpx;
|
|
||||||
width: 72rpx;
|
|
||||||
height: 72rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 4rpx 16rpx rgb(0 0 0 / 10%);
|
|
||||||
z-index: 6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { getMomentList } from '@/api/halo'
|
import { getMomentList } from '@/api/halo'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url'
|
import { checkAvatarUrl, checkThumbnailUrl } from '@/utils/url'
|
||||||
@@ -95,7 +94,6 @@ async function handleGetData() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getMomentList({ ...queryParams.value })
|
const res = await getMomentList({ ...queryParams.value })
|
||||||
console.log('获取瞬间数据成功', res)
|
|
||||||
loading.value = 'success'
|
loading.value = 'success'
|
||||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||||
hasNext.value = res.data.hasNext
|
hasNext.value = res.data.hasNext
|
||||||
@@ -250,7 +248,7 @@ onReachBottom(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view class="app-page box-border min-h-screen w-screen flex flex-col py-6">
|
<view class=" box-border min-h-screen w-screen flex flex-col py-6">
|
||||||
<uh-plugin-unavailable
|
<uh-plugin-unavailable
|
||||||
v-if="!uniHaloPluginAvailable"
|
v-if="!uniHaloPluginAvailable"
|
||||||
:plugin-id="uniHaloPluginId"
|
:plugin-id="uniHaloPluginId"
|
||||||
@@ -262,14 +260,14 @@ onReachBottom(() => {
|
|||||||
<wd-skeleton :row="3" :animated="true" />
|
<wd-skeleton :row="3" :animated="true" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="app-page-content">
|
<view v-else class="flex flex-col gap-y-2 p-4">
|
||||||
<view v-if="dataList.length === 0" class="min-h-[70vh] w-full flex items-center justify-center content-empty">
|
<view v-if="dataList.length === 0" class="min-h-[70vh] w-full flex items-center justify-center content-empty">
|
||||||
<wd-empty :description="t('common.empty')" />
|
<wd-empty :description="t('common.empty')" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<block v-else>
|
<block v-else>
|
||||||
<!-- 瞬间卡片 -->
|
<!-- 瞬间卡片 -->
|
||||||
<view v-for="moment in dataList" :key="moment.metadata.name" class="moment-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white shadow-sm">
|
<view v-for="moment in dataList" :key="moment.metadata.name" class="flex flex-col overflow-hidden rounded-xl bg-white shadow-sm">
|
||||||
<view class="head flex items-center p-3 pb-0">
|
<view class="head flex items-center p-3 pb-0">
|
||||||
<image class="avatar h-[66rpx] w-[66rpx] shrink-0 rounded-full" :src="moment.spec.owner?.avatar || bloggerInfo.avatar" mode="aspectFill" />
|
<image class="avatar h-[66rpx] w-[66rpx] shrink-0 rounded-full" :src="moment.spec.owner?.avatar || bloggerInfo.avatar" mode="aspectFill" />
|
||||||
<view class="nickname ml-3">
|
<view class="nickname ml-3">
|
||||||
@@ -361,19 +359,3 @@ onReachBottom(() => {
|
|||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.app-page {
|
|
||||||
/* 布局全部由 UnoCSS 原子类实现 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.moment-card {
|
|
||||||
.head {
|
|
||||||
.nickname {
|
|
||||||
.nickname-text {
|
|
||||||
/* 无额外样式 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user