补充各角色本人 Telegram 授权绑定界面、超级管理员回调配置入口及交互保护。
This commit is contained in:
parent
e3313d46a0
commit
c7bcde6f0b
@ -1,14 +1,24 @@
|
||||
<template>
|
||||
<ConfigEditor />
|
||||
<div :inert="callbackWorking || undefined">
|
||||
<ConfigEditor />
|
||||
</div>
|
||||
<TelegramCallbackSettings
|
||||
v-if="pageKey === 'telegram_push' && store.role === 'super_admin'"
|
||||
@working="callbackWorking = $event"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { provide } from 'vue'
|
||||
import { provide, ref } from 'vue'
|
||||
import { useGlobalStatusStore } from '@/stores/useGlobalStatusStore'
|
||||
import { usePage } from '../ConfigSettings.js'
|
||||
import ConfigEditor from '../ConfigEditor/ConfigEditor.vue'
|
||||
import TelegramCallbackSettings from './TelegramCallbackSettings/TelegramCallbackSettings.vue'
|
||||
|
||||
const props = defineProps({ pageKey: { type: String, required: true } })
|
||||
const page = usePage(props.pageKey)
|
||||
const store = useGlobalStatusStore()
|
||||
const callbackWorking = ref(false)
|
||||
provide('smail-config-page', page)
|
||||
defineExpose({ beforeLeave: page.beforeLeave })
|
||||
defineExpose({ beforeLeave: () => callbackWorking.value ? false : page.beforeLeave() })
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
.telegram-callback {
|
||||
margin: 0 20px 20px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.telegram-callback header,
|
||||
.telegram-callback-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.telegram-callback h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.telegram-callback p {
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.telegram-callback .el-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.telegram-callback-actions .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
import { computed, inject, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { http } from '@/utils/axios'
|
||||
import { useGlobalStatusStore } from '@/stores/useGlobalStatusStore'
|
||||
import { isRecentAuthenticationCancelled, requestWithRecentAuthentication } from '@/utils/recentAuthentication'
|
||||
|
||||
const endpoint = '/api/v1/plugin/settings/telegram_push/callback'
|
||||
|
||||
export function useTelegramCallbackSettings(emit) {
|
||||
const page = inject('smail-config-page')
|
||||
const store = useGlobalStatusStore()
|
||||
const working = ref('')
|
||||
const ready = ref(false)
|
||||
const checked = ref(false)
|
||||
const message = ref('')
|
||||
const errorMessage = ref('')
|
||||
let disposed = false
|
||||
let generation = 0
|
||||
const unavailable = computed(() => store.role !== 'super_admin' || page.loading.value ||
|
||||
page.sectionLoading.value || page.busy.value || page.hasChanges.value ||
|
||||
!!page.pendingCandidate.value || !!page.sectionError.value || !!page.availabilityError.value ||
|
||||
!page.canWrite.value || !page.pluginSecretStatus.value?.configured ||
|
||||
page.formConfig.value.plugins?.telegram_push?.enabled !== true)
|
||||
|
||||
// 配置或令牌发生变化后,旧的回调检查结果不能继续显示为已就绪。
|
||||
watch(() => [page.status.activeGeneration, page.pluginSecretStatus.value?.version, unavailable.value], () => {
|
||||
generation++
|
||||
checked.value = false
|
||||
ready.value = false
|
||||
message.value = ''
|
||||
})
|
||||
const operate = async action => {
|
||||
if (disposed || unavailable.value || working.value) return
|
||||
working.value = action
|
||||
emit('working', true)
|
||||
errorMessage.value = ''
|
||||
const current = generation
|
||||
try {
|
||||
if (action === 'configure') {
|
||||
try {
|
||||
await ElMessageBox.confirm('将使用已保存的系统机器人配置接收绑定请求,不会自动绑定任何账号,也不会覆盖其他应用的回调。', '启用授权回调', {
|
||||
type: 'warning', confirmButtonText: '启用或更新', cancelButtonText: '取消', closeOnClickModal: false
|
||||
})
|
||||
} catch { return }
|
||||
}
|
||||
const request = () => {
|
||||
if (disposed || unavailable.value || current !== generation) throw new Error('系统配置已变更,请刷新后重试')
|
||||
return action === 'configure' ? http.post(endpoint) : http.get(endpoint)
|
||||
}
|
||||
const response = action === 'configure' ? await requestWithRecentAuthentication(request) : await request()
|
||||
if (disposed || current !== generation) return
|
||||
if (response?.errorNo !== 0 || typeof response.data?.ready !== 'boolean') {
|
||||
throw new Error(response?.errorMsg || '读取授权回调状态失败')
|
||||
}
|
||||
checked.value = true
|
||||
ready.value = response.data.ready
|
||||
message.value = ready.value ? '回调已配置,各账号可前往“账号通知”绑定本人 Telegram。' : response.data.message || '尚未启用授权回调'
|
||||
} catch (error) {
|
||||
if (!disposed && !isRecentAuthenticationCancelled(error)) {
|
||||
checked.value = false
|
||||
ready.value = false
|
||||
errorMessage.value = error.message || '授权回调操作失败'
|
||||
}
|
||||
} finally {
|
||||
working.value = ''
|
||||
emit('working', false)
|
||||
}
|
||||
}
|
||||
const preventUnload = event => {
|
||||
if (!working.value) return
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
window.addEventListener('beforeunload', preventUnload)
|
||||
onBeforeUnmount(() => {
|
||||
disposed = true
|
||||
generation++
|
||||
window.removeEventListener('beforeunload', preventUnload)
|
||||
})
|
||||
return { working, ready, checked, message, errorMessage, unavailable, operate }
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<section class="telegram-callback" aria-label="Telegram 授权回调">
|
||||
<header>
|
||||
<h4>个人绑定接入</h4>
|
||||
<el-tag :type="ready ? 'success' : 'info'">{{ checked ? ready ? '已配置' : '未就绪' : '待检查' }}</el-tag>
|
||||
</header>
|
||||
<p>先保存并启用系统机器人,再配置授权回调。超级管理员也需在“账号通知”中单独绑定。</p>
|
||||
<el-alert v-if="errorMessage" :title="errorMessage" type="error" :closable="false" show-icon />
|
||||
<el-alert v-if="message" :title="message" :type="ready ? 'success' : 'warning'" :closable="false" show-icon />
|
||||
<div class="telegram-callback-actions">
|
||||
<el-button :disabled="unavailable || !!working" :loading="working === 'check'" @click="operate('check')">
|
||||
检查回调
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="unavailable || !!working" :loading="working === 'configure'" @click="operate('configure')">
|
||||
启用或更新回调
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTelegramCallbackSettings } from './TelegramCallbackSettings.js'
|
||||
|
||||
const emit = defineEmits(['working'])
|
||||
const { working, ready, checked, message, errorMessage, unavailable, operate } = useTelegramCallbackSettings(emit)
|
||||
</script>
|
||||
|
||||
<style scoped src="./TelegramCallbackSettings.css"></style>
|
||||
@ -31,14 +31,21 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.telegram-actions {
|
||||
.telegram-actions,
|
||||
.telegram-buttons,
|
||||
.telegram-identity {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.telegram-actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.telegram-actions .el-button {
|
||||
.telegram-actions .el-button,
|
||||
.telegram-buttons .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
.telegram-settings :deep(.el-form) {
|
||||
@ -52,8 +59,44 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.telegram-settings :deep(.el-form-item:first-child) {
|
||||
grid-column: 1 / -1;
|
||||
.telegram-identity {
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.telegram-identity strong {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.telegram-binding {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.telegram-binding img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.telegram-binding h4,
|
||||
.telegram-binding p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.telegram-binding a {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.telegram-dialog-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { computed, onMounted, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import QRCode from 'qrcode'
|
||||
import { http } from '@/utils/axios'
|
||||
import { isRecentAuthenticationCancelled, requestWithRecentAuthentication } from '@/utils/recentAuthentication'
|
||||
|
||||
const endpoint = '/api/v1/plugin/settings/telegram_push'
|
||||
const defaults = {
|
||||
chatId: '',
|
||||
enabled: false,
|
||||
showContent: false,
|
||||
spoilerContent: false,
|
||||
sendAttachments: false,
|
||||
@ -13,46 +15,58 @@ const defaults = {
|
||||
|
||||
export function useTelegramSettings() {
|
||||
const settings = reactive({ ...defaults })
|
||||
const status = reactive({ bound: false, displayName: '', needsRebind: false, available: false, message: '' })
|
||||
const binding = reactive({ ticket: '', phase: '', displayName: '', expiresAt: 0, url: '', image: '' })
|
||||
const version = ref(0)
|
||||
const ready = ref(false)
|
||||
const working = ref('')
|
||||
const errorMessage = ref('')
|
||||
const botUsername = ref('')
|
||||
const bindingError = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const polling = ref(false)
|
||||
const savedSettings = ref('')
|
||||
const hasChanges = computed(() => ready.value && JSON.stringify(settings) !== savedSettings.value)
|
||||
let disposed = false
|
||||
const botLink = computed(() => /^[a-zA-Z0-9_]{5,32}$/.test(botUsername.value)
|
||||
? `https://t.me/${botUsername.value}` : '')
|
||||
let generation = 0
|
||||
let pollTimer
|
||||
let expiryTimer
|
||||
const bindingLabel = computed(() => status.needsRebind ? '需要重新绑定' : status.bound ? '已绑定' : '未绑定')
|
||||
|
||||
const checkResponse = (response, fallback) => {
|
||||
if (!response || response.errorNo !== 0) throw new Error(response?.errorMsg || fallback)
|
||||
return response.data
|
||||
}
|
||||
const acceptSettings = data => {
|
||||
if (!data?.settings || !Number.isSafeInteger(data.version) || data.version < 0) {
|
||||
if (!data || typeof data.bound !== 'boolean' || !Number.isSafeInteger(data.version) || data.version < 0) {
|
||||
throw new Error('插件设置响应不完整,请重新加载')
|
||||
}
|
||||
for (const key of Object.keys(defaults)) {
|
||||
settings[key] = key === 'chatId' ? String(data.settings[key] || '') : data.settings[key] === true
|
||||
settings[key] = data[key] === true
|
||||
}
|
||||
status.bound = data.bound
|
||||
status.displayName = typeof data.displayName === 'string' ? data.displayName : ''
|
||||
status.needsRebind = data.needsRebind === true
|
||||
if (typeof data.available === 'boolean') {
|
||||
status.available = data.available
|
||||
status.message = typeof data.message === 'string' ? data.message : ''
|
||||
}
|
||||
version.value = data.version
|
||||
savedSettings.value = JSON.stringify(settings)
|
||||
ready.value = true
|
||||
}
|
||||
const beforeLeave = async () => {
|
||||
if (working.value) return false
|
||||
const confirmDiscard = async () => {
|
||||
if (!hasChanges.value) return true
|
||||
try {
|
||||
await ElMessageBox.confirm('当前通知设置尚未保存,确定离开吗?', '未保存的更改', {
|
||||
type: 'warning', confirmButtonText: '离开', cancelButtonText: '继续编辑'
|
||||
await ElMessageBox.confirm('当前通知设置尚未保存,确定放弃这些更改吗?', '未保存的更改', {
|
||||
type: 'warning', confirmButtonText: '放弃更改', cancelButtonText: '继续编辑'
|
||||
})
|
||||
return true
|
||||
return !disposed
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
const load = async () => {
|
||||
if (working.value || disposed) return
|
||||
if (working.value || disposed || dialogVisible.value) return
|
||||
working.value = 'load'
|
||||
ready.value = false
|
||||
errorMessage.value = ''
|
||||
@ -60,49 +74,190 @@ export function useTelegramSettings() {
|
||||
const response = await http.get(`${endpoint}/user`)
|
||||
if (disposed) return
|
||||
acceptSettings(checkResponse(response, '读取 Telegram 设置失败'))
|
||||
try {
|
||||
const info = checkResponse(await http.get(`${endpoint}/bot`), '读取机器人信息失败')
|
||||
if (!disposed) botUsername.value = info?.username || ''
|
||||
} catch {
|
||||
// 机器人不可用不应阻止用户停用通知或调整本人设置。
|
||||
botUsername.value = ''
|
||||
}
|
||||
} catch (error) {
|
||||
if (!disposed) errorMessage.value = error.message || '读取 Telegram 设置失败'
|
||||
} finally {
|
||||
working.value = ''
|
||||
}
|
||||
}
|
||||
const submit = async action => {
|
||||
if (!ready.value || working.value || disposed) return
|
||||
const chatId = settings.chatId.trim()
|
||||
if ((chatId && !/^-?[1-9][0-9]{0,18}$/.test(chatId)) || (action === 'test' && !chatId)) {
|
||||
errorMessage.value = '请输入有效的 Telegram 聊天 ID;留空可保存为停用状态'
|
||||
return
|
||||
const reportSync = data => {
|
||||
if (data.synced === false) {
|
||||
errorMessage.value = '设置已保存,但运行中的推送服务尚未同步,请联系超级管理员。'
|
||||
} else {
|
||||
ElMessage.success('Telegram 通知设置已保存')
|
||||
}
|
||||
working.value = action
|
||||
errorMessage.value = ''
|
||||
// 仅发送本人可维护的白名单字段,不接收插件 HTML 提供的路由或身份。
|
||||
const payload = { settings: { ...settings, chatId }, version: version.value }
|
||||
}
|
||||
const recentRequest = request => requestWithRecentAuthentication(() => {
|
||||
if (disposed) throw new Error('当前页面已关闭')
|
||||
return request()
|
||||
})
|
||||
const bindingRequest = (action, ticket = binding.ticket) => http.post(`${endpoint}/binding`, { action, ticket })
|
||||
const stopTimers = () => {
|
||||
clearTimeout(pollTimer)
|
||||
clearTimeout(expiryTimer)
|
||||
polling.value = false
|
||||
}
|
||||
const clearBinding = () => {
|
||||
generation++
|
||||
stopTimers()
|
||||
dialogVisible.value = false
|
||||
Object.assign(binding, { ticket: '', phase: '', displayName: '', expiresAt: 0, url: '', image: '' })
|
||||
bindingError.value = ''
|
||||
}
|
||||
const expire = () => {
|
||||
generation++
|
||||
stopTimers()
|
||||
binding.phase = 'expired'
|
||||
binding.url = ''
|
||||
binding.image = ''
|
||||
bindingError.value = '授权链接已过期,请关闭后重新申请。'
|
||||
}
|
||||
const poll = async () => {
|
||||
if (disposed || polling.value || !dialogVisible.value || !binding.ticket || binding.phase === 'expired') return
|
||||
if (Date.now() >= binding.expiresAt) return expire()
|
||||
clearTimeout(pollTimer)
|
||||
const current = generation
|
||||
polling.value = true
|
||||
bindingError.value = ''
|
||||
try {
|
||||
const response = action === 'test'
|
||||
? await http.post(`${endpoint}/test`, payload)
|
||||
: await http.put(`${endpoint}/user`, payload)
|
||||
if (disposed) return
|
||||
const data = checkResponse(response, action === 'test' ? '发送测试消息失败' : '保存 Telegram 设置失败')
|
||||
if (action !== 'test') acceptSettings(data)
|
||||
ElMessage.success(action === 'test' ? '测试消息已发送' : 'Telegram 设置已保存')
|
||||
const data = checkResponse(await bindingRequest('status'), '读取授权状态失败')
|
||||
if (disposed || current !== generation) return
|
||||
if (!['waiting', 'claimed'].includes(data?.phase)) throw new Error('授权状态异常,请重新绑定')
|
||||
binding.phase = data.phase
|
||||
binding.displayName = typeof data.displayName === 'string' ? data.displayName : ''
|
||||
if (data.phase === 'claimed') {
|
||||
binding.url = ''
|
||||
binding.image = ''
|
||||
} else {
|
||||
pollTimer = setTimeout(poll, 2000)
|
||||
}
|
||||
} catch (error) {
|
||||
if (!disposed) errorMessage.value = error.message || 'Telegram 操作失败'
|
||||
if (!disposed && current === generation) bindingError.value = error.message || '读取授权状态失败,请重试'
|
||||
} finally {
|
||||
if (current === generation) polling.value = false
|
||||
}
|
||||
}
|
||||
const closeBinding = async () => {
|
||||
if (working.value) return false
|
||||
working.value = 'cancel'
|
||||
generation++
|
||||
stopTimers()
|
||||
try {
|
||||
if (binding.ticket) checkResponse(await bindingRequest('cancel'), '取消绑定失败')
|
||||
clearBinding()
|
||||
return true
|
||||
} catch (error) {
|
||||
if (!disposed) bindingError.value = error.message || '取消绑定失败,请重试'
|
||||
return false
|
||||
} finally {
|
||||
working.value = ''
|
||||
}
|
||||
}
|
||||
const beginBinding = async () => {
|
||||
if (!ready.value || !status.available || working.value || disposed || dialogVisible.value) return
|
||||
working.value = 'begin'
|
||||
errorMessage.value = ''
|
||||
let ticket = ''
|
||||
try {
|
||||
if (!await confirmDiscard()) return
|
||||
const data = checkResponse(await recentRequest(() => bindingRequest('begin', '')), '创建授权请求失败')
|
||||
ticket = typeof data?.ticket === 'string' ? data.ticket : ''
|
||||
if (!/^[A-Za-z0-9_-]{43}$/.test(ticket)) throw new Error('绑定凭据无效,请重新申请')
|
||||
if (disposed) return
|
||||
const url = new URL(data.url)
|
||||
const expiresAt = Date.parse(data.expiresAt)
|
||||
if (url.origin !== 'https://t.me' || !/^\/[A-Za-z0-9_]{5,32}$/.test(url.pathname) ||
|
||||
url.username || url.password || url.hash || [...url.searchParams].length !== 1 ||
|
||||
url.searchParams.get('start') !== `smail_${ticket}` || !Number.isFinite(expiresAt) ||
|
||||
expiresAt <= Date.now() || expiresAt > Date.now() + 310000) {
|
||||
throw new Error('Telegram 授权地址或有效期异常,请重新申请')
|
||||
}
|
||||
const image = await QRCode.toDataURL(url.href, { width: 240, margin: 2 })
|
||||
if (disposed) return
|
||||
clearBinding()
|
||||
Object.assign(binding, { ticket, phase: 'waiting', displayName: '', expiresAt, url: url.href, image })
|
||||
dialogVisible.value = true
|
||||
ticket = ''
|
||||
expiryTimer = setTimeout(expire, Math.max(0, expiresAt - Date.now()))
|
||||
await poll()
|
||||
} catch (error) {
|
||||
if (!disposed && !isRecentAuthenticationCancelled(error)) errorMessage.value = error.message || '创建授权请求失败'
|
||||
} finally {
|
||||
if (/^[A-Za-z0-9_-]{43}$/.test(ticket)) {
|
||||
// 页面关闭或二维码生成失败时撤销请求,失败时仍由服务端有效期兜底。
|
||||
try { await bindingRequest('cancel', ticket) } catch { /* 不再展示已失效的授权地址。 */ }
|
||||
}
|
||||
working.value = ''
|
||||
}
|
||||
}
|
||||
const confirmBinding = async () => {
|
||||
if (disposed || working.value || binding.phase !== 'claimed') return
|
||||
if (Date.now() >= binding.expiresAt) return expire()
|
||||
working.value = 'confirm'
|
||||
bindingError.value = ''
|
||||
const ticket = binding.ticket
|
||||
try {
|
||||
const data = checkResponse(await recentRequest(() => {
|
||||
if (Date.now() >= binding.expiresAt) throw new Error('授权已过期,请重新绑定')
|
||||
return bindingRequest('confirm', ticket)
|
||||
}), '确认绑定失败')
|
||||
if (disposed) return
|
||||
acceptSettings(data)
|
||||
clearBinding()
|
||||
reportSync(data)
|
||||
} catch (error) {
|
||||
if (!disposed && !isRecentAuthenticationCancelled(error)) bindingError.value = error.message || '确认绑定失败'
|
||||
} finally {
|
||||
working.value = ''
|
||||
}
|
||||
}
|
||||
const submit = async action => {
|
||||
if (!ready.value || working.value || disposed || dialogVisible.value) return
|
||||
working.value = action
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
if (action === 'unbind') {
|
||||
try {
|
||||
await ElMessageBox.confirm('解除后将停止向此 Telegram 账号发送通知,确定继续吗?', '解除本人绑定', {
|
||||
type: 'warning', confirmButtonText: '解除绑定', cancelButtonText: '取消'
|
||||
})
|
||||
} catch { return }
|
||||
}
|
||||
// 页面不提交接收身份,只能修改本人偏好或使用已确认的绑定。
|
||||
const payload = { ...settings, version: version.value }
|
||||
const response = await recentRequest(() => action === 'test'
|
||||
? http.post(`${endpoint}/test`)
|
||||
: action === 'unbind'
|
||||
? http.delete(`${endpoint}/user`, { data: payload })
|
||||
: http.put(`${endpoint}/user`, payload))
|
||||
if (disposed) return
|
||||
const data = checkResponse(response, action === 'test' ? '发送测试消息失败' : '保存 Telegram 设置失败')
|
||||
if (action === 'test') ElMessage.success('测试请求已提交,请到 Telegram 核对')
|
||||
else {
|
||||
acceptSettings(data)
|
||||
reportSync(data)
|
||||
}
|
||||
} catch (error) {
|
||||
if (!disposed && !isRecentAuthenticationCancelled(error)) errorMessage.value = error.message || 'Telegram 操作失败'
|
||||
} finally {
|
||||
working.value = ''
|
||||
}
|
||||
}
|
||||
const beforeLeave = async () => {
|
||||
if (working.value || !await confirmDiscard()) return false
|
||||
if (!dialogVisible.value) return true
|
||||
try {
|
||||
await ElMessageBox.confirm('当前授权尚未完成,确定取消并离开吗?', '取消绑定', {
|
||||
type: 'warning', confirmButtonText: '取消并离开', cancelButtonText: '继续绑定'
|
||||
})
|
||||
} catch { return false }
|
||||
return closeBinding()
|
||||
}
|
||||
const refresh = async () => {
|
||||
if (await beforeLeave()) await load()
|
||||
if (!working.value && await confirmDiscard()) await load()
|
||||
}
|
||||
const preventUnload = event => {
|
||||
if (!hasChanges.value && !working.value) return
|
||||
if (!hasChanges.value && !working.value && !dialogVisible.value) return
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
@ -113,7 +268,14 @@ export function useTelegramSettings() {
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
disposed = true
|
||||
const ticket = binding.ticket
|
||||
clearBinding()
|
||||
if (typeof window !== 'undefined') window.removeEventListener('beforeunload', preventUnload)
|
||||
if (ticket) bindingRequest('cancel', ticket).catch(() => {})
|
||||
})
|
||||
return { settings, version, ready, working, errorMessage, botLink, load, refresh, submit, beforeLeave, hasChanges }
|
||||
return {
|
||||
settings, status, binding, ready, working, errorMessage, bindingError,
|
||||
dialogVisible, polling, hasChanges, bindingLabel, refresh, submit,
|
||||
beginBinding, confirmBinding, closeBinding, poll, beforeLeave
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,43 +1,115 @@
|
||||
<template>
|
||||
<section class="telegram-settings" aria-label="Telegram 推送设置">
|
||||
<section v-loading="!!working" class="telegram-settings" aria-label="本人 Telegram 通知">
|
||||
<header class="telegram-header">
|
||||
<div>
|
||||
<h4>Telegram 推送设置</h4>
|
||||
<p>只配置当前账户的邮件通知,聊天 ID 留空可停用推送。</p>
|
||||
<h4>Telegram 通知</h4>
|
||||
<p>绑定本人 Telegram,仅接收当前账号的邮件通知。</p>
|
||||
</div>
|
||||
<a v-if="botLink" :href="botLink" target="_blank" rel="noopener noreferrer">联系机器人</a>
|
||||
<el-tag :type="status.needsRebind ? 'warning' : status.bound ? 'success' : 'info'">
|
||||
{{ bindingLabel }}
|
||||
</el-tag>
|
||||
</header>
|
||||
<el-alert v-if="errorMessage" :title="errorMessage" type="error" :closable="false" show-icon />
|
||||
<el-form :model="settings" label-position="top" :disabled="!ready || !!working">
|
||||
<el-form-item label="Telegram 聊天 ID">
|
||||
<el-input v-model="settings.chatId" placeholder="填写聊天 ID,留空停用" maxlength="20" />
|
||||
<el-alert v-if="ready && status.message" :title="status.message" type="warning" :closable="false" show-icon />
|
||||
<el-alert
|
||||
v-if="status.needsRebind"
|
||||
title="机器人已变更或原绑定未经授权,请重新绑定后再接收通知。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="telegram-identity">
|
||||
<span class="telegram-hint">绑定账号</span>
|
||||
<strong>{{ status.bound ? status.displayName || '待重新授权的账号' : '尚未绑定' }}</strong>
|
||||
<div class="telegram-buttons">
|
||||
<el-button :disabled="!ready || !status.available || !!working" @click="beginBinding">
|
||||
{{ status.bound ? '重新绑定' : '授权绑定' }}
|
||||
</el-button>
|
||||
<el-button v-if="status.bound" type="danger" plain :disabled="!!working" @click="submit('unbind')">
|
||||
解除绑定
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-form label-position="left" label-width="120px" :disabled="!ready || !!working">
|
||||
<el-form-item label="邮件通知">
|
||||
<el-switch
|
||||
v-model="settings.enabled"
|
||||
:disabled="!status.bound || (!settings.enabled && (!status.available || status.needsRebind))"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示邮件内容">
|
||||
<el-switch v-model="settings.showContent" />
|
||||
<el-switch v-model="settings.showContent" :disabled="!status.bound" />
|
||||
</el-form-item>
|
||||
<el-form-item label="隐藏正文,点击后显示">
|
||||
<el-switch v-model="settings.spoilerContent" :disabled="!settings.showContent" />
|
||||
<el-form-item label="点击显示正文">
|
||||
<el-switch v-model="settings.spoilerContent" :disabled="!status.bound || !settings.showContent" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发送附件">
|
||||
<el-switch v-model="settings.sendAttachments" />
|
||||
<el-switch v-model="settings.sendAttachments" :disabled="!status.bound" />
|
||||
</el-form-item>
|
||||
<el-form-item label="禁用链接预览">
|
||||
<el-switch v-model="settings.disableLinkPreview" />
|
||||
<el-switch v-model="settings.disableLinkPreview" :disabled="!status.bound" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<p class="telegram-hint">启用正文或附件后,相应内容会发送到 Telegram,请确认聊天对象和隐私需求。</p>
|
||||
<p class="telegram-hint">开启正文或附件后,相应内容将发送到 Telegram。停用通知不会解除绑定。</p>
|
||||
<footer class="telegram-actions">
|
||||
<el-button :disabled="!!working" @click="refresh">重新加载</el-button>
|
||||
<el-button :disabled="!ready || !!working || !settings.chatId.trim()" @click="submit('test')">发送测试消息</el-button>
|
||||
<el-button type="primary" :disabled="!ready || !!working" @click="submit('save')">保存设置</el-button>
|
||||
<el-button
|
||||
:disabled="!ready || !!working || !status.available || !status.bound || status.needsRebind || hasChanges || !settings.enabled"
|
||||
@click="submit('test')"
|
||||
>
|
||||
发送测试消息
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="!ready || !!working || !hasChanges" @click="submit('save')">保存设置</el-button>
|
||||
</footer>
|
||||
<el-dialog
|
||||
:model-value="dialogVisible"
|
||||
title="绑定本人 Telegram"
|
||||
width="420px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!working"
|
||||
:show-close="!working"
|
||||
:before-close="closeBinding"
|
||||
>
|
||||
<div class="telegram-binding" aria-live="polite">
|
||||
<el-alert v-if="bindingError" :title="bindingError" type="warning" :closable="false" show-icon />
|
||||
<template v-if="binding.phase === 'claimed'">
|
||||
<h4>请核对 Telegram 身份</h4>
|
||||
<strong>{{ binding.displayName || '已授权的 Telegram 账号' }}</strong>
|
||||
<p>确认是本人后完成绑定;不是本人请取消。</p>
|
||||
</template>
|
||||
<template v-else-if="binding.phase !== 'expired'">
|
||||
<img v-if="binding.image" :src="binding.image" width="240" height="240" alt="本人 Telegram 授权二维码" />
|
||||
<a v-if="binding.url" :href="binding.url" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">
|
||||
打开 Telegram 授权
|
||||
</a>
|
||||
<p>点击链接或扫码,在机器人私聊中点击“开始”,再回到此处确认。</p>
|
||||
<span class="telegram-hint">链接有效期为 5 分钟,请勿转发。</span>
|
||||
</template>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="telegram-buttons telegram-dialog-actions">
|
||||
<el-button :disabled="!!working" @click="closeBinding">取消</el-button>
|
||||
<el-button v-if="bindingError && binding.phase !== 'expired'" :disabled="!!working || polling" @click="poll">
|
||||
重试状态查询
|
||||
</el-button>
|
||||
<el-button v-if="binding.phase === 'claimed'" type="primary" :disabled="!!working" @click="confirmBinding">
|
||||
确认绑定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTelegramSettings } from './TelegramSettings.js'
|
||||
|
||||
const { settings, ready, working, errorMessage, botLink, refresh, submit, beforeLeave } = useTelegramSettings()
|
||||
const {
|
||||
settings, status, binding, ready, working, errorMessage, bindingError,
|
||||
dialogVisible, polling, hasChanges, bindingLabel, refresh, submit,
|
||||
beginBinding, confirmBinding, closeBinding, poll, beforeLeave
|
||||
} = useTelegramSettings()
|
||||
|
||||
defineExpose({ beforeLeave })
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user