feat: add Telegram notification settings UI
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './finance';
|
||||||
export * from './menu';
|
export * from './menu';
|
||||||
|
export * from './telegram';
|
||||||
export * from './user';
|
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">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import type { TableColumnsType } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Divider,
|
Divider,
|
||||||
Form,
|
Form,
|
||||||
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
notification,
|
notification,
|
||||||
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createTelegramNotificationConfig,
|
||||||
|
deleteTelegramNotificationConfig,
|
||||||
|
getTelegramNotificationConfigs,
|
||||||
|
TelegramApi,
|
||||||
|
testTelegramNotificationConfig,
|
||||||
|
updateTelegramNotificationConfig,
|
||||||
|
} from '#/api/core/telegram';
|
||||||
|
|
||||||
defineOptions({ name: 'FinanceSettings' });
|
defineOptions({ name: 'FinanceSettings' });
|
||||||
|
|
||||||
// 系统设置
|
// 系统设置
|
||||||
@@ -36,18 +50,358 @@ const operationLoading = ref({
|
|||||||
reset: false,
|
reset: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 功能方法
|
interface TelegramConfigForm {
|
||||||
const saveCurrencySettings = (currency: string) => {
|
name: string;
|
||||||
console.log('货币设置更改为:', currency);
|
botToken: string;
|
||||||
localStorage.setItem('app-currency', currency);
|
chatId: string;
|
||||||
notification.success({
|
isEnabled: boolean;
|
||||||
message: '货币设置已更新',
|
notificationTypes: string[];
|
||||||
description: `默认货币已设置为 ${currency}`,
|
}
|
||||||
|
|
||||||
|
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 = () => {
|
const saveNotificationSettings = () => {
|
||||||
console.log('通知设置已保存:', settings.value.notifications);
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'app-notifications',
|
'app-notifications',
|
||||||
JSON.stringify(settings.value.notifications),
|
JSON.stringify(settings.value.notifications),
|
||||||
@@ -59,7 +413,6 @@ const saveNotificationSettings = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleAutoBackup = (enabled: boolean) => {
|
const toggleAutoBackup = (enabled: boolean) => {
|
||||||
console.log('自动备份:', enabled);
|
|
||||||
localStorage.setItem('app-auto-backup', enabled.toString());
|
localStorage.setItem('app-auto-backup', enabled.toString());
|
||||||
notification.info({
|
notification.info({
|
||||||
message: enabled ? '自动备份已启用' : '自动备份已禁用',
|
message: enabled ? '自动备份已启用' : '自动备份已禁用',
|
||||||
@@ -68,7 +421,6 @@ const toggleAutoBackup = (enabled: boolean) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleCompactMode = (enabled: boolean) => {
|
const toggleCompactMode = (enabled: boolean) => {
|
||||||
console.log('紧凑模式:', enabled);
|
|
||||||
document.documentElement.classList.toggle('compact', enabled);
|
document.documentElement.classList.toggle('compact', enabled);
|
||||||
localStorage.setItem('app-compact-mode', enabled.toString());
|
localStorage.setItem('app-compact-mode', enabled.toString());
|
||||||
notification.info({
|
notification.info({
|
||||||
@@ -77,7 +429,6 @@ const toggleCompactMode = (enabled: boolean) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleAutoLock = (enabled: boolean) => {
|
const toggleAutoLock = (enabled: boolean) => {
|
||||||
console.log('自动锁屏:', enabled);
|
|
||||||
localStorage.setItem('app-auto-lock', enabled.toString());
|
localStorage.setItem('app-auto-lock', enabled.toString());
|
||||||
notification.info({
|
notification.info({
|
||||||
message: enabled ? '自动锁屏已启用' : '自动锁屏已禁用',
|
message: enabled ? '自动锁屏已启用' : '自动锁屏已禁用',
|
||||||
@@ -85,7 +436,6 @@ const toggleAutoLock = (enabled: boolean) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleAnalytics = (enabled: boolean) => {
|
const toggleAnalytics = (enabled: boolean) => {
|
||||||
console.log('数据统计:', enabled);
|
|
||||||
localStorage.setItem('app-analytics', enabled.toString());
|
localStorage.setItem('app-analytics', enabled.toString());
|
||||||
notification.info({
|
notification.info({
|
||||||
message: enabled ? '数据统计已启用' : '数据统计已禁用',
|
message: enabled ? '数据统计已启用' : '数据统计已禁用',
|
||||||
@@ -239,7 +589,6 @@ const resetSystem = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveAllSettings = () => {
|
const saveAllSettings = () => {
|
||||||
console.log('保存所有设置:', settings.value);
|
|
||||||
localStorage.setItem('app-all-settings', JSON.stringify(settings.value));
|
localStorage.setItem('app-all-settings', JSON.stringify(settings.value));
|
||||||
notification.success({
|
notification.success({
|
||||||
message: '设置保存成功',
|
message: '设置保存成功',
|
||||||
@@ -306,7 +655,7 @@ onMounted(() => {
|
|||||||
console.error('设置恢复失败:', error);
|
console.error('设置恢复失败:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('系统设置页面加载完成');
|
fetchTelegramConfigs();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -440,7 +789,156 @@ onMounted(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</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>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ KT财务系统支持通过Telegram Bot向群组或个人发送账目记录通知
|
|||||||
### 2. 获取Chat ID
|
### 2. 获取Chat ID
|
||||||
|
|
||||||
#### 获取个人Chat ID:
|
#### 获取个人Chat ID:
|
||||||
|
|
||||||
1. 在Telegram中搜索 `@userinfobot`
|
1. 在Telegram中搜索 `@userinfobot`
|
||||||
2. 发送任意消息
|
2. 发送任意消息
|
||||||
3. Bot会返回你的Chat ID
|
3. Bot会返回你的Chat ID
|
||||||
|
|
||||||
#### 获取群组Chat ID:
|
#### 获取群组Chat ID:
|
||||||
|
|
||||||
1. 将你的Bot添加到群组
|
1. 将你的Bot添加到群组
|
||||||
2. 在群组中发送任意消息
|
2. 在群组中发送任意消息
|
||||||
3. 访问:`https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates`
|
3. 访问:`https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates`
|
||||||
@@ -44,6 +46,7 @@ GET /api/telegram/notifications
|
|||||||
```
|
```
|
||||||
|
|
||||||
**响应示例**:
|
**响应示例**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 0,
|
"code": 0,
|
||||||
@@ -78,6 +81,7 @@ Content-Type: application/json
|
|||||||
```
|
```
|
||||||
|
|
||||||
**说明**:
|
**说明**:
|
||||||
|
|
||||||
- `name`: 配置名称(必填)
|
- `name`: 配置名称(必填)
|
||||||
- `botToken`: Telegram Bot Token(必填)
|
- `botToken`: Telegram Bot Token(必填)
|
||||||
- `chatId`: 目标聊天ID(必填)
|
- `chatId`: 目标聊天ID(必填)
|
||||||
@@ -85,6 +89,7 @@ Content-Type: application/json
|
|||||||
- `isEnabled`: 是否启用(可选,默认:`true`)
|
- `isEnabled`: 是否启用(可选,默认:`true`)
|
||||||
|
|
||||||
**响应示例**:
|
**响应示例**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 0,
|
"code": 0,
|
||||||
@@ -159,9 +164,11 @@ Content-Type: application/json
|
|||||||
## 通知类型说明
|
## 通知类型说明
|
||||||
|
|
||||||
目前支持的通知类型:
|
目前支持的通知类型:
|
||||||
|
|
||||||
- `transaction`: 交易记录通知(新增、更新、删除账目)
|
- `transaction`: 交易记录通知(新增、更新、删除账目)
|
||||||
|
|
||||||
未来可扩展:
|
未来可扩展:
|
||||||
|
|
||||||
- `budget`: 预算提醒
|
- `budget`: 预算提醒
|
||||||
- `report`: 财务报表
|
- `report`: 财务报表
|
||||||
- `reimbursement`: 报销审批
|
- `reimbursement`: 报销审批
|
||||||
@@ -169,18 +176,23 @@ Content-Type: application/json
|
|||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
### Q: Bot无法发送消息到群组?
|
### Q: Bot无法发送消息到群组?
|
||||||
|
|
||||||
**A**: 请确保:
|
**A**: 请确保:
|
||||||
|
|
||||||
1. Bot已被添加到群组
|
1. Bot已被添加到群组
|
||||||
2. Bot在群组中有发送消息的权限
|
2. Bot在群组中有发送消息的权限
|
||||||
3. Chat ID正确(群组ID通常是负数)
|
3. Chat ID正确(群组ID通常是负数)
|
||||||
|
|
||||||
### Q: 如何禁用某个配置的通知?
|
### Q: 如何禁用某个配置的通知?
|
||||||
|
|
||||||
**A**: 调用更新API,设置 `isEnabled: false`
|
**A**: 调用更新API,设置 `isEnabled: false`
|
||||||
|
|
||||||
### Q: 可以配置多个Bot吗?
|
### Q: 可以配置多个Bot吗?
|
||||||
|
|
||||||
**A**: 可以!系统支持多个Bot配置,所有启用的配置都会收到通知。
|
**A**: 可以!系统支持多个Bot配置,所有启用的配置都会收到通知。
|
||||||
|
|
||||||
### Q: 消息会包含敏感信息吗?
|
### Q: 消息会包含敏感信息吗?
|
||||||
|
|
||||||
**A**: 消息只包含账目的基本信息(类型、金额、分类等),不包含用户身份等敏感信息。建议使用私密群组。
|
**A**: 消息只包含账目的基本信息(类型、金额、分类等),不包含用户身份等敏感信息。建议使用私密群组。
|
||||||
|
|
||||||
## 技术实现
|
## 技术实现
|
||||||
@@ -239,9 +251,27 @@ curl -X POST http://localhost:3000/api/finance/transactions \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## 下一步
|
## 前端配置界面
|
||||||
|
|
||||||
等你提供Telegram Bot Token后,我们可以:
|
Telegram 通知现已集成在 Web 端的系统设置页面:
|
||||||
1. 在前端添加通知配置管理界面
|
|
||||||
2. 测试实际的消息发送
|
- 入口路径:`财务系统 → ⚙️ 系统设置 → Telegram 通知配置`
|
||||||
3. 根据需要调整消息格式和内容
|
- 列表内容:展示配置名称、Bot Token(掩码)、Chat ID、通知类型、启用状态以及最近更新时间
|
||||||
|
- 支持操作:快速启用/禁用、编辑、发送测试通知、删除
|
||||||
|
|
||||||
|
### 新增或编辑配置
|
||||||
|
|
||||||
|
1. 点击「➕ 新增配置」或列表中的「编辑」按钮
|
||||||
|
2. 填写/更新以下字段:
|
||||||
|
- **配置名称**:用于标识通知触达对象(例如“财务通知群”)
|
||||||
|
- **Bot Token**:来自 @BotFather 的完整 Token
|
||||||
|
- **Chat ID**:个人或群组的 ID(群组需将 Bot 加入并通过 `getUpdates` 获取)
|
||||||
|
- **启用状态**:控制是否参与通知投递
|
||||||
|
3. 可直接在弹窗内点击「发送测试」,验证 Bot Token 与 Chat ID 是否有效
|
||||||
|
4. 点击「创建配置」或「保存更新」提交,成功后自动刷新列表
|
||||||
|
|
||||||
|
### 使用提示
|
||||||
|
|
||||||
|
- 所有启用的配置会在新增账目时同步收到通知
|
||||||
|
- 「测试」按钮会向对应的聊天发送标准测试消息,方便确认权限
|
||||||
|
- 删除配置后即刻停止向该聊天推送消息
|
||||||
|
|||||||
20669
pnpm-lock.yaml
generated
Normal file
20669
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
stylelint.config.mjs
Normal file
3
stylelint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import config from './internal/lint-configs/stylelint-config/index.mjs';
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -4,6 +4,10 @@
|
|||||||
"name": "@vben/backend",
|
"name": "@vben/backend",
|
||||||
"path": "apps/backend",
|
"path": "apps/backend",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "@vben/finance-mcp-service",
|
||||||
|
"path": "apps/finance-mcp-service",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "@vben/web-antd",
|
"name": "@vben/web-antd",
|
||||||
"path": "apps/web-antd",
|
"path": "apps/web-antd",
|
||||||
@@ -156,10 +160,6 @@
|
|||||||
"name": "@vben/utils",
|
"name": "@vben/utils",
|
||||||
"path": "packages/utils",
|
"path": "packages/utils",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "@vben/playground",
|
|
||||||
"path": "playground",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "@vben/turbo-run",
|
"name": "@vben/turbo-run",
|
||||||
"path": "scripts/turbo-run",
|
"path": "scripts/turbo-run",
|
||||||
|
|||||||
Reference in New Issue
Block a user