feat: add Telegram notification settings UI
Some checks failed
Deploy to Production / Build and Test (push) Has been cancelled
Deploy to Production / Deploy to Server (push) Has been cancelled

This commit is contained in:
你的用户名
2025-11-05 02:22:00 +08:00
parent 6108b9c5ed
commit a06a964bab
7 changed files with 21297 additions and 25 deletions

View File

@@ -1,3 +1,5 @@
export * from './auth';
export * from './finance';
export * from './menu';
export * from './telegram';
export * from './user';

View File

@@ -0,0 +1,70 @@
import { requestClient } from '#/api/request';
export namespace TelegramApi {
export interface NotificationConfig {
id: number;
name: string;
botToken: string;
chatId: string;
notificationTypes: string[];
isEnabled: boolean;
createdAt: string;
updatedAt: string;
}
export interface CreateNotificationConfigParams {
name: string;
botToken: string;
chatId: string;
notificationTypes?: string[];
isEnabled?: boolean;
}
export interface UpdateNotificationConfigParams {
name?: string;
botToken?: string;
chatId?: string;
notificationTypes?: string[];
isEnabled?: boolean;
}
export interface TestNotificationConfigParams {
botToken: string;
chatId: string;
}
}
export function getTelegramNotificationConfigs() {
return requestClient.get<TelegramApi.NotificationConfig[]>(
'/telegram/notifications',
);
}
export function createTelegramNotificationConfig(
data: TelegramApi.CreateNotificationConfigParams,
) {
return requestClient.post<TelegramApi.NotificationConfig>(
'/telegram/notifications',
data,
);
}
export function updateTelegramNotificationConfig(
id: number,
data: TelegramApi.UpdateNotificationConfigParams,
) {
return requestClient.put<TelegramApi.NotificationConfig>(
`/telegram/notifications/${id}`,
data,
);
}
export function deleteTelegramNotificationConfig(id: number) {
return requestClient.delete<{ id: number }>(`/telegram/notifications/${id}`);
}
export function testTelegramNotificationConfig(
data: TelegramApi.TestNotificationConfigParams,
) {
return requestClient.post<{ message: string }>('/telegram/test', data);
}

View File

@@ -1,17 +1,31 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import type { TableColumnsType } from 'ant-design-vue';
import { onMounted, reactive, ref } from 'vue';
import {
Button,
Card,
Divider,
Form,
Input,
Modal,
notification,
Space,
Switch,
Table,
Tag,
} from 'ant-design-vue';
import {
createTelegramNotificationConfig,
deleteTelegramNotificationConfig,
getTelegramNotificationConfigs,
TelegramApi,
testTelegramNotificationConfig,
updateTelegramNotificationConfig,
} from '#/api/core/telegram';
defineOptions({ name: 'FinanceSettings' });
// 系统设置
@@ -36,18 +50,358 @@ const operationLoading = ref({
reset: false,
});
// 功能方法
const saveCurrencySettings = (currency: string) => {
console.log('货币设置更改为:', currency);
localStorage.setItem('app-currency', currency);
notification.success({
message: '货币设置已更新',
description: `默认货币已设置为 ${currency}`,
interface TelegramConfigForm {
name: string;
botToken: string;
chatId: string;
isEnabled: boolean;
notificationTypes: string[];
}
const telegramConfigs = ref<TelegramApi.NotificationConfig[]>([]);
const telegramLoading = ref(false);
const telegramModalVisible = ref(false);
const telegramModalLoading = ref(false);
const telegramTestLoading = ref(false);
const testingRowId = ref<null | number>(null);
const togglingConfigId = ref<null | number>(null);
const editingTelegramConfig = ref<null | TelegramApi.NotificationConfig>(null);
const telegramForm = reactive<TelegramConfigForm>({
name: '',
botToken: '',
chatId: '',
isEnabled: true,
notificationTypes: ['transaction'],
});
const telegramColumns: TableColumnsType<TelegramApi.NotificationConfig> = [
{
title: '配置名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: 'Bot Token',
key: 'botToken',
width: 260,
},
{
title: 'Chat ID',
dataIndex: 'chatId',
key: 'chatId',
width: 200,
},
{
title: '通知类型',
key: 'notificationTypes',
width: 200,
},
{
title: '启用状态',
key: 'isEnabled',
width: 140,
},
{
title: '更新时间',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 200,
},
{
title: '操作',
key: 'actions',
fixed: 'right',
width: 220,
},
];
function resetTelegramForm() {
telegramForm.name = '';
telegramForm.botToken = '';
telegramForm.chatId = '';
telegramForm.isEnabled = true;
telegramForm.notificationTypes = ['transaction'];
}
function validateTelegramForm() {
if (!telegramForm.name.trim()) {
notification.error({
message: '请填写配置名称',
description: '例如:财务通知群或个人提醒',
});
return false;
}
if (!telegramForm.botToken.trim()) {
notification.error({
message: '请填写 Bot Token',
description: '可从 @BotFather 获取完整的 Bot Token',
});
return false;
}
if (!telegramForm.chatId.trim()) {
notification.error({
message: '请填写 Chat ID',
description: '个人或群组的 Chat ID 不能为空',
});
return false;
}
return true;
}
async function fetchTelegramConfigs() {
telegramLoading.value = true;
try {
telegramConfigs.value = await getTelegramNotificationConfigs();
} catch (error) {
console.error('加载 Telegram 配置失败:', error);
notification.error({
message: '加载 Telegram 配置失败',
description: '请稍后重试或检查后端服务状态',
});
} finally {
telegramLoading.value = false;
}
}
function openCreateTelegramConfig() {
editingTelegramConfig.value = null;
resetTelegramForm();
telegramModalVisible.value = true;
}
function openEditTelegramConfig(config: TelegramApi.NotificationConfig) {
editingTelegramConfig.value = config;
telegramForm.name = config.name;
telegramForm.botToken = config.botToken;
telegramForm.chatId = config.chatId;
telegramForm.isEnabled = config.isEnabled;
telegramForm.notificationTypes = [...config.notificationTypes];
telegramModalVisible.value = true;
}
function handleTelegramCancel() {
telegramModalVisible.value = false;
editingTelegramConfig.value = null;
resetTelegramForm();
}
async function handleModalTestTelegramConfig() {
const botToken = telegramForm.botToken.trim();
const chatId = telegramForm.chatId.trim();
if (!botToken || !chatId) {
notification.warning({
message: '请先填写完整的 Bot Token 和 Chat ID',
});
return;
}
telegramForm.botToken = botToken;
telegramForm.chatId = chatId;
telegramTestLoading.value = true;
try {
await testTelegramNotificationConfig({ botToken, chatId });
notification.success({
message: '测试消息已发送',
description: '请在 Telegram 中检查是否收到测试通知',
});
} catch (error) {
console.error('Telegram 测试失败:', error);
notification.error({
message: '测试失败',
description: '请检查 Bot Token、Chat ID 或网络连接',
});
} finally {
telegramTestLoading.value = false;
}
}
async function handleTelegramSubmit() {
if (!validateTelegramForm()) {
return;
}
const name = telegramForm.name.trim();
const botToken = telegramForm.botToken.trim();
const chatId = telegramForm.chatId.trim();
telegramForm.name = name;
telegramForm.botToken = botToken;
telegramForm.chatId = chatId;
telegramModalLoading.value = true;
try {
if (editingTelegramConfig.value) {
const payload: TelegramApi.UpdateNotificationConfigParams = {};
if (name !== editingTelegramConfig.value.name) {
payload.name = name;
}
if (botToken !== editingTelegramConfig.value.botToken) {
payload.botToken = botToken;
}
if (chatId !== editingTelegramConfig.value.chatId) {
payload.chatId = chatId;
}
if (
telegramForm.notificationTypes.join(',') !==
editingTelegramConfig.value.notificationTypes.join(',')
) {
payload.notificationTypes = [...telegramForm.notificationTypes];
}
if (telegramForm.isEnabled !== editingTelegramConfig.value.isEnabled) {
payload.isEnabled = telegramForm.isEnabled;
}
if (Object.keys(payload).length === 0) {
notification.info({
message: '配置未发生变化',
description: '如需更新请修改字段后再保存',
});
return;
}
await updateTelegramNotificationConfig(
editingTelegramConfig.value.id,
payload,
);
notification.success({
message: '配置已更新',
description: `${name}」已保存最新配置`,
});
} else {
await createTelegramNotificationConfig({
name,
botToken,
chatId,
notificationTypes: [...telegramForm.notificationTypes],
isEnabled: telegramForm.isEnabled,
});
notification.success({
message: '配置已创建',
description: `${name}」已加入通知列表`,
});
}
await fetchTelegramConfigs();
handleTelegramCancel();
} catch (error) {
console.error('保存 Telegram 配置失败:', error);
notification.error({
message: '保存失败',
description: '请检查信息是否正确或稍后再试',
});
} finally {
telegramModalLoading.value = false;
}
}
async function handleToggleTelegramConfig(
config: TelegramApi.NotificationConfig,
value: boolean,
) {
togglingConfigId.value = config.id;
try {
await updateTelegramNotificationConfig(config.id, { isEnabled: value });
await fetchTelegramConfigs();
notification.success({
message: value ? '配置已启用' : '配置已禁用',
description: `${config.name}」通知状态已更新`,
});
} catch (error) {
console.error('更新 Telegram 状态失败:', error);
notification.error({
message: '状态更新失败',
description: '请稍后重试',
});
} finally {
togglingConfigId.value = null;
}
}
async function handleTestExistingConfig(
config: TelegramApi.NotificationConfig,
) {
testingRowId.value = config.id;
try {
await testTelegramNotificationConfig({
botToken: config.botToken,
chatId: config.chatId,
});
notification.success({
message: '测试消息已发送',
description: `请在 Telegram 检查「${config.name}`,
});
} catch (error) {
console.error('测试 Telegram 配置失败:', error);
notification.error({
message: '测试失败',
description: '请检查 Bot Token 和 Chat ID',
});
} finally {
testingRowId.value = null;
}
}
function handleDeleteTelegramConfig(config: TelegramApi.NotificationConfig) {
Modal.confirm({
title: `确认删除配置「${config.name}」?`,
content: '删除后将无法继续向该目标发送 Telegram 通知。',
okText: '删除',
cancelText: '取消',
okButtonProps: { danger: true },
async onOk() {
try {
await deleteTelegramNotificationConfig(config.id);
notification.success({
message: '配置已删除',
description: `${config.name}」已移除`,
});
await fetchTelegramConfigs();
} catch (error) {
console.error('删除 Telegram 配置失败:', error);
notification.error({
message: '删除失败',
description: '请稍后重试',
});
throw error;
}
},
});
};
}
function formatDateTime(value: string) {
try {
return new Date(value).toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour12: false,
});
} catch {
return value;
}
}
function maskToken(token: string) {
if (token.length <= 10) {
return token;
}
return `${token.slice(0, 6)}...${token.slice(-4)}`;
}
const saveNotificationSettings = () => {
console.log('通知设置已保存:', settings.value.notifications);
localStorage.setItem(
'app-notifications',
JSON.stringify(settings.value.notifications),
@@ -59,7 +413,6 @@ const saveNotificationSettings = () => {
};
const toggleAutoBackup = (enabled: boolean) => {
console.log('自动备份:', enabled);
localStorage.setItem('app-auto-backup', enabled.toString());
notification.info({
message: enabled ? '自动备份已启用' : '自动备份已禁用',
@@ -68,7 +421,6 @@ const toggleAutoBackup = (enabled: boolean) => {
};
const toggleCompactMode = (enabled: boolean) => {
console.log('紧凑模式:', enabled);
document.documentElement.classList.toggle('compact', enabled);
localStorage.setItem('app-compact-mode', enabled.toString());
notification.info({
@@ -77,7 +429,6 @@ const toggleCompactMode = (enabled: boolean) => {
};
const toggleAutoLock = (enabled: boolean) => {
console.log('自动锁屏:', enabled);
localStorage.setItem('app-auto-lock', enabled.toString());
notification.info({
message: enabled ? '自动锁屏已启用' : '自动锁屏已禁用',
@@ -85,7 +436,6 @@ const toggleAutoLock = (enabled: boolean) => {
};
const toggleAnalytics = (enabled: boolean) => {
console.log('数据统计:', enabled);
localStorage.setItem('app-analytics', enabled.toString());
notification.info({
message: enabled ? '数据统计已启用' : '数据统计已禁用',
@@ -239,7 +589,6 @@ const resetSystem = () => {
};
const saveAllSettings = () => {
console.log('保存所有设置:', settings.value);
localStorage.setItem('app-all-settings', JSON.stringify(settings.value));
notification.success({
message: '设置保存成功',
@@ -306,7 +655,7 @@ onMounted(() => {
console.error('设置恢复失败:', error);
}
console.log('系统设置页面加载完成');
fetchTelegramConfigs();
});
</script>
@@ -440,7 +789,156 @@ onMounted(() => {
</Button>
</div>
</Card>
<Card class="lg:col-span-2">
<template #title>🚀 Telegram 通知配置</template>
<template #extra>
<Button type="primary" @click="openCreateTelegramConfig">
新增配置
</Button>
</template>
<Table
:columns="telegramColumns"
:data-source="telegramConfigs"
:loading="telegramLoading"
:pagination="false"
:row-key="(record) => record.id"
:scroll="{ x: 960 }"
bordered
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'botToken'">
<span class="font-mono text-xs">{{
maskToken(record.botToken)
}}</span>
</template>
<template v-else-if="column.key === 'notificationTypes'">
<Space :size="4" wrap>
<Tag
v-for="item in record.notificationTypes"
:key="item"
color="blue"
>
{{ item }}
</Tag>
</Space>
</template>
<template v-else-if="column.key === 'isEnabled'">
<Switch
:checked="record.isEnabled"
:loading="togglingConfigId === record.id"
@change="(value) => handleToggleTelegramConfig(record, value)"
/>
</template>
<template v-else-if="column.dataIndex === 'updatedAt'">
{{ formatDateTime(record.updatedAt) }}
</template>
<template v-else-if="column.key === 'actions'">
<Space size="small">
<Button
size="small"
type="link"
@click="openEditTelegramConfig(record)"
>
编辑
</Button>
<Button
size="small"
type="link"
:loading="testingRowId === record.id"
@click="handleTestExistingConfig(record)"
>
测试
</Button>
<Button
size="small"
type="link"
danger
@click="handleDeleteTelegramConfig(record)"
>
删除
</Button>
</Space>
</template>
</template>
</Table>
</Card>
</div>
<Modal
v-model:open="telegramModalVisible"
:confirm-loading="telegramModalLoading"
:title="editingTelegramConfig ? '编辑通知配置' : '新增通知配置'"
destroy-on-close
width="520px"
@cancel="handleTelegramCancel"
>
<Form layout="vertical">
<Form.Item label="配置名称" required>
<Input
v-model:value="telegramForm.name"
placeholder="例如财务通知群"
maxlength="100"
/>
</Form.Item>
<Form.Item label="Bot Token" required>
<Input
v-model:value="telegramForm.botToken"
placeholder="1234567890:ABCdefGHI..."
maxlength="255"
/>
<p class="mt-1 text-xs text-gray-500">
从 Telegram 的 @BotFather 获取完整的 Bot Token
</p>
</Form.Item>
<Form.Item label="Chat ID" required>
<Input
v-model:value="telegramForm.chatId"
placeholder="-1001234567890"
maxlength="64"
/>
<p class="mt-1 text-xs text-gray-500">
个人可使用 @userinfobot 查询,群组需将 Bot 加入后获取
</p>
</Form.Item>
<Form.Item label="启用状态">
<Switch v-model:checked="telegramForm.isEnabled" />
</Form.Item>
<Form.Item label="通知类型">
<Space :size="4" wrap>
<Tag
v-for="item in telegramForm.notificationTypes"
:key="item"
color="blue"
>
{{ item }}
</Tag>
</Space>
</Form.Item>
</Form>
<template #footer>
<Space>
<Button @click="handleTelegramCancel">取消</Button>
<Button
:loading="telegramTestLoading"
@click="handleModalTestTelegramConfig"
>
发送测试
</Button>
<Button
type="primary"
:loading="telegramModalLoading"
@click="handleTelegramSubmit"
>
{{ editingTelegramConfig ? '保存更新' : '创建配置' }}
</Button>
</Space>
</template>
</Modal>
</div>
</template>