完善Telegram个人授权绑定页面
Some checks are pending
Security Checks / security (push) Waiting to run

补充各角色本人 Telegram 授权绑定界面、超级管理员回调配置入口及交互保护。
This commit is contained in:
smail 2026-09-29 16:06:14 +08:00
parent e3313d46a0
commit c7bcde6f0b
7 changed files with 490 additions and 63 deletions

View File

@ -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>

View File

@ -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;
}

View File

@ -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 }
}

View File

@ -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>

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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>