Files
hfb_sys/frontend/src/features/admin/views/AdminPushView.vue
T

352 lines
10 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.
<script setup lang="ts">
import { Check, Delete, Edit, Plus, Refresh } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref } from 'vue'
import {
createPushChannel,
deletePushChannel,
fetchPushChannels,
fetchPushRules,
testPushChannel,
updatePushChannel,
updatePushRule,
type CreateChannelPayload,
type PushChannel,
type PushRule,
} from '@/features/admin/api/adminPush'
// ── 渠道状态 ──
const channels = ref<PushChannel[]>([])
const channelsLoading = ref(false)
const channelDialogVisible = ref(false)
const editingChannel = ref<PushChannel | null>(null)
const channelForm = ref<CreateChannelPayload>({ name: '', type: 'bark', config: {} })
// ── 规则状态 ──
const rules = ref<PushRule[]>([])
const rulesLoading = ref(false)
const ruleDialogVisible = ref(false)
const editingRule = ref<PushRule | null>(null)
const ruleForm = ref({ enabled: true, threshold: 5, message_template: '' })
const channelTypeOptions = [
{ label: 'iOS Bark', value: 'bark' },
{ label: 'WPush', value: 'wpush' },
]
const channelTypeLabel: Record<string, string> = {
bark: 'Bark',
wpush: 'WPush',
}
const eventNameLabel: Record<string, string> = {
qrcode_low_stock: '二维码库存预警',
}
onMounted(() => {
loadChannels()
loadRules()
})
// ── 渠道方法 ──
async function loadChannels() {
channelsLoading.value = true
try {
channels.value = await fetchPushChannels()
} finally {
channelsLoading.value = false
}
}
function openCreateChannel() {
editingChannel.value = null
channelForm.value = { name: '', type: 'bark', config: {} }
channelDialogVisible.value = true
}
function openEditChannel(row: PushChannel) {
editingChannel.value = row
channelForm.value = {
name: row.name,
type: row.type,
config: { ...row.config },
}
channelDialogVisible.value = true
}
async function saveChannel() {
if (!channelForm.value.name) {
ElMessage.warning('请输入渠道名称')
return
}
try {
if (editingChannel.value) {
await updatePushChannel(editingChannel.value.id, {
name: channelForm.value.name,
config: channelForm.value.config,
})
ElMessage.success('更新成功')
} else {
await createPushChannel(channelForm.value)
ElMessage.success('创建成功')
}
channelDialogVisible.value = false
await loadChannels()
} catch {
ElMessage.error('操作失败')
}
}
async function toggleChannel(row: PushChannel) {
try {
await updatePushChannel(row.id, { enabled: !row.enabled })
ElMessage.success(row.enabled ? '已禁用' : '已启用')
await loadChannels()
} catch {
ElMessage.error('操作失败')
}
}
async function handleDeleteChannel(row: PushChannel) {
try {
await ElMessageBox.confirm(`确定删除渠道「${row.name}」?`, '确认删除', { type: 'warning' })
await deletePushChannel(row.id)
ElMessage.success('已删除')
await loadChannels()
} catch {
// 取消
}
}
async function handleTestChannel(row: PushChannel) {
try {
await testPushChannel(row.id)
ElMessage.success('测试消息已发送,请检查是否收到')
} catch {
ElMessage.error('测试发送失败,请检查配置')
}
}
function channelConfigFields(type: string) {
if (type === 'bark') {
return [
{ key: 'device_key', label: 'DeviceKey', placeholder: 'Bark 推送 DeviceKey' },
{ key: 'server', label: '服务地址', placeholder: 'https://api.day.app(可选)' },
]
}
if (type === 'wpush') {
return [{ key: 'api_key', label: 'APIKey', placeholder: 'WPush 推送 APIKey' }]
}
return []
}
// ── 规则方法 ──
async function loadRules() {
rulesLoading.value = true
try {
rules.value = await fetchPushRules()
} finally {
rulesLoading.value = false
}
}
function openEditRule(row: PushRule) {
editingRule.value = row
ruleForm.value = {
enabled: row.enabled,
threshold: row.threshold,
message_template: row.message_template,
}
ruleDialogVisible.value = true
}
async function saveRule() {
if (!editingRule.value) return
try {
await updatePushRule(editingRule.value.id, ruleForm.value)
ElMessage.success('更新成功')
ruleDialogVisible.value = false
await loadRules()
} catch {
ElMessage.error('操作失败')
}
}
</script>
<template>
<section class="page">
<div class="page-header-row">
<div class="page-header">
<p class="eyebrow">Push Notifications</p>
<h1>推送通知</h1>
<p>配置站外推送渠道和通知规则二维码库存不足时自动预警</p>
</div>
</div>
<!-- 渠道管理 -->
<div class="section-header">
<h2>推送渠道</h2>
<div class="section-actions">
<el-button :icon="Refresh" :loading="channelsLoading" @click="loadChannels">刷新</el-button>
<el-button type="primary" :icon="Plus" @click="openCreateChannel">新增渠道</el-button>
</div>
</div>
<el-table v-loading="channelsLoading" :data="channels" class="table-panel">
<el-table-column label="名称" prop="name" min-width="120" />
<el-table-column label="类型" width="100">
<template #default="{ row }">
<el-tag effect="plain">{{ channelTypeLabel[row.type] || row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column label="配置" min-width="200">
<template #default="{ row }">
<code class="config-preview">{{ JSON.stringify(row.config) }}</code>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.enabled ? 'success' : 'info'" effect="plain">
{{ row.enabled ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" :icon="Edit" @click="openEditChannel(row)">编辑</el-button>
<el-button size="small" type="primary" @click="handleTestChannel(row)">测试</el-button>
<el-button size="small" :type="row.enabled ? 'warning' : 'success'" @click="toggleChannel(row)">
{{ row.enabled ? '禁用' : '启用' }}
</el-button>
<el-button size="small" type="danger" :icon="Delete" @click="handleDeleteChannel(row)" />
</template>
</el-table-column>
</el-table>
<el-empty v-if="!channelsLoading && channels.length === 0" description="暂无推送渠道,点击上方按钮新增" />
<!-- 规则管理 -->
<div class="section-header" style="margin-top: 32px">
<h2>通知规则</h2>
<el-button :icon="Refresh" :loading="rulesLoading" @click="loadRules">刷新</el-button>
</div>
<el-table v-loading="rulesLoading" :data="rules" class="table-panel">
<el-table-column label="事件" min-width="160">
<template #default="{ row }">
<span>{{ eventNameLabel[row.event] || row.event }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.enabled ? 'success' : 'info'" effect="plain">
{{ row.enabled ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="阈值" width="100" prop="threshold" />
<el-table-column label="消息模板" min-width="250" prop="message_template" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" :icon="Edit" @click="openEditRule(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<!-- 渠道编辑弹窗 -->
<el-dialog
v-model="channelDialogVisible"
:title="editingChannel ? '编辑渠道' : '新增渠道'"
width="500px"
>
<el-form label-width="80px">
<el-form-item label="名称">
<el-input v-model="channelForm.name" placeholder="如:我的 Bark" />
</el-form-item>
<el-form-item label="类型">
<el-select v-model="channelForm.type" :disabled="!!editingChannel">
<el-option
v-for="opt in channelTypeOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item v-for="field in channelConfigFields(channelForm.type)" :key="field.key" :label="field.label">
<el-input
v-model="channelForm.config[field.key]"
:placeholder="field.placeholder"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="channelDialogVisible = false">取消</el-button>
<el-button type="primary" :icon="Check" @click="saveChannel">保存</el-button>
</template>
</el-dialog>
<!-- 规则编辑弹窗 -->
<el-dialog v-model="ruleDialogVisible" title="编辑规则" width="500px">
<el-form label-width="80px">
<el-form-item label="启用">
<el-switch v-model="ruleForm.enabled" />
</el-form-item>
<el-form-item label="阈值">
<el-input-number v-model="ruleForm.threshold" :min="1" :max="999" />
<span class="form-hint">库存低于此值时触发预警</span>
</el-form-item>
<el-form-item label="消息模板">
<el-input v-model="ruleForm.message_template" type="textarea" :rows="3" placeholder="支持 {{.Count}} 占位符" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="ruleDialogVisible = false">取消</el-button>
<el-button type="primary" :icon="Check" @click="saveRule">保存</el-button>
</template>
</el-dialog>
</section>
</template>
<style scoped>
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.section-header h2 {
margin: 0;
font-size: 16px;
font-weight: 700;
}
.section-actions {
display: flex;
gap: 8px;
}
.config-preview {
display: inline-block;
max-width: 200px;
padding: 2px 6px;
border-radius: 4px;
background: #f4f5f7;
color: #5b6575;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.form-hint {
margin-left: 8px;
color: #9ca3af;
font-size: 12px;
}
</style>