From e715ca73506267ea35356900572e6b4069d8228e Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 7 Feb 2026 18:15:24 +0000 Subject: [PATCH] feat: add Vue3 frontend with TailwindCSS --- frontend/css/style.css | 4 + frontend/index.html | 233 +++++++++++++++++++++++++++++++++++++++++ frontend/js/app.js | 185 ++++++++++++++++++++++++++++++++ 3 files changed, 422 insertions(+) create mode 100644 frontend/css/style.css create mode 100644 frontend/index.html create mode 100644 frontend/js/app.js diff --git a/frontend/css/style.css b/frontend/css/style.css new file mode 100644 index 0000000..c6f4ed3 --- /dev/null +++ b/frontend/css/style.css @@ -0,0 +1,4 @@ +/* Custom styles */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..1c87542 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,233 @@ + + + + + + Apprise Notify Center + + + + + + +
+ + + + +
+ +
+
+

通道管理

+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
名称类型标签状态操作
{{ channel.name }} + {{ channel.type }} + + + {{ tag }} + + + + {{ channel.is_active ? '活跃' : '禁用' }} + + + + +
+
+
+ + +
+

手动发送通知

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+

发送历史

+
+ + + + + + + + + + + + + + + + + +
时间通道标题状态
{{ formatDate(n.created_at) }}{{ n.channel?.name || '-' }}{{ n.title || '-' }} + + {{ n.status }} + +
+
+
+
+ + +
+
+
+

添加通道

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+ {{ message.text }} +
+
+ + + + diff --git a/frontend/js/app.js b/frontend/js/app.js new file mode 100644 index 0000000..118dcf8 --- /dev/null +++ b/frontend/js/app.js @@ -0,0 +1,185 @@ +const { createApp, ref, onMounted } = Vue; + +createApp({ + setup() { + const currentView = ref('channels'); + const channels = ref([]); + const notifications = ref([]); + const showChannelModal = ref(false); + const sending = ref(false); + const message = ref(null); + + const channelForm = ref({ + name: '', + type: 'discord', + configJson: '{}', + tags: '', + is_active: true + }); + + const sendForm = ref({ + channels: [], + title: '', + body: '', + priority: 'normal' + }); + + const API_BASE = '/api'; + + const showMessage = (text, type = 'success') => { + message.value = { text, type }; + setTimeout(() => message.value = null, 3000); + }; + + const fetchChannels = async () => { + try { + const res = await fetch(`${API_BASE}/channels`); + const data = await res.json(); + channels.value = data.channels; + } catch (e) { + showMessage('获取通道失败', 'error'); + } + }; + + const fetchNotifications = async () => { + try { + const res = await fetch(`${API_BASE}/notifications`); + const data = await res.json(); + notifications.value = data.notifications; + } catch (e) { + showMessage('获取历史失败', 'error'); + } + }; + + const saveChannel = async () => { + try { + const config = JSON.parse(channelForm.value.configJson); + const tags = channelForm.value.tags.split(',').map(t => t.trim()).filter(t => t); + + const res = await fetch(`${API_BASE}/channels`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: channelForm.value.name, + type: channelForm.value.type, + config, + tags, + is_active: channelForm.value.is_active + }) + }); + + if (res.ok) { + showMessage('通道创建成功'); + showChannelModal.value = false; + fetchChannels(); + } else { + const err = await res.json(); + showMessage(err.detail || '创建失败', 'error'); + } + } catch (e) { + showMessage('配置 JSON 格式错误', 'error'); + } + }; + + const deleteChannel = async (id) => { + if (!confirm('确定删除此通道?')) return; + + try { + const res = await fetch(`${API_BASE}/channels/${id}`, { method: 'DELETE' }); + if (res.ok) { + showMessage('删除成功'); + fetchChannels(); + } else { + showMessage('删除失败', 'error'); + } + } catch (e) { + showMessage('删除失败', 'error'); + } + }; + + const testChannel = async (id) => { + try { + const res = await fetch(`${API_BASE}/channels/${id}/test`, { method: 'POST' }); + if (res.ok) { + showMessage('测试消息已发送'); + } else { + showMessage('测试失败', 'error'); + } + } catch (e) { + showMessage('测试失败', 'error'); + } + }; + + const sendNotification = async () => { + if (!sendForm.value.body) { + showMessage('请输入消息内容', 'error'); + return; + } + if (sendForm.value.channels.length === 0) { + showMessage('请至少选择一个通道', 'error'); + return; + } + + sending.value = true; + try { + const res = await fetch(`${API_BASE}/notify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + channels: sendForm.value.channels, + title: sendForm.value.title, + body: sendForm.value.body, + priority: sendForm.value.priority + }) + }); + + if (res.ok) { + showMessage('通知发送成功'); + sendForm.value = { channels: [], title: '', body: '', priority: 'normal' }; + } else { + showMessage('发送失败', 'error'); + } + } catch (e) { + showMessage('发送失败', 'error'); + } finally { + sending.value = false; + } + }; + + const formatDate = (dateStr) => { + if (!dateStr) return '-'; + return new Date(dateStr).toLocaleString('zh-CN'); + }; + + const getStatusClass = (status) => { + return { + 'px-2 py-1 rounded text-xs': true, + 'bg-green-100 text-green-800': status === 'sent', + 'bg-red-100 text-red-800': status === 'failed', + 'bg-yellow-100 text-yellow-800': status === 'pending' + }; + }; + + onMounted(() => { + fetchChannels(); + fetchNotifications(); + }); + + return { + currentView, + channels, + notifications, + showChannelModal, + channelForm, + sendForm, + sending, + message, + saveChannel, + deleteChannel, + testChannel, + sendNotification, + formatDate, + getStatusClass + }; + } +}).mount('#app');