feat: redesign system configuration workspace
This commit is contained in:
@@ -6,7 +6,6 @@ import AdminLoginView from "./components/AdminLoginView.vue";
|
||||
import TableRowActions from "./components/TableRowActions.vue";
|
||||
import {
|
||||
systemSettingDefinitions,
|
||||
systemSettingSections,
|
||||
type SystemSettingValue,
|
||||
} from "./config/systemSettings";
|
||||
import { api, clearToken, getToken, saveToken } from "./services/api";
|
||||
@@ -47,6 +46,9 @@ const UserManagementView = defineAsyncComponent(
|
||||
const SsoIntegrationView = defineAsyncComponent(
|
||||
() => import("./components/SsoIntegrationView.vue"),
|
||||
);
|
||||
const SystemConfigView = defineAsyncComponent(
|
||||
() => import("./components/SystemConfigView.vue"),
|
||||
);
|
||||
|
||||
const admin = ref<AdminProfile | null>(null);
|
||||
const activeMenu = ref("dashboard");
|
||||
@@ -55,6 +57,8 @@ const loading = ref(false);
|
||||
const entitlementPlans = ref<EntitlementPlan[]>([]);
|
||||
const models = ref<ModelItem[]>([]);
|
||||
const configs = ref<SystemConfigItem[]>([]);
|
||||
const savingSystemSettings = ref(false);
|
||||
const systemSettingsBaseline = ref<Record<string, string>>({});
|
||||
const previewKnowledgeId = ref<number | null>(null);
|
||||
const editingModelId = ref<number | null>(null);
|
||||
|
||||
@@ -174,6 +178,13 @@ const systemSettingValues = reactive<Record<string, SystemSettingValue>>({});
|
||||
systemSettingDefinitions.forEach((setting) => {
|
||||
systemSettingValues[setting.key] = setting.defaultValue;
|
||||
});
|
||||
const systemSettingsDirty = computed(() =>
|
||||
systemSettingDefinitions.some(
|
||||
(setting) =>
|
||||
serializeSystemSettingValue(systemSettingValues[setting.key]) !==
|
||||
systemSettingsBaseline.value[setting.key],
|
||||
),
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
if (!getToken()) return;
|
||||
@@ -490,20 +501,30 @@ function validateModelForm() {
|
||||
}
|
||||
|
||||
async function saveSystemSettings() {
|
||||
await Promise.all(
|
||||
systemSettingDefinitions.map((setting) =>
|
||||
api.saveConfig({
|
||||
configKey: setting.key,
|
||||
configValue: serializeSystemSettingValue(
|
||||
systemSettingValues[setting.key],
|
||||
),
|
||||
description: setting.description,
|
||||
}),
|
||||
),
|
||||
);
|
||||
ElMessage.success("系统配置已保存");
|
||||
configs.value = await api.configs();
|
||||
hydrateSystemSettings();
|
||||
if (savingSystemSettings.value || !systemSettingsDirty.value) return;
|
||||
savingSystemSettings.value = true;
|
||||
try {
|
||||
await Promise.all(
|
||||
systemSettingDefinitions.map((setting) =>
|
||||
api.saveConfig({
|
||||
configKey: setting.key,
|
||||
configValue: serializeSystemSettingValue(
|
||||
systemSettingValues[setting.key],
|
||||
),
|
||||
description: setting.description,
|
||||
}),
|
||||
),
|
||||
);
|
||||
configs.value = await api.configs();
|
||||
hydrateSystemSettings();
|
||||
ElMessage.success("系统配置已保存");
|
||||
} catch (error) {
|
||||
ElMessage.error(
|
||||
error instanceof Error ? error.message : "系统配置保存失败,请重试",
|
||||
);
|
||||
} finally {
|
||||
savingSystemSettings.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateSystemSettings() {
|
||||
@@ -516,6 +537,17 @@ function hydrateSystemSettings() {
|
||||
configMap.get(setting.key),
|
||||
);
|
||||
});
|
||||
systemSettingsBaseline.value = Object.fromEntries(
|
||||
systemSettingDefinitions.map((setting) => [
|
||||
setting.key,
|
||||
serializeSystemSettingValue(systemSettingValues[setting.key]),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function updateSystemSetting(key: string, value: SystemSettingValue) {
|
||||
if (systemSettingValues[key] === value) return;
|
||||
systemSettingValues[key] = value;
|
||||
}
|
||||
|
||||
function normalizeSystemSettingValue(
|
||||
@@ -1014,81 +1046,14 @@ async function clearFeishuCache() {
|
||||
|
||||
<SsoIntegrationView v-if="activeMenu === 'sso'" />
|
||||
|
||||
<template v-if="activeMenu === 'configs'">
|
||||
<div class="page-head inline">
|
||||
<div>
|
||||
<h2>系统配置</h2>
|
||||
<p>
|
||||
按业务分组维护运行时配置,默认每日额度已接入学员新增和导入。
|
||||
</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="saveSystemSettings"
|
||||
>保存系统配置</el-button
|
||||
>
|
||||
</div>
|
||||
<section class="settings-grid">
|
||||
<div
|
||||
v-for="section in systemSettingSections"
|
||||
:key="section.title"
|
||||
class="settings-section"
|
||||
>
|
||||
<div class="settings-section-head">
|
||||
<h3>{{ section.title }}</h3>
|
||||
<p>{{ section.description }}</p>
|
||||
</div>
|
||||
<div class="settings-fields">
|
||||
<label
|
||||
v-for="setting in section.settings"
|
||||
:key="setting.key"
|
||||
class="setting-field"
|
||||
>
|
||||
<span>{{ setting.label }}</span>
|
||||
<el-input-number
|
||||
v-if="setting.type === 'number'"
|
||||
v-model="systemSettingValues[setting.key]"
|
||||
:min="setting.min"
|
||||
:max="setting.max"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="setting.type === 'text'"
|
||||
v-model="systemSettingValues[setting.key]"
|
||||
:placeholder="setting.placeholder"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="setting.type === 'password'"
|
||||
v-model="systemSettingValues[setting.key]"
|
||||
:placeholder="setting.placeholder"
|
||||
type="password"
|
||||
show-password
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="setting.type === 'select'"
|
||||
v-model="systemSettingValues[setting.key]"
|
||||
:placeholder="setting.placeholder"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in setting.options || []"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-switch
|
||||
v-else
|
||||
v-model="systemSettingValues[setting.key]"
|
||||
/>
|
||||
<small>{{ setting.description }}</small>
|
||||
<small
|
||||
v-if="setting.valueHint"
|
||||
class="setting-value-hint"
|
||||
>
|
||||
{{ setting.valueHint(systemSettingValues[setting.key]) }}
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<SystemConfigView
|
||||
v-if="activeMenu === 'configs'"
|
||||
:values="systemSettingValues"
|
||||
:saving="savingSystemSettings"
|
||||
:dirty="systemSettingsDirty"
|
||||
@update="updateSystemSetting"
|
||||
@save="saveSystemSettings"
|
||||
/>
|
||||
|
||||
<RetrievalLogView v-if="activeMenu === 'retrievals'" />
|
||||
<AttentionManagementView v-if="activeMenu === 'attention'" />
|
||||
|
||||
@@ -0,0 +1,797 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import {
|
||||
systemSettingSections,
|
||||
type SystemSettingDefinition,
|
||||
type SystemSettingValue,
|
||||
} from "../config/systemSettings";
|
||||
|
||||
interface SettingGroupBlueprint {
|
||||
title: string;
|
||||
description: string;
|
||||
keys: string[];
|
||||
}
|
||||
|
||||
interface SettingGroup extends Omit<SettingGroupBlueprint, "keys"> {
|
||||
settings: SystemSettingDefinition[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
values: Record<string, SystemSettingValue>;
|
||||
saving: boolean;
|
||||
dirty: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [];
|
||||
update: [key: string, value: SystemSettingValue];
|
||||
}>();
|
||||
|
||||
const groupBlueprints: Record<string, SettingGroupBlueprint[]> = {
|
||||
用户与登录: [
|
||||
{
|
||||
title: "学员默认配置",
|
||||
description: "新学员创建、导入时采用的默认资料和额度。",
|
||||
keys: ["daily_chat_limit", "default_user_name_prefix"],
|
||||
},
|
||||
{
|
||||
title: "登录与验证码",
|
||||
description: "控制登录状态和验证码的有效时间。",
|
||||
keys: ["access_token_expire_minutes", "sms_code_expire_minutes"],
|
||||
},
|
||||
{
|
||||
title: "短信服务",
|
||||
description: "在模拟短信与阿里云短信之间切换,并维护短信通道参数。",
|
||||
keys: [
|
||||
"mock_sms_enabled",
|
||||
"mock_sms_code",
|
||||
"aliyun_sms_access_key_id",
|
||||
"aliyun_sms_access_key_secret",
|
||||
"aliyun_sms_sign_name",
|
||||
"aliyun_sms_template_code",
|
||||
"aliyun_sms_template_param_key",
|
||||
"aliyun_sms_endpoint",
|
||||
],
|
||||
},
|
||||
],
|
||||
"AI 问答": [
|
||||
{
|
||||
title: "回答体验",
|
||||
description: "控制会话记忆、引用展示和模型请求的基本行为。",
|
||||
keys: [
|
||||
"ai_timeout_seconds",
|
||||
"mock_model_enabled",
|
||||
"chat_context_message_count",
|
||||
"show_reference_sources",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "模型分流",
|
||||
description: "决定正式问答在不同问题类型下如何选择模型。",
|
||||
keys: ["chat_model_routing_mode"],
|
||||
},
|
||||
{
|
||||
title: "并发与排队",
|
||||
description: "控制集中提问时的执行上限、等待容量和超时保护。",
|
||||
keys: [
|
||||
"chat_max_active_requests",
|
||||
"chat_max_queue_size",
|
||||
"chat_queue_timeout_seconds",
|
||||
"chat_active_lease_seconds",
|
||||
],
|
||||
},
|
||||
],
|
||||
外部服务: [
|
||||
{
|
||||
title: "飞书接入",
|
||||
description: "配置飞书知识库直读和独立检索服务。",
|
||||
keys: ["feishu_search_url", "feishu_app_id", "feishu_app_secret"],
|
||||
},
|
||||
{
|
||||
title: "稳定性策略",
|
||||
description: "控制飞书接口慢响应和临时失败时的处理方式。",
|
||||
keys: ["feishu_timeout_seconds", "feishu_retry_count"],
|
||||
},
|
||||
],
|
||||
系统开关: [
|
||||
{
|
||||
title: "审计与数据",
|
||||
description: "控制后台审计信息和聊天记录导出能力。",
|
||||
keys: ["show_operation_log_detail", "chat_export_enabled"],
|
||||
},
|
||||
{
|
||||
title: "运行状态",
|
||||
description: "用于系统维护期间的访问和提示策略。",
|
||||
keys: ["maintenance_mode"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const wideSettingKeys = new Set([
|
||||
"chat_model_routing_mode",
|
||||
"chat_max_active_requests",
|
||||
"feishu_search_url",
|
||||
]);
|
||||
|
||||
const activeSectionTitle = ref(systemSettingSections[0]?.title || "");
|
||||
|
||||
const activeSection = computed(
|
||||
() =>
|
||||
systemSettingSections.find(
|
||||
(section) => section.title === activeSectionTitle.value,
|
||||
) || systemSettingSections[0],
|
||||
);
|
||||
|
||||
const activeGroups = computed<SettingGroup[]>(() => {
|
||||
const section = activeSection.value;
|
||||
if (!section) return [];
|
||||
|
||||
const byKey = new Map(section.settings.map((setting) => [setting.key, setting]));
|
||||
const configuredKeys = new Set<string>();
|
||||
const groups = (groupBlueprints[section.title] || []).map((group) => {
|
||||
const settings = group.keys
|
||||
.map((key) => byKey.get(key))
|
||||
.filter((setting): setting is SystemSettingDefinition => Boolean(setting));
|
||||
settings.forEach((setting) => configuredKeys.add(setting.key));
|
||||
return { title: group.title, description: group.description, settings };
|
||||
});
|
||||
|
||||
const ungrouped = section.settings.filter(
|
||||
(setting) => !configuredKeys.has(setting.key),
|
||||
);
|
||||
if (ungrouped.length) {
|
||||
groups.push({
|
||||
title: "其他设置",
|
||||
description: "尚未归入固定分组的新配置项。",
|
||||
settings: ungrouped,
|
||||
});
|
||||
}
|
||||
return groups.filter((group) => group.settings.length);
|
||||
});
|
||||
|
||||
const statusItems = computed(() => [
|
||||
{
|
||||
label: "短信通道",
|
||||
value: props.values.mock_sms_enabled ? "模拟验证码" : "阿里云短信",
|
||||
tone: props.values.mock_sms_enabled ? "warning" : "success",
|
||||
},
|
||||
{
|
||||
label: "AI 模式",
|
||||
value: props.values.mock_model_enabled ? "模拟模型" : "真实模型",
|
||||
tone: props.values.mock_model_enabled ? "warning" : "success",
|
||||
},
|
||||
{
|
||||
label: "会话记忆",
|
||||
value:
|
||||
Number(props.values.chat_context_message_count) > 0
|
||||
? `最近 ${props.values.chat_context_message_count} 条`
|
||||
: "已关闭",
|
||||
tone:
|
||||
Number(props.values.chat_context_message_count) > 0 ? "neutral" : "warning",
|
||||
},
|
||||
{
|
||||
label: "问答容量",
|
||||
value: `${props.values.chat_max_active_requests || 0} 并发`,
|
||||
tone: "neutral",
|
||||
},
|
||||
]);
|
||||
|
||||
function updateSetting(key: string, value: unknown) {
|
||||
if (value === undefined || value === null) return;
|
||||
emit("update", key, value as SystemSettingValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="system-config-page">
|
||||
<header class="system-config-header">
|
||||
<div>
|
||||
<div class="system-config-title-line">
|
||||
<h2>系统配置</h2>
|
||||
<span :class="['save-state', { dirty }]">
|
||||
{{ dirty ? "有未保存修改" : "配置已同步" }}
|
||||
</span>
|
||||
</div>
|
||||
<p>集中维护学员登录、AI 问答、飞书接入和系统运行策略。</p>
|
||||
</div>
|
||||
<div class="system-config-actions">
|
||||
<small>保存后立即对后续请求生效</small>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
:disabled="!dirty"
|
||||
@click="emit('save')"
|
||||
>
|
||||
保存修改
|
||||
</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="config-status-strip" aria-label="当前关键配置摘要">
|
||||
<div
|
||||
v-for="item in statusItems"
|
||||
:key="item.label"
|
||||
class="config-status-item"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<strong :class="`tone-${item.tone}`">{{ item.value }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="config-workspace">
|
||||
<nav class="config-section-nav" aria-label="系统配置分组">
|
||||
<div class="config-nav-heading">
|
||||
<strong>配置分类</strong>
|
||||
<span>选择一个分类集中编辑</span>
|
||||
</div>
|
||||
<button
|
||||
v-for="section in systemSettingSections"
|
||||
:key="section.title"
|
||||
type="button"
|
||||
:class="{ active: activeSectionTitle === section.title }"
|
||||
@click="activeSectionTitle = section.title"
|
||||
>
|
||||
<span>
|
||||
<strong>{{ section.title }}</strong>
|
||||
<small>{{ section.description }}</small>
|
||||
</span>
|
||||
<b>{{ section.settings.length }}</b>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<section v-if="activeSection" class="config-editor-panel">
|
||||
<header class="config-editor-heading">
|
||||
<div>
|
||||
<span>当前分类</span>
|
||||
<h3>{{ activeSection.title }}</h3>
|
||||
<p>{{ activeSection.description }}</p>
|
||||
</div>
|
||||
<strong>{{ activeSection.settings.length }} 项配置</strong>
|
||||
</header>
|
||||
|
||||
<div class="config-groups">
|
||||
<article
|
||||
v-for="group in activeGroups"
|
||||
:key="group.title"
|
||||
class="config-group-card"
|
||||
>
|
||||
<header>
|
||||
<h4>{{ group.title }}</h4>
|
||||
<p>{{ group.description }}</p>
|
||||
</header>
|
||||
|
||||
<div class="config-fields-grid">
|
||||
<div
|
||||
v-for="setting in group.settings"
|
||||
:key="setting.key"
|
||||
:class="[
|
||||
'config-field',
|
||||
`config-field-${setting.type}`,
|
||||
{ wide: wideSettingKeys.has(setting.key) },
|
||||
]"
|
||||
>
|
||||
<template v-if="setting.type === 'switch'">
|
||||
<div class="config-switch-copy">
|
||||
<strong>{{ setting.label }}</strong>
|
||||
<small>{{ setting.description }}</small>
|
||||
</div>
|
||||
<el-switch
|
||||
:model-value="Boolean(values[setting.key])"
|
||||
@update:model-value="updateSetting(setting.key, $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<label :for="`setting-${setting.key}`">
|
||||
<strong>{{ setting.label }}</strong>
|
||||
<span v-if="setting.type === 'password'">敏感配置</span>
|
||||
</label>
|
||||
<el-input-number
|
||||
v-if="setting.type === 'number'"
|
||||
:id="`setting-${setting.key}`"
|
||||
:model-value="Number(values[setting.key])"
|
||||
:min="setting.min"
|
||||
:max="setting.max"
|
||||
controls-position="right"
|
||||
@update:model-value="updateSetting(setting.key, $event)"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="setting.type === 'text'"
|
||||
:id="`setting-${setting.key}`"
|
||||
:model-value="String(values[setting.key] ?? '')"
|
||||
:placeholder="setting.placeholder"
|
||||
clearable
|
||||
@update:model-value="updateSetting(setting.key, $event)"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="setting.type === 'password'"
|
||||
:id="`setting-${setting.key}`"
|
||||
:model-value="String(values[setting.key] ?? '')"
|
||||
:placeholder="setting.placeholder"
|
||||
type="password"
|
||||
show-password
|
||||
@update:model-value="updateSetting(setting.key, $event)"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="setting.type === 'select'"
|
||||
:id="`setting-${setting.key}`"
|
||||
:model-value="String(values[setting.key] ?? '')"
|
||||
:placeholder="setting.placeholder"
|
||||
@update:model-value="updateSetting(setting.key, $event)"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in setting.options || []"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
<small>{{ setting.description }}</small>
|
||||
<small
|
||||
v-if="setting.valueHint"
|
||||
class="config-value-hint"
|
||||
>
|
||||
{{ setting.valueHint(values[setting.key]) }}
|
||||
</small>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.system-config-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.system-config-header {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.system-config-title-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.system-config-title-line h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.system-config-header p {
|
||||
margin: 7px 0 0;
|
||||
color: #687a75;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.save-state {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 9px;
|
||||
border-radius: 999px;
|
||||
background: #eaf3f0;
|
||||
color: #55716a;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.save-state::before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #62a38d;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.save-state.dirty {
|
||||
background: #fff4df;
|
||||
color: #95651b;
|
||||
}
|
||||
|
||||
.save-state.dirty::before {
|
||||
background: #d79a37;
|
||||
}
|
||||
|
||||
.system-config-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.system-config-actions small {
|
||||
color: #83918d;
|
||||
}
|
||||
|
||||
.system-config-actions .el-button {
|
||||
min-width: 104px;
|
||||
}
|
||||
|
||||
.config-status-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
overflow: hidden;
|
||||
border: 1px solid #dfe9e5;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.config-status-item {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
padding: 15px 18px;
|
||||
}
|
||||
|
||||
.config-status-item + .config-status-item {
|
||||
border-left: 1px solid #e6eeeb;
|
||||
}
|
||||
|
||||
.config-status-item span {
|
||||
color: #7a8b86;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-status-item strong {
|
||||
overflow: hidden;
|
||||
color: #2d463f;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-status-item .tone-success {
|
||||
color: #197653;
|
||||
}
|
||||
|
||||
.config-status-item .tone-warning {
|
||||
color: #a26919;
|
||||
}
|
||||
|
||||
.config-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: 248px minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-section-nav,
|
||||
.config-editor-panel {
|
||||
border: 1px solid #dfe8e5;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.config-section-nav {
|
||||
position: sticky;
|
||||
top: 18px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.config-nav-heading {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 5px 7px 11px;
|
||||
}
|
||||
|
||||
.config-nav-heading strong {
|
||||
color: #29423b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.config-nav-heading span {
|
||||
color: #879691;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-section-nav button {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 11px;
|
||||
background: transparent;
|
||||
color: #49605a;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s ease, background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.config-section-nav button:hover {
|
||||
background: #f5f8f7;
|
||||
}
|
||||
|
||||
.config-section-nav button.active {
|
||||
border-color: #bcded2;
|
||||
background: #edf7f3;
|
||||
color: #176d53;
|
||||
}
|
||||
|
||||
.config-section-nav button > span {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-section-nav button strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.config-section-nav button small {
|
||||
overflow: hidden;
|
||||
color: #82908c;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-section-nav button b {
|
||||
min-width: 26px;
|
||||
padding: 4px 7px;
|
||||
border-radius: 999px;
|
||||
background: #eef3f1;
|
||||
color: #71817c;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.config-section-nav button.active b {
|
||||
background: #d9eee7;
|
||||
color: #176d53;
|
||||
}
|
||||
|
||||
.config-editor-panel {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.config-editor-heading {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 22px 24px;
|
||||
border-bottom: 1px solid #e5ecea;
|
||||
background: linear-gradient(135deg, #fbfdfc, #f4f9f7);
|
||||
}
|
||||
|
||||
.config-editor-heading > div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-editor-heading span {
|
||||
color: #248064;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.config-editor-heading h3 {
|
||||
margin: 6px 0 0;
|
||||
color: #203b33;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.config-editor-heading p {
|
||||
margin: 7px 0 0;
|
||||
color: #70827d;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.config-editor-heading > strong {
|
||||
flex: 0 0 auto;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: #e7f2ee;
|
||||
color: #4d6d64;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-groups {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.config-group-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid #e1eae7;
|
||||
border-radius: 13px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.config-group-card > header {
|
||||
padding: 15px 18px;
|
||||
border-bottom: 1px solid #e8efed;
|
||||
background: #fafcfb;
|
||||
}
|
||||
|
||||
.config-group-card h4 {
|
||||
margin: 0;
|
||||
color: #2c453e;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.config-group-card header p {
|
||||
margin: 5px 0 0;
|
||||
color: #7b8c87;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.config-fields-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
padding: 4px 18px 18px;
|
||||
}
|
||||
|
||||
.config-field {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 15px 16px 2px 0;
|
||||
}
|
||||
|
||||
.config-field:nth-child(even):not(.wide) {
|
||||
padding-right: 0;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid #edf2f0;
|
||||
}
|
||||
|
||||
.config-field.wide {
|
||||
grid-column: 1 / -1;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.config-field label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-field label strong,
|
||||
.config-switch-copy strong {
|
||||
color: #324841;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.config-field label span {
|
||||
flex: 0 0 auto;
|
||||
padding: 3px 7px;
|
||||
border-radius: 999px;
|
||||
background: #fff1ed;
|
||||
color: #a4503f;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.config-field > small,
|
||||
.config-switch-copy small {
|
||||
color: #7b8985;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.config-field :deep(.el-input-number),
|
||||
.config-field :deep(.el-input),
|
||||
.config-field :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-field :deep(.el-input__wrapper),
|
||||
.config-field :deep(.el-select__wrapper),
|
||||
.config-field :deep(.el-input-number) {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.config-field-switch {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
min-height: 70px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.config-switch-copy {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-value-hint {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #cde4db;
|
||||
border-radius: 9px;
|
||||
background: #f2f9f6;
|
||||
color: #2c6e58 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1320px) {
|
||||
.config-workspace {
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.config-status-strip {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.config-status-item:nth-child(3) {
|
||||
border-left: 0;
|
||||
border-top: 1px solid #e6eeeb;
|
||||
}
|
||||
|
||||
.config-status-item:nth-child(4) {
|
||||
border-top: 1px solid #e6eeeb;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.system-config-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.system-config-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.config-workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.config-section-nav {
|
||||
position: static;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.config-nav-heading {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.config-status-strip,
|
||||
.config-section-nav,
|
||||
.config-fields-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.config-status-item + .config-status-item,
|
||||
.config-status-item:nth-child(3),
|
||||
.config-status-item:nth-child(4) {
|
||||
border-top: 1px solid #e6eeeb;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.config-field,
|
||||
.config-field:nth-child(even):not(.wide) {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1678,67 +1678,6 @@ textarea {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
padding: 18px;
|
||||
border: 1px solid #dfe8e5;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.settings-section-head h3 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.settings-section-head p {
|
||||
margin: 6px 0 0;
|
||||
color: #667a73;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.settings-fields {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.setting-field {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.setting-field > span {
|
||||
color: #223631;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.setting-field > small {
|
||||
color: #667a73;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.setting-field > .setting-value-hint {
|
||||
padding: 9px 11px;
|
||||
border: 1px solid #cde6dc;
|
||||
border-radius: 8px;
|
||||
background: #f3faf7;
|
||||
color: #246b52;
|
||||
}
|
||||
|
||||
.setting-field .el-input-number,
|
||||
.setting-field .el-input,
|
||||
.setting-field .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-drawer .el-date-editor.el-input {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -2343,8 +2282,7 @@ textarea {
|
||||
@media (max-width: 1120px) {
|
||||
.student-tools,
|
||||
.user-list-toolbar,
|
||||
.agent-workbench,
|
||||
.settings-grid {
|
||||
.agent-workbench {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user