update
This commit is contained in:
@@ -1,170 +1,206 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="handleSubmit" class="notification-setting-form">
|
<form @submit.prevent="handleSubmit" class="space-y-6 text-foreground"> <!-- Form container with spacing -->
|
||||||
<h3>{{ isEditing ? $t('settings.notifications.form.editTitle') : $t('settings.notifications.form.addTitle') }}</h3>
|
<h3 class="text-lg font-semibold mb-4 pb-2 border-b border-border"> <!-- Title -->
|
||||||
|
{{ isEditing ? $t('settings.notifications.form.editTitle') : $t('settings.notifications.form.addTitle') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div class="mb-3">
|
<!-- General Settings -->
|
||||||
<label for="setting-name" class="form-label">{{ $t('settings.notifications.form.name') }}</label>
|
<div class="space-y-4">
|
||||||
<input type="text" id="setting-name" v-model="formData.name" class="form-control" required>
|
<div>
|
||||||
</div>
|
<label for="setting-name" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.name') }}</label>
|
||||||
|
<input type="text" id="setting-name" v-model="formData.name" required
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
<div class="flex items-center">
|
||||||
<input type="checkbox" id="setting-enabled" v-model="formData.enabled" class="form-check-input">
|
<input type="checkbox" id="setting-enabled" v-model="formData.enabled"
|
||||||
<label for="setting-enabled" class="form-check-label">{{ $t('common.enabled') }}</label>
|
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer">
|
||||||
</div>
|
<label for="setting-enabled" class="text-sm text-foreground cursor-pointer">{{ $t('common.enabled') }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div>
|
||||||
<label for="setting-channel-type" class="form-label">{{ $t('settings.notifications.form.channelType') }}</label>
|
<label for="setting-channel-type" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.channelType') }}</label>
|
||||||
<select id="setting-channel-type" v-model="formData.channel_type" class="form-select" required :disabled="isEditing">
|
<select id="setting-channel-type" v-model="formData.channel_type" required :disabled="isEditing"
|
||||||
<option value="webhook">{{ $t('settings.notifications.types.webhook') }}</option>
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary appearance-none bg-no-repeat bg-right pr-8 disabled:opacity-70 disabled:bg-header/50"
|
||||||
<option value="email">{{ $t('settings.notifications.types.email') }}</option>
|
style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%236c757d\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M2 5l6 6 6-6\'/%3e%3c/svg%3e'); background-position: right 0.75rem center; background-size: 16px 12px;">
|
||||||
<option value="telegram">{{ $t('settings.notifications.types.telegram') }}</option>
|
<option value="webhook">{{ $t('settings.notifications.types.webhook') }}</option>
|
||||||
</select>
|
<option value="email">{{ $t('settings.notifications.types.email') }}</option>
|
||||||
<small v-if="isEditing" class="text-muted">{{ $t('settings.notifications.form.channelTypeEditNote') }}</small>
|
<option value="telegram">{{ $t('settings.notifications.types.telegram') }}</option>
|
||||||
|
</select>
|
||||||
|
<small v-if="isEditing" class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.channelTypeEditNote') }}</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Channel Specific Config -->
|
<!-- Channel Specific Config -->
|
||||||
<div v-if="formData.channel_type === 'webhook'" class="channel-config mb-3 p-3 border rounded">
|
<div class="border border-border rounded-md p-4 mt-4 bg-header/30 space-y-4"> <!-- Config section container -->
|
||||||
<h4>{{ $t('settings.notifications.types.webhook') }} {{ $t('common.settings') }}</h4>
|
<h4 class="text-base font-semibold mb-3 pb-2 border-b border-border/50"> <!-- Config section title -->
|
||||||
<div class="mb-3">
|
{{ $t(`settings.notifications.types.${formData.channel_type}`) }} {{ $t('common.settings') }}
|
||||||
<label for="webhook-url" class="form-label">URL</label>
|
</h4>
|
||||||
<input type="url" id="webhook-url" v-model="webhookConfig.url" class="form-control" required>
|
|
||||||
</div>
|
<!-- Webhook Config -->
|
||||||
<div class="mb-3">
|
<div v-if="formData.channel_type === 'webhook'" class="space-y-4">
|
||||||
<label for="webhook-method" class="form-label">{{ $t('settings.notifications.form.webhookMethod') }}</label>
|
<div>
|
||||||
<select id="webhook-method" v-model="webhookConfig.method" class="form-select">
|
<label for="webhook-url" class="block text-sm font-medium text-text-secondary mb-1">URL</label>
|
||||||
|
<input type="url" id="webhook-url" v-model="webhookConfig.url" required
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="webhook-method" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.webhookMethod') }}</label>
|
||||||
|
<select id="webhook-method" v-model="webhookConfig.method"
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary appearance-none bg-no-repeat bg-right pr-8"
|
||||||
|
style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%236c757d\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M2 5l6 6 6-6\'/%3e%3c/svg%3e'); background-position: right 0.75rem center; background-size: 16px 12px;">
|
||||||
<option value="POST">POST</option>
|
<option value="POST">POST</option>
|
||||||
<option value="GET">GET</option>
|
<option value="GET">GET</option>
|
||||||
<option value="PUT">PUT</option>
|
<option value="PUT">PUT</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="webhook-headers" class="form-label">{{ $t('settings.notifications.form.webhookHeaders') }} (JSON)</label>
|
|
||||||
<textarea id="webhook-headers" v-model="webhookHeadersString" class="form-control" rows="3" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer ..."}'></textarea>
|
|
||||||
<small v-if="headerError" class="text-danger">{{ headerError }}</small>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="webhook-body" class="form-label">{{ $t('settings.notifications.form.webhookBodyTemplate') }}</label>
|
|
||||||
<textarea id="webhook-body" v-model="webhookConfig.bodyTemplate" class="form-control" rows="3" :placeholder="$t('settings.notifications.form.webhookBodyPlaceholder')"></textarea>
|
|
||||||
<small class="text-muted">{{ $t('settings.notifications.form.templateHelp') }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="formData.channel_type === 'email'" class="channel-config mb-3 p-3 border rounded">
|
|
||||||
<h4>{{ $t('settings.notifications.types.email') }} {{ $t('common.settings') }}</h4>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="email-to" class="form-label">{{ $t('settings.notifications.form.emailTo') }}</label>
|
|
||||||
<input type="email" id="email-to" v-model="emailConfig.to" class="form-control" required placeholder="recipient1@example.com, recipient2@example.com">
|
|
||||||
<small class="text-muted">{{ $t('settings.notifications.form.emailToHelp') }}</small>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="email-subject" class="form-label">{{ $t('settings.notifications.form.emailSubjectTemplate') }}</label>
|
|
||||||
<input type="text" id="email-subject" v-model="emailConfig.subjectTemplate" class="form-control" :placeholder="$t('settings.notifications.form.emailSubjectPlaceholder')">
|
|
||||||
<small class="text-muted">{{ $t('settings.notifications.form.templateHelp') }}</small>
|
|
||||||
</div>
|
|
||||||
<!-- SMTP Settings -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp-host" class="form-label">{{ $t('settings.notifications.form.smtpHost') }}</label>
|
|
||||||
<input type="text" id="smtp-host" v-model="emailConfig.smtpHost" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label for="smtp-port" class="form-label">{{ $t('settings.notifications.form.smtpPort') }}</label>
|
|
||||||
<input type="number" id="smtp-port" v-model.number="emailConfig.smtpPort" class="form-control" required>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 mb-3 d-flex align-items-end">
|
<div>
|
||||||
<div class="form-check">
|
<label for="webhook-headers" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.webhookHeaders') }} (JSON)</label>
|
||||||
<input type="checkbox" id="smtp-secure" v-model="emailConfig.smtpSecure" class="form-check-input">
|
<textarea id="webhook-headers" v-model="webhookHeadersString" rows="3" placeholder='{"Content-Type": "application/json", "Authorization": "Bearer ..."}'
|
||||||
<label for="smtp-secure" class="form-check-label">{{ $t('settings.notifications.form.smtpSecure') }} (TLS)</label>
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary font-mono text-sm"></textarea>
|
||||||
</div>
|
<small v-if="headerError" class="block mt-1 text-xs text-error">{{ headerError }}</small> <!-- Use text-error -->
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="webhook-body" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.webhookBodyTemplate') }}</label>
|
||||||
|
<textarea id="webhook-body" v-model="webhookConfig.bodyTemplate" rows="3" :placeholder="$t('settings.notifications.form.webhookBodyPlaceholder')"
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary font-mono text-sm"></textarea>
|
||||||
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.templateHelp') }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp-user" class="form-label">{{ $t('settings.notifications.form.smtpUser') }}</label>
|
|
||||||
<input type="text" id="smtp-user" v-model="emailConfig.smtpUser" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp-pass" class="form-label">{{ $t('settings.notifications.form.smtpPass') }}</label>
|
|
||||||
<input type="password" id="smtp-pass" v-model="emailConfig.smtpPass" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="smtp-from" class="form-label">{{ $t('settings.notifications.form.smtpFrom') }}</label>
|
|
||||||
<input type="email" id="smtp-from" v-model="emailConfig.from" class="form-control" required placeholder="sender@example.com">
|
|
||||||
<small class="text-muted">{{ $t('settings.notifications.form.smtpFromHelp') }}</small>
|
|
||||||
</div>
|
|
||||||
<!-- Removed duplicate test button from here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="formData.channel_type === 'telegram'" class="channel-config mb-3 p-3 border rounded">
|
<!-- Email Config -->
|
||||||
<h4>{{ $t('settings.notifications.types.telegram') }} {{ $t('common.settings') }}</h4>
|
<div v-if="formData.channel_type === 'email'" class="space-y-4">
|
||||||
<div class="mb-3">
|
<div>
|
||||||
<label for="telegram-token" class="form-label">{{ $t('settings.notifications.form.telegramToken') }}</label>
|
<label for="email-to" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.emailTo') }}</label>
|
||||||
<input type="password" id="telegram-token" v-model="telegramConfig.botToken" class="form-control" required>
|
<input type="email" id="email-to" v-model="emailConfig.to" required placeholder="recipient1@example.com, recipient2@example.com"
|
||||||
<small class="text-muted">{{ $t('settings.notifications.form.telegramTokenHelp') }}</small>
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.emailToHelp') }}</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email-subject" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.emailSubjectTemplate') }}</label>
|
||||||
|
<input type="text" id="email-subject" v-model="emailConfig.subjectTemplate" :placeholder="$t('settings.notifications.form.emailSubjectPlaceholder')"
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.templateHelp') }}</small>
|
||||||
|
</div>
|
||||||
|
<!-- SMTP Settings -->
|
||||||
|
<div>
|
||||||
|
<label for="smtp-host" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.smtpHost') }}</label>
|
||||||
|
<input type="text" id="smtp-host" v-model="emailConfig.smtpHost" required
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="smtp-port" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.smtpPort') }}</label>
|
||||||
|
<input type="number" id="smtp-port" v-model.number="emailConfig.smtpPort" required
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end pb-1"> <!-- Align checkbox with bottom of port input -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" id="smtp-secure" v-model="emailConfig.smtpSecure"
|
||||||
|
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer">
|
||||||
|
<label for="smtp-secure" class="text-sm text-foreground cursor-pointer">{{ $t('settings.notifications.form.smtpSecure') }} (TLS)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="smtp-user" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.smtpUser') }}</label>
|
||||||
|
<input type="text" id="smtp-user" v-model="emailConfig.smtpUser" autocomplete="off"
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="smtp-pass" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.smtpPass') }}</label>
|
||||||
|
<input type="password" id="smtp-pass" v-model="emailConfig.smtpPass" autocomplete="new-password"
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="smtp-from" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.smtpFrom') }}</label>
|
||||||
|
<input type="email" id="smtp-from" v-model="emailConfig.from" required placeholder="sender@example.com"
|
||||||
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.smtpFromHelp') }}</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="telegram-chatid" class="form-label">{{ $t('settings.notifications.form.telegramChatId') }}</label>
|
|
||||||
<input type="text" id="telegram-chatid" v-model="telegramConfig.chatId" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="telegram-message" class="form-label">{{ $t('settings.notifications.form.telegramMessageTemplate') }}</label>
|
|
||||||
<textarea id="telegram-message" v-model="telegramConfig.messageTemplate" class="form-control" rows="3" :placeholder="$t('settings.notifications.form.telegramMessagePlaceholder')"></textarea>
|
|
||||||
<small class="text-muted">{{ $t('settings.notifications.form.templateHelp') }}</small>
|
|
||||||
</div>
|
|
||||||
<!-- Test button moved below -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Unified Test Button Area -->
|
<!-- Telegram Config -->
|
||||||
<div class="test-button-area"> <!-- Added class -->
|
<div v-if="formData.channel_type === 'telegram'" class="space-y-4">
|
||||||
<!-- Show button if editing OR if adding and required fields are filled -->
|
<div>
|
||||||
<button
|
<label for="telegram-token" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.telegramToken') }}</label>
|
||||||
v-if="isEditing || canTestUnsaved"
|
<input type="password" id="telegram-token" v-model="telegramConfig.botToken" required autocomplete="new-password"
|
||||||
type="button"
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
@click="handleTestNotification"
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.telegramTokenHelp') }}</small>
|
||||||
class="btn btn-outline-secondary btn-sm"
|
</div>
|
||||||
:disabled="testingNotification"
|
<div>
|
||||||
>
|
<label for="telegram-chatid" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.telegramChatId') }}</label>
|
||||||
<span v-if="testingNotification" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<input type="text" id="telegram-chatid" v-model="telegramConfig.chatId" required
|
||||||
{{ testingNotification ? $t('common.testing') : $t('settings.notifications.form.testButton') }}
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary">
|
||||||
</button>
|
</div>
|
||||||
<!-- Show hint if adding and required fields are NOT filled -->
|
<div>
|
||||||
<small v-else class="d-block mt-2 text-muted">
|
<label for="telegram-message" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.notifications.form.telegramMessageTemplate') }}</label>
|
||||||
{{ $t('settings.notifications.form.fillRequiredToTest') }}
|
<textarea id="telegram-message" v-model="telegramConfig.messageTemplate" rows="3" :placeholder="$t('settings.notifications.form.telegramMessagePlaceholder')"
|
||||||
</small>
|
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary font-mono text-sm"></textarea>
|
||||||
<!-- Show test result message if available -->
|
<small class="block mt-1 text-xs text-text-secondary">{{ $t('settings.notifications.form.templateHelp') }}</small>
|
||||||
<small v-if="testResult" :class="['d-block mt-2', testResult.success ? 'text-success' : 'text-danger']">
|
</div>
|
||||||
{{ testResult.message }}
|
</div>
|
||||||
</small>
|
|
||||||
|
<!-- Unified Test Button Area -->
|
||||||
|
<div class="text-center pt-4 mt-4 border-t border-border/50"> <!-- Test button container -->
|
||||||
|
<button
|
||||||
|
v-if="isEditing || canTestUnsaved"
|
||||||
|
type="button"
|
||||||
|
@click="handleTestNotification"
|
||||||
|
:disabled="testingNotification"
|
||||||
|
class="px-3 py-1.5 border border-border rounded-md text-sm font-medium text-text-secondary bg-background hover:bg-header focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center"
|
||||||
|
>
|
||||||
|
<svg v-if="testingNotification" class="animate-spin -ml-0.5 mr-2 h-4 w-4 text-text-secondary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
{{ testingNotification ? $t('common.testing') : $t('settings.notifications.form.testButton') }}
|
||||||
|
</button>
|
||||||
|
<!-- Show hint if adding and required fields are NOT filled -->
|
||||||
|
<small v-else class="block mt-2 text-xs text-text-secondary">
|
||||||
|
{{ $t('settings.notifications.form.fillRequiredToTest') }}
|
||||||
|
</small>
|
||||||
|
<!-- Show test result message if available -->
|
||||||
|
<small v-if="testResult" :class="['block mt-2 text-xs', testResult.success ? 'text-success' : 'text-error']"> <!-- Use text-success/text-error -->
|
||||||
|
{{ testResult.message }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Enabled Events -->
|
<!-- Enabled Events -->
|
||||||
<div class="mb-3">
|
<div>
|
||||||
<label class="form-label">{{ $t('settings.notifications.form.enabledEvents') }}</label>
|
<label class="block text-sm font-medium text-text-secondary mb-2">{{ $t('settings.notifications.form.enabledEvents') }}</label>
|
||||||
<div class="enabled-events-grid"> <!-- Changed class -->
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-4 gap-y-2"> <!-- Responsive grid for events -->
|
||||||
<div v-for="event in allNotificationEvents" :key="event"> <!-- Removed col classes -->
|
<div v-for="event in allNotificationEvents" :key="event">
|
||||||
<div class="form-check">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:id="'event-' + event"
|
:id="'event-' + event"
|
||||||
:value="event"
|
:value="event"
|
||||||
v-model="formData.enabled_events"
|
v-model="formData.enabled_events"
|
||||||
class="form-check-input"
|
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
<label :for="'event-' + event" class="form-check-label">{{ getEventDisplayName(event) }}</label>
|
<label :for="'event-' + event" class="text-sm text-foreground cursor-pointer select-none">{{ getEventDisplayName(event) }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="form-actions"> <!-- Added class -->
|
<!-- Form Actions -->
|
||||||
<button type="button" @click="handleCancel" class="btn btn-secondary me-2">{{ $t('common.cancel') }}</button>
|
<div class="flex justify-end space-x-3 pt-5 mt-6 border-t border-border">
|
||||||
<button type="submit" class="btn btn-primary" :disabled="store.isLoading || !!headerError || testingNotification">
|
<button type="button" @click="handleCancel"
|
||||||
|
class="px-4 py-2 bg-transparent text-text-secondary border border-border rounded-md shadow-sm hover:bg-border hover:text-foreground focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button type="submit" :disabled="store.isLoading || !!headerError || testingNotification"
|
||||||
|
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out">
|
||||||
{{ store.isLoading ? $t('common.saving') : $t('common.save') }}
|
{{ store.isLoading ? $t('common.saving') : $t('common.save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="formError" class="alert alert-danger mt-3">{{ formError }}</div>
|
<div v-if="formError" class="p-3 mt-3 border-l-4 border-error bg-error/10 text-error text-sm rounded">{{ formError }}</div> <!-- Use error colors -->
|
||||||
<div v-if="testError" class="alert alert-danger mt-3">{{ testError }}</div>
|
<div v-if="testError" class="p-3 mt-3 border-l-4 border-error bg-error/10 text-error text-sm rounded">{{ testError }}</div> <!-- Use error colors -->
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -457,234 +493,5 @@ const handleTestNotification = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Form container - Inherits styles from .form-section in parent */
|
/* Remove all scoped styles as they are now handled by Tailwind utility classes */
|
||||||
.notification-setting-form {
|
|
||||||
color: var(--text-color);
|
|
||||||
max-width: 800px; /* Limit form width */
|
|
||||||
margin: 0 auto; /* Center the form */
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: var(--text-color);
|
|
||||||
margin-bottom: calc(var(--base-margin) * 1.5); /* Adjust margin */
|
|
||||||
padding-bottom: var(--base-margin);
|
|
||||||
border-bottom: 1px solid var(--border-color-light, var(--border-color)); /* Lighter border */
|
|
||||||
font-size: 1.4rem; /* Adjust size */
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-3 {
|
|
||||||
margin-bottom: calc(var(--base-margin) * 1.2) !important; /* Consistent margin */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Elements Styling (Consistent with SettingsView) */
|
|
||||||
.form-label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: calc(var(--base-margin) / 3);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-color);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control, .form-select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem 0.7rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 5px;
|
|
||||||
font-family: var(--font-family-sans-serif);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: var(--input-bg-color, var(--app-bg-color));
|
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
}
|
|
||||||
.form-control:focus, .form-select:focus {
|
|
||||||
border-color: var(--link-active-color);
|
|
||||||
outline: 0;
|
|
||||||
box-shadow: 0 0 0 3px rgba(var(--rgb-link-active-color, 0, 123, 255), 0.2);
|
|
||||||
}
|
|
||||||
.form-select {
|
|
||||||
appearance: none; /* Custom arrow styling might be needed */
|
|
||||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 0.75rem center;
|
|
||||||
background-size: 16px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea.form-control {
|
|
||||||
min-height: 100px; /* Adjust height */
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Checkbox Styling (Consistent with SettingsView) */
|
|
||||||
.form-check {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: 0;
|
|
||||||
margin-bottom: calc(var(--base-margin) / 2); /* Spacing for checkbox groups */
|
|
||||||
}
|
|
||||||
.form-check-input {
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
margin-right: 0.7rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
appearance: none;
|
|
||||||
background-color: var(--input-bg-color, var(--app-bg-color));
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
|
||||||
}
|
|
||||||
.form-check-input:checked {
|
|
||||||
background-color: var(--button-bg-color);
|
|
||||||
border-color: var(--button-bg-color);
|
|
||||||
}
|
|
||||||
.form-check-input:checked::after {
|
|
||||||
content: '✔';
|
|
||||||
position: absolute;
|
|
||||||
color: var(--button-text-color);
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
font-size: 0.85em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.form-check-input:focus {
|
|
||||||
box-shadow: 0 0 0 3px rgba(var(--rgb-link-active-color, 0, 123, 255), 0.2);
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
.form-check-label {
|
|
||||||
margin-bottom: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Channel Config Section */
|
|
||||||
.channel-config {
|
|
||||||
border: 1px solid var(--border-color-light, var(--border-color)); /* Lighter border */
|
|
||||||
border-radius: 6px; /* Slightly rounded */
|
|
||||||
padding: calc(var(--base-padding) * 1.2); /* Adjust padding */
|
|
||||||
margin-top: var(--base-margin);
|
|
||||||
margin-bottom: calc(var(--base-margin) * 1.5); /* Ensure space below */
|
|
||||||
background-color: rgba(0,0,0,0.02); /* Very subtle background */
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-config h4 {
|
|
||||||
font-size: 1.1rem; /* Adjust size */
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: var(--base-margin);
|
|
||||||
color: var(--text-color);
|
|
||||||
padding-bottom: calc(var(--base-margin) * 0.75);
|
|
||||||
border-bottom: 1px dashed var(--border-color-light, var(--border-color));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enabled Events Layout */
|
|
||||||
.enabled-events-grid { /* Use this class on the div wrapping the events */
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* Responsive columns */
|
|
||||||
gap: calc(var(--base-margin) / 2) var(--base-margin); /* Row and column gap */
|
|
||||||
margin-top: calc(var(--base-margin) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Helper Text and Errors */
|
|
||||||
.text-muted {
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
font-size: 0.85em;
|
|
||||||
display: block;
|
|
||||||
margin-top: calc(var(--base-margin) / 3);
|
|
||||||
}
|
|
||||||
.text-danger, .alert-danger {
|
|
||||||
color: #842029;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
.text-success {
|
|
||||||
color: #0f5132;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
.alert-danger { /* Style for form error */
|
|
||||||
background-color: #f8d7da;
|
|
||||||
border: 1px solid #f5c2c7;
|
|
||||||
border-left: 4px solid #842029;
|
|
||||||
padding: 0.8rem 1rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-top: var(--base-margin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Test Button Area */
|
|
||||||
.test-button-area { /* Add this class to the div wrapping the test button */
|
|
||||||
margin-top: calc(var(--base-margin) * 1.5);
|
|
||||||
margin-bottom: calc(var(--base-margin) * 1.5);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.test-button-area .btn-outline-secondary {
|
|
||||||
/* Use base btn-outline-secondary */
|
|
||||||
}
|
|
||||||
.test-button-area .text-muted,
|
|
||||||
.test-button-area .text-danger,
|
|
||||||
.test-button-area .text-success {
|
|
||||||
margin-top: calc(var(--base-margin) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Button Styles (Inherited from parent component's style block) */
|
|
||||||
/* Ensure .btn, .btn-primary, .btn-secondary, .btn-sm are defined there or globally */
|
|
||||||
.spinner-border-sm {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
border-width: 0.2em;
|
|
||||||
vertical-align: -0.125em;
|
|
||||||
margin-right: 0.3rem; /* Space between spinner and text */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Final Action Buttons */
|
|
||||||
.form-actions { /* Add this class to the div wrapping save/cancel */
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: calc(var(--base-margin) * 2);
|
|
||||||
padding-top: var(--base-margin);
|
|
||||||
border-top: 1px solid var(--border-color-light, var(--border-color));
|
|
||||||
}
|
|
||||||
.form-actions .btn {
|
|
||||||
margin-left: var(--base-margin);
|
|
||||||
/* Re-affirming button styles for clarity */
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.1s ease;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
.form-actions .btn:hover:not(:disabled) {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
.form-actions .btn:active:not(:disabled) {
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
.form-actions .btn:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
.form-actions .btn-primary {
|
|
||||||
background-color: var(--button-bg-color);
|
|
||||||
border-color: var(--button-bg-color);
|
|
||||||
color: var(--button-text-color);
|
|
||||||
}
|
|
||||||
.form-actions .btn-primary:hover:not(:disabled) {
|
|
||||||
background-color: var(--button-hover-bg-color);
|
|
||||||
border-color: var(--button-hover-bg-color);
|
|
||||||
}
|
|
||||||
.form-actions .btn-secondary {
|
|
||||||
background-color: var(--secondary-button-bg-color, var(--header-bg-color));
|
|
||||||
color: var(--secondary-button-text-color, var(--text-color));
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
.form-actions .btn-secondary:hover:not(:disabled) {
|
|
||||||
background-color: var(--secondary-button-hover-bg-color, var(--border-color));
|
|
||||||
border-color: var(--border-color);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,42 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="notification-settings">
|
<div class="p-0"> <!-- Remove padding from here, parent view provides it -->
|
||||||
<h2>{{ $t('settings.notifications.title') }}</h2>
|
<h2 class="text-xl font-semibold text-foreground mb-4 pb-2 border-b border-border"> <!-- Title styling -->
|
||||||
|
{{ $t('settings.notifications.title') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<div v-if="store.isLoading" class="loading-indicator">
|
<div v-if="store.isLoading" class="p-4 text-center text-text-secondary italic"> <!-- Loading state -->
|
||||||
{{ $t('common.loading') }}
|
{{ $t('common.loading') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="store.error" class="error-message">
|
<div v-if="store.error" class="p-4 mb-4 border-l-4 border-error bg-error/10 text-error rounded"> <!-- Error state using error color -->
|
||||||
{{ store.error }}
|
{{ store.error }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!store.isLoading && !store.error">
|
<div v-if="!store.isLoading && !store.error">
|
||||||
<button @click="showAddForm = true" class="btn btn-primary mb-3">
|
<button @click="showAddForm = true" class="px-4 py-2 bg-button text-button-text rounded hover:bg-button-hover mb-4 inline-flex items-center text-sm font-medium"> <!-- Add button -->
|
||||||
<i class="fas fa-plus me-1"></i> {{ $t('settings.notifications.addChannel') }} <!-- Optional: Add icon -->
|
<i class="fas fa-plus mr-1 text-xs"></i> {{ $t('settings.notifications.addChannel') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="settings.length === 0" class="alert alert-info">
|
<div v-if="settings.length === 0" class="p-4 mb-4 border-l-4 border-blue-400 bg-blue-100 text-blue-700 rounded"> <!-- Info state (using blue for now) -->
|
||||||
{{ $t('settings.notifications.noChannels') }}
|
{{ $t('settings.notifications.noChannels') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Use a container for the list -->
|
<!-- Notification List -->
|
||||||
<div v-else class="notification-list-container">
|
<div v-else class="grid gap-4 mt-4">
|
||||||
<div v-for="setting in settings" :key="setting.id" class="notification-item">
|
<div v-for="setting in settings" :key="setting.id" class="bg-background border border-border rounded-lg p-4 flex justify-between items-start gap-4 shadow-sm hover:shadow-md transition-shadow duration-200"> <!-- List item card -->
|
||||||
<div class="notification-item-details">
|
<div class="flex-grow"> <!-- Details section -->
|
||||||
<strong class="notification-name">{{ setting.name }}</strong>
|
<strong class="block font-semibold text-base mb-1 text-foreground">{{ setting.name }}</strong>
|
||||||
<div class="notification-badges">
|
<div class="flex items-center space-x-2 mb-2"> <!-- Badges section -->
|
||||||
<span class="badge badge-channel-type me-1">{{ getChannelTypeName(setting.channel_type) }}</span>
|
<span class="px-2 py-0.5 rounded-full text-xs font-semibold bg-header border border-border text-text-secondary uppercase tracking-wider"> <!-- Channel type badge -->
|
||||||
<span :class="['badge', setting.enabled ? 'badge-status-enabled' : 'badge-status-disabled']">
|
{{ getChannelTypeName(setting.channel_type) }}
|
||||||
{{ setting.enabled ? $t('common.enabled') : $t('common.disabled') }}
|
</span>
|
||||||
|
<span :class="[
|
||||||
|
'px-2 py-0.5 rounded-full text-xs font-semibold border uppercase tracking-wider',
|
||||||
|
setting.enabled
|
||||||
|
? 'bg-success/10 text-success border-success/30'
|
||||||
|
: 'bg-warning/10 text-warning border-warning/30'
|
||||||
|
]"> <!-- Status badge using success/warning colors -->
|
||||||
|
{{ setting.enabled ? $t('common.enabled') : $t('common.disabled') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<small class="notification-events">{{ getEventNames(setting.enabled_events) }}</small>
|
<small class="block text-sm text-text-secondary mt-1">{{ getEventNames(setting.enabled_events) }}</small> <!-- Events text -->
|
||||||
</div>
|
</div>
|
||||||
<div class="notification-item-actions">
|
<div class="flex items-center flex-shrink-0 space-x-3"> <!-- Actions section -->
|
||||||
<button @click="editSetting(setting)" class="btn btn-sm btn-secondary me-2"> <!-- Changed to btn-secondary -->
|
<button @click="editSetting(setting)" class="text-link hover:text-link-hover text-sm font-medium hover:underline"> <!-- Edit button (link style) -->
|
||||||
<i class="fas fa-pencil-alt"></i> {{ $t('common.edit') }} <!-- Optional: Add icon -->
|
<i class="fas fa-pencil-alt mr-1 text-xs"></i>{{ $t('common.edit') }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="confirmDelete(setting)" class="btn btn-sm btn-danger"> <!-- Changed to btn-danger -->
|
<button @click="confirmDelete(setting)" class="text-error hover:text-error/80 text-sm font-medium hover:underline"> <!-- Delete button (error color) -->
|
||||||
<i class="fas fa-trash-alt"></i> {{ $t('common.delete') }} <!-- Optional: Add icon -->
|
<i class="fas fa-trash-alt mr-1 text-xs"></i>{{ $t('common.delete') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,15 +53,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add/Edit Form Section -->
|
<!-- Add/Edit Form Section -->
|
||||||
<div v-if="showAddForm || editingSetting" class="form-section">
|
<div v-if="showAddForm || editingSetting" class="mt-6 p-6 border border-border bg-background rounded-lg shadow-sm"> <!-- Form container -->
|
||||||
<!-- Use a simple conditional rendering for the form for now -->
|
|
||||||
<!-- TODO: Consider using a proper modal component for better UX -->
|
|
||||||
<NotificationSettingForm
|
<NotificationSettingForm
|
||||||
v-if="showAddForm || editingSetting"
|
v-if="showAddForm || editingSetting"
|
||||||
:initial-data="editingSetting"
|
:initial-data="editingSetting"
|
||||||
@save="handleSave"
|
@save="handleSave"
|
||||||
@cancel="closeForm"
|
@cancel="closeForm"
|
||||||
class="mt-4"
|
class="mt-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -120,211 +127,5 @@ const handleSave = (savedSetting: NotificationSetting) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.notification-settings {
|
/* Remove all scoped styles as they are now handled by Tailwind utility classes */
|
||||||
padding: var(--base-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: var(--text-color);
|
|
||||||
margin-bottom: calc(var(--base-margin) * 1.5); /* Adjust margin */
|
|
||||||
padding-bottom: var(--base-margin);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
font-size: 1.6rem; /* Adjust size */
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-indicator, .error-message, .alert-info {
|
|
||||||
margin-top: var(--base-margin);
|
|
||||||
padding: var(--base-padding);
|
|
||||||
border-radius: 6px; /* Consistent radius */
|
|
||||||
border: 1px solid transparent; /* Base border */
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-indicator {
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
font-style: italic;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: #842029;
|
|
||||||
background-color: #f8d7da;
|
|
||||||
border-color: #f5c2c7;
|
|
||||||
border-left: 4px solid #842029;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-info {
|
|
||||||
color: var(--info-text-color, #0c5460);
|
|
||||||
background-color: var(--info-bg-color, #d1ecf1);
|
|
||||||
border-color: var(--info-border-color, #bee5eb);
|
|
||||||
border-left: 4px solid var(--info-border-color, #bee5eb);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add Channel Button */
|
|
||||||
.btn-primary {
|
|
||||||
margin-bottom: calc(var(--base-margin) * 1.5) !important; /* Ensure spacing below button */
|
|
||||||
/* Inherits base btn styles */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Notification List Container */
|
|
||||||
.notification-list-container {
|
|
||||||
margin-top: var(--base-margin);
|
|
||||||
display: grid; /* Use grid for layout */
|
|
||||||
gap: var(--base-margin); /* Space between items */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Individual Notification Item (Card-like) */
|
|
||||||
.notification-item {
|
|
||||||
background-color: var(--content-bg-color, var(--app-bg-color));
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
padding: var(--base-padding);
|
|
||||||
border-radius: 8px; /* Rounded corners */
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start; /* Align items to the top */
|
|
||||||
gap: var(--base-margin); /* Space between details and actions */
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04); /* Subtle shadow */
|
|
||||||
transition: box-shadow 0.2s ease;
|
|
||||||
}
|
|
||||||
.notification-item:hover {
|
|
||||||
box-shadow: 0 3px 8px rgba(0,0,0,0.06); /* Slightly larger shadow on hover */
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-item-details {
|
|
||||||
flex-grow: 1; /* Allow details to take up available space */
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-name {
|
|
||||||
font-weight: 600; /* Make name slightly bolder */
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
display: block; /* Ensure it takes its own line */
|
|
||||||
margin-bottom: calc(var(--base-margin) / 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-badges {
|
|
||||||
margin-bottom: calc(var(--base-margin) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-events {
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
font-size: 0.9em;
|
|
||||||
display: block; /* Ensure it takes its own line */
|
|
||||||
margin-top: calc(var(--base-margin) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-item-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center; /* Align buttons vertically */
|
|
||||||
flex-shrink: 0; /* Prevent actions from shrinking */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Badge Styling */
|
|
||||||
.badge {
|
|
||||||
padding: 0.3em 0.7em; /* Adjust padding */
|
|
||||||
font-size: 0.75rem; /* Adjust size */
|
|
||||||
border-radius: 4px; /* Slightly rounded */
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase; /* Optional: Uppercase text */
|
|
||||||
letter-spacing: 0.5px; /* Optional: Add letter spacing */
|
|
||||||
line-height: 1; /* Ensure consistent height */
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-channel-type {
|
|
||||||
background-color: var(--secondary-button-bg-color, var(--header-bg-color));
|
|
||||||
color: var(--secondary-button-text-color, var(--text-color));
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
.badge-status-enabled {
|
|
||||||
background-color: var(--success-bg-color, #d1e7dd); /* Use variable or fallback */
|
|
||||||
color: var(--success-text-color, #0f5132);
|
|
||||||
border: 1px solid var(--success-border-color, #badbcc);
|
|
||||||
}
|
|
||||||
.badge-status-disabled {
|
|
||||||
background-color: var(--warning-bg-color, #fff3cd); /* Use variable or fallback */
|
|
||||||
color: var(--warning-text-color, #664d03);
|
|
||||||
border: 1px solid var(--warning-border-color, #ffecb5);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Action Buttons within list item */
|
|
||||||
.notification-item-actions .btn {
|
|
||||||
margin: 0; /* Remove default btn margin */
|
|
||||||
/* Use btn-sm for smaller buttons */
|
|
||||||
}
|
|
||||||
.notification-item-actions .btn-secondary {
|
|
||||||
/* Uses base btn-secondary styles */
|
|
||||||
}
|
|
||||||
.notification-item-actions .btn-danger {
|
|
||||||
/* Uses base btn-danger styles */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Form Section Styling */
|
|
||||||
.form-section {
|
|
||||||
margin-top: calc(var(--base-margin) * 2); /* More space before form */
|
|
||||||
padding: calc(var(--base-padding) * 1.5); /* More padding */
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background-color: var(--content-bg-color, var(--app-bg-color)); /* Match item background */
|
|
||||||
border-radius: 8px; /* Consistent radius */
|
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05); /* Consistent shadow */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inherited Button Styles (Ensure consistency with SettingsView/AuditLogView) */
|
|
||||||
.btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.1s ease;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
.btn:hover:not(:disabled) {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
.btn:active:not(:disabled) {
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
.btn:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
.btn-primary {
|
|
||||||
background-color: var(--button-bg-color);
|
|
||||||
border-color: var(--button-bg-color);
|
|
||||||
color: var(--button-text-color);
|
|
||||||
}
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
background-color: var(--button-hover-bg-color);
|
|
||||||
border-color: var(--button-hover-bg-color);
|
|
||||||
}
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: var(--secondary-button-bg-color, var(--header-bg-color));
|
|
||||||
color: var(--secondary-button-text-color, var(--text-color));
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
.btn-secondary:hover:not(:disabled) {
|
|
||||||
background-color: var(--secondary-button-hover-bg-color, var(--border-color));
|
|
||||||
border-color: var(--border-color);
|
|
||||||
}
|
|
||||||
.btn-danger {
|
|
||||||
background-color: var(--danger-color, #dc3545);
|
|
||||||
color: white;
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
.btn-danger:hover:not(:disabled) {
|
|
||||||
background-color: var(--danger-hover-color, #bb2d3b);
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
.btn-sm { /* Small button variant */
|
|
||||||
padding: 0.25rem 0.6rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
.mb-3 { /* Bootstrap margin bottom utility */
|
|
||||||
margin-bottom: var(--base-margin) !important;
|
|
||||||
}
|
|
||||||
.me-1 { margin-right: 0.25rem !important; }
|
|
||||||
.me-2 { margin-right: 0.5rem !important; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -30,46 +30,46 @@ const formatTimestamp = (timestamp: number | null): string => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-4"> <!-- Container with top margin -->
|
<div class="mt-4"> <!-- Container with top margin -->
|
||||||
<div v-if="isLoading" class="p-4 border border-border rounded-md mb-4 text-text-secondary bg-header/50"> <!-- Loading state with Tailwind -->
|
<div v-if="isLoading" class="p-4 border border-border rounded-md mb-4 text-text-secondary bg-header/50 text-center italic"> <!-- Loading state consistent with Notifications -->
|
||||||
{{ t('proxies.loading') }}
|
{{ t('proxies.loading') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="error" class="p-4 border border-error/30 bg-error/10 text-error rounded-md mb-4"> <!-- Use semantic error colors -->
|
<div v-else-if="error" class="p-4 mb-4 border-l-4 border-error bg-error/10 text-error rounded"> <!-- Error state consistent with Notifications -->
|
||||||
{{ t('proxies.error', { error: error }) }}
|
{{ t('proxies.error', { error: error }) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="proxies.length === 0" class="p-4 border border-border rounded-md mb-4 text-text-secondary"> <!-- No proxies state with Tailwind -->
|
<div v-else-if="proxies.length === 0" class="p-4 mb-4 border-l-4 border-blue-400 bg-blue-100 text-blue-700 rounded"> <!-- No proxies state consistent with Notifications (using blue for now) -->
|
||||||
{{ t('proxies.noProxies') }}
|
{{ t('proxies.noProxies') }}
|
||||||
</div>
|
</div>
|
||||||
<table v-else class="w-full border-collapse mt-4 text-sm"> <!-- Table with Tailwind -->
|
|
||||||
<thead>
|
<!-- Proxy List using Card Layout -->
|
||||||
<tr class="bg-header"> <!-- Table header row -->
|
<div v-else class="grid gap-4 mt-4">
|
||||||
<th class="px-4 py-2 border border-border text-left font-medium text-text-secondary">{{ t('proxies.table.name') }}</th>
|
<div v-for="proxy in proxies" :key="proxy.id" class="bg-background border border-border rounded-lg p-4 flex justify-between items-start gap-4 shadow-sm hover:shadow-md transition-shadow duration-200"> <!-- Proxy item card -->
|
||||||
<th class="px-4 py-2 border border-border text-left font-medium text-text-secondary">{{ t('proxies.table.type') }}</th>
|
<div class="flex-grow space-y-1"> <!-- Details section -->
|
||||||
<th class="px-4 py-2 border border-border text-left font-medium text-text-secondary">{{ t('proxies.table.host') }}</th>
|
<strong class="block font-semibold text-base text-foreground">{{ proxy.name }}</strong>
|
||||||
<th class="px-4 py-2 border border-border text-left font-medium text-text-secondary">{{ t('proxies.table.port') }}</th>
|
<div class="flex items-center space-x-2"> <!-- Type Badge -->
|
||||||
<th class="px-4 py-2 border border-border text-left font-medium text-text-secondary">{{ t('proxies.table.user') }}</th>
|
<span class="px-2 py-0.5 rounded-full text-xs font-semibold bg-header border border-border text-text-secondary uppercase tracking-wider">
|
||||||
<th class="px-4 py-2 border border-border text-left font-medium text-text-secondary">{{ t('proxies.table.updatedAt') }}</th>
|
{{ proxy.type }}
|
||||||
<th class="px-4 py-2 border border-border text-left font-medium text-text-secondary">{{ t('proxies.table.actions') }}</th>
|
</span>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
<div class="text-sm text-text-secondary"> <!-- Host & Port -->
|
||||||
<tbody>
|
<i class="fas fa-server mr-1 text-xs opacity-70"></i> {{ proxy.host }}:{{ proxy.port }}
|
||||||
<tr v-for="proxy in proxies" :key="proxy.id" class="odd:bg-background even:bg-header/50 hover:bg-link-active-bg/50"> <!-- Table body rows with alternating background and hover -->
|
</div>
|
||||||
<td class="px-4 py-2 border border-border">{{ proxy.name }}</td>
|
<div v-if="proxy.username" class="text-sm text-text-secondary"> <!-- Username (optional) -->
|
||||||
<td class="px-4 py-2 border border-border">{{ proxy.type }}</td>
|
<i class="fas fa-user mr-1 text-xs opacity-70"></i> {{ proxy.username }}
|
||||||
<td class="px-4 py-2 border border-border">{{ proxy.host }}</td>
|
</div>
|
||||||
<td class="px-4 py-2 border border-border">{{ proxy.port }}</td>
|
<div class="text-xs text-text-secondary pt-1"> <!-- Updated At -->
|
||||||
<td class="px-4 py-2 border border-border">{{ proxy.username || '-' }}</td>
|
<i class="fas fa-clock mr-1 opacity-70"></i> {{ t('common.updated') }}: {{ formatTimestamp(proxy.updated_at) }}
|
||||||
<td class="px-4 py-2 border border-border whitespace-nowrap">{{ formatTimestamp(proxy.updated_at) }}</td>
|
</div>
|
||||||
<td class="px-4 py-2 border border-border space-x-2 whitespace-nowrap"> <!-- Actions cell with spacing -->
|
</div>
|
||||||
<button @click="emit('edit-proxy', proxy)" class="text-link hover:text-link-hover hover:underline text-xs font-medium"> <!-- Edit button with link style -->
|
<div class="flex items-center flex-shrink-0 space-x-3 pt-1"> <!-- Actions section -->
|
||||||
{{ t('proxies.actions.edit') }}
|
<button @click="emit('edit-proxy', proxy)" class="text-link hover:text-link-hover text-sm font-medium hover:underline"> <!-- Edit button (link style) -->
|
||||||
</button>
|
<i class="fas fa-pencil-alt mr-1 text-xs"></i>{{ t('proxies.actions.edit') }}
|
||||||
<button @click="handleDelete(proxy)" class="text-error hover:text-error/80 hover:underline text-xs font-medium"> <!-- Use semantic error color for delete -->
|
</button>
|
||||||
{{ t('proxies.actions.delete') }}
|
<button @click="handleDelete(proxy)" class="text-error hover:text-error/80 text-sm font-medium hover:underline"> <!-- Delete button (error color) -->
|
||||||
</button>
|
<i class="fas fa-trash-alt mr-1 text-xs"></i>{{ t('proxies.actions.delete') }}
|
||||||
</td>
|
</button>
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="notifications-view">
|
<div class="p-4 bg-background text-foreground"> <!-- Outer container with padding -->
|
||||||
<!-- <h1>{{ $t('nav.notifications') }}</h1> --> <!-- Add nav.notifications to i18n later -->
|
<div class="max-w-6xl mx-auto"> <!-- Inner container for max-width and centering -->
|
||||||
<h1>通知管理</h1> <!-- Temporary title -->
|
<!-- Removed temporary h1 title -->
|
||||||
<NotificationSettings />
|
<NotificationSettings />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -11,9 +12,5 @@ import NotificationSettings from '../components/NotificationSettings.vue';
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.notifications-view {
|
/* Remove scoped styles as they are now handled by Tailwind utility classes */
|
||||||
padding: var(--base-padding, 20px); /* 使用变量 */
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: var(--app-bg-color);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -42,28 +42,32 @@ const closeForm = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-4 bg-background text-foreground"> <!-- Ensure Tailwind padding, background, and text color -->
|
<div class="p-4 bg-background text-foreground"> <!-- Outer container with padding -->
|
||||||
<h2 class="text-xl font-semibold mb-4">{{ t('proxies.title') }}</h2> <!-- Ensure Tailwind typography -->
|
<div class="max-w-6xl mx-auto"> <!-- Inner container for max-width and centering -->
|
||||||
|
<h2 class="text-xl font-semibold text-foreground mb-4 pb-2 border-b border-border"> <!-- Title styling consistent with Notifications -->
|
||||||
|
{{ t('proxies.title') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="openAddForm"
|
@click="openAddForm"
|
||||||
v-if="!showForm"
|
v-if="!showForm"
|
||||||
class="px-4 py-2 bg-button text-button-text rounded hover:bg-button-hover mb-4 transition duration-150 ease-in-out"
|
class="px-4 py-2 bg-button text-button-text rounded hover:bg-button-hover mb-4 inline-flex items-center text-sm font-medium"
|
||||||
> <!-- Ensure Tailwind button styles -->
|
> <!-- Button styling consistent with Notifications -->
|
||||||
{{ t('proxies.addProxy') }}
|
<i class="fas fa-plus mr-1 text-xs"></i> {{ t('proxies.addProxy') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 添加/编辑代理表单 -->
|
<!-- 添加/编辑代理表单 -->
|
||||||
<AddProxyForm
|
<AddProxyForm
|
||||||
v-if="showForm"
|
v-if="showForm"
|
||||||
:proxy-to-edit="editingProxy"
|
:proxy-to-edit="editingProxy"
|
||||||
@close="closeForm"
|
@close="closeForm"
|
||||||
@proxy-added="handleProxyAdded"
|
@proxy-added="handleProxyAdded"
|
||||||
@proxy-updated="handleProxyUpdated"
|
@proxy-updated="handleProxyUpdated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 代理列表 -->
|
<!-- 代理列表 -->
|
||||||
<ProxyList @edit-proxy="handleEditRequest" />
|
<ProxyList @edit-proxy="handleEditRequest" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user