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');