Files
urldb/web/components/WechatBotTab.vue
2025-10-31 13:36:07 +08:00

456 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="tab-content-container">
<div class="space-y-6">
<!-- 基础配置 -->
<n-card title="基础配置" class="mb-6">
<n-form :model="configForm" label-placement="left" label-width="120px">
<n-form-item label="AppID">
<n-input v-model:value="configForm.app_id" placeholder="请输入微信公众号AppID" />
</n-form-item>
<n-form-item label="AppSecret">
<n-input v-model:value="configForm.app_secret" type="password" placeholder="请输入AppSecret" show-password-on="click" />
</n-form-item>
<n-form-item label="Token">
<n-input v-model:value="configForm.token" placeholder="请输入Token用于消息验证" />
</n-form-item>
<n-form-item label="EncodingAESKey">
<n-input v-model:value="configForm.encoding_aes_key" type="password" placeholder="请输入EncodingAESKey可选用于消息加密" show-password-on="click" />
</n-form-item>
</n-form>
</n-card>
<!-- 功能配置 -->
<n-card title="功能配置" class="mb-6">
<n-form :model="configForm" label-placement="left" label-width="120px">
<n-form-item label="启用机器人">
<n-switch v-model:value="configForm.enabled" />
</n-form-item>
<n-form-item label="自动回复">
<n-switch v-model:value="configForm.auto_reply_enabled" />
</n-form-item>
<n-form-item label="欢迎消息">
<n-input v-model:value="configForm.welcome_message" type="textarea" :rows="3" placeholder="新用户关注时发送的欢迎消息" />
</n-form-item>
<n-form-item label="搜索结果限制">
<n-input-number v-model:value="configForm.search_limit" :min="1" :max="10" placeholder="搜索结果返回数量" />
</n-form-item>
</n-form>
</n-card>
<!-- 微信公众号验证文件上传 -->
<n-card title="微信公众号验证文件" class="mb-6">
<div class="space-y-4">
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<h4 class="font-medium text-blue-800 dark:text-blue-200 mb-2">验证文件上传说明</h4>
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2">
微信公众号需要上传一个TXT验证文件到网站根目录请按照以下步骤操作
</p>
<ol class="text-sm text-gray-700 dark:text-gray-300 list-decimal list-inside space-y-1">
<li>点击下方"选择文件"按钮选择微信提供的TXT验证文件</li>
<li>点击"上传验证文件"按钮上传文件</li>
<li>上传成功后文件将可通过网站根目录直接访问</li>
<li>在微信公众平台完成域名验证</li>
</ol>
</div>
<div class="flex items-center space-x-4">
<n-upload
ref="uploadRef"
:show-file-list="false"
:accept="'.txt'"
:max="1"
:custom-request="handleUpload"
@before-upload="beforeUpload"
@change="handleFileChange"
>
<n-button type="primary">
<template #icon>
<i class="fas fa-file-upload"></i>
</template>
选择TXT文件
</n-button>
</n-upload>
<n-button
type="success"
@click="triggerUpload"
:disabled="!selectedFile"
:loading="uploading"
>
<template #icon>
<i class="fas fa-cloud-upload-alt"></i>
</template>
上传验证文件
</n-button>
</div>
<div v-if="uploadResult" class="p-3 rounded-md bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300">
<div class="flex items-center">
<i class="fas fa-check-circle mr-2"></i>
<span>文件上传成功</span>
</div>
<p class="text-xs mt-1">文件名: {{ uploadResult.file_name }}</p>
<p class="text-xs">访问地址: {{ getFullUrl(uploadResult.access_url) }}</p>
</div>
</div>
</n-card>
<!-- 微信公众号平台配置说明 -->
<n-card title="微信公众号平台配置" class="mb-6">
<div class="space-y-4">
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<h4 class="font-medium text-blue-800 dark:text-blue-200 mb-2">服务器配置</h4>
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2">
在微信公众平台后台的<strong>开发 > 基本配置 > 服务器配置</strong>中设置
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 block">URL</label>
<div class="flex items-center space-x-2">
<n-input :value="serverUrl" readonly class="flex-1" />
<n-button size="small" @click="copyToClipboard(serverUrl)" type="primary">
<template #icon>
<i class="fas fa-copy"></i>
</template>
复制
</n-button>
</div>
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 block">Token</label>
<div class="flex items-center space-x-2">
<n-input :value="configForm.token" readonly class="flex-1" />
<n-button size="small" @click="copyToClipboard(configForm.token)" type="primary">
<template #icon>
<i class="fas fa-copy"></i>
</template>
复制
</n-button>
</div>
</div>
</div>
</div>
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
<h4 class="font-medium text-green-800 dark:text-green-200 mb-2">消息加解密配置</h4>
<p class="text-sm text-gray-700 dark:text-gray-300">
如果需要启用消息加密请在微信公众平台选择<strong>安全模式</strong>并填写上面的EncodingAESKey
</p>
</div>
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4">
<h4 class="font-medium text-yellow-800 dark:text-yellow-200 mb-2">注意事项</h4>
<ul class="text-sm text-gray-700 dark:text-gray-300 list-disc list-inside space-y-1">
<li>服务器必须支持HTTPS微信要求</li>
<li>域名必须已备案</li>
<li>首次配置时微信会发送GET请求验证服务器</li>
<li>配置完成后记得点击"启用"按钮</li>
</ul>
</div>
</div>
</n-card>
<!-- 操作按钮 -->
<div class="flex justify-end space-x-4">
<n-button @click="resetForm">重置</n-button>
<n-button type="primary" @click="saveConfig" :loading="loading">保存配置</n-button>
</div>
<!-- 状态信息 -->
<n-card title="运行状态" class="mt-6">
<div class="space-y-4">
<div class="flex items-center space-x-4">
<n-tag :type="botStatus.overall_status ? 'success' : 'default'">
{{ botStatus.status_text || '未知状态' }}
</n-tag>
<n-tag v-if="botStatus.config" :type="botStatus.config.enabled ? 'success' : 'default'">
配置状态: {{ botStatus.config.enabled ? '已启用' : '已禁用' }}
</n-tag>
<n-tag v-if="botStatus.config" :type="botStatus.config.app_id_configured ? 'success' : 'warning'">
AppID: {{ botStatus.config.app_id_configured ? '已配置' : '未配置' }}
</n-tag>
</div>
<div v-if="!botStatus.overall_status && botStatus.config && botStatus.config.enabled" class="bg-orange-50 dark:bg-orange-900/20 rounded-lg p-3">
<p class="text-sm text-orange-800 dark:text-orange-200">
<i class="fas fa-exclamation-triangle mr-2"></i>
机器人已启用但未运行请检查配置是否正确或查看系统日志
</p>
</div>
</div>
</n-card>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { useNotification } from 'naive-ui'
import { useWechatApi } from '~/composables/useApi'
// 定义配置表单类型
interface WechatBotConfigForm {
enabled: boolean
app_id: string
app_secret: string
token: string
encoding_aes_key: string
welcome_message: string
auto_reply_enabled: boolean
search_limit: number
}
const notification = useNotification()
const loading = ref(false)
const wechatApi = useWechatApi()
const botStatus = ref({
overall_status: false,
status_text: '',
config: null as any,
runtime: null as any
})
// 验证文件上传相关
const uploadRef = ref()
const selectedFile = ref<File | null>(null)
const uploading = ref(false)
const uploadResult = ref<any>(null)
// 配置表单 - 直接使用 reactive
const configForm = reactive<WechatBotConfigForm>({
enabled: false,
app_id: '',
app_secret: '',
token: '',
encoding_aes_key: '',
welcome_message: '欢迎关注老九网盘资源库!发送关键词即可搜索资源。',
auto_reply_enabled: true,
search_limit: 5
})
// 计算服务器URL
const serverUrl = computed(() => {
if (process.client) {
return `${window.location.origin}/api/wechat/callback`
}
return 'https://yourdomain.com/api/wechat/callback'
})
// 获取机器人配置
const fetchBotConfig = async () => {
try {
const response = await wechatApi.getBotConfig()
if (response) {
// 直接更新 configForm
configForm.enabled = response.enabled || false
configForm.app_id = response.app_id || ''
configForm.app_secret = response.app_secret || '' // 现在所有字段都不敏感
configForm.token = response.token || ''
configForm.encoding_aes_key = response.encoding_aes_key || ''
configForm.welcome_message = response.welcome_message || '欢迎关注老九网盘资源库!发送关键词即可搜索资源。'
configForm.auto_reply_enabled = response.auto_reply_enabled || true
configForm.search_limit = response.search_limit || 5
}
} catch (error) {
console.error('获取微信机器人配置失败:', error)
notification.error({
content: '获取配置失败',
duration: 3000
})
}
}
// 获取机器人状态
const fetchBotStatus = async () => {
try {
const response = await wechatApi.getBotStatus()
if (response) {
botStatus.value = response
}
} catch (error) {
console.error('获取微信机器人状态失败:', error)
notification.error({
content: '获取状态失败',
duration: 3000
})
}
}
// 保存配置
const saveConfig = async () => {
loading.value = true
try {
// 直接保存所有字段,不检测变更
const payload = {
enabled: configForm.enabled,
app_id: configForm.app_id,
app_secret: configForm.app_secret,
token: configForm.token,
encoding_aes_key: configForm.encoding_aes_key,
welcome_message: configForm.welcome_message,
auto_reply_enabled: configForm.auto_reply_enabled,
search_limit: configForm.search_limit
}
const response = await wechatApi.updateBotConfig(payload)
if (response.success) {
notification.success({
content: '配置保存成功',
duration: 3000
})
// 重新获取状态和配置
await fetchBotConfig()
await fetchBotStatus()
} else {
throw new Error(response.message || '保存失败')
}
} catch (error: any) {
console.error('保存微信机器人配置失败:', error)
notification.error({
content: error.message || '保存配置失败',
duration: 3000
})
} finally {
loading.value = false
}
}
// 重置表单
const resetForm = () => {
// 重新获取原始配置
fetchBotConfig()
}
// 复制到剪贴板
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
notification.success({
content: '已复制到剪贴板',
duration: 2000
})
} catch (error) {
console.error('复制失败:', error)
notification.error({
content: '复制失败',
duration: 2000
})
}
}
// 验证文件上传相关函数
const beforeUpload = (options: { file: any, fileList: any[] }) => {
// 从 options 中提取文件
const file = options?.file?.file || options?.file
// 检查文件对象是否有效
if (!file || !file.name) {
notification.error({
content: '文件选择失败,请重试',
duration: 2000
})
return false
}
// 验证文件类型 - 使用多重检查确保是TXT文件
const isValid = file.type === 'text/plain' ||
file.name.toLowerCase().endsWith('.txt') ||
file.type === 'text/plain;charset=utf-8'
if (!isValid) {
notification.error({
content: '请上传TXT文件',
duration: 2000
})
selectedFile.value = null // 清空之前的选择
return false // 阻止上传无效文件
}
// 保存选中的文件并更新状态
selectedFile.value = file
return false // 阻止自动上传
}
const handleUpload = ({ file, onSuccess, onError }: any) => {
// 这个函数不会被调用,因为我们阻止了自动上传
}
// 文件选择变化时的处理函数
const handleFileChange = (options: { file: any, fileList: any[] }) => {
// 从 change 事件中提取文件信息
const file = options?.file?.file || options?.file
if (file && file.name) {
// 更新选中的文件
selectedFile.value = file
}
}
const triggerUpload = async () => {
if (!selectedFile.value) {
notification.warning({
content: '请选择要上传的TXT文件',
duration: 2000
})
return
}
uploading.value = true
try {
const formData = new FormData()
formData.append('file', selectedFile.value)
const response = await wechatApi.uploadVerifyFile(formData)
if (response.success) {
uploadResult.value = response
notification.success({
content: '验证文件上传成功',
duration: 3000
})
// 清空选择的文件
selectedFile.value = null
if (uploadRef.value) {
uploadRef.value.clear()
}
} else {
throw new Error(response.message || '上传失败')
}
} catch (error: any) {
console.error('上传验证文件失败:', error)
notification.error({
content: error.message || '上传验证文件失败',
duration: 3000
})
} finally {
uploading.value = false
}
}
const getFullUrl = (path: string) => {
if (process.client) {
return `${window.location.origin}${path}`
}
return path
}
// 页面加载时获取配置和状态
onMounted(async () => {
await fetchBotConfig()
await fetchBotStatus()
// 定期刷新状态
const interval = setInterval(fetchBotStatus, 30000)
onUnmounted(() => clearInterval(interval))
})
</script>
<style scoped>
/* 微信公众号机器人标签样式 */
.tab-content-container {
height: calc(100vh - 240px);
overflow-y: auto;
padding-bottom: 1rem;
}
</style>