1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00

chore: 批量新增项目依赖、工具函数、页面与组件资源

1. 新增mp-html、qs等生产依赖,补全项目基础库
2. 新增平台判断、缓存、工具函数等通用工具集
3. 新增标签页、网站浏览页、关于页等业务页面
4. 新增分类卡片、通知弹窗等业务组件
5. 新增uts-progressNotification、liu-poster、uhalo-upgrade等uni模块
6. 补充audio/video组件样式补件,修复uni-components路径缺失问题
7. 新增环境变量Halo个人令牌配置项
8. 重构store导出结构,新增appConfig/halo/setting三个状态模块
9. 新增tsconfig编译目标配置,适配更高版本ES语法
This commit is contained in:
小莫唐尼
2026-08-31 19:56:16 +08:00
parent ba5b77568b
commit 067f3ed98a
147 changed files with 20866 additions and 325 deletions
@@ -0,0 +1,321 @@
<script lang="ts" setup>
/**
* 评论弹窗(源自旧项目 components/comment-modal,新建复刻)
* 支持新增评论 / 回复评论,含匿名评论验证码(cookie 链路)
*/
import { computed, ref, watch } from 'vue'
import { addPostComment, addPostCommentReply } from '@/api/halo'
import { getCommentWidgetCaptcha, getCommentWidgetConfig } from '@/api/uni-halo'
import { setCache } from '@/utils/storage'
import { deepMerge } from '@/utils/merge'
const props = withDefaults(defineProps<{
show: boolean
isComment?: boolean
title?: string
postName: string
}>(), {
isComment: false,
title: '',
})
const emit = defineEmits<{
(e: 'on-close', data: { isSubmit: boolean, refresh: boolean }): void
}>()
const isShow = ref(false)
interface ICaptchaConfig {
security?: {
captcha?: {
anonymousCommentCaptcha?: boolean
[key: string]: unknown
}
}
editor?: {
placeholder?: string
}
[key: string]: unknown
}
interface ICommentForm {
allowNotification: boolean
author: string
avatar: string
authorUrl: string
content: string
email: string
postName: string
captchaCode?: string
}
const config = ref<ICaptchaConfig>({
security: {
captcha: {
anonymousCommentCaptcha: false,
},
},
editor: {
placeholder: '请输入内容,不超过200字符...',
},
})
const captchaData = ref<{ image: string, status: 'loading' | 'success' | 'fail' }>({
image: '',
status: 'loading',
})
const form = ref<ICommentForm>({
allowNotification: true,
author: '',
avatar: '',
authorUrl: '',
content: '',
email: '',
postName: props.postName,
captchaCode: undefined,
})
const calcTitle = computed(() => {
if (props.isComment)
return props.title || '新增评论'
return `回复用户:${props.title}`
})
function handleResetForm() {
form.value = {
allowNotification: true,
author: '',
avatar: '',
authorUrl: '',
content: '',
email: '',
postName: props.postName,
captchaCode: undefined,
}
}
/** 获取评论组件配置(验证码开关) */
async function handleGetConfig() {
try {
const res = await getCommentWidgetConfig()
config.value = deepMerge(config.value as Record<string, unknown>, res.data as Record<string, unknown>) as ICaptchaConfig
if (config.value?.security?.captcha?.anonymousCommentCaptcha) {
handleGetCaptchaImage()
}
}
catch (err) {
console.error('获取验证码配置失败', err)
}
}
/** 获取评论验证码图片 */
async function handleGetCaptchaImage() {
captchaData.value.status = 'loading'
try {
const res = await getCommentWidgetCaptcha()
form.value.captchaCode = undefined
captchaData.value.image = res.data as unknown as string
captchaData.value.status = 'success'
}
catch (err) {
console.error('获取验证码失败', err)
captchaData.value.status = 'fail'
captchaData.value.image = ''
}
}
/** 初始化访客信息 */
function handleInitVisitor() {
const visitor = uni.getStorageSync('Visitor')
if (!visitor)
return
try {
const v = JSON.parse(visitor)
form.value.author = v.author || ''
form.value.avatar = v.avatar || ''
form.value.email = v.email || ''
form.value.authorUrl = v.authorUrl || ''
}
catch {
// ignore
}
}
/** 保存访客信息 */
function handleSetVisitor() {
setCache('Visitor', {
author: form.value.author,
avatar: form.value.avatar,
email: form.value.email,
authorUrl: form.value.authorUrl,
})
}
/** 提交校验 */
function validateForm(): boolean {
if (!form.value.content.trim()) {
uni.showToast({ icon: 'none', title: '请填写评论内容' })
return false
}
if (!form.value.author.trim()) {
uni.showToast({ icon: 'none', title: '请填写昵称' })
return false
}
if (!form.value.email.trim()) {
uni.showToast({ icon: 'none', title: '请填写邮箱' })
return false
}
if (config.value?.security?.captcha?.anonymousCommentCaptcha && !form.value.captchaCode?.trim()) {
uni.showToast({ icon: 'none', title: '请填写验证码结果!' })
return false
}
return true
}
async function handleHandle() {
if (!validateForm())
return
uni.showLoading({ title: '正在提交...' })
try {
if (props.isComment) {
// 新增评论
await addPostComment({
raw: form.value.content,
content: form.value.content,
owner: {
displayName: form.value.author,
email: form.value.email,
website: form.value.authorUrl,
},
subjectRef: {
group: 'content.halo.run',
kind: 'Post',
name: form.value.postName,
version: 'v1alpha1',
},
captchaCode: config.value?.security?.captcha?.anonymousCommentCaptcha ? form.value.captchaCode : undefined,
})
uni.showToast({ icon: 'none', title: '评论成功,可能需要审核!' })
}
else {
// 回复评论
await addPostCommentReply(form.value.postName, {
raw: form.value.content,
content: form.value.content,
owner: {
displayName: form.value.author,
email: form.value.email,
website: form.value.authorUrl,
},
captchaCode: config.value?.security?.captcha?.anonymousCommentCaptcha ? form.value.captchaCode : undefined,
})
uni.showToast({ icon: 'none', title: '回复成功,可能需要审核!' })
}
handleSetVisitor()
handleClose(true)
handleResetForm()
}
catch (err) {
console.error('评论提交失败', err)
if (config.value?.security?.captcha?.anonymousCommentCaptcha) {
captchaData.value.status = 'success'
form.value.captchaCode = undefined
handleGetCaptchaImage()
}
uni.showToast({ icon: 'none', title: '提交失败' })
}
finally {
uni.hideLoading()
}
}
function handleOnChange(isOpen: boolean) {
isShow.value = isOpen
if (!isOpen) {
emit('on-close', { isSubmit: false, refresh: false })
}
}
function handleClose(refresh = false) {
isShow.value = false
emit('on-close', { isSubmit: true, refresh })
}
watch(() => props.show, (newVal) => {
if (!newVal)
return
isShow.value = true
handleResetForm()
form.value.postName = props.postName
handleGetConfig()
handleInitVisitor()
})
</script>
<template>
<wd-popup v-model="isShow" position="bottom" custom-style="border-radius:12rpx 12rpx 0 0;" @close="handleOnChange(false)">
<view class="uh-comment-modal box-border max-h-[71vh] overflow-y-auto p-3">
<view class="title my-6 text-center text-[32rpx] font-bold">
{{ calcTitle }}
</view>
<view class="form">
<view class="form-item mb-5 flex items-center">
<textarea
v-model="form.content"
class="content-input box-border w-full rounded-xl bg-[#f5f5f5] p-5 text-[26rpx]"
:placeholder="config.editor?.placeholder || '请输入内容,不超过200字符...'"
:maxlength="200"
style="height: 200rpx;"
/>
</view>
<view class="form-item mb-5 flex items-center">
<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="请输入您的昵称...">
</view>
<view class="form-item mb-5 flex items-center">
<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="请输入您的邮箱...">
</view>
<view class="form-item mb-5 flex items-center">
<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="请输入您的网址...">
</view>
<!-- 匿名评论验证码 -->
<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>
<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="请输入验证码结果">
<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-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 class="submit-btn my-6">
<wd-button type="primary" block size="medium" @click="handleHandle">
提交
</wd-button>
</view>
</view>
</view>
</wd-popup>
</template>
<style scoped lang="scss">
.uh-comment-modal {
.content-input {
min-height: 200rpx;
}
}
</style>