This commit is contained in:
Baobhan Sith
2025-04-18 19:01:58 +08:00
parent 7a2056763d
commit 5c6cc05c54
7 changed files with 854 additions and 623 deletions
@@ -114,7 +114,7 @@
</div> </div>
<!-- Unified Test Button Area --> <!-- Unified Test Button Area -->
<div class="mb-3 text-center"> <div class="test-button-area"> <!-- Added class -->
<!-- Show button if editing OR if adding and required fields are filled --> <!-- Show button if editing OR if adding and required fields are filled -->
<button <button
v-if="isEditing || canTestUnsaved" v-if="isEditing || canTestUnsaved"
@@ -140,8 +140,8 @@
<!-- Enabled Events --> <!-- Enabled Events -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{{ $t('settings.notifications.form.enabledEvents') }}</label> <label class="form-label">{{ $t('settings.notifications.form.enabledEvents') }}</label>
<div class="row"> <div class="enabled-events-grid"> <!-- Changed class -->
<div v-for="event in allNotificationEvents" :key="event" class="col-md-4 col-sm-6"> <div v-for="event in allNotificationEvents" :key="event"> <!-- Removed col classes -->
<div class="form-check"> <div class="form-check">
<input <input
type="checkbox" type="checkbox"
@@ -157,7 +157,7 @@
</div> </div>
<div class="d-flex justify-content-end"> <div class="form-actions"> <!-- Added class -->
<button type="button" @click="handleCancel" class="btn btn-secondary me-2">{{ $t('common.cancel') }}</button> <button type="button" @click="handleCancel" class="btn btn-secondary me-2">{{ $t('common.cancel') }}</button>
<button type="submit" class="btn btn-primary" :disabled="store.isLoading || !!headerError || testingNotification"> <button type="submit" class="btn btn-primary" :disabled="store.isLoading || !!headerError || testingNotification">
{{ store.isLoading ? $t('common.saving') : $t('common.save') }} {{ store.isLoading ? $t('common.saving') : $t('common.save') }}
@@ -457,162 +457,234 @@ const handleTestNotification = async () => {
</script> </script>
<style scoped> <style scoped>
/* Form container - Inherits styles from .form-section in parent */
.notification-setting-form { .notification-setting-form {
padding: var(--base-padding);
background-color: var(--app-bg-color); /* Use app background */
border: 1px solid var(--border-color); /* Add border consistent with theme */
border-radius: 4px;
color: var(--text-color); color: var(--text-color);
/* Removed box-shadow for a flatter look, can be added back if desired */ max-width: 800px; /* Limit form width */
margin: 0 auto; /* Center the form */
} }
h3 { h3 {
color: var(--text-color); color: var(--text-color);
margin-bottom: calc(var(--base-margin) * 2); margin-bottom: calc(var(--base-margin) * 1.5); /* Adjust margin */
padding-bottom: var(--base-margin); padding-bottom: var(--base-margin);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color-light, var(--border-color)); /* Lighter border */
font-size: 1.25rem; /* Slightly larger heading */ font-size: 1.4rem; /* Adjust size */
font-weight: 600;
} }
.mb-3 { /* Bootstrap margin bottom class */ .mb-3 {
margin-bottom: calc(var(--base-margin) * 1.5) !important; /* Use variable, increase slightly */ margin-bottom: calc(var(--base-margin) * 1.2) !important; /* Consistent margin */
} }
/* Form Elements Styling (Consistent with SettingsView) */
.form-label { .form-label {
display: block; display: block;
margin-bottom: calc(var(--base-margin) / 2); margin-bottom: calc(var(--base-margin) / 3);
font-weight: 600;
color: var(--text-color); color: var(--text-color);
font-weight: bold; font-size: 0.9rem;
} }
.form-control, .form-select, .form-check-input { .form-control, .form-select {
background-color: var(--app-bg-color); width: 100%;
color: var(--text-color); padding: 0.5rem 0.7rem;
box-sizing: border-box;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
padding: 0.5rem 0.75rem; border-radius: 5px;
border-radius: 4px; font-family: var(--font-family-sans-serif);
width: 100%; /* Ensure inputs take full width */ font-size: 0.95rem;
box-sizing: border-box; /* Include padding and border in element's total width */ 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 { .form-control:focus, .form-select:focus {
border-color: var(--link-active-color); /* Highlight focus */ border-color: var(--link-active-color);
outline: 0; outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); /* Optional focus shadow */ 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 { textarea.form-control {
min-height: 80px; /* Give textareas more space */ min-height: 100px; /* Adjust height */
resize: vertical;
} }
/* Checkbox Styling (Consistent with SettingsView) */
.form-check { .form-check {
display: flex; /* Align checkbox and label */ display: flex;
align-items: center; align-items: center;
padding-left: 0; /* Reset Bootstrap padding */ padding-left: 0;
margin-bottom: calc(var(--base-margin) / 2); /* Spacing for checkbox groups */
} }
.form-check-input { .form-check-input {
width: auto; /* Don't force checkbox to full width */ width: 1.2em;
margin-right: 0.5rem; /* Space between checkbox and label */ height: 1.2em;
margin-top: 0; /* Align vertically */ margin-right: 0.7rem;
border: 1px solid var(--border-color); /* Ensure border is visible */ flex-shrink: 0;
} appearance: none;
.form-check-label { background-color: var(--input-bg-color, var(--app-bg-color));
margin-bottom: 0; /* Reset label margin */
font-weight: normal; /* Normal weight for checkbox labels */
}
.text-muted {
color: var(--text-color-secondary);
font-size: 0.85em;
display: block; /* Ensure it takes its own line */
margin-top: calc(var(--base-margin) / 2);
}
.text-danger {
color: #dc3545; /* Keep specific danger color */
}
.text-success {
color: #198754; /* Keep specific success color */
}
.channel-config {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 4px;
padding: var(--base-padding); 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-top: var(--base-margin);
background-color: var(--header-bg-color); /* Slightly different background */ 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 { .channel-config h4 {
font-size: 1rem; font-size: 1.1rem; /* Adjust size */
font-weight: bold; font-weight: 600;
margin-bottom: var(--base-margin); margin-bottom: var(--base-margin);
color: var(--text-color); color: var(--text-color);
padding-bottom: calc(var(--base-margin) / 2); padding-bottom: calc(var(--base-margin) * 0.75);
border-bottom: 1px dashed var(--border-color); /* Dashed separator */ border-bottom: 1px dashed var(--border-color-light, var(--border-color));
} }
/* Button styling (reuse from NotificationSettings or define globally) */ /* Enabled Events Layout */
.btn { .enabled-events-grid { /* Use this class on the div wrapping the events */
padding: 0.5rem 1rem; display: grid;
border-radius: 4px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* Responsive columns */
cursor: pointer; gap: calc(var(--base-margin) / 2) var(--base-margin); /* Row and column gap */
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; margin-top: calc(var(--base-margin) / 2);
font-weight: bold;
} }
.btn-primary {
background-color: var(--button-bg-color); /* Helper Text and Errors */
border: 1px solid var(--button-bg-color); .text-muted {
color: var(--button-text-color);
}
.btn-primary:hover {
background-color: var(--button-hover-bg-color);
border-color: var(--button-hover-bg-color);
color: var(--button-text-color);
}
.btn-primary:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.btn-secondary {
background-color: var(--text-color-secondary);
border: 1px solid var(--text-color-secondary);
color: var(--app-bg-color);
}
.btn-secondary:hover {
background-color: var(--text-color);
border-color: var(--text-color);
color: var(--app-bg-color);
}
.btn-outline-secondary {
color: var(--text-color-secondary); color: var(--text-color-secondary);
border: 1px solid var(--border-color); font-size: 0.85em;
background-color: transparent; display: block;
margin-top: calc(var(--base-margin) / 3);
} }
.btn-outline-secondary:hover { .text-danger, .alert-danger {
background-color: var(--header-bg-color); color: #842029;
color: var(--text-color); font-size: 0.9em;
border-color: var(--border-color);
} }
.btn-sm { .text-success {
padding: 0.25rem 0.5rem; color: #0f5132;
font-size: 0.875rem; 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 { .spinner-border-sm {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
border-width: 0.2em; border-width: 0.2em;
vertical-align: -0.125em; /* Align spinner better with text */ vertical-align: -0.125em;
margin-right: 0.3rem; /* Space between spinner and text */
} }
.alert { /* General alert styling */ /* Final Action Buttons */
padding: var(--base-padding); .form-actions { /* Add this class to the div wrapping save/cancel */
margin-top: var(--base-margin); display: flex;
border: 1px solid transparent; justify-content: flex-end;
border-radius: 4px; margin-top: calc(var(--base-margin) * 2);
padding-top: var(--base-margin);
border-top: 1px solid var(--border-color-light, var(--border-color));
} }
.alert-danger { .form-actions .btn {
color: #842029; margin-left: var(--base-margin);
background-color: #f8d7da; /* Re-affirming button styles for clarity */
border-color: #f5c2c7; 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>
@@ -11,37 +11,40 @@
<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="btn btn-primary mb-3">
{{ $t('settings.notifications.addChannel') }} <i class="fas fa-plus me-1"></i> {{ $t('settings.notifications.addChannel') }} <!-- Optional: Add icon -->
</button> </button>
<div v-if="settings.length === 0" class="alert alert-info"> <div v-if="settings.length === 0" class="alert alert-info">
{{ $t('settings.notifications.noChannels') }} {{ $t('settings.notifications.noChannels') }}
</div> </div>
<ul v-else class="list-group"> <!-- Use a container for the list -->
<li v-for="setting in settings" :key="setting.id" class="list-group-item d-flex justify-content-between align-items-center"> <div v-else class="notification-list-container">
<div> <div v-for="setting in settings" :key="setting.id" class="notification-item">
<strong class="me-2">{{ setting.name }}</strong> <div class="notification-item-details">
<span class="badge bg-secondary me-1">{{ getChannelTypeName(setting.channel_type) }}</span> <strong class="notification-name">{{ setting.name }}</strong>
<span :class="['badge', setting.enabled ? 'bg-success' : 'bg-warning']"> <div class="notification-badges">
<span class="badge badge-channel-type me-1">{{ getChannelTypeName(setting.channel_type) }}</span>
<span :class="['badge', setting.enabled ? 'badge-status-enabled' : 'badge-status-disabled']">
{{ setting.enabled ? $t('common.enabled') : $t('common.disabled') }} {{ setting.enabled ? $t('common.enabled') : $t('common.disabled') }}
</span> </span>
<small class="d-block text-muted">{{ getEventNames(setting.enabled_events) }}</small>
</div> </div>
<div> <small class="notification-events">{{ getEventNames(setting.enabled_events) }}</small>
<button @click="editSetting(setting)" class="btn btn-sm btn-outline-secondary me-2"> </div>
{{ $t('common.edit') }} <div class="notification-item-actions">
<button @click="editSetting(setting)" class="btn btn-sm btn-secondary me-2"> <!-- Changed to btn-secondary -->
<i class="fas fa-pencil-alt"></i> {{ $t('common.edit') }} <!-- Optional: Add icon -->
</button> </button>
<button @click="confirmDelete(setting)" class="btn btn-sm btn-outline-danger"> <button @click="confirmDelete(setting)" class="btn btn-sm btn-danger"> <!-- Changed to btn-danger -->
{{ $t('common.delete') }} <i class="fas fa-trash-alt"></i> {{ $t('common.delete') }} <!-- Optional: Add icon -->
</button> </button>
</div> </div>
</li> </div>
</ul> </div>
</div> </div>
<!-- Add/Edit Form Modal (Placeholder - will create NotificationSettingForm.vue next) --> <!-- Add/Edit Form Section -->
<div v-if="showAddForm || editingSetting" class="modal-placeholder"> <div v-if="showAddForm || editingSetting" class="form-section">
<!-- Use a simple conditional rendering for the form for now --> <!-- Use a simple conditional rendering for the form for now -->
<!-- TODO: Consider using a proper modal component for better UX --> <!-- TODO: Consider using a proper modal component for better UX -->
<NotificationSettingForm <NotificationSettingForm
@@ -119,132 +122,209 @@ const handleSave = (savedSetting: NotificationSetting) => {
<style scoped> <style scoped>
.notification-settings { .notification-settings {
padding: var(--base-padding); padding: var(--base-padding);
/* Inherits text color and background from parent (NotificationsView) */
} }
h2 { h2 {
color: var(--text-color); color: var(--text-color);
margin-bottom: calc(var(--base-margin) * 2); margin-bottom: calc(var(--base-margin) * 1.5); /* Adjust margin */
padding-bottom: var(--base-margin); padding-bottom: var(--base-margin);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
font-size: 1.6rem; /* Adjust size */
} }
.loading-indicator, .error-message, .alert-info { .loading-indicator, .error-message, .alert-info {
margin-top: var(--base-margin); margin-top: var(--base-margin);
padding: var(--base-padding); padding: var(--base-padding);
border-radius: 4px; border-radius: 6px; /* Consistent radius */
border: 1px solid transparent; /* Base border */
}
.loading-indicator {
color: var(--text-color-secondary); color: var(--text-color-secondary);
background-color: var(--header-bg-color); /* Use header bg for subtle background */ font-style: italic;
border: 1px solid var(--border-color); text-align: center;
} }
.error-message { .error-message {
color: #dc3545; /* Keep specific error color */ color: #842029;
border-color: #dc3545; background-color: #f8d7da;
background-color: #f8d7da; /* Light red background for errors */ border-color: #f5c2c7;
border-left: 4px solid #842029;
} }
.alert-info { .alert-info {
color: var(--text-color); /* Use primary text color for info */ color: var(--info-text-color, #0c5460);
} background-color: var(--info-bg-color, #d1ecf1);
border-color: var(--info-border-color, #bee5eb);
.btn { /* General button styling */ border-left: 4px solid var(--info-border-color, #bee5eb);
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
margin: var(--base-margin) 0; /* Add some default margin */
} }
/* Add Channel Button */
.btn-primary { .btn-primary {
background-color: var(--button-bg-color); margin-bottom: calc(var(--base-margin) * 1.5) !important; /* Ensure spacing below button */
border: 1px solid var(--button-bg-color); /* Inherits base btn styles */
color: var(--button-text-color);
}
.btn-primary:hover {
background-color: var(--button-hover-bg-color);
border-color: var(--button-hover-bg-color);
color: var(--button-text-color);
} }
.btn-outline-secondary { /* Notification List Container */
color: var(--text-color-secondary); .notification-list-container {
border: 1px solid var(--border-color);
background-color: transparent;
}
.btn-outline-secondary:hover {
background-color: var(--header-bg-color);
color: var(--text-color);
border-color: var(--border-color);
}
.btn-outline-danger {
color: #dc3545; /* Keep specific danger color */
border: 1px solid #dc3545;
background-color: transparent;
}
.btn-outline-danger:hover {
background-color: #dc3545;
color: var(--button-text-color);
border-color: #dc3545;
}
.list-group {
list-style: none;
padding: 0;
margin-top: var(--base-margin); margin-top: var(--base-margin);
display: grid; /* Use grid for layout */
gap: var(--base-margin); /* Space between items */
} }
.list-group-item { /* Individual Notification Item (Card-like) */
background-color: var(--app-bg-color); .notification-item {
background-color: var(--content-bg-color, var(--app-bg-color));
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
color: var(--text-color); color: var(--text-color);
padding: var(--base-padding); padding: var(--base-padding);
margin-bottom: var(--base-margin); /* Space between items */ border-radius: 8px; /* Rounded corners */
border-radius: 4px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; 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;
} }
.list-group-item:last-child { .notification-item:hover {
margin-bottom: 0; box-shadow: 0 3px 8px rgba(0,0,0,0.06); /* Slightly larger shadow on hover */
} }
.list-group-item strong { .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); color: var(--text-color);
display: block; /* Ensure it takes its own line */
margin-bottom: calc(var(--base-margin) / 4);
} }
.list-group-item .text-muted { .notification-badges {
margin-bottom: calc(var(--base-margin) / 2);
}
.notification-events {
color: var(--text-color-secondary); color: var(--text-color-secondary);
font-size: 0.9em; font-size: 0.9em;
display: block; /* Ensure it takes its own line */
margin-top: calc(var(--base-margin) / 2); 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 { .badge {
padding: 0.3em 0.6em; padding: 0.3em 0.7em; /* Adjust padding */
font-size: 0.8em; font-size: 0.75rem; /* Adjust size */
border-radius: 0.25rem; border-radius: 4px; /* Slightly rounded */
font-weight: bold; font-weight: 600;
text-transform: uppercase; /* Optional: Uppercase text */
letter-spacing: 0.5px; /* Optional: Add letter spacing */
line-height: 1; /* Ensure consistent height */
} }
.badge.bg-secondary { .badge-channel-type {
background-color: var(--text-color-secondary); background-color: var(--secondary-button-bg-color, var(--header-bg-color));
color: var(--app-bg-color); /* Use app background for contrast */ color: var(--secondary-button-text-color, var(--text-color));
}
.badge.bg-success {
background-color: #198754; /* Keep specific success color */
color: #fff;
}
.badge.bg-warning {
background-color: #ffc107; /* Keep specific warning color */
color: #000; /* Black text for better contrast on yellow */
}
.modal-placeholder {
margin-top: calc(var(--base-margin) * 3);
padding: var(--base-padding);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
background-color: var(--header-bg-color);
border-radius: 4px;
} }
.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>
+2 -1
View File
@@ -246,7 +246,8 @@
"alreadyConnected": "An active SSH connection already exists.", "alreadyConnected": "An active SSH connection already exists.",
"unknown": "Unknown status", "unknown": "Unknown status",
"wsClosedWillRetry": "WebSocket connection closed, will attempt reconnect {attempt} in {seconds} seconds...", "wsClosedWillRetry": "WebSocket connection closed, will attempt reconnect {attempt} in {seconds} seconds...",
"reconnecting": "Attempting to reconnect..." "reconnecting": "Attempting to reconnect...",
"reconnectFailed": "Reconnect failed"
}, },
"terminal": { "terminal": {
"infoPrefix": "[INFO]", "infoPrefix": "[INFO]",
+2 -1
View File
@@ -244,7 +244,8 @@
"alreadyConnected": "已存在活动的 SSH 连接。", "alreadyConnected": "已存在活动的 SSH 连接。",
"unknown": "未知状态", "unknown": "未知状态",
"wsClosedWillRetry": "WebSocket 连接已关闭,将在 {seconds} 秒后尝试第 {attempt} 次重连...", "wsClosedWillRetry": "WebSocket 连接已关闭,将在 {seconds} 秒后尝试第 {attempt} 次重连...",
"reconnecting": "正在尝试重新连接..." "reconnecting": "正在尝试重新连接...",
"reconnectFailed": "重连失败"
}, },
"selectConnectionPrompt": "请选择一个连接", "selectConnectionPrompt": "请选择一个连接",
"selectConnectionHint": "从左侧列表中选择一个连接,或点击'添加新连接'按钮创建一个新连接。", "selectConnectionHint": "从左侧列表中选择一个连接,或点击'添加新连接'按钮创建一个新连接。",
+4 -2
View File
@@ -47,8 +47,10 @@ a:hover {
/* 可以添加更多全局样式规则 */ /* 可以添加更多全局样式规则 */
/* 为 xterm 终端添加内边距 */ /* 为 xterm 终端添加内边距 */
.xterm-screen {
padding: 10px; /* 你可以根据需要调整这个值 */ .xterm{
padding: 10px 10px 10px 10px;
} }
/* 为历史记录和快捷命令列表设置字体 */ /* 为历史记录和快捷命令列表设置字体 */
+103 -86
View File
@@ -15,7 +15,8 @@
<div v-if="logs.length === 0" class="alert alert-info"> <div v-if="logs.length === 0" class="alert alert-info">
{{ $t('auditLog.noLogs') }} {{ $t('auditLog.noLogs') }}
</div> </div>
<div v-else> <div v-else> <!-- Wrapper for v-else content -->
<div class="table-container"> <!-- Add table container -->
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
@@ -35,7 +36,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> <!-- End table container -->
<!-- Pagination Controls --> <!-- Pagination Controls -->
<nav aria-label="Audit Log Pagination" v-if="totalPages > 1"> <nav aria-label="Audit Log Pagination" v-if="totalPages > 1">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
@@ -54,9 +55,9 @@
<div class="text-center text-muted mt-2"> <div class="text-center text-muted mt-2">
{{ $t('auditLog.paginationInfo', { currentPage, totalPages, totalLogs }) }} {{ $t('auditLog.paginationInfo', { currentPage, totalPages, totalLogs }) }}
</div> </div>
</div> </div> <!-- This closes the v-else block starting implicitly at line 18 -->
</div> </div> <!-- This closes the v-if block starting at line 14 -->
</div> </div> <!-- This closes the root div -->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -145,155 +146,171 @@ const paginationRange = computed(() => {
<style scoped> <style scoped>
.audit-log-view { .audit-log-view {
padding: var(--base-padding, 20px); /* 使用变量 */ padding: var(--base-padding, 20px);
color: var(--text-color); color: var(--text-color);
background-color: var(--app-bg-color); background-color: var(--app-bg-color);
min-height: calc(100vh - 60px); /* Example: Adjust based on header/footer height */ min-height: calc(100vh - 60px); /* Adjust based on actual header/footer */
max-width: 1400px; /* Limit max width for better readability on large screens */
margin: 0 auto; /* Center the view */
} }
.audit-log-view h1 { .audit-log-view h1 {
margin-bottom: calc(var(--base-margin, 1rem) * 2); /* Add space below title */ margin-bottom: calc(var(--base-margin, 1rem) * 1.5); /* Adjust space */
color: var(--text-color); /* Ensure title color */ padding-bottom: var(--base-margin, 1rem);
border-bottom: 1px solid var(--border-color);
font-size: 1.8rem;
color: var(--text-color);
} }
.loading-indicator, .error-message { .loading-indicator, .error-message, .alert {
margin-top: var(--base-margin, 1rem); /* 使用变量 */ margin-top: var(--base-margin, 1rem);
padding: var(--base-padding, 1rem);
border-radius: 6px; /* Consistent border radius */
text-align: center; text-align: center;
color: var(--text-color-secondary); /* 使用次要文本颜色 */ }
.loading-indicator {
color: var(--text-color-secondary);
font-style: italic;
} }
.error-message { .error-message {
color: var(--bs-danger); /* 保留特定错误颜色 */ color: #842029;
background-color: #f8d7da;
border: 1px solid #f5c2c7;
border-left-width: 4px;
} }
/* 表格容器,增加边框和圆角 */ /* Table container with shadow and border */
.table-container { .table-container {
border: 1px solid var(--border-color, #dee2e6); border: 1px solid var(--border-color, #dee2e6);
border-radius: 5px; border-radius: 8px; /* Match SettingsView */
overflow: hidden; /* Ensures border-radius clips table corners */ overflow: hidden; /* Clip table corners */
margin-top: var(--base-margin, 1rem); margin-top: var(--base-margin, 1rem);
background-color: var(--app-bg-color); /* Ensure background */ background-color: var(--content-bg-color, var(--app-bg-color)); /* Match SettingsView */
box-shadow: 0 2px 5px rgba(0,0,0,0.05); /* Match SettingsView */
overflow-x: auto; /* Allow horizontal scroll on small screens */
} }
/* 表格样式 */ /* Table base styles */
.table { .table {
width: 100%; width: 100%;
/* margin-top: var(--base-margin, 1rem); */ /* Moved margin to container */ border-collapse: collapse;
border-collapse: collapse; /* 移除单元格间距 */ color: var(--text-color);
/* background-color: var(--app-bg-color); */ /* Background on container */ border: none; /* Container handles border */
color: var(--text-color); /* 确保文本颜色 */ font-size: 0.95rem; /* Slightly larger font */
border: none; /* Remove table's own border if container has one */
} }
.table th, .table th,
.table td { .table td {
padding: 0.8rem 1rem; /* Slightly increase padding */ padding: 0.9rem 1.1rem; /* Adjust padding */
vertical-align: middle; /* Align vertically in the middle */ vertical-align: middle;
border-top: 1px solid var(--border-color, #dee2e6); /* 使用变量 */ border-top: 1px solid var(--border-color-light, var(--border-color)); /* Use lighter border */
text-align: left; /* 确保左对齐 */ text-align: left;
} }
/* Remove top border for the first row */ /* Remove top border for the first body row */
.table tbody tr:first-child td { .table tbody tr:first-child td {
border-top: none; border-top: none;
} }
/* Table header */
.table thead th { .table thead th {
padding: 0.8rem 1rem; /* Match cell padding */
vertical-align: bottom; vertical-align: bottom;
border-bottom: 2px solid var(--border-color, #dee2e6); /* 使用变量,加粗底部边框 */ border-bottom: 2px solid var(--border-color, #dee2e6);
border-top: none; /* No top border for header */ border-top: none;
background-color: var(--header-bg-color, #f8f9fa); /* 使用变量 */ background-color: var(--table-header-bg-color, var(--header-bg-color)); /* Use variable */
color: var(--text-color); /* 确保表头文本颜色 */ color: var(--table-header-text-color, var(--text-color)); /* Use variable */
font-weight: 600; /* Slightly less bold */ font-weight: 600;
white-space: nowrap; /* Prevent header text wrapping */ white-space: nowrap;
} }
/* 条纹样式 */ /* Striped rows */
.table-striped tbody tr:nth-of-type(odd) { .table-striped tbody tr:nth-of-type(odd) {
background-color: var(--header-bg-color, #f8f9fa); /* Use header bg for subtle stripe */ background-color: var(--table-stripe-bg-color, rgba(0,0,0,0.02)); /* More subtle stripe */
/* Ensure text color remains readable */
color: var(--text-color); color: var(--text-color);
} }
.table-striped tbody tr:nth-of-type(even) { .table-striped tbody tr:nth-of-type(even) {
background-color: var(--app-bg-color); /* Ensure even rows match app background */ background-color: transparent; /* Use container background */
} }
/* Hover effect */
/* 悬停样式 */
.table-hover tbody tr:hover { .table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.05); /* Subtle hover effect */ background-color: var(--table-hover-bg-color, rgba(0,0,0,0.04)); /* Subtle hover */
/* Or use a variable like --row-hover-bg-color */ cursor: default;
cursor: default; /* Indicate non-interactive rows */
} }
/* Details <pre> styling */
pre { pre {
white-space: pre-wrap; /* Allow wrapping */ white-space: pre-wrap;
word-break: break-all; /* Break long strings */ word-break: break-all;
background-color: var(--app-bg-color); /* Match app background */ background-color: var(--code-bg-color, var(--header-bg-color)); /* Use code background */
padding: calc(var(--base-padding, 0.5rem) * 0.8); /* Slightly smaller padding */ padding: 0.6rem 0.8rem; /* Adjust padding */
border: 1px solid var(--border-color, #dee2e6); /* 添加边框 */ border: 1px solid var(--border-color-light, var(--border-color));
border-radius: 4px; /* Consistent border radius */ border-radius: 5px; /* Consistent radius */
font-size: 0.85em; /* Slightly smaller font */ font-size: 0.88em; /* Adjust font size */
color: var(--text-color); /* 确保文本颜色 */ color: var(--code-text-color, var(--text-color)); /* Use code text color */
max-height: 150px; /* Limit height */ max-height: 180px; /* Increase max height slightly */
overflow-y: auto; /* Add scroll if needed */ overflow-y: auto;
margin: 0; /* Remove default margin */ margin: 0;
font-family: var(--font-family-monospace); /* Use monospace font */
} }
/* 分页样式 */ /* Pagination styling */
.pagination { .pagination {
margin-top: calc(var(--base-margin, 1rem) * 1.5); /* 使用变量 */ margin-top: calc(var(--base-margin, 1rem) * 2); /* Increase top margin */
justify-content: center; /* Ensure centered */
border: none;
} }
.page-item .page-link { .page-item .page-link {
color: var(--link-color, #007bff); /* 使用变量 */ color: var(--link-color, #007bff);
background-color: var(--app-bg-color); background-color: var(--content-bg-color, var(--app-bg-color)); /* Match container bg */
border: 1px solid var(--border-color, #dee2e6); border: 1px solid var(--border-color, #dee2e6);
margin: 0 2px; /* Add small horizontal margin */ margin: 0 3px; /* Adjust margin */
border-radius: 4px; /* Add border radius */ border-radius: 5px; /* Match other elements */
transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out, border-color 0.15s ease-in-out; /* Smooth transition */ padding: 0.4rem 0.8rem; /* Adjust padding */
transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
font-size: 0.9rem;
} }
.page-item.active .page-link { .page-item.active .page-link {
z-index: 3; z-index: 3;
color: var(--button-text-color, #fff); /* 使用变量 */ color: var(--button-text-color, #fff);
background-color: var(--button-bg-color, #007bff); /* 使用变量 */ background-color: var(--button-bg-color, #007bff);
border-color: var(--button-bg-color, #007bff); /* 使用变量 */ border-color: var(--button-bg-color, #007bff);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
} }
.page-item.disabled .page-link { .page-item.disabled .page-link {
color: var(--text-color-secondary, #6c757d); /* 使用变量 */ color: var(--text-color-secondary, #6c757d);
pointer-events: none; pointer-events: none;
background-color: var(--app-bg-color); background-color: var(--content-bg-color, var(--app-bg-color));
border-color: var(--border-color, #dee2e6); border-color: var(--border-color, #dee2e6);
opacity: 0.65; /* Indicate disabled state */ opacity: 0.6; /* Adjust opacity */
} }
.page-link:hover:not(.active) { /* Apply hover only if not active */ .page-link:hover { /* Combined hover for active/inactive */
color: var(--link-hover-color, #0056b3); /* 使用变量 */ z-index: 2;
background-color: var(--header-bg-color, #e9ecef); /* 使用变量 */ }
.page-item:not(.active) .page-link:hover {
color: var(--link-hover-color, #0056b3);
background-color: var(--header-bg-color, #e9ecef);
border-color: var(--border-color, #dee2e6); border-color: var(--border-color, #dee2e6);
} }
/* Remove border from the pagination container itself */ /* Alert Info styling */
.pagination {
border: none;
}
/* Alert 样式 */
.alert-info { .alert-info {
color: var(--text-color); /* 调整颜色使其更通用 */ color: var(--info-text-color, #0c5460); /* Specific info color */
background-color: var(--header-bg-color, #e9ecef); /* 使用变量 */ background-color: var(--info-bg-color, #d1ecf1); /* Specific info background */
border-color: var(--border-color, #dee2e6); /* 使用变量 */ border: 1px solid var(--info-border-color, #bee5eb); /* Specific info border */
padding: var(--base-padding, 1rem); border-left-width: 4px; /* Add left accent border */
margin-top: var(--base-margin, 1rem); text-align: left; /* Align text left */
border-radius: 0.25rem;
} }
.text-muted { .text-muted {
color: var(--text-color-secondary) !important; /* 确保覆盖 Bootstrap */ color: var(--text-color-secondary) !important;
font-size: 0.9rem;
} }
</style> </style>
+258 -200
View File
@@ -6,6 +6,9 @@
<div v-if="settingsLoading" class="loading-message">{{ $t('common.loading') }}</div> <div v-if="settingsLoading" class="loading-message">{{ $t('common.loading') }}</div>
<div v-if="settingsError" class="error-message">{{ settingsError }}</div> <div v-if="settingsError" class="error-message">{{ settingsError }}</div>
<div class="settings-grid">
<!-- Column 1: Security Settings -->
<div class="settings-column">
<div class="settings-section"> <div class="settings-section">
<h2>{{ $t('settings.changePassword.title') }}</h2> <h2>{{ $t('settings.changePassword.title') }}</h2>
<form @submit.prevent="handleChangePassword"> <form @submit.prevent="handleChangePassword">
@@ -26,52 +29,6 @@
</form> </form>
</div> </div>
<!-- 语言设置 -->
<div class="settings-section">
<h2>{{ $t('settings.language.title') }}</h2>
<form @submit.prevent="handleUpdateLanguage">
<div class="form-group">
<label for="languageSelect">{{ $t('settings.language.selectLabel') }}</label>
<select id="languageSelect" v-model="selectedLanguage" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
<button type="submit" :disabled="languageLoading">{{ languageLoading ? $t('common.saving') : $t('settings.language.saveButton') }}</button>
<p v-if="languageMessage" :class="{ 'success-message': languageSuccess, 'error-message': !languageSuccess }">{{ languageMessage }}</p>
</form>
</div>
<!-- 弹窗编辑器设置 -->
<div class="settings-section">
<h2>{{ $t('settings.popupEditor.title') }}</h2>
<form @submit.prevent="handleUpdatePopupEditorSetting">
<div class="form-group form-group-checkbox">
<input type="checkbox" id="showPopupEditor" v-model="popupEditorEnabled">
<label for="showPopupEditor">{{ $t('settings.popupEditor.enableLabel') }}</label>
</div>
<button type="submit" :disabled="popupEditorLoading">{{ popupEditorLoading ? $t('common.saving') : $t('settings.popupEditor.saveButton') }}</button>
<p v-if="popupEditorMessage" :class="{ 'success-message': popupEditorSuccess, 'error-message': !popupEditorSuccess }">{{ popupEditorMessage }}</p>
</form>
</div>
<!-- 共享编辑器标签页设置 -->
<div class="settings-section">
<h2>{{ $t('settings.shareEditorTabs.title') }}</h2>
<form @submit.prevent="handleUpdateShareTabsSetting">
<div class="form-group form-group-checkbox">
<input type="checkbox" id="shareEditorTabs" v-model="shareTabsEnabled">
<label for="shareEditorTabs">{{ $t('settings.shareEditorTabs.enableLabel') }}</label>
</div>
<p class="setting-description">{{ $t('settings.shareEditorTabs.description') }}</p>
<button type="submit" :disabled="shareTabsLoading">{{ shareTabsLoading ? $t('common.saving') : $t('settings.shareEditorTabs.saveButton') }}</button>
<p v-if="shareTabsMessage" :class="{ 'success-message': shareTabsSuccess, 'error-message': !shareTabsSuccess }">{{ shareTabsMessage }}</p>
</form>
</div>
<hr>
<div class="settings-section"> <div class="settings-section">
<h2>Passkey 设置</h2> <h2>Passkey 设置</h2>
<p>使用 Passkey无密码认证提升安全性和便捷性您可以注册新的 Passkey 用于登录</p> <p>使用 Passkey无密码认证提升安全性和便捷性您可以注册新的 Passkey 用于登录</p>
@@ -86,8 +43,7 @@
<div class="settings-section"> <div class="settings-section">
<h2>{{ $t('settings.twoFactor.title') }}</h2> <h2>{{ $t('settings.twoFactor.title') }}</h2>
<!-- 2FA Content remains the same -->
<!-- 如果 2FA 已启用 -->
<div v-if="twoFactorEnabled"> <div v-if="twoFactorEnabled">
<p class="success-message">{{ $t('settings.twoFactor.status.enabled') }}</p> <p class="success-message">{{ $t('settings.twoFactor.status.enabled') }}</p>
<form @submit.prevent="handleDisable2FA"> <form @submit.prevent="handleDisable2FA">
@@ -98,16 +54,11 @@
<button type="submit" :disabled="twoFactorLoading">{{ twoFactorLoading ? $t('common.loading') : $t('settings.twoFactor.disable.button') }}</button> <button type="submit" :disabled="twoFactorLoading">{{ twoFactorLoading ? $t('common.loading') : $t('settings.twoFactor.disable.button') }}</button>
</form> </form>
</div> </div>
<!-- 如果 2FA 未启用 -->
<div v-else> <div v-else>
<p>{{ $t('settings.twoFactor.status.disabled') }}</p> <p>{{ $t('settings.twoFactor.status.disabled') }}</p>
<!-- 如果不在设置流程中显示启用按钮 -->
<button v-if="!isSettingUp2FA" @click="handleSetup2FA" :disabled="twoFactorLoading"> <button v-if="!isSettingUp2FA" @click="handleSetup2FA" :disabled="twoFactorLoading">
{{ twoFactorLoading ? $t('common.loading') : $t('settings.twoFactor.enable.button') }} {{ twoFactorLoading ? $t('common.loading') : $t('settings.twoFactor.enable.button') }}
</button> </button>
<!-- 如果正在设置中 -->
<div v-if="isSettingUp2FA && setupData"> <div v-if="isSettingUp2FA && setupData">
<p>{{ $t('settings.twoFactor.setup.scanQrCode') }}</p> <p>{{ $t('settings.twoFactor.setup.scanQrCode') }}</p>
<img :src="setupData.qrCodeUrl" alt="QR Code"> <img :src="setupData.qrCodeUrl" alt="QR Code">
@@ -118,16 +69,61 @@
<input type="text" id="verificationCode" v-model="verificationCode" required pattern="\d{6}" title="请输入 6 位数字验证码"> <input type="text" id="verificationCode" v-model="verificationCode" required pattern="\d{6}" title="请输入 6 位数字验证码">
</div> </div>
<button type="submit" :disabled="twoFactorLoading">{{ twoFactorLoading ? $t('common.loading') : $t('settings.twoFactor.setup.verifyButton') }}</button> <button type="submit" :disabled="twoFactorLoading">{{ twoFactorLoading ? $t('common.loading') : $t('settings.twoFactor.setup.verifyButton') }}</button>
<button type="button" @click="cancelSetup" :disabled="twoFactorLoading" style="margin-left: 10px;">{{ $t('common.cancel') }}</button> <button type="button" @click="cancelSetup" :disabled="twoFactorLoading" class="btn-secondary" style="margin-left: 10px;">{{ $t('common.cancel') }}</button>
</form> </form>
</div> </div>
</div> </div>
<!-- 显示 2FA 操作的消息 -->
<p v-if="twoFactorMessage" :class="{ 'success-message': twoFactorSuccess, 'error-message': !twoFactorSuccess }">{{ twoFactorMessage }}</p> <p v-if="twoFactorMessage" :class="{ 'success-message': twoFactorSuccess, 'error-message': !twoFactorSuccess }">{{ twoFactorMessage }}</p>
</div> </div>
</div>
<hr> <!-- Column 2: Interface & Network Settings -->
<div class="settings-column">
<div class="settings-section">
<h2>{{ $t('settings.language.title') }}</h2>
<form @submit.prevent="handleUpdateLanguage">
<div class="form-group">
<label for="languageSelect">{{ $t('settings.language.selectLabel') }}</label>
<select id="languageSelect" v-model="selectedLanguage">
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
<button type="submit" :disabled="languageLoading">{{ languageLoading ? $t('common.saving') : $t('settings.language.saveButton') }}</button>
<p v-if="languageMessage" :class="{ 'success-message': languageSuccess, 'error-message': !languageSuccess }">{{ languageMessage }}</p>
</form>
</div>
<div class="settings-section">
<h2>{{ $t('settings.appearance.title') }}</h2>
<p>{{ $t('settings.appearance.description') }}</p>
<button @click="openStyleCustomizer">{{ t('settings.appearance.customizeButton') }}</button>
</div>
<div class="settings-section">
<h2>{{ $t('settings.popupEditor.title') }}</h2>
<form @submit.prevent="handleUpdatePopupEditorSetting">
<div class="form-group form-group-checkbox">
<input type="checkbox" id="showPopupEditor" v-model="popupEditorEnabled">
<label for="showPopupEditor">{{ $t('settings.popupEditor.enableLabel') }}</label>
</div>
<button type="submit" :disabled="popupEditorLoading">{{ popupEditorLoading ? $t('common.saving') : $t('settings.popupEditor.saveButton') }}</button>
<p v-if="popupEditorMessage" :class="{ 'success-message': popupEditorSuccess, 'error-message': !popupEditorSuccess }">{{ popupEditorMessage }}</p>
</form>
</div>
<div class="settings-section">
<h2>{{ $t('settings.shareEditorTabs.title') }}</h2>
<form @submit.prevent="handleUpdateShareTabsSetting">
<div class="form-group form-group-checkbox">
<input type="checkbox" id="shareEditorTabs" v-model="shareTabsEnabled">
<label for="shareEditorTabs">{{ $t('settings.shareEditorTabs.enableLabel') }}</label>
</div>
<p class="setting-description">{{ $t('settings.shareEditorTabs.description') }}</p>
<button type="submit" :disabled="shareTabsLoading">{{ shareTabsLoading ? $t('common.saving') : $t('settings.shareEditorTabs.saveButton') }}</button>
<p v-if="shareTabsMessage" :class="{ 'success-message': shareTabsSuccess, 'error-message': !shareTabsSuccess }">{{ shareTabsMessage }}</p>
</form>
</div>
<div class="settings-section"> <div class="settings-section">
<h2>{{ $t('settings.ipWhitelist.title') }}</h2> <h2>{{ $t('settings.ipWhitelist.title') }}</h2>
@@ -135,25 +131,17 @@
<form @submit.prevent="handleUpdateIpWhitelist"> <form @submit.prevent="handleUpdateIpWhitelist">
<div class="form-group"> <div class="form-group">
<label for="ipWhitelist">{{ $t('settings.ipWhitelist.label') }}</label> <label for="ipWhitelist">{{ $t('settings.ipWhitelist.label') }}</label>
<textarea id="ipWhitelist" v-model="ipWhitelistInput" rows="5"></textarea> <textarea id="ipWhitelist" v-model="ipWhitelistInput" rows="4"></textarea> <!-- Reduced rows -->
<small>{{ $t('settings.ipWhitelist.hint') }}</small> <small>{{ $t('settings.ipWhitelist.hint') }}</small>
</div> </div>
<button type="submit" :disabled="ipWhitelistLoading">{{ ipWhitelistLoading ? $t('common.saving') : $t('settings.ipWhitelist.saveButton') }}</button> <button type="submit" :disabled="ipWhitelistLoading">{{ ipWhitelistLoading ? $t('common.saving') : $t('settings.ipWhitelist.saveButton') }}</button>
<p v-if="ipWhitelistMessage" :class="{ 'success-message': ipWhitelistSuccess, 'error-message': !ipWhitelistSuccess }">{{ ipWhitelistMessage }}</p> <p v-if="ipWhitelistMessage" :class="{ 'success-message': ipWhitelistSuccess, 'error-message': !ipWhitelistSuccess }">{{ ipWhitelistMessage }}</p>
</form> </form>
</div> </div>
<!-- 外观设置 -->
<div class="settings-section">
<h2>{{ $t('settings.appearance.title') }}</h2>
<p>{{ $t('settings.appearance.description') }}</p>
<button @click="openStyleCustomizer">{{ t('settings.appearance.customizeButton') }}</button>
</div> </div>
<!-- Column 3: IP Blacklist (Spans across columns if needed, or stays in its own area) -->
<hr> <div class="settings-section settings-section-full-width">
<div class="settings-section">
<h2>IP 黑名单管理</h2> <h2>IP 黑名单管理</h2>
<p>配置登录失败次数限制和自动封禁时长本地地址 (127.0.0.1, ::1) 不会被封禁</p> <p>配置登录失败次数限制和自动封禁时长本地地址 (127.0.0.1, ::1) 不会被封禁</p>
@@ -171,13 +159,13 @@
<p v-if="blacklistSettingsMessage" :class="{ 'success-message': blacklistSettingsSuccess, 'error-message': !blacklistSettingsSuccess }">{{ blacklistSettingsMessage }}</p> <p v-if="blacklistSettingsMessage" :class="{ 'success-message': blacklistSettingsSuccess, 'error-message': !blacklistSettingsSuccess }">{{ blacklistSettingsMessage }}</p>
</form> </form>
<hr style="margin-top: 20px; margin-bottom: 20px;"> <hr class="section-divider">
<h3>当前已封禁的 IP 地址</h3> <h3>当前已封禁的 IP 地址</h3>
<div v-if="ipBlacklist.loading" class="loading-message">正在加载黑名单...</div> <div v-if="ipBlacklist.loading" class="loading-message">正在加载黑名单...</div>
<div v-if="ipBlacklist.error" class="error-message">{{ ipBlacklist.error }}</div> <div v-if="ipBlacklist.error" class="error-message">{{ ipBlacklist.error }}</div>
<div v-if="!ipBlacklist.loading && !ipBlacklist.error"> <div v-if="!ipBlacklist.loading && !ipBlacklist.error" class="table-container">
<table v-if="ipBlacklist.entries.length > 0" class="blacklist-table"> <table v-if="ipBlacklist.entries.length > 0" class="blacklist-table">
<thead> <thead>
<tr> <tr>
@@ -198,7 +186,7 @@
<button <button
@click="handleDeleteIp(entry.ip)" @click="handleDeleteIp(entry.ip)"
:disabled="blacklistDeleteLoading && blacklistToDeleteIp === entry.ip" :disabled="blacklistDeleteLoading && blacklistToDeleteIp === entry.ip"
class="btn-danger" class="btn-danger btn-small"
> >
{{ (blacklistDeleteLoading && blacklistToDeleteIp === entry.ip) ? '删除中...' : '移除' }} {{ (blacklistDeleteLoading && blacklistToDeleteIp === entry.ip) ? '删除中...' : '移除' }}
</button> </button>
@@ -210,7 +198,7 @@
<p v-if="blacklistDeleteError" class="error-message">{{ blacklistDeleteError }}</p> <p v-if="blacklistDeleteError" class="error-message">{{ blacklistDeleteError }}</p>
</div> </div>
</div> </div>
</div> <!-- End of settings-grid -->
</div> </div>
</template> </template>
@@ -577,99 +565,125 @@ onMounted(async () => {
padding: var(--base-padding); padding: var(--base-padding);
color: var(--text-color); color: var(--text-color);
background-color: var(--app-bg-color); background-color: var(--app-bg-color);
max-width: 1200px; /* 限制最大宽度 */
margin: 0 auto; /* 居中 */
} }
h1 { h1 {
margin-bottom: calc(var(--base-margin) * 3); margin-bottom: calc(var(--base-margin) * 2); /* 减小标题下边距 */
padding-bottom: var(--base-margin); padding-bottom: var(--base-margin);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
font-size: 1.8rem; /* 稍大标题 */
color: var(--text-color);
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); /* 响应式网格布局 */
gap: calc(var(--base-margin) * 2); /* 网格间距 */
}
.settings-column {
display: flex;
flex-direction: column;
gap: calc(var(--base-margin) * 2); /* 列内项目间距 */
} }
.settings-section { .settings-section {
margin-bottom: calc(var(--base-margin) * 4); /* 增加区域间距 */ /* margin-bottom: calc(var(--base-margin) * 2); 移除独立下边距,由 grid gap 控制 */
padding: calc(var(--base-padding) * 1.5); /* 增加区域内边距 */ padding: calc(var(--base-padding) * 1.2); /* 调整内边距 */
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 6px; /* 圆润的边角 */ border-radius: 8px; /* 圆润的边角 */
background-color: var(--app-bg-color); background-color: var(--content-bg-color, var(--app-bg-color)); /* 使用内容背景色,回退到应用背景色 */
/* box-shadow: 0 1px 3px rgba(0,0,0,0.05); */ /* 可选:添加微阴影 */ box-shadow: 0 2px 5px rgba(0,0,0,0.05); /* 添加微阴影 */
display: flex; /* 使 section 内部元素可以更好地控制 */
flex-direction: column; /* 默认垂直排列 */
height: 100%; /* 让 section 填充 grid 单元格高度 */
}
.settings-section-full-width {
grid-column: 1 / -1; /* 让黑名单部分横跨所有列 */
} }
.settings-section h2 { .settings-section h2 {
font-size: 1.3rem; font-size: 1.2rem; /* 调整标题大小 */
color: var(--text-color); color: var(--text-color);
margin-top: 0; margin-top: 0;
margin-bottom: calc(var(--base-margin) * 2); margin-bottom: var(--base-margin); /* 减小标题下边距 */
padding-bottom: var(--base-margin); padding-bottom: calc(var(--base-margin) * 0.75);
border-bottom: 1px dashed var(--border-color); /* 虚线分隔 */ border-bottom: 1px solid var(--border-color-light, var(--border-color)); /* 使用更浅的边框色 */
} }
.settings-section p:not([class*="-message"]) { /* 普通段落样式 */ .settings-section p:not([class*="-message"]) {
color: var(--text-color-secondary); color: var(--text-color-secondary);
line-height: 1.6; line-height: 1.6;
margin-bottom: var(--base-margin); margin-bottom: var(--base-margin);
font-size: 0.95rem; /* 调整段落字体大小 */
} }
.setting-description { /* 特定描述文本 */ .setting-description {
font-size: 0.9em; font-size: 0.85em; /* 调整描述字体大小 */
color: var(--text-color-secondary); color: var(--text-color-secondary);
margin-bottom: var(--base-margin); margin-bottom: var(--base-margin);
} }
.form-group { .form-group {
margin-bottom: calc(var(--base-margin) * 1.5); margin-bottom: var(--base-margin); /* 减小表单组间距 */
} }
label { label {
display: block; display: block;
margin-bottom: calc(var(--base-margin) / 2); margin-bottom: calc(var(--base-margin) / 3); /* 减小标签下边距 */
font-weight: bold; font-weight: 600; /* 稍粗字体 */
color: var(--text-color); color: var(--text-color);
font-size: 0.9rem; /* 调整标签字体大小 */
} }
input[type="password"], input[type="password"],
input[type="text"], input[type="text"],
input[type="number"], /* 添加 number 类型 */ input[type="number"],
textarea, textarea,
select { select {
width: 100%; width: 100%;
padding: 0.6rem 0.8rem; /* 调整内边距 */ padding: 0.5rem 0.7rem; /* 调整内边距 */
box-sizing: border-box; box-sizing: border-box;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 4px; border-radius: 5px; /* 调整圆角 */
font-family: var(--font-family-sans-serif); font-family: var(--font-family-sans-serif);
font-size: 1rem; font-size: 0.95rem; /* 调整输入框字体大小 */
color: var(--text-color); color: var(--text-color);
background-color: var(--app-bg-color); background-color: var(--input-bg-color, var(--app-bg-color)); /* 输入框背景色 */
transition: border-color 0.2s ease; transition: border-color 0.2s ease, box-shadow 0.2s ease;
} }
input:focus, textarea:focus, select:focus { input:focus, textarea:focus, select:focus {
border-color: var(--link-active-color); /* 聚焦时高亮边框 */ border-color: var(--link-active-color);
outline: 0; outline: 0;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15); /* 细微的聚焦阴影 */ box-shadow: 0 0 0 3px rgba(var(--rgb-link-active-color, 0, 123, 255), 0.2); /* 使用变量颜色 */
} }
textarea { textarea {
resize: vertical; resize: vertical;
min-height: 100px; min-height: 80px; /* 减小最小高度 */
} }
small { small {
display: block; display: block;
margin-top: calc(var(--base-margin) / 2); margin-top: calc(var(--base-margin) / 3);
font-size: 0.85em; font-size: 0.8em; /* 调整提示字体大小 */
color: var(--text-color-secondary); color: var(--text-color-secondary);
} }
button, .btn { /* 统一按钮样式 */ button, .btn {
padding: 0.6rem 1.2rem; /* 调整按钮内边距 */ padding: 0.5rem 1rem; /* 调整按钮内边距 */
cursor: pointer; cursor: pointer;
background-color: var(--button-bg-color); background-color: var(--button-bg-color);
color: var(--button-text-color); color: var(--button-text-color);
border: 1px solid var(--button-bg-color); /* 添加边框 */ border: 1px solid transparent; /* 默认透明边框 */
border-radius: 4px; border-radius: 5px; /* 调整圆角 */
font-weight: bold; font-weight: 600; /* 稍粗字体 */
transition: background-color 0.2s ease, border-color 0.2s ease; transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.1s ease;
margin-right: var(--base-margin); /* 按钮间距 */ margin-right: calc(var(--base-margin) / 2); /* 减小按钮间距 */
font-size: 0.95rem; /* 调整按钮字体大小 */
line-height: 1.5; /* 确保文字垂直居中 */
} }
button:last-of-type, .btn:last-of-type { button:last-of-type, .btn:last-of-type {
margin-right: 0; margin-right: 0;
@@ -678,139 +692,159 @@ button:last-of-type, .btn:last-of-type {
button:hover:not(:disabled), .btn:hover:not(:disabled) { button:hover:not(:disabled), .btn:hover:not(:disabled) {
background-color: var(--button-hover-bg-color); background-color: var(--button-hover-bg-color);
border-color: var(--button-hover-bg-color); border-color: var(--button-hover-bg-color);
transform: translateY(-1px); /* 轻微上移效果 */
}
button:active:not(:disabled), .btn:active:not(:disabled) {
transform: translateY(0px); /* 按下时复原 */
} }
button:disabled, .btn:disabled { button:disabled, .btn:disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.6; opacity: 0.65; /* 调整禁用透明度 */
} }
/* 次要按钮样式 (例如取消按钮) */ /* 次要按钮样式 */
button[type="button"], .btn-secondary { .btn-secondary {
background-color: var(--header-bg-color); background-color: var(--secondary-button-bg-color, var(--header-bg-color));
color: var(--text-color); color: var(--secondary-button-text-color, var(--text-color));
border-color: var(--border-color); border: 1px solid var(--border-color);
} }
button[type="button"]:hover:not(:disabled), .btn-secondary:hover:not(:disabled) { .btn-secondary:hover:not(:disabled) {
background-color: var(--border-color); background-color: var(--secondary-button-hover-bg-color, var(--border-color));
border-color: var(--border-color); border-color: var(--border-color);
} }
hr { /* 危险按钮样式 (用于移除黑名单) */
.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-danger:disabled {
background-color: var(--danger-color, #dc3545);
opacity: 0.65;
}
.btn-small { /* 小按钮 */
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
}
hr.section-divider { /* 区域内分隔线 */
border: none; border: none;
border-top: 1px solid var(--border-color); border-top: 1px dashed var(--border-color-light, var(--border-color));
margin: calc(var(--base-margin) * 4) 0; /* 增加分隔线间距 */ margin: var(--base-margin) 0;
} }
code { code {
background-color: var(--header-bg-color); background-color: var(--code-bg-color, var(--header-bg-color));
padding: 0.2em 0.4em; padding: 0.2em 0.5em;
border-radius: 3px; border-radius: 4px;
color: var(--text-color); color: var(--code-text-color, var(--text-color));
font-family: monospace; font-family: var(--font-family-monospace);
font-size: 0.9em; font-size: 0.9em;
border: 1px solid var(--border-color-light, var(--border-color));
} }
img { img { /* 二维码图片 */
display: block; display: block;
margin: var(--base-margin) auto; /* 居中显示 */ margin: var(--base-margin) auto;
max-width: 200px; max-width: 180px; /* 调整大小 */
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
padding: 5px; padding: 4px;
background-color: white; /* 确保二维码背景是白色 */ background-color: white;
border-radius: 4px;
} }
/* 消息提示样式优化 */
.success-message, .error-message {
padding: 0.8rem 1rem; /* 调整内边距 */
border-radius: 5px;
margin-top: var(--base-margin);
font-size: 0.9rem;
border-left-width: 4px; /* 左侧加粗边框 */
}
.success-message { .success-message {
color: #198754; /* Bootstrap success color */ color: #0f5132;
background-color: #d1e7dd; /* Light green background */ background-color: #d1e7dd;
border: 1px solid #a3cfbb; border-color: #badbcc;
padding: var(--base-padding); border-left-color: #0f5132;
border-radius: 4px;
margin-top: var(--base-margin);
} }
.error-message { .error-message {
color: #842029; /* Bootstrap danger color */ color: #842029;
background-color: #f8d7da; /* Light red background */ background-color: #f8d7da;
border: 1px solid #f5c2c7; border-color: #f5c2c7;
padding: var(--base-padding); border-left-color: #842029;
border-radius: 4px;
margin-top: var(--base-margin);
} }
.loading-message { .loading-message {
margin-top: var(--base-margin); margin-top: var(--base-margin);
color: var(--text-color-secondary); color: var(--text-color-secondary);
font-style: italic; font-style: italic;
font-size: 0.9rem;
} }
/* Blacklist Table Styles */ /* 黑名单表格样式优化 */
.table-container {
overflow-x: auto; /* 允许水平滚动 */
margin-top: var(--base-margin);
}
.blacklist-table { .blacklist-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-top: var(--base-padding); font-size: 0.9rem; /* 调整表格字体 */
font-size: 0.95rem; white-space: nowrap; /* 防止单元格内容换行 */
} }
.blacklist-table th, .blacklist-table th,
.blacklist-table td { .blacklist-table td {
border: 1px solid var(--border-color); border: 1px solid var(--border-color-light, var(--border-color));
padding: 0.75rem; /* 调整单元格内边距 */ padding: 0.6rem 0.8rem; /* 调整单元格内边距 */
text-align: left; text-align: left;
vertical-align: middle; vertical-align: middle;
} }
.blacklist-table th { .blacklist-table th {
background-color: var(--header-bg-color); background-color: var(--table-header-bg-color, var(--header-bg-color));
font-weight: bold; font-weight: 600;
color: var(--text-color); color: var(--table-header-text-color, var(--text-color));
} }
.blacklist-table tr:nth-child(even) { .blacklist-table tr:nth-child(even) {
background-color: var(--header-bg-color); /* 斑马纹 */ background-color: var(--table-stripe-bg-color, var(--header-bg-color));
}
.blacklist-table tr:hover {
background-color: var(--table-hover-bg-color, rgba(0,0,0,0.03)); /* 悬停高亮 */
} }
.blacklist-table .btn-danger { /* 复选框组样式优化 */
background-color: #dc3545;
color: var(--button-text-color);
border: none;
padding: 0.3rem 0.6rem; /* 调整小按钮内边距 */
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.blacklist-table .btn-danger:disabled {
background-color: #f8d7da;
cursor: not-allowed;
opacity: 0.7;
}
/* 复选框组样式 */
.form-group-checkbox { .form-group-checkbox {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: var(--base-margin); /* 确保与其他组间距一致 */ margin-bottom: var(--base-margin);
cursor: pointer; /* 让整个区域可点击 */
} }
.form-group-checkbox input[type="checkbox"] { .form-group-checkbox input[type="checkbox"] {
width: auto; width: 1.2em; /* 增大复选框 */
margin-right: 0.6rem; /* 调整复选框和标签间距 */ height: 1.2em;
flex-shrink: 0; /* 防止复选框被压缩 */ margin-right: 0.7rem;
appearance: none; /* 自定义样式 */ flex-shrink: 0;
background-color: var(--app-bg-color); appearance: none;
background-color: var(--input-bg-color, var(--app-bg-color));
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
width: 1.1em; border-radius: 4px; /* 调整圆角 */
height: 1.1em;
border-radius: 3px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
top: 2px; /* 微调垂直对齐 */ top: 0; /* 移除微调 */
transition: background-color 0.2s ease, border-color 0.2s ease;
} }
.form-group-checkbox input[type="checkbox"]:checked { .form-group-checkbox input[type="checkbox"]:checked {
background-color: var(--button-bg-color); background-color: var(--button-bg-color);
border-color: var(--button-bg-color); border-color: var(--button-bg-color);
} }
/* 添加勾选标记 */
.form-group-checkbox input[type="checkbox"]:checked::after { .form-group-checkbox input[type="checkbox"]:checked::after {
content: '✔'; content: '✔';
position: absolute; position: absolute;
@@ -818,44 +852,68 @@ img {
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
font-size: 0.8em; font-size: 0.85em; /* 调整勾选标记大小 */
font-weight: bold;
}
.form-group-checkbox input[type="checkbox"]:focus {
box-shadow: 0 0 0 3px rgba(var(--rgb-link-active-color, 0, 123, 255), 0.2);
} }
.form-group-checkbox label { .form-group-checkbox label {
display: inline-block; display: inline-block;
margin-bottom: 0; margin-bottom: 0;
cursor: pointer; cursor: pointer;
font-weight: normal; /* 普通标签字体重量 */ font-weight: normal;
font-size: 0.95rem; /* 调整标签字体 */
user-select: none; /* 防止选中文字 */
} }
/* 黑名单配置表单样式优化 */
/* Blacklist Settings Form Styles */
.blacklist-settings-form { .blacklist-settings-form {
margin-top: var(--base-padding); margin-top: var(--base-margin);
padding-top: var(--base-padding); padding-top: var(--base-margin);
border-top: 1px dashed var(--border-color); border-top: 1px dashed var(--border-color-light, var(--border-color));
display: flex; /* 使用 Flex 布局 */
flex-wrap: wrap; /* 允许换行 */
align-items: flex-end; /* 底部对齐 */
gap: var(--base-margin); /* 项目间距 */
} }
.blacklist-settings-form .inline-group { .blacklist-settings-form .inline-group {
display: inline-flex; /* 使用 flex 布局 */ display: flex;
align-items: center; /* 垂直居中 */ flex-direction: column; /* 垂直排列标签和输入框 */
margin-right: calc(var(--base-margin) * 2); margin: 0; /* 移除独立 margin */
margin-bottom: var(--base-margin);
} }
.blacklist-settings-form .inline-group label { .blacklist-settings-form .inline-group label {
margin-right: var(--base-margin); margin-bottom: calc(var(--base-margin) / 4); /* 减小小间距 */
margin-bottom: 0; /* 移除 label 下边距 */ white-space: nowrap;
white-space: nowrap; /* 防止标签换行 */
} }
.blacklist-settings-form .inline-group input[type="number"] { .blacklist-settings-form .inline-group input[type="number"] {
width: 80px; /* 固定宽度 */ width: 100px; /* 调整宽度 */
padding: 0.5rem; /* 调整内边距 */ padding: 0.4rem 0.6rem; /* 调整内边距 */
} }
.blacklist-settings-form button { .blacklist-settings-form button {
vertical-align: middle; /* 尝试对齐按钮 */ margin-left: auto; /* 将按钮推到右侧(如果空间允许) */
align-self: flex-end; /* 确保按钮在底部 */
} }
.blacklist-settings-form p { .blacklist-settings-form p { /* 消息提示 */
margin-top: var(--base-margin); margin-top: 0; /* 移除顶部间距 */
width: 100%; /* 占满整行 */
order: 3; /* 确保消息在最后 */
}
/* 响应式调整 */
@media (max-width: 900px) {
.settings-grid {
grid-template-columns: 1fr; /* 在较小屏幕上变为单列 */
}
.blacklist-settings-form {
flex-direction: column; /* 强制垂直排列 */
align-items: stretch; /* 拉伸项目 */
}
.blacklist-settings-form button {
margin-left: 0; /* 移除左外边距 */
width: 100%; /* 按钮占满宽度 */
margin-top: var(--base-margin); /* 添加顶部间距 */
}
} }
</style> </style>