Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/model/Model.vue

1164 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import {computed, onMounted, ref} from 'vue';
import {$t} from '@easyflow/locales';
import {Delete, Edit, Plus, Select, Setting} from '@element-plus/icons-vue';
import {
ElButton,
ElEmpty,
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElMessageBox,
ElTag,
} from 'element-plus';
import {getLlmProviderList} from '#/api/ai/llm.js';
import {api} from '#/api/request.js';
import ListPageShell from '#/components/page/ListPageShell.vue';
import ActiveModelWorkspace from '#/views/ai/model/ActiveModelWorkspace.vue';
import AddModelModal from '#/views/ai/model/AddModelModal.vue';
import AddModelProviderModal from '#/views/ai/model/AddModelProviderModal.vue';
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
import {getProviderPresetByValue} from '#/views/ai/model/modelUtils/defaultIcon';
import {modelTypes} from '#/views/ai/model/modelUtils/modelTypes';
import {
createProviderDraft,
getProviderConfigMetrics,
isProviderDraftDirty,
} from '#/views/ai/model/modelUtils/providerDraft';
import ModelVerifyConfig from '#/views/ai/model/ModelVerifyConfig.vue';
import ModelViewItemOperation from '#/views/ai/model/ModelViewItemOperation.vue';
type ModelWorkspaceView = 'active' | 'provider';
interface ModelGroup {
groupName: string;
llmList: any[];
}
const providers = ref<any[]>([]);
const providerCounts = ref<Record<string, number>>({});
const selectedProviderId = ref('');
const activeModelType = ref('allModel');
const workspaceView = ref<ModelWorkspaceView>('provider');
const showProviderAdvanced = ref(false);
const providerDraft = ref(createProviderDraft());
const groupedModels = ref<Record<string, ModelGroup[]>>({
chatModel: [],
embeddingModel: [],
rerankModel: [],
});
const isPageLoading = ref(false);
const isDetailLoading = ref(false);
const isSaveLoading = ref(false);
const addLlmProviderRef = ref();
const llmVerifyConfigRef = ref();
const addLlmRef = ref();
const activeWorkspaceRef = ref();
const selectedProvider = computed(() =>
providers.value.find((item) => item.id === selectedProviderId.value),
);
const selectedPreset = computed(() =>
getProviderPresetByValue(selectedProvider.value?.providerType),
);
const workspaceModelTypes = [
{
label: '全部模型',
value: 'allModel',
},
...modelTypes,
];
const currentModelGroups = computed(() => {
if (activeModelType.value !== 'allModel') {
return groupedModels.value[activeModelType.value] || [];
}
const mergedGroups = new Map<string, any[]>();
for (const modelType of ['chatModel', 'embeddingModel', 'rerankModel']) {
for (const group of groupedModels.value[modelType] || []) {
if (!mergedGroups.has(group.groupName)) {
mergedGroups.set(group.groupName, []);
}
mergedGroups.get(group.groupName)?.push(...group.llmList);
}
}
return [...mergedGroups.entries()].map(([groupName, llmList]) => ({
groupName,
llmList,
}));
});
const currentModelTypeLabel = computed(() =>
activeModelType.value === 'allModel'
? '全部模型'
: modelTypes.find((item) => item.value === activeModelType.value)?.label ||
$t('llmProvider.chatModel'),
);
const actionModelType = computed(() =>
activeModelType.value === 'allModel' ? 'chatModel' : activeModelType.value,
);
const selectedProviderTypeLabel = computed(
() =>
selectedPreset.value?.label || selectedProvider.value?.providerType || '-',
);
const isProviderDirty = computed(() =>
isProviderDraftDirty(selectedProvider.value, providerDraft.value),
);
const currentProviderMetrics = computed(() =>
getProviderConfigMetrics(
{
...selectedProvider.value,
...providerDraft.value,
},
selectedPreset.value,
),
);
const toGroupList = (modelMap: Record<string, any[]>) =>
Object.entries(modelMap || {}).map(([groupName, llmList]) => ({
groupName,
llmList,
}));
const countModels = (data: any) => {
let total = 0;
for (const key of ['chatModel', 'embeddingModel', 'rerankModel']) {
const groups = Object.values(data?.[key] || {}) as any[][];
for (const items of groups) {
total += items.length;
}
}
return total;
};
const syncProviderDraft = (provider?: any) => {
providerDraft.value = createProviderDraft(provider);
};
const syncGroupedModels = (data: any) => {
groupedModels.value = {
chatModel: toGroupList(data?.chatModel || {}),
embeddingModel: toGroupList(data?.embeddingModel || {}),
rerankModel: toGroupList(data?.rerankModel || {}),
};
};
const confirmDiscardProviderDraft = async () => {
if (!isProviderDirty.value) {
return true;
}
try {
await ElMessageBox.confirm(
'当前服务商配置还没有保存,离开后未保存修改会丢失。',
'未保存的更改',
{
confirmButtonText: '继续',
cancelButtonText: $t('message.cancel'),
type: 'warning',
},
);
return true;
} catch {
return false;
}
};
const loadProviderDetail = async (
providerId: string,
options: { keepDraft?: boolean } = {},
) => {
if (!providerId) {
syncGroupedModels({});
if (!options.keepDraft) {
syncProviderDraft();
}
return;
}
isDetailLoading.value = true;
try {
const res = await api.get(`/api/v1/model/getList?providerId=${providerId}`, {});
if (res.errorCode === 0) {
syncGroupedModels(res.data);
providerCounts.value = {
...providerCounts.value,
[providerId]: countModels(res.data),
};
if (!options.keepDraft) {
syncProviderDraft(selectedProvider.value);
}
} else {
ElMessage.error(res.message || $t('ui.actionMessage.operationFailed'));
}
} finally {
isDetailLoading.value = false;
}
};
const refreshProviderCounts = async (list: any[]) => {
const entries = await Promise.all(
list.map(async (provider) => {
try {
const res = await api.get(
`/api/v1/model/getList?providerId=${provider.id}`,
{},
);
return [provider.id, res.errorCode === 0 ? countModels(res.data) : 0];
} catch {
return [provider.id, 0];
}
}),
);
providerCounts.value = Object.fromEntries(entries);
};
const loadProviders = async (
preferredId?: string,
refreshCounts = false,
keepDraft = false,
) => {
isPageLoading.value = true;
const previousId = selectedProviderId.value;
try {
const res = await getLlmProviderList();
const list = res?.data || [];
providers.value = list;
if (refreshCounts) {
await refreshProviderCounts(list);
}
if (list.length === 0) {
selectedProviderId.value = '';
syncProviderDraft();
syncGroupedModels({});
return;
}
let nextProviderId = list[0].id;
if (preferredId && list.some((item: any) => item.id === preferredId)) {
nextProviderId = preferredId;
} else if (
selectedProviderId.value &&
list.some((item: any) => item.id === selectedProviderId.value)
) {
nextProviderId = selectedProviderId.value;
}
selectedProviderId.value = nextProviderId;
showProviderAdvanced.value = false;
await loadProviderDetail(nextProviderId, {
keepDraft: keepDraft && previousId === nextProviderId,
});
} finally {
isPageLoading.value = false;
}
};
const switchWorkspaceView = async (target: ModelWorkspaceView) => {
if (workspaceView.value === target) {
return;
}
if (!(await confirmDiscardProviderDraft())) {
return;
}
workspaceView.value = target;
if (target === 'active') {
await activeWorkspaceRef.value?.reloadData?.();
}
};
const selectProvider = async (provider: any) => {
if (!provider?.id || provider.id === selectedProviderId.value) {
return;
}
if (!(await confirmDiscardProviderDraft())) {
return;
}
selectedProviderId.value = provider.id;
activeModelType.value = 'allModel';
showProviderAdvanced.value = false;
syncProviderDraft(provider);
await loadProviderDetail(provider.id);
};
const saveProviderDraft = async () => {
if (!selectedProviderId.value || !isProviderDirty.value) {
return;
}
isSaveLoading.value = true;
try {
const res = await api.post('/api/v1/modelProvider/update', {
id: selectedProviderId.value,
...providerDraft.value,
});
if (res.errorCode === 0) {
ElMessage.success(res.message || '配置已保存');
await loadProviders(selectedProviderId.value, true);
} else {
ElMessage.error(res.message || $t('ui.actionMessage.operationFailed'));
}
} finally {
isSaveLoading.value = false;
}
};
const handleDeleteProvider = async (provider: any) => {
try {
await ElMessageBox.confirm(
$t('message.deleteAlert'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
},
);
} catch {
return;
}
const res = await api.post('/api/v1/modelProvider/remove', {
id: provider.id,
});
if (res.errorCode === 0) {
ElMessage.success(res.message);
const remaining = providers.value.find((item) => item.id !== provider.id);
await loadProviders(remaining?.id, true);
await activeWorkspaceRef.value?.reloadData?.();
}
};
const openAddProviderDialog = () => {
addLlmProviderRef.value.openAddDialog();
};
const openEditProviderDialog = () => {
if (!selectedProvider.value) {
return;
}
addLlmProviderRef.value.openEditDialog(selectedProvider.value);
};
const handleProviderModalReload = async () => {
await loadProviders(selectedProviderId.value, true);
await activeWorkspaceRef.value?.reloadData?.();
};
const handleAddLlm = (modelType = activeModelType.value) => {
const targetModelType =
modelType === 'allModel' || !modelType ? 'chatModel' : modelType;
addLlmRef.value.openAddDialog(targetModelType);
};
const handleDeleteLlm = (id: string) => {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
}).then(async () => {
const res = await api.post('/api/v1/model/remove', { id });
if (res.errorCode === 0) {
ElMessage.success($t('message.deleteOkMessage'));
await loadProviderDetail(selectedProviderId.value, {
keepDraft: isProviderDirty.value,
});
await activeWorkspaceRef.value?.reloadData?.();
await refreshProviderCounts(providers.value);
}
});
};
const handleEditLlm = async (id: string) => {
const res = await api.get(`/api/v1/model/detail?id=${id}`);
if (res.errorCode === 0) {
addLlmRef.value.openEditDialog(res.data);
}
};
const handleGroupNameUpdateModel = (groupName: string) => {
ElMessageBox.confirm(
`确认删除分组「${groupName}」下的所有模型吗?该操作不可恢复。`,
$t('message.noticeTitle'),
{
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
},
).then(async () => {
const res = await api.post('/api/v1/model/removeByEntity', {
providerId: selectedProviderId.value,
groupName,
});
if (res.errorCode === 0) {
await loadProviderDetail(selectedProviderId.value, {
keepDraft: isProviderDirty.value,
});
await activeWorkspaceRef.value?.reloadData?.();
await refreshProviderCounts(providers.value);
}
});
};
const handleTest = () => {
if (!selectedProviderId.value) {
return;
}
llmVerifyConfigRef.value.openDialog(selectedProviderId.value);
};
const handleModelDataReload = async () => {
await loadProviderDetail(selectedProviderId.value, {
keepDraft: isProviderDirty.value,
});
await activeWorkspaceRef.value?.reloadData?.();
await refreshProviderCounts(providers.value);
};
const handleActiveProviderStatsRefresh = async () => {
if (isProviderDirty.value) {
await refreshProviderCounts(providers.value);
return;
}
await loadProviders(selectedProviderId.value, true, true);
};
onMounted(() => {
loadProviders(undefined, true);
});
</script>
<template>
<ListPageShell class="model-page-shell" surface="plain" :content-padding="20">
<template #filters>
<div class="workspace-switch">
<button
type="button"
class="workspace-switch__item"
:class="{ 'is-active': workspaceView === 'provider' }"
@click="switchWorkspaceView('provider')"
>
服务商配置
</button>
<button
type="button"
class="workspace-switch__item"
:class="{ 'is-active': workspaceView === 'active' }"
@click="switchWorkspaceView('active')"
>
已配置模型
</button>
</div>
</template>
<section v-if="workspaceView === 'provider'" class="provider-workspace">
<aside class="provider-list">
<div class="provider-list__head">
<h3>服务商</h3>
<ElButton circle :icon="Plus" @click="openAddProviderDialog" />
</div>
<div
v-if="providers.length === 0 && !isPageLoading"
class="provider-list__empty"
>
<p>还没有服务商先添加一个开始配置</p>
<ElButton type="primary" @click="openAddProviderDialog">
添加服务商
</ElButton>
</div>
<div v-else class="provider-list__body">
<button
v-for="provider in providers"
:key="provider.id"
type="button"
class="provider-item"
:class="{ 'is-active': provider.id === selectedProviderId }"
@click="selectProvider(provider)"
>
<ModelProviderBadge
:icon="provider.icon"
:provider-name="provider.providerName"
:provider-type="provider.providerType"
:size="40"
/>
<div class="provider-item__content">
<strong>{{ provider.providerName }}</strong>
<span>{{ providerCounts[provider.id] || 0 }} 个模型</span>
</div>
</button>
</div>
</aside>
<section class="provider-detail">
<div v-if="selectedProvider" class="provider-detail__content">
<header class="provider-detail__header">
<div class="provider-detail__identity">
<ModelProviderBadge
:icon="selectedProvider.icon"
:provider-name="selectedProvider.providerName"
:provider-type="selectedProvider.providerType"
:size="56"
/>
<div>
<h2>{{ selectedProvider.providerName }}</h2>
<p>
{{
selectedPreset?.description ||
'当前服务商使用自定义配置接入。'
}}
</p>
<div class="provider-detail__meta">
<ElTag effect="plain">
{{
selectedPreset?.mode === 'self-hosted'
? '自部署'
: '云服务'
}}
</ElTag>
<span>配置完成度 {{ currentProviderMetrics.ratio }}%</span>
<span>
{{ providerCounts[selectedProvider.id] || 0 }} 个模型
</span>
</div>
</div>
</div>
<div class="provider-detail__actions">
<ElButton :icon="Select" @click="handleTest">验证配置</ElButton>
<ElButton :icon="Edit" @click="openEditProviderDialog">
编辑服务商
</ElButton>
<ElButton
text
class="is-danger"
:icon="Delete"
@click="handleDeleteProvider(selectedProvider)"
>
删除
</ElButton>
</div>
</header>
<section class="provider-card">
<div class="provider-card__head">
<div>
<h3>默认连接配置</h3>
<p>主流程优先配置 API 密钥Endpoint 和对话路径</p>
</div>
<span
class="provider-card__status"
:class="{ 'is-complete': currentProviderMetrics.complete }"
>
{{
currentProviderMetrics.complete
? '配置完整'
: `还差 ${currentProviderMetrics.total - currentProviderMetrics.completed}`
}}
</span>
</div>
<ElForm
label-position="top"
class="provider-config-form"
@submit.prevent
>
<ElFormItem label="服务商类型">
<ElInput :model-value="selectedProviderTypeLabel" disabled />
</ElFormItem>
<ElFormItem :label="$t('llmProvider.providerName')">
<ElInput
:model-value="selectedProvider.providerName"
disabled
/>
</ElFormItem>
<ElFormItem :label="$t('llmProvider.apiKey')">
<ElInput
v-model="providerDraft.apiKey"
type="password"
show-password
placeholder="填写服务商密钥"
/>
</ElFormItem>
<ElFormItem :label="$t('llmProvider.endpoint')">
<ElInput
v-model.trim="providerDraft.endpoint"
placeholder="填写网关地址"
/>
</ElFormItem>
<ElFormItem :label="$t('llmProvider.chatPath')">
<ElInput
v-model.trim="providerDraft.chatPath"
placeholder="对话模型请求路径"
/>
</ElFormItem>
<div class="provider-config-form__advanced">
<button
type="button"
class="provider-config-form__advanced-toggle"
@click="showProviderAdvanced = !showProviderAdvanced"
>
<span>高级设置</span>
<span>{{ showProviderAdvanced ? '收起' : '展开' }}</span>
</button>
<div
v-if="showProviderAdvanced"
class="provider-config-form__advanced-body"
>
<ElFormItem :label="$t('llmProvider.embedPath')">
<ElInput
v-model.trim="providerDraft.embedPath"
placeholder="向量模型请求路径"
/>
</ElFormItem>
<ElFormItem :label="$t('llmProvider.rerankPath')">
<ElInput
v-model.trim="providerDraft.rerankPath"
placeholder="Rerank 请求路径"
/>
</ElFormItem>
<p class="provider-config-form__hint">
{{
selectedPreset?.description ||
'该服务商为自定义接入,建议在编辑弹窗中维护图标和更多信息。'
}}
</p>
</div>
</div>
</ElForm>
<div class="provider-card__footer">
<p v-if="isProviderDirty" class="provider-card__tip">
你有未保存的改动点击保存后才会生效
</p>
<div class="provider-card__actions">
<ElButton @click="syncProviderDraft(selectedProvider)">
重置改动
</ElButton>
<ElButton
type="primary"
:loading="isSaveLoading"
:disabled="!isProviderDirty"
:icon="Setting"
@click="saveProviderDraft"
>
保存配置
</ElButton>
</div>
</div>
</section>
<section class="provider-card">
<div class="provider-card__head">
<div>
<h3>服务商模型工作区</h3>
<p>按模型能力分组管理已配置模型</p>
</div>
<div class="provider-card__head-actions">
<ElButton
type="primary"
:icon="Plus"
@click="handleAddLlm(actionModelType)"
>
新增模型
</ElButton>
</div>
</div>
<div class="provider-card__segment">
<button
v-for="item in workspaceModelTypes"
:key="item.value"
type="button"
class="provider-card__segment-item"
:class="{ 'is-active': activeModelType === item.value }"
@click="activeModelType = item.value"
>
{{ item.label }}
</button>
</div>
<div v-if="isDetailLoading" class="provider-card__state">
正在载入 {{ currentModelTypeLabel }}...
</div>
<div
v-else-if="currentModelGroups.length === 0"
class="provider-card__empty"
>
<ElEmpty :description="`当前还没有${currentModelTypeLabel}`">
<ElButton type="primary" @click="handleAddLlm(actionModelType)">
添加第一个模型
</ElButton>
</ElEmpty>
</div>
<div v-else class="provider-card__groups">
<section
v-for="group in currentModelGroups"
:key="group.groupName"
class="provider-group"
>
<div class="provider-group__head">
<div>
<h4>{{ group.groupName }}</h4>
<p>{{ group.llmList.length }} 个模型</p>
</div>
<ElButton
text
class="is-danger"
@click="handleGroupNameUpdateModel(group.groupName)"
>
删除分组
</ElButton>
</div>
<ModelViewItemOperation
:llm-list="group.llmList"
@delete-llm="handleDeleteLlm"
@edit-llm="handleEditLlm"
/>
</section>
</div>
</section>
</div>
<div v-else class="provider-detail__empty">
<ElEmpty description="还没有可用服务商,先添加一个开始配置模型。">
<ElButton type="primary" @click="openAddProviderDialog">
添加服务商
</ElButton>
</ElEmpty>
</div>
</section>
</section>
<ActiveModelWorkspace
v-else
ref="activeWorkspaceRef"
:providers="providers"
@create-model="handleAddLlm"
@edit-model="handleEditLlm"
@refresh-provider-stats="handleActiveProviderStatsRefresh"
/>
<AddModelProviderModal
ref="addLlmProviderRef"
@reload="handleProviderModalReload"
/>
<AddModelModal
ref="addLlmRef"
:provider-id="selectedProviderId"
@reload="handleModelDataReload"
/>
<ModelVerifyConfig ref="llmVerifyConfigRef" />
</ListPageShell>
</template>
<style scoped>
.model-page-shell {
min-height: calc(100vh - 112px);
}
.workspace-switch {
display: inline-flex;
gap: 4px;
padding: 4px;
background: hsl(var(--surface-contrast-soft) / 66%);
border: 1px solid hsl(var(--divider-faint) / 58%);
border-radius: 12px;
}
.workspace-switch__item {
min-width: 104px;
padding: 8px 12px;
font-size: 13px;
font-weight: 500;
color: hsl(var(--text-muted));
background: transparent;
border: none;
border-radius: 8px;
}
.workspace-switch__item.is-active {
color: hsl(var(--text-strong));
background: hsl(var(--surface-panel));
box-shadow: 0 1px 2px hsl(var(--foreground) / 8%);
}
.provider-workspace {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
gap: 16px;
height: 100%;
min-height: 0;
}
.provider-list {
display: flex;
flex-direction: column;
min-height: 0;
padding: 14px;
background: hsl(var(--surface-panel) / 90%);
border: 1px solid hsl(var(--divider-faint) / 52%);
border-radius: 14px;
}
.provider-list__head {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
padding-bottom: 10px;
border-bottom: 1px solid hsl(var(--divider-faint) / 42%);
}
.provider-list__head h3,
.provider-detail__identity h2,
.provider-card__head h3,
.provider-group__head h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: hsl(var(--text-strong));
}
.provider-list__body {
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
min-height: 0;
padding-top: 10px;
overflow: auto;
}
.provider-item {
display: flex;
gap: 10px;
align-items: flex-start;
width: 100%;
padding: 10px;
text-align: left;
background: hsl(var(--surface-panel));
border: 1px solid hsl(var(--divider-faint) / 52%);
border-radius: 10px;
}
.provider-item.is-active {
background: hsl(var(--primary) / 6%);
border-color: hsl(var(--primary) / 40%);
}
.provider-item__content {
display: flex;
flex: 1;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.provider-item__content strong {
font-size: 14px;
color: hsl(var(--text-strong));
}
.provider-item__content span,
.provider-list__empty p,
.provider-detail__identity p,
.provider-card__head p,
.provider-card__tip,
.provider-group__head p,
.provider-card__state,
.provider-config-form__hint {
font-size: 12px;
line-height: 1.6;
color: hsl(var(--text-muted));
}
.provider-list__empty {
display: flex;
flex: 1;
flex-direction: column;
gap: 10px;
align-items: flex-start;
justify-content: center;
}
.provider-detail {
min-height: 0;
overflow: auto;
}
.provider-detail__content {
display: flex;
flex-direction: column;
gap: 14px;
}
.provider-detail__header,
.provider-detail__identity,
.provider-card__head,
.provider-card__footer,
.provider-group__head {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
}
.provider-detail__header,
.provider-card {
padding: 16px;
background: hsl(var(--surface-panel) / 92%);
border: 1px solid hsl(var(--divider-faint) / 54%);
border-radius: 14px;
}
.provider-detail__identity {
flex: 1;
align-items: flex-start;
justify-content: flex-start;
}
.provider-detail__meta {
display: inline-flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-top: 8px;
}
.provider-detail__meta span {
font-size: 12px;
color: hsl(var(--text-muted));
}
.provider-detail__actions {
display: inline-flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.provider-card {
display: flex;
flex-direction: column;
gap: 12px;
}
.provider-card__head {
align-items: flex-start;
}
.provider-card__head-actions {
display: inline-flex;
gap: 8px;
align-items: center;
}
.provider-card__status {
display: inline-flex;
align-items: center;
padding: 4px 10px;
font-size: 12px;
color: hsl(var(--destructive));
border: 1px solid hsl(var(--destructive) / 22%);
border-radius: 999px;
}
.provider-card__status.is-complete {
color: hsl(var(--success));
border-color: hsl(var(--success) / 32%);
}
.provider-config-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.provider-config-form :deep(.el-form-item:last-child) {
margin-bottom: 0;
}
.provider-config-form__advanced {
display: flex;
flex-direction: column;
grid-column: 1 / -1;
gap: 10px;
}
.provider-config-form__advanced-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 10px;
font-size: 13px;
color: hsl(var(--text-muted));
background: hsl(var(--surface-contrast-soft) / 68%);
border: 1px solid hsl(var(--divider-faint) / 56%);
border-radius: 10px;
}
.provider-config-form__advanced-body {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.provider-config-form__hint {
grid-column: 1 / -1;
margin: 0;
}
.provider-card__actions {
display: inline-flex;
gap: 8px;
align-items: center;
}
.provider-card__footer {
justify-content: flex-end;
}
.provider-card__footer .provider-card__actions {
margin-left: auto;
}
.provider-card__tip {
margin: 0;
margin-right: auto;
}
.provider-card__segment {
display: inline-flex;
gap: 6px;
padding: 4px;
background: hsl(var(--surface-contrast-soft) / 66%);
border: 1px solid hsl(var(--divider-faint) / 56%);
border-radius: 10px;
}
.provider-card__segment-item {
padding: 7px 12px;
font-size: 13px;
color: hsl(var(--text-muted));
background: transparent;
border: none;
border-radius: 8px;
}
.provider-card__segment-item.is-active {
color: hsl(var(--text-strong));
background: hsl(var(--surface-panel));
}
.provider-card__state {
padding: 6px 0;
}
.provider-card__empty,
.provider-detail__empty {
display: flex;
min-height: 300px;
}
.provider-card__empty :deep(.el-empty),
.provider-detail__empty :deep(.el-empty) {
margin: auto;
}
.provider-card__groups {
display: flex;
flex-direction: column;
gap: 10px;
}
.provider-group {
padding: 12px;
background: hsl(var(--surface-panel));
border: 1px solid hsl(var(--divider-faint) / 52%);
border-radius: 10px;
}
.provider-group__head {
margin-bottom: 8px;
}
.is-danger {
color: hsl(var(--destructive));
}
@media (max-width: 1024px) {
.provider-workspace {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.provider-detail__header,
.provider-card__head,
.provider-card__footer {
flex-direction: column;
align-items: flex-start;
}
.provider-card__footer {
width: 100%;
}
.provider-card__footer .provider-card__actions {
justify-content: flex-end;
width: 100%;
}
.provider-config-form,
.provider-config-form__advanced-body {
grid-template-columns: minmax(0, 1fr);
}
.provider-card__head-actions,
.provider-detail__actions {
flex-wrap: wrap;
width: 100%;
}
}
</style>