You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/ui/page/sys/settings.html

737 lines
24 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta name="description" content="System Settings Management">
<title>{{ $t('nav.settings') }}</title>
<style>
.settings-container {
padding: 20px;
max-width: 900px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: var(--color-text, #1f2937);
margin: 0;
}
.settings-card {
background: var(--bg-color-secondary, #fff);
border-radius: var(--border-radius, 8px);
box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.05));
margin-bottom: 20px;
overflow: hidden;
}
.card-header {
background: var(--bg-color-tertiary, #f9fafb);
padding: 16px 20px;
border-bottom: 1px solid var(--color-border, #e5e7eb);
font-weight: 600;
font-size: 16px;
color: var(--color-text, #1f2937);
display: flex;
align-items: center;
gap: 8px;
}
.card-header i {
color: var(--color-primary, #4f46e5);
}
.card-body {
padding: 20px;
}
.setting-item {
display: flex;
align-items: flex-start;
padding: 16px 0;
border-bottom: 1px solid var(--color-border, #e5e7eb);
}
.setting-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.setting-item:first-child {
padding-top: 0;
}
.setting-info {
flex: 1;
margin-right: 20px;
}
.setting-label {
font-weight: 500;
color: var(--color-text, #1f2937);
margin-bottom: 4px;
}
.setting-desc {
font-size: 13px;
color: var(--color-text-light, #6b7280);
}
.setting-control {
min-width: 200px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.setting-input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--color-border, #d1d5db);
border-radius: var(--border-radius, 6px);
font-size: 14px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.setting-input:focus {
outline: none;
border-color: var(--color-primary, #4f46e5);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
}
.setting-select {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--color-border, #d1d5db);
border-radius: var(--border-radius, 6px);
font-size: 14px;
background-color: var(--bg-color-secondary, #fff);
cursor: pointer;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 48px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: var(--color-primary, #4f46e5);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(24px);
}
.actions-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--color-border, #e5e7eb);
}
.btn {
padding: 10px 20px;
border-radius: var(--border-radius, 6px);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-primary {
background-color: var(--color-primary, #4f46e5);
color: white;
}
.btn-primary:hover {
background-color: var(--color-primary-dark, #4338ca);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background-color: var(--bg-color-tertiary, #f3f4f6);
color: var(--color-text, #1f2937);
border: 1px solid var(--color-border, #d1d5db);
}
.btn-secondary:hover {
background-color: var(--bg-color-secondary, #e5e7eb);
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-border, #e5e7eb);
border-top-color: var(--color-primary, #4f46e5);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.json-editor {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
min-height: 80px;
resize: vertical;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: var(--color-primary-light, #e0e7ff);
color: var(--color-primary, #4f46e5);
border-radius: 4px;
font-size: 13px;
}
.tag-remove {
cursor: pointer;
opacity: 0.6;
}
.tag-remove:hover {
opacity: 1;
}
.tag-input {
border: none;
background: transparent;
padding: 4px 8px;
font-size: 13px;
min-width: 100px;
}
.tag-input:focus {
outline: none;
}
</style>
</head>
<body>
<div class="settings-container">
<div class="page-header">
<h1 class="page-title">{{ $t('nav.settings') }}</h1>
</div>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
</div>
<!-- Application Settings -->
<div class="settings-card">
<div class="card-header">
<i class="fa-solid fa-cube"></i>
{{ $t('settings.category.app') }}
</div>
<div class="card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.app.name') }}</div>
<div class="setting-desc">{{ $t('settings.app.name_desc') }}</div>
</div>
<div class="setting-control">
<input type="text" class="setting-input" v:value="settings['app.name']" />
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.app.id') }}</div>
<div class="setting-desc">{{ $t('settings.app.id_desc') }}</div>
</div>
<div class="setting-control">
<input type="text" class="setting-input" v:value="settings['app.id']" />
</div>
</div>
</div>
</div>
<!-- Authentication Settings -->
<div class="settings-card">
<div class="card-header">
<i class="fa-solid fa-shield-halved"></i>
{{ $t('settings.category.auth') }}
</div>
<div class="card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.auth.reg_require_email') }}</div>
<div class="setting-desc">{{ $t('settings.auth.reg_require_email_desc') }}</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" :checked="settings['auth.reg.require_email'] === 'true'" @change="toggleSetting('auth.reg.require_email')" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.auth.reg_require_phone') }}</div>
<div class="setting-desc">{{ $t('settings.auth.reg_require_phone_desc') }}</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" :checked="settings['auth.reg.require_phone'] === 'true'" @change="toggleSetting('auth.reg.require_phone')" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.auth.login_methods') }}</div>
<div class="setting-desc">{{ $t('settings.auth.login_methods_desc') }}</div>
</div>
<div class="setting-control">
<input type="text" class="setting-input json-editor" v:value="settings['auth.login.methods']" placeholder='["password", "email_code"]' />
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.auth.password_fields') }}</div>
<div class="setting-desc">{{ $t('settings.auth.password_fields_desc') }}</div>
</div>
<div class="setting-control">
<input type="text" class="setting-input json-editor" v:value="settings['auth.login.password_fields']" placeholder='["username", "email"]' />
</div>
</div>
</div>
</div>
<!-- Security Settings -->
<div class="settings-card">
<div class="card-header">
<i class="fa-solid fa-lock"></i>
{{ $t('settings.category.security') }}
</div>
<div class="card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.security.captcha_enabled') }}</div>
<div class="setting-desc">{{ $t('settings.security.captcha_enabled_desc') }}</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" :checked="settings['security.captcha_enabled'] === 'true'" @change="toggleSetting('security.captcha_enabled')" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.security.max_login_attempts') }}</div>
<div class="setting-desc">{{ $t('settings.security.max_login_attempts_desc') }}</div>
</div>
<div class="setting-control">
<input type="number" class="setting-input" v:value="settings['security.max_login_attempts']" />
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.security.bcrypt_cost') }}</div>
<div class="setting-desc">{{ $t('settings.security.bcrypt_cost_desc') }}</div>
</div>
<div class="setting-control">
<input type="number" class="setting-input" v:value="settings['security.bcrypt_cost']" min="4" max="31" />
</div>
</div>
</div>
</div>
<!-- Verification Code Settings -->
<div class="settings-card">
<div class="card-header">
<i class="fa-solid fa-key"></i>
{{ $t('settings.category.code') }}
</div>
<div class="card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.code.expiry') }}</div>
<div class="setting-desc">{{ $t('settings.code.expiry_desc') }}</div>
</div>
<div class="setting-control">
<input type="number" class="setting-input" v:value="settings['code.expiry']" />
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.code.length') }}</div>
<div class="setting-desc">{{ $t('settings.code.length_desc') }}</div>
</div>
<div class="setting-control">
<input type="number" class="setting-input" v:value="settings['code.length']" />
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.code.max_attempt') }}</div>
<div class="setting-desc">{{ $t('settings.code.max_attempt_desc') }}</div>
</div>
<div class="setting-control">
<input type="number" class="setting-input" v:value="settings['code.max_attempt']" />
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.code.send_interval') }}</div>
<div class="setting-desc">{{ $t('settings.code.send_interval_desc') }}</div>
</div>
<div class="setting-control">
<input type="number" class="setting-input" v:value="settings['code.send_interval']" />
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.code.max_daily_count') }}</div>
<div class="setting-desc">{{ $t('settings.code.max_daily_count_desc') }}</div>
</div>
<div class="setting-control">
<input type="number" class="setting-input" v:value="settings['code.max_daily_count']" />
</div>
</div>
</div>
</div>
<!-- Email Settings -->
<div class="settings-card">
<div class="card-header">
<i class="fa-solid fa-envelope"></i>
{{ $t('settings.category.email') }}
</div>
<div class="card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.email.enabled') }}</div>
<div class="setting-desc">{{ $t('settings.email.enabled_desc') }}</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" :checked="settings['email.enabled'] === 'true'" @change="toggleSetting('email.enabled')" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.email.provider') }}</div>
<div class="setting-desc">{{ $t('settings.email.provider_desc') }}</div>
</div>
<div class="setting-control">
<select class="setting-select" v:value="settings['email.provider']">
<option value="smtp">SMTP</option>
<option value="sendgrid">SendGrid</option>
</select>
</div>
</div>
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.email.smtp_host') }}</div>
<div class="setting-desc">{{ $t('settings.email.smtp_host_desc') }}</div>
</div>
<div class="setting-control">
<input type="text" class="setting-input" v:value="settings['email.smtp.host']" />
</div>
</div>
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.email.smtp_port') }}</div>
<div class="setting-desc">{{ $t('settings.email.smtp_port_desc') }}</div>
</div>
<div class="setting-control">
<input type="number" class="setting-input" v:value="settings['email.smtp.port']" />
</div>
</div>
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.email.smtp_user') }}</div>
<div class="setting-desc">{{ $t('settings.email.smtp_user_desc') }}</div>
</div>
<div class="setting-control">
<input type="text" class="setting-input" v:value="settings['email.smtp.user']" />
</div>
</div>
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.email.smtp_pass') }}</div>
<div class="setting-desc">{{ $t('settings.email.smtp_pass_desc') }}</div>
</div>
<div class="setting-control">
<input type="password" class="setting-input" v:value="settings['email.smtp.pass']" placeholder="******" />
</div>
</div>
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.email.from') }}</div>
<div class="setting-desc">{{ $t('settings.email.from_desc') }}</div>
</div>
<div class="setting-control">
<input type="email" class="setting-input" v:value="settings['email.from']" />
</div>
</div>
<div class="setting-item" v-if="settings['email.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.email.from_name') }}</div>
<div class="setting-desc">{{ $t('settings.email.from_name_desc') }}</div>
</div>
<div class="setting-control">
<input type="text" class="setting-input" v:value="settings['email.from_name']" />
</div>
</div>
</div>
</div>
<!-- SMS Settings -->
<div class="settings-card">
<div class="card-header">
<i class="fa-solid fa-comment-sms"></i>
{{ $t('settings.category.sms') }}
</div>
<div class="card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.sms.enabled') }}</div>
<div class="setting-desc">{{ $t('settings.sms.enabled_desc') }}</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" :checked="settings['sms.enabled'] === 'true'" @change="toggleSetting('sms.enabled')" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item" v-if="settings['sms.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.sms.provider') }}</div>
<div class="setting-desc">{{ $t('settings.sms.provider_desc') }}</div>
</div>
<div class="setting-control">
<select class="setting-select" v:value="settings['sms.provider']">
<option value="aliyun">{{ $t('settings.sms.provider_aliyun') }}</option>
<option value="tencent">{{ $t('settings.sms.provider_tencent') }}</option>
</select>
</div>
</div>
<div class="setting-item" v-if="settings['sms.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.sms.access_key') }}</div>
<div class="setting-desc">{{ $t('settings.sms.access_key_desc') }}</div>
</div>
<div class="setting-control">
<input type="text" class="setting-input" v:value="settings['sms.access_key']" />
</div>
</div>
<div class="setting-item" v-if="settings['sms.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.sms.access_secret') }}</div>
<div class="setting-desc">{{ $t('settings.sms.access_secret_desc') }}</div>
</div>
<div class="setting-control">
<input type="password" class="setting-input" v:value="settings['sms.access_secret']" placeholder="******" />
</div>
</div>
<div class="setting-item" v-if="settings['sms.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.sms.sign_name') }}</div>
<div class="setting-desc">{{ $t('settings.sms.sign_name_desc') }}</div>
</div>
<div class="setting-control">
<input type="text" class="setting-input" v:value="settings['sms.sign_name']" />
</div>
</div>
<div class="setting-item" v-if="settings['sms.enabled'] === 'true'">
<div class="setting-info">
<div class="setting-label">{{ $t('settings.sms.template_code') }}</div>
<div class="setting-desc">{{ $t('settings.sms.template_code_desc') }}</div>
</div>
<div class="setting-control">
<input type="text" class="setting-input" v:value="settings['sms.template_code']" />
</div>
</div>
</div>
</div>
<div class="actions-bar">
<button class="btn btn-secondary" @click="resetSettings">{{ $t('common.reset') }}</button>
<button class="btn btn-primary" @click="saveSettings" :disabled="saving">
{{ saving ? $t('common.saving') : $t('common.save') }}
</button>
</div>
</div>
</body>
<script setup>
loading = true;
saving = false;
settings = {};
originalSettings = {};
// Setting keys to manage
settingKeys = [
'app.name',
'app.id',
'auth.reg.require_email',
'auth.reg.require_phone',
'auth.login.methods',
'auth.login.password_fields',
'security.captcha_enabled',
'security.max_login_attempts',
'security.bcrypt_cost',
'code.expiry',
'code.length',
'code.max_attempt',
'code.send_interval',
'code.max_daily_count',
'email.enabled',
'email.provider',
'email.smtp.host',
'email.smtp.port',
'email.smtp.user',
'email.smtp.pass',
'email.from',
'email.from_name',
'sms.enabled',
'sms.provider',
'sms.access_key',
'sms.access_secret',
'sms.sign_name',
'sms.template_code'
];
// Load settings from API
loadSettings = async () => {
loading = true;
try {
const response = await $axios.get('/api/settings');
if (response && response.items) {
const settingsMap = {};
response.items.forEach(item => {
settingsMap[item.key] = item.value;
});
settings = settingsMap;
originalSettings = { ...settingsMap };
}
} catch (error) {
$message.error(error.message || $t('settings.load_failed'));
} finally {
loading = false;
}
};
// Toggle boolean setting
toggleSetting = (key) => {
settings[key] = settings[key] === 'true' ? 'false' : 'true';
};
// Reset to original values
resetSettings = () => {
settings = { ...originalSettings };
$message.info($t('settings.reset_done'));
};
// Save all settings
saveSettings = async () => {
saving = true;
try {
const settingsToUpdate = [];
settingKeys.forEach(key => {
if (settings[key] !== undefined && settings[key] !== originalSettings[key]) {
settingsToUpdate.push({
key: key,
value: String(settings[key])
});
}
});
if (settingsToUpdate.length === 0) {
$message.info($t('settings.no_changes'));
saving = false;
return;
}
await $axios.put('/api/settings', { settings: settingsToUpdate });
originalSettings = { ...settings };
$message.success($t('settings.save_success'));
} catch (error) {
$message.error(error.message || $t('settings.save_failed'));
} finally {
saving = false;
}
};
// Initialize
loadSettings();
</script>
</html>