Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/model/UnifiedGatewayWorkspace.vue
陈子默 7e7c236c2a fix: 修复管理端前端 lint 与构建问题
- 收敛 easyflow-ui-admin 的 lint、格式和类型问题

- 修正 demo 页面与管理端前端构建失败点

- 验证 pnpm lint 与 pnpm build 均已通过
2026-04-05 21:39:13 +08:00

1262 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 type {
BatchModelInvokePublishPayload,
llmType,
ModelInvokeConfigPayload,
} from '#/api/ai/llm';
import { computed, onMounted, reactive, ref } from 'vue';
import { useAppConfig } from '@easyflow/hooks';
import {
ElAlert,
ElButton,
ElCheckbox,
ElEmpty,
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElMessageBox,
ElSwitch,
ElTag,
} from 'element-plus';
import {
batchUpdateInvokePublishStatus,
getInvokeModelList,
updateModelInvokeConfig,
} from '#/api/ai/llm';
import { api } from '#/api/request';
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
const emit = defineEmits<{
(e: 'updated'): void;
}>();
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
interface DraftState {
id: string;
invokeCode: string;
publishEnabled: boolean;
}
interface FilterState {
keyword: string;
}
const isLoading = ref(false);
const isSaving = ref(false);
const isPublishing = ref(false);
const isBatchPublishing = ref(false);
const lastErrorMessage = ref('');
const selectedModelId = ref('');
const autoGeneratedInvokeCode = ref('');
const publishBaseUrl = ref('');
const checkedModelIds = ref<string[]>([]);
const modelRows = ref<llmType[]>([]);
const filterState = reactive<FilterState>({
keyword: '',
});
const draft = reactive<DraftState>({
id: '',
invokeCode: '',
publishEnabled: false,
});
const normalizePublishBaseUrl = (value?: string) => {
const raw = String(value || '').trim();
if (!raw) {
return '';
}
if (/^https?:\/\//i.test(raw)) {
return raw.replace(/\/+$/, '');
}
if (raw.startsWith('//')) {
return `${window.location.protocol}${raw}`.replace(/\/+$/, '');
}
const firstSegment = raw.split('/')[0] || '';
const hasPort = /:\d+$/.test(firstSegment);
let hostAndPath = raw;
if (
!hasPort &&
firstSegment === window.location.hostname &&
window.location.port
) {
hostAndPath = `${firstSegment}:${window.location.port}${raw.slice(firstSegment.length)}`;
}
return `${window.location.protocol}//${hostAndPath}`.replace(/\/+$/, '');
};
const resolveFlowBaseUrl = (value?: string) => {
const normalized = normalizePublishBaseUrl(value);
if (!normalized) {
return '';
}
if (/\/flow$/i.test(normalized)) {
return normalized;
}
return `${normalized}/flow`;
};
const apiEndpoint = computed(() => {
const base =
resolveFlowBaseUrl(publishBaseUrl.value) ||
resolveFlowBaseUrl(apiURL) ||
resolveFlowBaseUrl(window.location.origin);
return `${base}/v1/chat/completions`;
});
const selectedModel = computed(() =>
modelRows.value.find((item) => String(item.id) === selectedModelId.value),
);
const visibleModelIds = computed(() =>
filteredModels.value.map((item) => String(item.id)),
);
const checkedCount = computed(() => checkedModelIds.value.length);
const checkedModels = computed(() =>
modelRows.value.filter((item) =>
checkedModelIds.value.includes(String(item.id)),
),
);
const shouldBatchUnpublish = computed(
() =>
checkedModels.value.length > 0 &&
checkedModels.value.every((item) => Boolean(item.publishEnabled)),
);
const batchActionLabel = computed(() =>
shouldBatchUnpublish.value ? '取消发布' : '发布',
);
const isAllVisibleChecked = computed(
() =>
visibleModelIds.value.length > 0 &&
visibleModelIds.value.every((id) => checkedModelIds.value.includes(id)),
);
const isPartiallyVisibleChecked = computed(
() =>
checkedModelIds.value.length > 0 &&
visibleModelIds.value.some((id) => checkedModelIds.value.includes(id)) &&
!isAllVisibleChecked.value,
);
const filteredModels = computed(() => {
const keyword = filterState.keyword.trim().toLowerCase();
return modelRows.value.filter((item) => {
if (!keyword) {
return true;
}
const searchTargets = [
item.title,
item.modelName,
item.invokeCode,
item.modelProvider?.providerName,
]
.filter(Boolean)
.map((text) => String(text).toLowerCase());
return searchTargets.some((text) => text.includes(keyword));
});
});
const providerType = computed(
() =>
selectedModel.value?.modelProvider?.providerType ||
selectedModel.value?.aiLlmProvider?.providerType ||
'-',
);
const upstreamModelName = computed(
() => selectedModel.value?.modelName || selectedModel.value?.llmModel || '-',
);
const capabilityTags = computed(() => {
const tags = ['文本', '流式'];
if (selectedModel.value?.supportImage) {
tags.push(
selectedModel.value?.supportImageB64Only
? '图片输入Base64'
: '图片输入',
);
}
if (selectedModel.value?.supportTool) {
tags.push('tools');
}
if (selectedModel.value?.supportToolMessage) {
tags.push('tool 消息');
}
return tags;
});
const authExample = computed(() => 'Authorization: Bearer <your-access-token>');
const statusHints = computed(() => {
const hints: string[] = [];
if (!draft.invokeCode.trim()) {
hints.push(
'当前未单独配置对外模型标识invokeCode保存时会默认按上游模型 ID 生成。',
);
}
return hints;
});
const curlExample = computed(
() => `curl ${apiEndpoint.value} \\
-H "Content-Type: application/json" \\
-H "${authExample.value}" \\
-d '{
"model": "${draft.invokeCode.trim() || 'your-model-code'}",
"messages": [
{
"role": "user",
"content": "你好,介绍一下你自己。"
}
]
}'`,
);
const jsExample = computed(
() => `await fetch("${apiEndpoint.value}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer <your-access-token>"
},
body: JSON.stringify({
model: "${draft.invokeCode.trim() || 'your-model-code'}",
messages: [
{ role: "user", content: "你好,介绍一下你自己。" }
]
})
});`,
);
const pythonExample = computed(
() => `import requests
resp = requests.post(
"${apiEndpoint.value}",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer <your-access-token>",
},
json={
"model": "${draft.invokeCode.trim() || 'your-model-code'}",
"messages": [
{"role": "user", "content": "你好,介绍一下你自己。"}
],
},
timeout=60,
)
print(resp.json())`,
);
const buildDefaultInvokeCode = (modelName?: string) => {
const normalized = String(modelName || '')
.trim()
.replaceAll(/[^\w.:-]+/g, '-')
.replaceAll(/-+/g, '-')
.replaceAll(/^[^\da-z]+/gi, '')
.slice(0, 128);
if (!normalized) {
return '';
}
return normalized.length === 1 ? `${normalized}-model` : normalized;
};
const isInvokeCodeDirty = computed(() => {
if (!selectedModel.value) {
return false;
}
return (
draft.invokeCode.trim() !==
(selectedModel.value.invokeCode || autoGeneratedInvokeCode.value)
);
});
const syncDraft = (model?: llmType | null) => {
if (!model) {
selectedModelId.value = '';
draft.id = '';
draft.invokeCode = '';
draft.publishEnabled = false;
autoGeneratedInvokeCode.value = '';
return;
}
autoGeneratedInvokeCode.value = buildDefaultInvokeCode(
model.modelName || model.llmModel,
);
selectedModelId.value = String(model.id);
draft.id = String(model.id);
draft.invokeCode = model.invokeCode || autoGeneratedInvokeCode.value;
draft.publishEnabled = Boolean(model.publishEnabled);
};
const confirmDiscardDraft = async () => {
if (!isInvokeCodeDirty.value) {
return true;
}
try {
await ElMessageBox.confirm(
'当前统一网关配置还没有保存,离开后未保存修改会丢失。',
'未保存的更改',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning',
},
);
return true;
} catch {
return false;
}
};
const loadModels = async (preferredId?: string) => {
isLoading.value = true;
lastErrorMessage.value = '';
try {
const res = await getInvokeModelList();
if (res.errorCode !== 0) {
modelRows.value = [];
syncDraft();
lastErrorMessage.value = res.message || '加载统一网关模型失败';
return;
}
modelRows.value = res.data || [];
let nextSelectedId = String(modelRows.value[0]?.id || '');
if (
preferredId &&
modelRows.value.some((item) => String(item.id) === preferredId)
) {
nextSelectedId = preferredId;
} else if (
selectedModelId.value &&
modelRows.value.some((item) => String(item.id) === selectedModelId.value)
) {
nextSelectedId = selectedModelId.value;
}
const nextModel = modelRows.value.find(
(item) => String(item.id) === nextSelectedId,
);
checkedModelIds.value = checkedModelIds.value.filter((id) =>
modelRows.value.some((item) => String(item.id) === id),
);
syncDraft(nextModel || null);
} catch (error) {
modelRows.value = [];
syncDraft();
lastErrorMessage.value =
(error as Error)?.message || '加载统一网关模型失败';
} finally {
isLoading.value = false;
}
};
const loadPublishBaseUrl = async () => {
try {
const res = await api.get('/api/v1/sysOption/list', {
params: {
keys: ['chat_publish_base_url'],
},
});
if (res.errorCode === 0) {
publishBaseUrl.value = String(
res.data?.chat_publish_base_url || '',
).trim();
}
} catch {
publishBaseUrl.value = '';
}
};
const handleSelectModel = async (model: llmType) => {
if (String(model.id) === selectedModelId.value) {
return;
}
if (!(await confirmDiscardDraft())) {
return;
}
syncDraft(model);
};
const copyText = async (value: string) => {
if (!navigator?.clipboard?.writeText) {
return;
}
await navigator.clipboard.writeText(value);
ElMessage.success('已复制');
};
const syncCheckedModelIds = () => {
const validIds = new Set(modelRows.value.map((item) => String(item.id)));
checkedModelIds.value = checkedModelIds.value.filter((id) =>
validIds.has(id),
);
};
const applyUpdatedModel = (
updatedModel: llmType,
options?: {
draftInvokeCode?: string;
preserveInvokeCodeDraft?: boolean;
},
) => {
syncCheckedModelIds();
modelRows.value = modelRows.value.map((item) =>
String(item.id) === String(updatedModel.id) ? updatedModel : item,
);
if (String(updatedModel.id) !== selectedModelId.value) {
return;
}
autoGeneratedInvokeCode.value = buildDefaultInvokeCode(
updatedModel.modelName || updatedModel.llmModel,
);
draft.id = String(updatedModel.id);
draft.publishEnabled = Boolean(updatedModel.publishEnabled);
draft.invokeCode = options?.preserveInvokeCodeDraft
? options.draftInvokeCode || ''
: updatedModel.invokeCode || autoGeneratedInvokeCode.value;
};
const applyUpdatedModels = (
updatedModels: llmType[],
options?: {
draftInvokeCode?: string;
preserveInvokeCodeDraft?: boolean;
},
) => {
if (updatedModels.length === 0) {
return;
}
const updatedMap = new Map(
updatedModels.map((item) => [String(item.id), item] as const),
);
modelRows.value = modelRows.value.map(
(item) => updatedMap.get(String(item.id)) || item,
);
syncCheckedModelIds();
const currentModel = updatedMap.get(selectedModelId.value);
if (!currentModel) {
return;
}
autoGeneratedInvokeCode.value = buildDefaultInvokeCode(
currentModel.modelName || currentModel.llmModel,
);
draft.id = String(currentModel.id);
draft.publishEnabled = Boolean(currentModel.publishEnabled);
draft.invokeCode = options?.preserveInvokeCodeDraft
? options.draftInvokeCode || ''
: currentModel.invokeCode || autoGeneratedInvokeCode.value;
};
const handleToggleModelChecked = (modelId: string, checked: boolean) => {
const next = new Set(checkedModelIds.value);
if (checked) {
next.add(modelId);
} else {
next.delete(modelId);
}
checkedModelIds.value = [...next];
};
const handleToggleVisibleChecked = (checked: boolean | number | string) => {
const next = new Set(checkedModelIds.value);
for (const modelId of visibleModelIds.value) {
if (checked) {
next.add(modelId);
} else {
next.delete(modelId);
}
}
checkedModelIds.value = [...next];
};
const handlePublishToggle = async (nextValue: boolean | number | string) => {
if (!draft.id || !selectedModel.value || isPublishing.value) {
return;
}
const publishEnabled = Boolean(nextValue);
const previousValue = draft.publishEnabled;
const draftInvokeCode = draft.invokeCode;
const preserveInvokeCodeDraft = isInvokeCodeDirty.value;
draft.publishEnabled = publishEnabled;
isPublishing.value = true;
try {
const payload: ModelInvokeConfigPayload = {
id: draft.id,
invokeCode: draft.invokeCode.trim(),
publishEnabled,
};
const res = await updateModelInvokeConfig(payload);
if (res.errorCode !== 0) {
draft.publishEnabled = previousValue;
ElMessage.error(res.message || '更新发布状态失败');
return;
}
applyUpdatedModel(res.data as llmType, {
preserveInvokeCodeDraft,
draftInvokeCode,
});
ElMessage.success(publishEnabled ? '已发布该模型' : '已关闭该模型发布');
emit('updated');
} finally {
isPublishing.value = false;
}
};
const handleBatchPublish = async () => {
const targetIds = checkedModelIds.value;
if (targetIds.length === 0) {
ElMessage.warning('请先选择模型');
return;
}
const publishEnabled = !shouldBatchUnpublish.value;
const actionText = publishEnabled ? '发布' : '取消发布';
const scopeText = `所选 ${targetIds.length} 个模型`;
try {
await ElMessageBox.confirm(
`确认${actionText}${scopeText}吗?`,
`批量${actionText}`,
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
},
);
} catch {
return;
}
const draftInvokeCode = draft.invokeCode;
const preserveInvokeCodeDraft = isInvokeCodeDirty.value;
isBatchPublishing.value = true;
try {
const payload: BatchModelInvokePublishPayload = {
ids: targetIds,
publishEnabled,
};
const res = await batchUpdateInvokePublishStatus(payload);
if (res.errorCode !== 0) {
ElMessage.error(res.message || `批量${actionText}失败`);
return;
}
applyUpdatedModels((res.data || []) as llmType[], {
preserveInvokeCodeDraft,
draftInvokeCode,
});
checkedModelIds.value = [];
ElMessage.success(`${actionText}${scopeText}`);
emit('updated');
} finally {
isBatchPublishing.value = false;
}
};
const saveInvokeConfig = async () => {
if (!draft.id) {
return;
}
isSaving.value = true;
try {
const payload: ModelInvokeConfigPayload = {
id: draft.id,
invokeCode: draft.invokeCode.trim(),
publishEnabled: Boolean(selectedModel.value?.publishEnabled),
};
const res = await updateModelInvokeConfig(payload);
if (res.errorCode !== 0) {
ElMessage.error(res.message || '保存统一网关配置失败');
return;
}
applyUpdatedModel(res.data as llmType);
ElMessage.success(res.message || '统一网关配置已保存');
emit('updated');
} finally {
isSaving.value = false;
}
};
const resetDraft = () => {
syncDraft(selectedModel.value || null);
};
onMounted(() => {
loadPublishBaseUrl();
loadModels();
});
defineExpose({
async reloadData(preferredId?: string) {
await loadModels(preferredId);
},
async confirmBeforeLeave() {
return confirmDiscardDraft();
},
});
</script>
<template>
<section class="gateway-workspace">
<aside class="gateway-workspace__sidebar">
<div class="gateway-workspace__filters">
<ElInput
v-model.trim="filterState.keyword"
clearable
placeholder="搜索模型名、调用标识"
/>
<div class="gateway-workspace__batch">
<ElCheckbox
:model-value="isAllVisibleChecked"
:indeterminate="isPartiallyVisibleChecked"
@change="handleToggleVisibleChecked"
>
全选当前筛选
</ElCheckbox>
<ElButton
size="small"
type="primary"
plain
:loading="isBatchPublishing"
:disabled="checkedCount === 0"
@click="handleBatchPublish"
>
{{ batchActionLabel }}
</ElButton>
</div>
</div>
<div v-if="isLoading" class="gateway-workspace__state">
正在加载可发布模型...
</div>
<div
v-else-if="lastErrorMessage"
class="gateway-workspace__state gateway-workspace__state--error"
>
{{ lastErrorMessage }}
</div>
<div v-else-if="modelRows.length === 0" class="gateway-workspace__empty">
<ElEmpty description="还没有聊天模型,先去已配置模型里新增一个。" />
</div>
<div
v-else-if="filteredModels.length === 0"
class="gateway-workspace__empty"
>
<ElEmpty description="没有符合条件的模型,试试调整搜索条件。" />
</div>
<div v-else class="gateway-workspace__list">
<div
v-for="model in filteredModels"
:key="model.id"
class="gateway-model-item"
:class="{ 'is-active': String(model.id) === selectedModelId }"
role="button"
tabindex="0"
@click="handleSelectModel(model)"
@keydown.enter.prevent="handleSelectModel(model)"
@keydown.space.prevent="handleSelectModel(model)"
>
<ElCheckbox
class="gateway-model-item__checkbox"
:model-value="checkedModelIds.includes(String(model.id))"
@click.stop
@change="
(checked) =>
handleToggleModelChecked(String(model.id), Boolean(checked))
"
/>
<ModelProviderBadge
:icon="model.modelProvider?.icon || model.aiLlmProvider?.icon"
:provider-name="
model.modelProvider?.providerName ||
model.aiLlmProvider?.providerName
"
:provider-type="
model.modelProvider?.providerType ||
model.aiLlmProvider?.providerType
"
:size="44"
/>
<div class="gateway-model-item__content">
<strong>{{ model.title }}</strong>
</div>
<ElTag
:type="model.publishEnabled ? 'success' : 'info'"
effect="plain"
size="small"
>
{{ model.publishEnabled ? '已发布' : '未发布' }}
</ElTag>
</div>
</div>
</aside>
<section class="gateway-workspace__detail">
<div
v-if="!selectedModel"
class="gateway-workspace__empty gateway-workspace__empty--detail"
>
<ElEmpty description="请选择一个聊天模型开始配置统一网关。" />
</div>
<div v-else class="gateway-workspace__detail-body">
<section class="gateway-card gateway-card--hero">
<div class="gateway-hero__title">
<ModelProviderBadge
:icon="
selectedModel.modelProvider?.icon ||
selectedModel.aiLlmProvider?.icon
"
:provider-name="
selectedModel.modelProvider?.providerName ||
selectedModel.aiLlmProvider?.providerName
"
:provider-type="
selectedModel.modelProvider?.providerType ||
selectedModel.aiLlmProvider?.providerType
"
:size="48"
/>
<div class="gateway-hero__title-text">
<h3>{{ selectedModel.title }}</h3>
<span
v-if="!draft.publishEnabled"
class="gateway-card__warning-text"
>
未发布
</span>
</div>
</div>
<div class="gateway-card__tags">
<div class="gateway-publish-inline">
<span class="gateway-publish-inline__label">API 发布</span>
<ElSwitch
:model-value="draft.publishEnabled"
inline-prompt
active-text=""
inactive-text=""
:loading="isPublishing"
@change="handlePublishToggle"
/>
</div>
</div>
</section>
<ElAlert
v-for="hint in statusHints"
:key="hint"
type="warning"
:closable="false"
:title="hint"
/>
<section class="gateway-card">
<div class="gateway-card__head">
<h4>模型映射配置</h4>
</div>
<ElForm
label-position="top"
class="gateway-form gateway-form--compact"
>
<div class="gateway-form__grid">
<ElFormItem label="上游模型 ID">
<ElInput :model-value="upstreamModelName" disabled />
</ElFormItem>
<ElFormItem label="服务商类型">
<ElInput :model-value="providerType" disabled />
</ElFormItem>
</div>
<div class="gateway-form__invoke-row">
<ElFormItem label="对外模型标识">
<ElInput
v-model.trim="draft.invokeCode"
placeholder="默认按模型 ID 生成,可手动修改"
/>
</ElFormItem>
<div class="gateway-form__actions">
<ElButton :disabled="!isInvokeCodeDirty" @click="resetDraft">
重置改动
</ElButton>
<ElButton
type="primary"
:loading="isSaving"
:disabled="!isInvokeCodeDirty"
@click="saveInvokeConfig"
>
保存配置
</ElButton>
</div>
</div>
</ElForm>
</section>
<section class="gateway-card">
<div class="gateway-card__head">
<div>
<h4>调用信息</h4>
<p>统一网关固定走 OpenAI-compatible chat/completions 协议</p>
</div>
<ElButton text size="small" @click="copyText(apiEndpoint)">
复制地址
</ElButton>
</div>
<div class="gateway-info-grid">
<div class="gateway-info-item">
<span>调用地址</span>
<strong>{{ apiEndpoint }}</strong>
</div>
<div class="gateway-info-item">
<span>鉴权 Header</span>
<strong>{{ authExample }}</strong>
</div>
</div>
</section>
<section class="gateway-card">
<div class="gateway-card__head">
<div>
<h4>能力说明</h4>
<p>这里展示的是统一网关可透传的当前模型能力</p>
</div>
</div>
<div class="gateway-card__tags">
<ElTag v-for="tag in capabilityTags" :key="tag" effect="plain">
{{ tag }}
</ElTag>
</div>
</section>
<section class="gateway-card">
<div class="gateway-card__head">
<div>
<h4>调用示例</h4>
<p>示例中的 `model` 固定使用当前配置的 invokeCode</p>
</div>
</div>
<div class="gateway-example-list">
<article class="gateway-example-item">
<div class="gateway-example-item__head">
<h5>curl</h5>
<ElButton text size="small" @click="copyText(curlExample)">
复制
</ElButton>
</div>
<pre>{{ curlExample }}</pre>
</article>
<article class="gateway-example-item">
<div class="gateway-example-item__head">
<h5>JavaScript</h5>
<ElButton text size="small" @click="copyText(jsExample)">
复制
</ElButton>
</div>
<pre>{{ jsExample }}</pre>
</article>
<article class="gateway-example-item">
<div class="gateway-example-item__head">
<h5>Python</h5>
<ElButton text size="small" @click="copyText(pythonExample)">
复制
</ElButton>
</div>
<pre>{{ pythonExample }}</pre>
</article>
</div>
</section>
</div>
</section>
</section>
</template>
<style scoped>
.gateway-workspace {
display: grid;
grid-template-columns: 320px minmax(0, 1fr);
gap: 16px;
height: calc(100vh - 240px);
min-height: 0;
}
.gateway-workspace__sidebar,
.gateway-card {
display: flex;
flex-direction: column;
gap: 14px;
padding: 18px;
background: hsl(var(--surface-panel) / 92%);
border: 1px solid hsl(var(--divider-faint) / 54%);
border-radius: 16px;
}
.gateway-workspace__sidebar {
height: 100%;
min-height: 0;
overflow: hidden;
}
.gateway-card__head h4,
.gateway-card--hero h3,
.gateway-example-item__head h5 {
margin: 0;
color: hsl(var(--text-strong));
}
.gateway-card__head p,
.gateway-card--hero p,
.gateway-form__switch-row span,
.gateway-workspace__state {
margin: 6px 0 0;
font-size: 13px;
line-height: 1.6;
color: hsl(var(--text-muted));
}
.gateway-card__warning-text {
display: inline-flex;
align-items: center;
font-size: 14px;
font-weight: 700;
line-height: 1;
color: var(--el-color-warning);
}
.gateway-workspace__filters {
display: flex;
flex-direction: column;
gap: 12px;
}
.gateway-workspace__batch {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: hsl(var(--surface-contrast-soft) / 48%);
border: 1px solid hsl(var(--divider-faint) / 40%);
border-radius: 14px;
}
.gateway-workspace__list {
display: flex;
flex: 1;
flex-direction: column;
gap: 10px;
min-height: 0;
overflow: auto;
}
.gateway-model-item {
display: flex;
gap: 12px;
align-items: center;
width: 100%;
padding: 12px;
text-align: left;
background: hsl(var(--surface-panel));
border: 1px solid hsl(var(--divider-faint) / 50%);
border-radius: 14px;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease;
}
.gateway-model-item:hover {
border-color: hsl(var(--primary) / 30%);
box-shadow: 0 16px 30px -28px hsl(var(--foreground) / 34%);
transform: translateY(-1px);
}
.gateway-model-item.is-active {
background: hsl(var(--primary) / 7%);
border-color: hsl(var(--primary) / 42%);
}
.gateway-model-item__content {
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
min-width: 0;
}
.gateway-model-item__checkbox {
flex-shrink: 0;
}
.gateway-model-item__content strong {
display: flex;
align-items: center;
min-height: 24px;
font-size: 14px;
line-height: 1.2;
color: hsl(var(--text-strong));
}
.gateway-model-item__content span {
font-size: 12px;
line-height: 1.5;
color: hsl(var(--text-muted));
word-break: break-all;
}
.gateway-workspace__detail {
height: 100%;
min-height: 0;
overflow: auto;
}
.gateway-workspace__detail-body {
display: flex;
flex-direction: column;
gap: 14px;
}
.gateway-card--hero {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.gateway-hero__title {
display: flex;
gap: 14px;
align-items: center;
}
.gateway-hero__title-text {
display: flex;
gap: 10px;
align-items: center;
}
.gateway-hero__title-text h3 {
font-weight: 700;
}
.gateway-card__head,
.gateway-example-item__head {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
}
.gateway-card__tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.gateway-publish-inline {
display: flex;
gap: 10px;
align-items: center;
justify-content: flex-end;
}
.gateway-publish-inline__label {
font-size: 13px;
font-weight: 600;
color: hsl(var(--text-muted));
}
.gateway-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.gateway-form--compact {
gap: 12px;
}
.gateway-form__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 12px;
}
.gateway-form__switch-row {
display: flex;
gap: 12px;
align-items: center;
min-height: 32px;
}
.gateway-form__invoke-row {
display: flex;
gap: 12px;
align-items: flex-end;
}
.gateway-form__invoke-row :deep(.el-form-item) {
flex: 1;
margin-bottom: 0;
}
.gateway-form__actions {
display: flex;
flex-shrink: 0;
gap: 12px;
justify-content: flex-end;
}
.gateway-info-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.gateway-info-item,
.gateway-example-item {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px;
background: hsl(var(--surface-contrast-soft) / 56%);
border: 1px solid hsl(var(--divider-faint) / 48%);
border-radius: 14px;
}
.gateway-info-item span {
font-size: 12px;
color: hsl(var(--text-muted));
}
.gateway-info-item strong {
font-size: 13px;
line-height: 1.6;
color: hsl(var(--text-strong));
word-break: break-all;
}
.gateway-example-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.gateway-example-item pre {
padding: 14px;
margin: 0;
overflow: auto;
font-size: 12px;
line-height: 1.7;
color: hsl(var(--text-strong));
background: hsl(var(--surface-panel));
border: 1px solid hsl(var(--divider-faint) / 40%);
border-radius: 12px;
}
.gateway-workspace__empty,
.gateway-workspace__state {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
}
.gateway-workspace__empty--detail {
min-height: 420px;
padding: 18px;
background: hsl(var(--surface-panel) / 92%);
border: 1px solid hsl(var(--divider-faint) / 54%);
border-radius: 16px;
}
.gateway-workspace__state--error {
color: hsl(var(--danger));
}
@media (max-width: 1200px) {
.gateway-workspace {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 900px) {
.gateway-card--hero,
.gateway-card__head,
.gateway-example-item__head {
flex-direction: column;
align-items: flex-start;
}
.gateway-form__grid,
.gateway-info-grid {
grid-template-columns: minmax(0, 1fr);
}
.gateway-form__switch-row {
flex-direction: column;
align-items: flex-start;
}
.gateway-form__invoke-row {
flex-direction: column;
align-items: stretch;
}
.gateway-publish-inline {
flex-direction: column;
align-items: flex-start;
}
.gateway-hero__title {
width: 100%;
}
}
</style>