Files
Auto_Bangumi/webui/src/components/setting/config-notification.vue
Estrella Pan b570073000 fix(ui): align notification settings with project design system
Replace non-existent ab-dialog with ab-popup for proper modal behavior,
use ab-button/ab-label components and consistent spacing/styles matching
other settings panels (search-provider, proxy).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:44:11 +01:00

555 lines
15 KiB
Vue

<script lang="ts" setup>
import type { NotificationProviderConfig, NotificationType } from '#/config';
import type { TupleToUnion } from '#/utils';
import { apiNotification } from '@/api/notification';
const { t } = useMyI18n();
const { getSettingGroup } = useConfigStore();
const notificationRef = getSettingGroup('notification');
// Provider types with display names
const providerTypes: { value: TupleToUnion<NotificationType>; label: string }[] = [
{ value: 'telegram', label: 'Telegram' },
{ value: 'discord', label: 'Discord' },
{ value: 'bark', label: 'Bark' },
{ value: 'server-chan', label: 'Server Chan' },
{ value: 'wecom', label: 'WeChat Work' },
{ value: 'gotify', label: 'Gotify' },
{ value: 'pushover', label: 'Pushover' },
{ value: 'webhook', label: 'Webhook' },
];
// Provider field configurations
const providerFields: Record<
string,
{ key: keyof NotificationProviderConfig; label: string; placeholder: string }[]
> = {
telegram: [
{ key: 'token', label: 'Bot Token', placeholder: 'bot token' },
{ key: 'chat_id', label: 'Chat ID', placeholder: 'chat id' },
],
discord: [
{
key: 'webhook_url',
label: 'Webhook URL',
placeholder: 'https://discord.com/api/webhooks/...',
},
],
bark: [
{ key: 'device_key', label: 'Device Key', placeholder: 'device key' },
{
key: 'server_url',
label: 'Server URL (optional)',
placeholder: 'https://api.day.app',
},
],
'server-chan': [{ key: 'token', label: 'SendKey', placeholder: 'sendkey' }],
wecom: [
{ key: 'webhook_url', label: 'Webhook URL', placeholder: 'webhook url' },
{ key: 'token', label: 'Key', placeholder: 'key' },
],
gotify: [
{
key: 'server_url',
label: 'Server URL',
placeholder: 'https://gotify.example.com',
},
{ key: 'token', label: 'App Token', placeholder: 'app token' },
],
pushover: [
{ key: 'user_key', label: 'User Key', placeholder: 'user key' },
{ key: 'api_token', label: 'API Token', placeholder: 'api token' },
],
webhook: [
{
key: 'url',
label: 'Webhook URL',
placeholder: 'https://example.com/webhook',
},
{
key: 'template',
label: 'Template (JSON)',
placeholder: '{"title": "{{title}}", "episode": {{episode}}}',
},
],
};
// Dialog state
const showAddDialog = ref(false);
const showEditDialog = ref(false);
const editingIndex = ref(-1);
const newProvider = ref<NotificationProviderConfig>({
type: 'telegram',
enabled: true,
});
// Testing state
const testingIndex = ref(-1);
const testResult = ref<{ success: boolean; message: string } | null>(null);
// Computed properties to access notification settings
const notificationEnabled = computed({
get: () => notificationRef.value.enable,
set: (val) => {
notificationRef.value.enable = val;
},
});
const providers = computed({
get: () => notificationRef.value.providers || [],
set: (val) => {
notificationRef.value.providers = val;
},
});
// Initialize providers array if not exists
if (!notificationRef.value.providers) {
notificationRef.value.providers = [];
}
function getProviderLabel(type: string): string {
return providerTypes.find((p) => p.value === type)?.label || type;
}
function getProviderIcon(type: string): string {
const icons: Record<string, string> = {
telegram: 'i-simple-icons-telegram',
discord: 'i-simple-icons-discord',
bark: 'i-carbon-notification',
'server-chan': 'i-simple-icons-wechat',
wecom: 'i-simple-icons-wechat',
gotify: 'i-carbon-notification-filled',
pushover: 'i-carbon-mobile',
webhook: 'i-carbon-webhook',
};
return icons[type] || 'i-carbon-notification';
}
function openAddDialog() {
newProvider.value = {
type: 'telegram',
enabled: true,
};
testResult.value = null;
showAddDialog.value = true;
}
function openEditDialog(index: number) {
editingIndex.value = index;
newProvider.value = { ...providers.value[index] };
testResult.value = null;
showEditDialog.value = true;
}
function addProvider() {
const newProviders = [...providers.value, { ...newProvider.value }];
providers.value = newProviders;
showAddDialog.value = false;
}
function saveProvider() {
if (editingIndex.value >= 0) {
const newProviders = [...providers.value];
newProviders[editingIndex.value] = { ...newProvider.value };
providers.value = newProviders;
}
showEditDialog.value = false;
editingIndex.value = -1;
}
function removeProvider(index: number) {
const newProviders = providers.value.filter((_, i) => i !== index);
providers.value = newProviders;
}
function toggleProvider(index: number) {
const newProviders = [...providers.value];
newProviders[index] = {
...newProviders[index],
enabled: !newProviders[index].enabled,
};
providers.value = newProviders;
}
async function testProvider(index: number) {
testingIndex.value = index;
testResult.value = null;
try {
const response = await apiNotification.testProvider({ provider_index: index });
testResult.value = {
success: response.data.success,
message: response.data.message_zh || response.data.message,
};
} catch (error: any) {
testResult.value = {
success: false,
message: error.message || 'Test failed',
};
} finally {
testingIndex.value = -1;
}
}
async function testNewProvider() {
testingIndex.value = -999; // Special index for new provider
testResult.value = null;
try {
const response = await apiNotification.testProviderConfig(
newProvider.value as any
);
testResult.value = {
success: response.data.success,
message: response.data.message_zh || response.data.message,
};
} catch (error: any) {
testResult.value = {
success: false,
message: error.message || 'Test failed',
};
} finally {
testingIndex.value = -1;
}
}
function getFieldsForType(type: string) {
return providerFields[type] || [];
}
</script>
<template>
<ab-fold-panel :title="$t('config.notification_set.title')">
<div space-y-8>
<!-- Global enable switch -->
<ab-setting
config-key="enable"
:label="() => t('config.notification_set.enable')"
type="switch"
v-model:data="notificationEnabled"
bottom-line
/>
<!-- Provider list -->
<div v-if="notificationEnabled" space-y-8>
<div
v-for="(provider, index) in providers"
:key="index"
class="provider-item"
:class="{ 'provider-disabled': !provider.enabled }"
>
<div class="provider-info">
<div class="provider-name">
<div :class="getProviderIcon(provider.type)" />
{{ getProviderLabel(provider.type) }}
<span v-if="!provider.enabled" class="disabled-badge">
{{ $t('config.notification_set.disabled') }}
</span>
</div>
</div>
<div class="provider-actions">
<ab-button
size="small"
type="secondary"
:disabled="testingIndex === index"
:title="$t('config.notification_set.test')"
@click="testProvider(index)"
>
<div
v-if="testingIndex === index"
i-carbon-circle-dash
animate-spin
/>
<div v-else i-carbon-play />
</ab-button>
<ab-button
size="small"
type="secondary"
:title="$t('config.notification_set.edit')"
@click="openEditDialog(index)"
>
<div i-carbon-edit />
</ab-button>
<ab-button
size="small"
type="secondary"
:title="
provider.enabled
? $t('config.notification_set.disable')
: $t('config.notification_set.enable_provider')
"
@click="toggleProvider(index)"
>
<div
:class="provider.enabled ? 'i-carbon-view' : 'i-carbon-view-off'"
/>
</ab-button>
<ab-button
size="small"
type="warn"
:title="$t('config.notification_set.remove')"
@click="removeProvider(index)"
>
<div i-carbon-trash-can />
</ab-button>
</div>
</div>
<!-- Test result message -->
<div
v-if="testResult"
class="test-result"
:class="testResult.success ? 'test-success' : 'test-error'"
>
{{ testResult.message }}
</div>
<div line></div>
<!-- Add provider button -->
<div flex="~ justify-end">
<ab-button size="small" type="primary" @click="openAddDialog">
<div i-carbon-add />
{{ $t('config.notification_set.add_provider') }}
</ab-button>
</div>
</div>
</div>
<!-- Add Dialog -->
<ab-popup
v-model:show="showAddDialog"
:title="$t('config.notification_set.add_provider')"
css="w-400"
>
<div space-y-16>
<ab-label :label="$t('config.notification_set.type')">
<select v-model="newProvider.type" ab-input>
<option v-for="pt in providerTypes" :key="pt.value" :value="pt.value">
{{ pt.label }}
</option>
</select>
</ab-label>
<ab-label
v-for="field in getFieldsForType(newProvider.type)"
:key="field.key"
:label="field.label"
>
<input
v-if="field.key !== 'template'"
v-model="(newProvider as any)[field.key]"
:placeholder="field.placeholder"
ab-input
/>
<textarea
v-else
v-model="(newProvider as any)[field.key]"
:placeholder="field.placeholder"
ab-input
class="field-textarea"
rows="3"
/>
</ab-label>
<div
v-if="testResult"
class="test-result"
:class="testResult.success ? 'test-success' : 'test-error'"
>
{{ testResult.message }}
</div>
<div line></div>
<div flex="~ justify-between items-center">
<ab-button
size="small"
type="secondary"
:disabled="testingIndex === -999"
@click="testNewProvider"
>
<div
v-if="testingIndex === -999"
i-carbon-circle-dash
animate-spin
/>
<div v-else i-carbon-play />
{{ $t('config.notification_set.test') }}
</ab-button>
<div flex="~ gap-8">
<ab-button size="small" type="warn" @click="showAddDialog = false">
{{ $t('config.cancel') }}
</ab-button>
<ab-button size="small" type="primary" @click="addProvider">
{{ $t('config.apply') }}
</ab-button>
</div>
</div>
</div>
</ab-popup>
<!-- Edit Dialog -->
<ab-popup
v-model:show="showEditDialog"
:title="$t('config.notification_set.edit_provider')"
css="w-400"
>
<div space-y-16>
<ab-label :label="$t('config.notification_set.type')">
<select v-model="newProvider.type" ab-input disabled>
<option v-for="pt in providerTypes" :key="pt.value" :value="pt.value">
{{ pt.label }}
</option>
</select>
</ab-label>
<ab-label
v-for="field in getFieldsForType(newProvider.type)"
:key="field.key"
:label="field.label"
>
<input
v-if="field.key !== 'template'"
v-model="(newProvider as any)[field.key]"
:placeholder="field.placeholder"
ab-input
/>
<textarea
v-else
v-model="(newProvider as any)[field.key]"
:placeholder="field.placeholder"
ab-input
class="field-textarea"
rows="3"
/>
</ab-label>
<div
v-if="testResult"
class="test-result"
:class="testResult.success ? 'test-success' : 'test-error'"
>
{{ testResult.message }}
</div>
<div line></div>
<div flex="~ justify-between items-center">
<ab-button
size="small"
type="secondary"
:disabled="testingIndex === -999"
@click="testNewProvider"
>
<div
v-if="testingIndex === -999"
i-carbon-circle-dash
animate-spin
/>
<div v-else i-carbon-play />
{{ $t('config.notification_set.test') }}
</ab-button>
<div flex="~ gap-8">
<ab-button size="small" type="warn" @click="showEditDialog = false">
{{ $t('config.cancel') }}
</ab-button>
<ab-button size="small" type="primary" @click="saveProvider">
{{ $t('config.apply') }}
</ab-button>
</div>
</div>
</div>
</ab-popup>
</ab-fold-panel>
</template>
<style lang="scss" scoped>
.provider-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px;
background: var(--color-surface-elevated, #f9fafb);
border-radius: 8px;
transition: background-color var(--transition-normal), opacity var(--transition-normal);
:root.dark & {
background: var(--color-surface-elevated, #1f2937);
}
}
.provider-disabled {
opacity: 0.5;
}
.provider-info {
flex: 1;
min-width: 0;
}
.provider-name {
font-weight: 500;
font-size: 14px;
color: var(--color-text);
display: flex;
align-items: center;
gap: 8px;
}
.disabled-badge {
font-size: 11px;
font-weight: 500;
padding: 2px 6px;
border-radius: 4px;
background: var(--color-danger);
color: white;
opacity: 0.8;
}
.provider-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
:deep(.btn--small) {
min-width: 32px;
width: 32px;
height: 32px;
padding: 0;
}
:deep(.n-spin-container),
:deep(.n-spin-content) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
}
.field-textarea {
resize: none;
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
font-size: 13px;
}
.test-result {
font-size: 12px;
padding: 8px 12px;
border-radius: 6px;
}
.test-success {
color: var(--color-success, #22c55e);
background: color-mix(in srgb, var(--color-success, #22c55e) 10%, transparent);
}
.test-error {
color: var(--color-danger, #ef4444);
background: color-mix(in srgb, var(--color-danger, #ef4444) 10%, transparent);
}
</style>