mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-27 11:19:45 +08:00
feat: add wechat clawbot notification setup UI
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import api from '@/api'
|
||||
import { NotificationConf } from '@/api/types'
|
||||
import { getLogoUrl } from '@/utils/imageUtils'
|
||||
import { useToast } from 'vue-toastification'
|
||||
@@ -45,6 +46,7 @@ const notificationInfo = ref<NotificationConf>({
|
||||
// 各通知类型的名称字典
|
||||
const notificationTypeNames: { [key: string]: string } = {
|
||||
wechat: t('notification.wechat.name'),
|
||||
wechatclawbot: t('notification.wechatclawbot.name'),
|
||||
telegram: t('notification.telegram.name'),
|
||||
qqbot: t('notification.qqbot.name'),
|
||||
vocechat: t('notification.vocechat.name'),
|
||||
@@ -68,6 +70,18 @@ const notificationTypes = [
|
||||
{ value: '其它', title: t('notificationSwitch.other') },
|
||||
]
|
||||
|
||||
interface WechatClawBotStatus {
|
||||
connected?: boolean
|
||||
account_id?: string | null
|
||||
qrcode?: string | null
|
||||
qrcode_url?: string | null
|
||||
qrcode_status?: string | null
|
||||
qrcode_updated_at?: number | null
|
||||
known_targets?: Array<{ userid: string; username: string; last_active?: number | null }>
|
||||
default_target?: string | null
|
||||
base_url?: string | null
|
||||
}
|
||||
|
||||
function ensureWechatConfigDefaults(notification: NotificationConf) {
|
||||
if (notification.type !== 'wechat') {
|
||||
return
|
||||
@@ -83,6 +97,39 @@ function ensureWechatConfigDefaults(notification: NotificationConf) {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureWechatClawBotConfigDefaults(notification: NotificationConf) {
|
||||
if (notification.type !== 'wechatclawbot') {
|
||||
return
|
||||
}
|
||||
if (!notification.config) {
|
||||
notification.config = {}
|
||||
}
|
||||
if (!notification.config.WECHATCLAWBOT_BASE_URL) {
|
||||
notification.config.WECHATCLAWBOT_BASE_URL = 'https://ilinkai.weixin.qq.com'
|
||||
}
|
||||
if (!notification.config.WECHATCLAWBOT_POLL_TIMEOUT) {
|
||||
notification.config.WECHATCLAWBOT_POLL_TIMEOUT = 25
|
||||
}
|
||||
}
|
||||
|
||||
const wechatClawBotLoading = ref(false)
|
||||
const wechatClawBotActionLoading = ref(false)
|
||||
const wechatClawBotStatus = ref<WechatClawBotStatus | null>(null)
|
||||
let wechatClawBotTimer: number | null = null
|
||||
|
||||
function getWechatClawBotRequestParams(extraParams: Record<string, any> = {}) {
|
||||
const config = notificationInfo.value.config || {}
|
||||
return {
|
||||
source: notificationInfo.value.name,
|
||||
fallback_source: props.notification.name,
|
||||
WECHATCLAWBOT_BASE_URL: config.WECHATCLAWBOT_BASE_URL,
|
||||
WECHATCLAWBOT_DEFAULT_TARGET: config.WECHATCLAWBOT_DEFAULT_TARGET,
|
||||
WECHATCLAWBOT_ADMINS: config.WECHATCLAWBOT_ADMINS,
|
||||
WECHATCLAWBOT_POLL_TIMEOUT: config.WECHATCLAWBOT_POLL_TIMEOUT,
|
||||
...extraParams,
|
||||
}
|
||||
}
|
||||
|
||||
const isWechatBotMode = computed({
|
||||
get: () => notificationInfo.value.config?.WECHAT_MODE === 'bot',
|
||||
set: value => {
|
||||
@@ -101,7 +148,11 @@ function openNotificationInfoDialog() {
|
||||
// 替换成深复制,避免修改时影响原数据
|
||||
notificationInfo.value = cloneDeep(props.notification)
|
||||
ensureWechatConfigDefaults(notificationInfo.value)
|
||||
ensureWechatClawBotConfigDefaults(notificationInfo.value)
|
||||
notificationInfoDialog.value = true
|
||||
if (notificationInfo.value.type === 'wechatclawbot') {
|
||||
fetchWechatClawBotStatus(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存详情数据
|
||||
@@ -117,16 +168,137 @@ function saveNotificationInfo() {
|
||||
return
|
||||
}
|
||||
ensureWechatConfigDefaults(notificationInfo.value)
|
||||
ensureWechatClawBotConfigDefaults(notificationInfo.value)
|
||||
notificationInfoDialog.value = false
|
||||
emit('change', notificationInfo.value, props.notification.name)
|
||||
emit('done')
|
||||
}
|
||||
|
||||
function clearWechatClawBotTimer() {
|
||||
if (wechatClawBotTimer) {
|
||||
window.clearTimeout(wechatClawBotTimer)
|
||||
wechatClawBotTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleWechatClawBotRefresh() {
|
||||
clearWechatClawBotTimer()
|
||||
if (!notificationInfoDialog.value || notificationInfo.value.type !== 'wechatclawbot') {
|
||||
return
|
||||
}
|
||||
const connected = wechatClawBotStatus.value?.connected
|
||||
const pendingStatus = ['waiting', 'scanned'].includes((wechatClawBotStatus.value?.qrcode_status || '').toLowerCase())
|
||||
if (connected || pendingStatus) {
|
||||
wechatClawBotTimer = window.setTimeout(() => {
|
||||
fetchWechatClawBotStatus(false)
|
||||
}, connected ? 10000 : 3000)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWechatClawBotStatus(autoGenerateQrcode = false) {
|
||||
if (notificationInfo.value.type !== 'wechatclawbot' || !notificationInfo.value.name) {
|
||||
return
|
||||
}
|
||||
wechatClawBotLoading.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get('notification/wechatclawbot/status', {
|
||||
params: getWechatClawBotRequestParams({ auto_generate_qrcode: autoGenerateQrcode }),
|
||||
})
|
||||
if (result.success) {
|
||||
wechatClawBotStatus.value = result.data
|
||||
scheduleWechatClawBotRefresh()
|
||||
} else {
|
||||
wechatClawBotStatus.value = null
|
||||
clearWechatClawBotTimer()
|
||||
$toast.error(result.message || t('notification.wechatclawbot.statusLoadFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
clearWechatClawBotTimer()
|
||||
$toast.error(t('notification.wechatclawbot.statusLoadFailed'))
|
||||
} finally {
|
||||
wechatClawBotLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWechatClawBotQrcode() {
|
||||
if (!notificationInfo.value.name) {
|
||||
return
|
||||
}
|
||||
wechatClawBotActionLoading.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('notification/wechatclawbot/refresh', null, {
|
||||
params: getWechatClawBotRequestParams(),
|
||||
})
|
||||
if (result.success) {
|
||||
wechatClawBotStatus.value = result.data
|
||||
scheduleWechatClawBotRefresh()
|
||||
$toast.success(t('notification.wechatclawbot.qrcodeRefreshSuccess'))
|
||||
} else {
|
||||
$toast.error(result.message || t('notification.wechatclawbot.qrcodeRefreshFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('notification.wechatclawbot.qrcodeRefreshFailed'))
|
||||
} finally {
|
||||
wechatClawBotActionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function logoutWechatClawBot() {
|
||||
if (!notificationInfo.value.name) {
|
||||
return
|
||||
}
|
||||
wechatClawBotActionLoading.value = true
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('notification/wechatclawbot/logout', null, {
|
||||
params: getWechatClawBotRequestParams(),
|
||||
})
|
||||
if (result.success) {
|
||||
$toast.success(result.message || t('notification.wechatclawbot.logoutSuccess'))
|
||||
await fetchWechatClawBotStatus(true)
|
||||
} else {
|
||||
$toast.error(result.message || t('notification.wechatclawbot.logoutFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('notification.wechatclawbot.logoutFailed'))
|
||||
} finally {
|
||||
wechatClawBotActionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatWechatClawBotTime(timestamp?: number | null) {
|
||||
if (!timestamp) {
|
||||
return ''
|
||||
}
|
||||
return new Date(timestamp * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
const wechatClawBotStatusText = computed(() => {
|
||||
const status = (wechatClawBotStatus.value?.qrcode_status || '').toLowerCase()
|
||||
if (wechatClawBotStatus.value?.connected) {
|
||||
return t('notification.wechatclawbot.connected')
|
||||
}
|
||||
if (status === 'scanned') {
|
||||
return t('notification.wechatclawbot.scanned')
|
||||
}
|
||||
if (status === 'expired') {
|
||||
return t('notification.wechatclawbot.expired')
|
||||
}
|
||||
if (status === 'confirmed') {
|
||||
return t('notification.wechatclawbot.confirmed')
|
||||
}
|
||||
return t('notification.wechatclawbot.waiting')
|
||||
})
|
||||
|
||||
// 根据存储类型选择图标
|
||||
const getIcon = computed(() => {
|
||||
switch (props.notification.type) {
|
||||
case 'wechat':
|
||||
return getLogoUrl('wechat')
|
||||
case 'wechatclawbot':
|
||||
return getLogoUrl('wechatclawbot')
|
||||
case 'telegram':
|
||||
return getLogoUrl('telegram')
|
||||
case 'qqbot':
|
||||
@@ -148,8 +320,15 @@ const getIcon = computed(() => {
|
||||
|
||||
// 按钮点击
|
||||
function onClose() {
|
||||
clearWechatClawBotTimer()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
watch(notificationInfoDialog, value => {
|
||||
if (!value) {
|
||||
clearWechatClawBotTimer()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
@@ -347,6 +526,137 @@ function onClose() {
|
||||
</VCol>
|
||||
</template>
|
||||
</VRow>
|
||||
<VRow v-else-if="notificationInfo.type == 'wechatclawbot'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.name"
|
||||
:label="t('notification.name')"
|
||||
:placeholder="t('notification.name')"
|
||||
:hint="t('notification.nameHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHATCLAWBOT_BASE_URL"
|
||||
:label="t('notification.wechatclawbot.baseUrl')"
|
||||
:hint="t('notification.wechatclawbot.baseUrlHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHATCLAWBOT_DEFAULT_TARGET"
|
||||
:label="t('notification.wechatclawbot.defaultTarget')"
|
||||
:placeholder="t('notification.wechatclawbot.defaultTargetPlaceholder')"
|
||||
:hint="t('notification.wechatclawbot.defaultTargetHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-arrow-right"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHATCLAWBOT_ADMINS"
|
||||
:label="t('notification.wechatclawbot.admins')"
|
||||
:placeholder="t('notification.wechatclawbot.adminsPlaceholder')"
|
||||
:hint="t('notification.wechatclawbot.adminsHint')"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-account-supervisor"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="notificationInfo.config.WECHATCLAWBOT_POLL_TIMEOUT"
|
||||
:label="t('notification.wechatclawbot.pollTimeout')"
|
||||
:hint="t('notification.wechatclawbot.pollTimeoutHint')"
|
||||
persistent-hint
|
||||
type="number"
|
||||
prepend-inner-icon="mdi-timer-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VCard variant="tonal" class="pa-4">
|
||||
<div class="d-flex flex-wrap align-center justify-space-between gap-3 mb-3">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">{{ t('notification.wechatclawbot.loginStatus') }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">{{ wechatClawBotStatusText }}</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<VBtn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
:loading="wechatClawBotLoading"
|
||||
@click.stop="fetchWechatClawBotStatus(true)"
|
||||
>
|
||||
{{ t('common.refresh') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:loading="wechatClawBotActionLoading"
|
||||
@click.stop="refreshWechatClawBotQrcode"
|
||||
>
|
||||
{{ t('notification.wechatclawbot.refreshQrcode') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
size="small"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
:loading="wechatClawBotActionLoading"
|
||||
:disabled="!wechatClawBotStatus?.connected"
|
||||
@click.stop="logoutWechatClawBot"
|
||||
>
|
||||
{{ t('notification.wechatclawbot.logout') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol cols="12" md="5">
|
||||
<div class="rounded text-center p-3 border h-100 d-flex align-center justify-center min-h-[16rem]">
|
||||
<VImg
|
||||
v-if="wechatClawBotStatus?.qrcode_url"
|
||||
:src="wechatClawBotStatus.qrcode_url"
|
||||
width="220"
|
||||
height="220"
|
||||
class="mx-auto"
|
||||
/>
|
||||
<VProgressCircular v-else-if="wechatClawBotLoading" indeterminate color="primary" />
|
||||
<div v-else class="text-body-2 text-medium-emphasis">
|
||||
{{ t('notification.wechatclawbot.noQrcode') }}
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol cols="12" md="7">
|
||||
<VAlert variant="tonal" :type="wechatClawBotStatus?.connected ? 'success' : 'info'" class="mb-3">
|
||||
<div class="text-body-2">{{ t('notification.wechatclawbot.scanHint') }}</div>
|
||||
<div v-if="wechatClawBotStatus?.account_id" class="mt-2">
|
||||
{{ t('notification.wechatclawbot.accountId') }}: {{ wechatClawBotStatus.account_id }}
|
||||
</div>
|
||||
<div v-if="wechatClawBotStatus?.qrcode_updated_at" class="mt-2">
|
||||
{{ t('notification.wechatclawbot.qrcodeUpdatedAt') }}:
|
||||
{{ formatWechatClawBotTime(wechatClawBotStatus.qrcode_updated_at) }}
|
||||
</div>
|
||||
</VAlert>
|
||||
<div class="text-subtitle-2 mb-2">{{ t('notification.wechatclawbot.knownTargets') }}</div>
|
||||
<VList v-if="wechatClawBotStatus?.known_targets?.length" density="compact" class="border rounded">
|
||||
<VListItem
|
||||
v-for="item in wechatClawBotStatus.known_targets"
|
||||
:key="item.userid"
|
||||
:title="item.username || item.userid"
|
||||
:subtitle="`${item.userid}${item.last_active ? ` · ${formatWechatClawBotTime(item.last_active)}` : ''}`"
|
||||
/>
|
||||
</VList>
|
||||
<div v-else class="text-body-2 text-medium-emphasis">
|
||||
{{ t('notification.wechatclawbot.noKnownTargets') }}
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow v-else-if="notificationInfo.type == 'telegram'">
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
|
||||
@@ -91,6 +91,7 @@ const userForm = ref<ExtendedUser>({
|
||||
},
|
||||
settings: {
|
||||
wechat_userid: null,
|
||||
wechatclawbot_userid: null,
|
||||
telegram_userid: null,
|
||||
slack_userid: null,
|
||||
discord_userid: null,
|
||||
@@ -503,6 +504,15 @@ onMounted(() => {
|
||||
prepend-inner-icon="mdi-wechat"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="userForm.settings.wechatclawbot_userid"
|
||||
density="comfortable"
|
||||
clearable
|
||||
:label="t('dialog.userAddEdit.wechatClawBot')"
|
||||
prepend-inner-icon="mdi-robot-happy-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="userForm.settings.telegram_userid"
|
||||
|
||||
Reference in New Issue
Block a user