feat: add Telegram notification settings UI
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
export * from './auth';
|
||||
export * from './finance';
|
||||
export * from './menu';
|
||||
export * from './telegram';
|
||||
export * from './user';
|
||||
|
||||
70
apps/web-antd/src/api/core/telegram.ts
Normal file
70
apps/web-antd/src/api/core/telegram.ts
Normal 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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user