268 lines
10 KiB
JavaScript
268 lines
10 KiB
JavaScript
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
import { ElMessageBox, ElNotification } from 'element-plus'
|
|
import { http } from '@/utils/axios'
|
|
import { useGlobalStatusStore } from '@/stores/useGlobalStatusStore'
|
|
import { isRecentAuthenticationCancelled, requestWithRecentAuthentication } from '@/utils/recentAuthentication'
|
|
|
|
export function usePage(props = {}) {
|
|
const globalStatus = useGlobalStatusStore()
|
|
const manager = computed(() => globalStatus.hasCapability('notification.manage'))
|
|
const loading = ref(false)
|
|
const submitting = ref(false)
|
|
const error = ref('')
|
|
const status = ref(null)
|
|
const accounts = ref([])
|
|
const page = ref(1)
|
|
const total = ref(0)
|
|
const selectedID = ref(Number(props.userId || globalStatus.userInfos?.id || 0))
|
|
const form = reactive({ enabled: false, minute_limit: 10, day_limit: 100 })
|
|
const secret = ref('')
|
|
const secretAccount = ref('')
|
|
const secretVisible = ref(false)
|
|
const seconds = ref(0)
|
|
const verifyVisible = ref(false)
|
|
const verifyPassword = ref('')
|
|
const manageVisible = ref(false)
|
|
let verifyTarget = null
|
|
let secretSequence = 0
|
|
let secretTimer = null
|
|
let requestSequence = 0
|
|
let accountsSequence = 0
|
|
let disposed = false
|
|
|
|
const canManage = computed(() => manager.value && Boolean(status.value?.can_manage))
|
|
const canReveal = computed(() => Boolean(status.value?.can_reveal))
|
|
const stateText = computed(() => {
|
|
if (!status.value?.enabled) return '未开启'
|
|
if (!status.value.account_available) return '账号不可用'
|
|
return status.value.configured ? '已开启,可发信' : '已开启,待生成凭据'
|
|
})
|
|
const dirty = computed(() => status.value && (
|
|
form.enabled !== status.value.enabled || form.minute_limit !== status.value.minute_limit || form.day_limit !== status.value.day_limit
|
|
))
|
|
|
|
const dataOf = result => {
|
|
if (result?.errorNo !== 0) throw new Error(result?.errorMsg || '操作失败')
|
|
return result.data
|
|
}
|
|
const showError = cause => {
|
|
if (isRecentAuthenticationCancelled(cause) || cause === 'cancel' || cause === 'close') return
|
|
ElNotification({ title: '操作失败', message: cause?.message || '请稍后重试', type: 'error' })
|
|
}
|
|
const clearSecret = () => {
|
|
secretSequence += 1
|
|
if (secretTimer) clearInterval(secretTimer)
|
|
secretTimer = null
|
|
secret.value = ''
|
|
secretAccount.value = ''
|
|
seconds.value = 0
|
|
secretVisible.value = false
|
|
}
|
|
|
|
const clearVerification = () => {
|
|
verifyPassword.value = ''
|
|
verifyTarget = null
|
|
verifyVisible.value = false
|
|
}
|
|
const showSecret = (code, account) => {
|
|
clearSecret()
|
|
if (!code || document.hidden) return
|
|
secret.value = code
|
|
secretAccount.value = account
|
|
secretVisible.value = true
|
|
const expiresAt = Date.now() + 60000
|
|
seconds.value = 60
|
|
secretTimer = setInterval(() => {
|
|
seconds.value = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000))
|
|
if (seconds.value === 0) clearSecret()
|
|
}, 250)
|
|
}
|
|
const openVerify = () => {
|
|
if (!canReveal.value || loading.value || submitting.value || dirty.value) return
|
|
clearSecret()
|
|
verifyPassword.value = ''
|
|
verifyTarget = status.value
|
|
verifyVisible.value = true
|
|
}
|
|
const openManage = async row => {
|
|
if (loading.value || submitting.value || !(await beforeLeave())) return
|
|
clearSecret()
|
|
status.value = null
|
|
selectedID.value = row.user_id
|
|
await loadStatus()
|
|
if (!disposed) manageVisible.value = true
|
|
}
|
|
const closeManage = async () => {
|
|
if (!(await beforeLeave())) return false
|
|
manageVisible.value = false
|
|
clearSecret()
|
|
return true
|
|
}
|
|
const revealAccount = async row => {
|
|
if (loading.value || submitting.value || !row.configured || !(await beforeLeave())) return
|
|
clearSecret()
|
|
status.value = null
|
|
selectedID.value = row.user_id
|
|
await loadStatus()
|
|
if (disposed || !status.value || status.value.user_id !== row.user_id) return
|
|
if (canReveal.value) openVerify()
|
|
}
|
|
const verifyAndReveal = async () => {
|
|
if (!verifyTarget || !verifyPassword.value || submitting.value || !canReveal.value) return
|
|
const target = verifyTarget
|
|
const sequence = ++secretSequence
|
|
submitting.value = true
|
|
try {
|
|
const data = dataOf(await http.post('/api/v1/system/notification/credential/reveal', {
|
|
user_id: target.user_id, version: target.version, password: verifyPassword.value
|
|
}))
|
|
if (disposed || sequence !== secretSequence || target.user_id !== status.value?.user_id) return
|
|
clearVerification()
|
|
showSecret(data.code, target.account)
|
|
} catch (cause) {
|
|
if (disposed || sequence !== secretSequence) return
|
|
verifyPassword.value = ''
|
|
showError(cause)
|
|
await Promise.all([loadStatus(), loadAccounts()])
|
|
if (disposed || sequence !== secretSequence) return
|
|
if (!canReveal.value || target.user_id !== status.value?.user_id || target.version !== status.value?.version) {
|
|
clearSecret()
|
|
clearVerification()
|
|
}
|
|
} finally {
|
|
verifyPassword.value = ''
|
|
submitting.value = false
|
|
}
|
|
}
|
|
const hideOnBackground = () => {
|
|
if (!document.hidden) return
|
|
clearSecret()
|
|
clearVerification()
|
|
}
|
|
|
|
const loadStatus = async () => {
|
|
const sequence = ++requestSequence
|
|
loading.value = true
|
|
error.value = ''
|
|
try {
|
|
const data = dataOf(await http.get('/api/v1/system/notification/status', { params: { user_id: selectedID.value || undefined } }))
|
|
if (disposed || sequence !== requestSequence) return
|
|
status.value = data
|
|
Object.assign(form, { enabled: data.enabled, minute_limit: data.minute_limit, day_limit: data.day_limit })
|
|
} catch (cause) {
|
|
if (disposed || sequence !== requestSequence) return
|
|
status.value = null
|
|
error.value = cause?.message || '读取通知发信状态失败'
|
|
} finally {
|
|
if (!disposed && sequence === requestSequence) loading.value = false
|
|
}
|
|
}
|
|
const loadAccounts = async () => {
|
|
if (!manager.value || props.userId) return
|
|
const sequence = ++accountsSequence
|
|
try {
|
|
const data = dataOf(await http.get('/api/v1/system/notification/accounts', { params: { page: page.value } }))
|
|
if (disposed || sequence !== accountsSequence) return
|
|
accounts.value = data.list || []
|
|
total.value = data.total || 0
|
|
} catch (cause) {
|
|
if (!disposed && sequence === accountsSequence) {
|
|
accounts.value = []
|
|
showError(cause)
|
|
}
|
|
}
|
|
}
|
|
const beforeLeave = async () => {
|
|
if (submitting.value) return false
|
|
if (!dirty.value) return true
|
|
try {
|
|
await ElMessageBox.confirm('通知授权尚未保存,是否放弃本次修改?', '离开确认', {
|
|
confirmButtonText: '放弃修改', cancelButtonText: '继续编辑', type: 'warning'
|
|
})
|
|
return true
|
|
} catch { return false }
|
|
}
|
|
const selectAccount = async row => {
|
|
if (loading.value || !(await beforeLeave())) return
|
|
clearSecret()
|
|
status.value = null
|
|
selectedID.value = row.user_id
|
|
await loadStatus()
|
|
}
|
|
const refresh = async () => {
|
|
if (!(await beforeLeave())) return
|
|
clearSecret()
|
|
await Promise.all([loadStatus(), loadAccounts()])
|
|
}
|
|
const save = async () => {
|
|
if (!canManage.value || !status.value || submitting.value) return
|
|
const target = status.value
|
|
const input = { user_id: target.user_id, version: target.version, ...form }
|
|
if (input.day_limit < input.minute_limit) {
|
|
showError(new Error('每日限额不能小于每分钟限额'))
|
|
return
|
|
}
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
input.enabled ? `保存账号「${target.account}」的通知发信授权与限额?` : `关闭账号「${target.account}」的通知发信后,旧凭据将永久撤销;普通收发信不受影响。`,
|
|
'确认通知授权', { confirmButtonText: '确认保存', cancelButtonText: '取消', type: 'warning' }
|
|
)
|
|
submitting.value = true
|
|
dataOf(await requestWithRecentAuthentication(() => http.post('/api/v1/system/notification/authorization', input)))
|
|
clearSecret()
|
|
ElNotification({ title: '已保存', message: '账号通知授权已更新', type: 'success' })
|
|
await Promise.all([loadStatus(), loadAccounts()])
|
|
} catch (cause) {
|
|
showError(cause)
|
|
} finally { submitting.value = false }
|
|
}
|
|
const rotate = async () => {
|
|
if (!canManage.value || !status.value?.enabled || submitting.value || dirty.value) return
|
|
const target = status.value
|
|
try {
|
|
await ElMessageBox.confirm(`为「${target.account}」生成新凭据?旧通知凭据和旧连接将失效,普通邮箱凭据不变。`, '确认重置凭据', {
|
|
confirmButtonText: '生成新凭据', cancelButtonText: '取消', type: 'warning'
|
|
})
|
|
submitting.value = true
|
|
const sequence = ++secretSequence
|
|
const data = dataOf(await requestWithRecentAuthentication(() => http.post('/api/v1/system/notification/credential', {
|
|
user_id: target.user_id, version: target.version
|
|
})))
|
|
if (disposed) return
|
|
if (sequence === secretSequence) showSecret(data.code, target.account)
|
|
await Promise.all([loadStatus(), loadAccounts()])
|
|
} catch (cause) { showError(cause) }
|
|
finally { submitting.value = false }
|
|
}
|
|
const copySecret = async () => {
|
|
if (!secret.value) return
|
|
try {
|
|
await navigator.clipboard.writeText(secret.value)
|
|
ElNotification({ title: '已复制', message: '请安全交付给账号使用者,不要放入日志或代码仓库', type: 'success' })
|
|
} catch { showError(new Error('复制失败,请手动复制授权码')) }
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadStatus()
|
|
loadAccounts()
|
|
document.addEventListener('visibilitychange', hideOnBackground)
|
|
})
|
|
onUnmounted(() => {
|
|
disposed = true
|
|
requestSequence += 1
|
|
accountsSequence += 1
|
|
clearSecret()
|
|
clearVerification()
|
|
manageVisible.value = false
|
|
document.removeEventListener('visibilitychange', hideOnBackground)
|
|
})
|
|
|
|
return {
|
|
manager, loading, submitting, error, status, accounts, page, total, form,
|
|
secret, secretAccount, secretVisible, seconds, canManage, stateText, dirty,
|
|
canReveal, verifyVisible, verifyPassword, openVerify, verifyAndReveal, clearVerification,
|
|
loadAccounts, selectAccount: openManage, openManage, closeManage, revealAccount, refresh, save, rotate, clearSecret, copySecret, beforeLeave,
|
|
manageVisible
|
|
}
|
|
}
|