| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036 |
- <template>
- <div class="ai-page ai-model">
- <!-- 顶部工具卡:页签切换 + 摘要 + 当前页签操作(与插件与技能页同一套卡片规范) -->
- <section class="ai-model__toolbar">
- <div class="ai-model__card-title">模型管理</div>
- <div class="ai-model__tabs" role="tablist" aria-label="模型分类">
- <button
- v-for="tab in TABS"
- :key="tab.key"
- type="button"
- role="tab"
- class="ai-model__tab"
- :class="{ 'is-on': activeTab === tab.key }"
- :aria-selected="activeTab === tab.key"
- @click="activeTab = tab.key"
- >
- <Icon :icon="tab.icon" :size="14" />
- <span>{{ tab.label }}</span>
- <span class="ai-model__tab-count">{{ listOf(tab.key).length }}</span>
- </button>
- </div>
- <span class="ai-model__summary">{{ summary }}</span>
- <span class="ai-model__spacer"></span>
- <div class="ai-model__actions">
- <a-button :loading="loading" @click="load">
- <template #icon><Icon icon="mdi:refresh" :size="14" /></template>
- 刷新
- </a-button>
- <a-button type="primary" @click="openCreate">
- <template #icon><Icon icon="mdi:plus" :size="14" /></template>
- 新增{{ activeMeta.label }}
- </a-button>
- </div>
- </section>
- <!-- 内容卡:标题/说明 + 模型卡片网格 -->
- <section class="ai-model__panel">
- <div class="ai-model__panel-head">
- <span class="ai-model__card-title">{{ activeMeta.label }}</span>
- <span class="ai-model__panel-desc">{{ activeMeta.desc }}</span>
- <span class="ai-model__spacer"></span>
- <span class="ai-model__panel-count">共 {{ currentModels.length }} 个</span>
- </div>
- <div class="ai-model__panel-body">
- <a-spin :spinning="loading">
- <div v-if="currentModels.length" class="ai-model__grid">
- <article v-for="record in currentModels" :key="record.id" class="ai-model__card">
- <div class="ai-model__card-head">
- <span class="ai-model__card-icon" :class="activeTab === 'embedding' ? 'is-embedding' : 'is-llm'">
- <Icon :icon="activeMeta.icon" :size="18" />
- </span>
- <div class="ai-model__card-heading">
- <div class="ai-model__name-row">
- <span class="ai-model__name" :title="record.name || record.modelId">
- {{ record.name || record.modelId }}
- </span>
- <span v-if="record.defaultModel" class="ai-model__chip is-primary">
- 默认{{ activeTab === 'embedding' ? '向量' : '对话' }}
- </span>
- </div>
- <span class="ai-model__card-sub">{{ record.provider || '未指定厂商' }}</span>
- </div>
- <span class="ai-model__status" :class="statusMeta(record.status).cls">
- {{ statusMeta(record.status).label }}
- </span>
- </div>
- <div class="ai-model__card-meta">
- <span class="ai-model__meta-label">模型 ID</span>
- <span class="ai-model__mono ai-model__ellipsis" :title="record.modelId">{{
- record.modelId || '—'
- }}</span>
- </div>
- <div class="ai-model__card-meta">
- <span class="ai-model__meta-label">接口地址</span>
- <span class="ai-model__mono ai-model__ellipsis" :title="record.baseUrl">{{
- record.baseUrl || '—'
- }}</span>
- </div>
- <div class="ai-model__card-sub">更新:{{ record.updatedAt || record.createdAt || '—' }}</div>
- <div class="ai-model__card-ops">
- <button
- type="button"
- class="ai-model__op"
- :disabled="!!testing[record.id]"
- @click="onTestConnection(record)"
- >
- <Icon v-if="testing[record.id]" icon="mdi:loading" :size="12" spin />
- 测试连接
- </button>
- <button type="button" class="ai-model__op" @click="openEdit(record)">编辑</button>
- <span class="ai-model__ops-sp"></span>
- <button
- type="button"
- class="ai-model__op"
- :disabled="record.defaultModel"
- @click="onSetDefault(record, activeMeta)"
- >
- {{ record.defaultModel ? '已是默认' : '设为默认' }}
- </button>
- <button type="button" class="ai-model__op is-danger" @click="onDelete(record)">删除</button>
- </div>
- </article>
- </div>
- <EmptyState
- v-else-if="!loading"
- :title="activeTab === 'embedding' ? '还没有向量模型' : '还没有对话模型'"
- :description="
- activeTab === 'embedding'
- ? '向量模型用于表结构语义检索,点击右上角「新增向量模型」添加。'
- : '点击右上角「新增对话模型」添加一个,AI 对话才能开始工作。'
- "
- :min-height="300"
- description-width="min(100%, 520px)"
- />
- </a-spin>
- </div>
- </section>
- <ModelFormModal
- v-model:open="formOpen"
- :record="editing"
- :providers="providers"
- :default-type="activeTab"
- @saved="onSaved"
- />
- </div>
- </template>
- <script lang="ts" setup name="AiModelIndex">
- /**
- * AI 模型管理页
- *
- * 对接 AgentModelController(/models/**,Result 包装)。
- * 模型按用途类型分两个 Tab:对话模型(llm)/ 向量模型(embedding),
- * 两类各自维护一个默认模型(后端 AgentModelService#setDefaultModel 按 type 隔离)。
- * 模型数量为个位数级,故不做服务端分页,列表一次拉全量。
- */
- import { computed, onMounted, ref } from 'vue';
- import { Button, Modal, Spin } from 'ant-design-vue';
- import { EmptyState } from '@/core/components/EmptyState';
- import Icon from '@/core/components/Icon/src/Icon.vue';
- import { useMessage } from '@/core/hooks/web/useMessage';
- import * as modelApi from '../../api/modelApi';
- import type { AiModel, ModelProvider } from '../../api/types';
- import ModelFormModal from '../../components/ModelFormModal.vue';
- const AButton = Button;
- const ASpin = Spin;
- const { showMessage } = useMessage();
- /** 用途类型:与后端 ModelTypeEnum 一致 */
- type ModelTypeKey = 'llm' | 'embedding';
- interface ModelTab {
- key: ModelTypeKey;
- label: string;
- icon: string;
- desc: string;
- }
- const TABS: ModelTab[] = [
- {
- key: 'llm',
- label: '对话模型',
- icon: 'mdi:message-text-outline',
- desc: '对话与工具调用使用的大语言模型,每个类型各保留一个默认模型。',
- },
- {
- key: 'embedding',
- label: '向量模型',
- icon: 'mdi:database-search-outline',
- desc: '用于表结构语义检索的向量模型,与对话模型分别维护默认项。',
- },
- ];
- const models = ref<AiModel[]>([]);
- const providers = ref<ModelProvider[]>([]);
- const loading = ref(false);
- const formOpen = ref(false);
- const editing = ref<AiModel | null>(null);
- const activeTab = ref<ModelTypeKey>('llm');
- const activeMeta = computed(() => TABS.find((t) => t.key === activeTab.value) || TABS[0]);
- /** 对话模型:type 为空的历史数据同样视为对话模型(与后端 ModelTypeEnum.fromCode 一致) */
- const llmModels = computed(() => models.value.filter((m) => !m.type || m.type === 'llm'));
- const embeddingModels = computed(() => models.value.filter((m) => m.type === 'embedding'));
- function listOf(key: ModelTypeKey) {
- return key === 'embedding' ? embeddingModels.value : llmModels.value;
- }
- const currentModels = computed(() => listOf(activeTab.value));
- const summary = computed(() => `${llmModels.value.length} 个对话模型 · ${embeddingModels.value.length} 个向量模型`);
- /**
- * 状态展示:后端 StatusEnum 为 ACTIVE / SUSPENDED / ARCHIVED / DISABLED / DELETED
- * (DELETED 已被列表接口过滤,不会出现)
- */
- const STATUS_META: Record<string, { label: string; cls: string }> = {
- ACTIVE: { label: '活跃', cls: 'is-on' },
- SUSPENDED: { label: '暂停', cls: 'is-warn' },
- ARCHIVED: { label: '归档', cls: 'is-off' },
- DISABLED: { label: '禁用', cls: 'is-off' },
- DELETED: { label: '已删除', cls: 'is-off' },
- };
- function statusMeta(status?: string) {
- if (!status) return { label: '未知', cls: 'is-off' };
- return STATUS_META[status] || { label: status, cls: 'is-off' };
- }
- onMounted(load);
- async function load() {
- loading.value = true;
- try {
- const [list, providerList] = await Promise.all([
- modelApi.listModels(),
- modelApi.listProviders().catch(() => [] as ModelProvider[]),
- ]);
- models.value = list || [];
- providers.value = providerList || [];
- } catch (e: any) {
- showMessage(e?.message || '加载模型列表失败', 'error');
- } finally {
- loading.value = false;
- }
- }
- /** 新增时默认落在当前 Tab 的类型下,避免用户新增完发现"不在当前列表里" */
- function openCreate() {
- editing.value = null;
- formOpen.value = true;
- }
- function openEdit(record: AiModel) {
- editing.value = record;
- formOpen.value = true;
- }
- async function onSaved() {
- showMessage('已保存', 'success');
- await load();
- }
- /**
- * 设为默认:默认按类型隔离(对话 / 向量各一个),因此这里不再拦截向量模型。
- */
- async function onSetDefault(record: any, tab: ModelTab) {
- try {
- await modelApi.setDefaultModel(record.id);
- showMessage(`已将「${record.name || record.modelId}」设为默认${tab.label}`, 'success');
- await load();
- } catch (e: any) {
- showMessage(e?.message || '设置失败', 'error');
- }
- }
- /** 正在测试连通性的模型 id → loading(整体替换赋值保证响应式) */
- const testing = ref<Record<string, boolean>>({});
- /**
- * 测试连通性:服务端对已保存配置发起一次最小化真实调用。
- * 连通失败是预期结果,接口正常返回 success=false + 失败原因。
- */
- async function onTestConnection(record: AiModel) {
- testing.value = { ...testing.value, [record.id]: true };
- const name = record.name || record.modelId;
- try {
- const res = await modelApi.testModel(record.id);
- if (res.success) {
- showMessage(`「${name}」连接成功,耗时 ${res.latencyMs ?? 0}ms`, 'success');
- } else {
- showMessage(`「${name}」连接失败:${res.message || '未知原因'}`, 'error');
- }
- } catch (e: any) {
- showMessage(e?.message || `「${name}」测试请求失败`, 'error');
- } finally {
- testing.value = { ...testing.value, [record.id]: false };
- }
- }
- function onDelete(record: any) {
- const isEmbedding = record.type === 'embedding';
- Modal.confirm({
- title: '删除模型',
- content: `确认删除「${record.name || record.modelId}」?已绑定该模型的会话将回退到默认${
- isEmbedding ? '向量' : '对话'
- }模型。`,
- okText: '删除',
- okType: 'primary',
- okButtonProps: { danger: true },
- cancelText: '取消',
- onOk: async () => {
- await modelApi.deleteModel(record.id);
- showMessage('已删除', 'success');
- await load();
- },
- });
- }
- </script>
- <style lang="less" scoped>
- .ai-model {
- /* Tabs 撑起整页骨架:页签栏即顶部条,内容区自己滚动 */
- &__tabs {
- flex: 1 1 auto;
- min-height: 0;
- :deep(.ant-tabs-nav) {
- flex: 0 0 auto;
- margin: 0;
- padding: 0 var(--ai-sp-2);
- background: var(--ai-bg);
- border-bottom: 1px solid var(--ai-border);
- /* antd 自带的发丝线与顶部条边框重叠 */
- &::before {
- display: none;
- }
- }
- /* 靠页签内边距撑出顶部条高度(--ai-header-h),下划线才能压在分隔线上 */
- :deep(.ant-tabs-tab) {
- padding: 15px 0;
- font-size: 13.5px;
- }
- :deep(.ant-tabs-extra-content) {
- margin-left: var(--ai-sp-3);
- }
- /* 内容区内边距取分析页那一档(8px),左右与页签栏文字对齐 */
- :deep(.ant-tabs-content-holder) {
- flex: 1 1 auto;
- min-height: 0;
- padding: var(--ai-sp-2);
- overflow-y: auto;
- overscroll-behavior: contain;
- }
- }
- &__tab-count {
- display: inline-block;
- min-width: 18px;
- margin-left: var(--ai-sp-2);
- padding: 0 5px;
- font-size: 11px;
- line-height: 17px;
- text-align: center;
- color: var(--ai-text-2);
- background: var(--ai-bg-subtle);
- border: 1px solid var(--ai-border);
- border-radius: 999px;
- }
- /* 卡片网格:间距取分析页那一档(8px);圆角比页面外壳(12px)低一档,用 --ai-radius(8px) */
- &__grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
- gap: var(--ai-sp-2);
- }
- &__card {
- display: flex;
- flex-direction: column;
- gap: var(--ai-sp-2);
- min-width: 0;
- padding: var(--ai-sp-3);
- background: var(--ai-bg);
- border: 1px solid var(--ai-border);
- border-radius: var(--ai-radius);
- transition:
- border-color 0.2s ease,
- box-shadow 0.2s ease,
- transform 0.2s ease;
- &:hover {
- border-color: var(--ai-border-strong);
- box-shadow: var(--ai-shadow);
- transform: translateY(-2px);
- }
- }
- &__card-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--ai-sp-3);
- min-width: 0;
- }
- &__card-meta {
- display: flex;
- align-items: center;
- gap: var(--ai-sp-2);
- min-width: 0;
- /* 一行里两个 mono:厂商 chip 不收缩,长的那段(Base URL)自己省略 */
- > span:last-child {
- flex: 1 1 auto;
- min-width: 0;
- }
- }
- &__empty {
- padding: var(--ai-sp-4) var(--ai-sp-3);
- font-size: 12.5px;
- line-height: 1.7;
- text-align: center;
- color: var(--ai-text-2);
- background-color: var(--ai-bg-subtle);
- border: 1px dashed var(--ai-border);
- border-radius: var(--ai-radius);
- }
- &__name {
- display: flex;
- align-items: center;
- gap: var(--ai-sp-2);
- min-width: 0;
- }
- &__name-text {
- font-size: 13.5px;
- font-weight: 600;
- color: var(--ai-text);
- }
- &__badge {
- flex: 0 0 auto;
- padding: 0 6px;
- font-size: 11px;
- line-height: 17px;
- white-space: nowrap;
- color: var(--ai-primary);
- background: var(--ai-primary-soft);
- border-radius: var(--ai-radius-sm);
- }
- &__sub {
- margin-top: 2px;
- font-family: 'SFMono-Regular', Consolas, monospace;
- font-size: 11.5px;
- color: var(--ai-text-2);
- }
- &__chip {
- display: inline-block;
- flex: 0 0 auto;
- padding: 1px 8px;
- font-size: 12px;
- line-height: 20px;
- white-space: nowrap;
- color: var(--ai-text);
- background: var(--ai-bg-subtle);
- border: 1px solid var(--ai-border);
- border-radius: 999px;
- }
- &__mono {
- font-family: 'SFMono-Regular', Consolas, monospace;
- font-size: 12px;
- color: var(--ai-text-2);
- }
- &__ellipsis {
- display: inline-block;
- max-width: 100%;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- &__status {
- flex: 0 0 auto;
- font-size: 12px;
- white-space: nowrap;
- &.is-on {
- color: @success-color;
- }
- &.is-warn {
- color: @warning-color;
- }
- &.is-off {
- color: var(--ai-text-2);
- }
- }
- &__card-ops {
- display: flex;
- align-items: center;
- gap: var(--ai-sp-1);
- margin-top: auto;
- padding-top: var(--ai-sp-2);
- border-top: 1px solid var(--ai-border);
- }
- }
- /* ======================================================================
- * 卡片化改版:对齐自主模式「持续联系」页与插件与技能页的视觉规范
- * 页面底 #f4f6fa · 白卡圆角 14 · 描边 #e7ebf3 · 标题蓝条 · 主按钮 #2a50ec
- * ==================================================================== */
- .ai-page.ai-model {
- gap: 8px;
- width: 100%;
- height: 100%;
- min-height: 0;
- margin: 0;
- padding: 8px 10px;
- background: #f4f6fa;
- border: 0;
- border-radius: 0;
- box-sizing: border-box;
- overflow: hidden;
- }
- .ai-model {
- &__toolbar {
- display: flex;
- flex: 0 0 auto;
- align-items: center;
- gap: 12px;
- min-height: 52px;
- padding: 10px 14px;
- background: #fff;
- border: 1px solid #e7ebf3;
- border-radius: 14px;
- box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
- box-sizing: border-box;
- }
- &__card-title {
- position: relative;
- flex: 0 0 auto;
- padding-left: 11px;
- color: #111827;
- font-size: 15px;
- font-weight: 600;
- line-height: 22px;
- white-space: nowrap;
- &::before {
- content: '';
- position: absolute;
- top: 4px;
- bottom: 4px;
- left: 0;
- width: 3px;
- background: #2a50ec;
- border-radius: 2px;
- }
- }
- &__tabs {
- display: inline-flex;
- flex: 0 0 auto;
- align-items: center;
- gap: 2px;
- padding: 3px;
- background: #f4f6fa;
- border: 1px solid #e7ebf3;
- border-radius: 10px;
- }
- &__tab {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- height: 28px;
- padding: 0 10px;
- color: #5b6577;
- font-size: 12.5px;
- font-weight: 500;
- font-family: inherit;
- line-height: 1;
- white-space: nowrap;
- background: transparent;
- border: 0;
- border-radius: 8px;
- cursor: pointer;
- transition:
- color 0.15s ease,
- background-color 0.15s ease,
- box-shadow 0.15s ease;
- &:hover {
- color: #2a50ec;
- background: rgb(42 80 236 / 6%);
- }
- &.is-on {
- color: #2a50ec;
- font-weight: 600;
- background: #fff;
- box-shadow: 0 1px 3px rgb(16 24 40 / 10%);
- }
- }
- &__tab-count {
- display: inline-block;
- min-width: 18px;
- margin-left: 0;
- padding: 0 5px;
- color: inherit;
- font-size: 11px;
- line-height: 16px;
- text-align: center;
- background: rgb(42 80 236 / 8%);
- border: 0;
- border-radius: 999px;
- }
- &__summary {
- max-width: 220px;
- overflow: hidden;
- color: #98a2b3;
- font-size: 11.5px;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- &__spacer {
- flex: 1 1 auto;
- }
- &__actions {
- display: flex;
- flex: 0 0 auto;
- align-items: center;
- gap: 8px;
- :deep(.ant-btn) {
- height: 30px;
- padding: 0 14px;
- color: #5b6577;
- font-size: 12px;
- font-weight: 500;
- background: #fff;
- border: 1px solid #e7ebf3;
- border-radius: 8px;
- box-shadow: none;
- &:hover,
- &:focus {
- color: #2a50ec;
- background: #f8faff;
- border-color: #bfcdfb;
- }
- }
- :deep(.ant-btn-primary) {
- color: #fff;
- background: #2a50ec;
- border-color: #2a50ec;
- &:hover,
- &:focus {
- color: #fff;
- background: #4b6bf0;
- border-color: #4b6bf0;
- }
- &:active {
- background: #1e3ed0;
- border-color: #1e3ed0;
- }
- }
- :deep(.ant-btn[disabled]) {
- color: rgb(0 0 0 / 25%);
- background: #f5f5f5;
- border-color: #e7ebf3;
- }
- }
- &__panel {
- display: flex;
- flex: 1 1 auto;
- flex-direction: column;
- min-height: 0;
- background: #fff;
- border: 1px solid #e7ebf3;
- border-radius: 14px;
- box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
- overflow: hidden;
- }
- &__panel-head {
- display: flex;
- flex: 0 0 auto;
- align-items: center;
- gap: 10px;
- min-height: 48px;
- padding: 8px 14px;
- border-bottom: 1px solid #eff2f7;
- box-sizing: border-box;
- }
- &__panel-desc {
- min-width: 0;
- overflow: hidden;
- color: #98a2b3;
- font-size: 11.5px;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- &__panel-count {
- color: #98a2b3;
- font-size: 11.5px;
- white-space: nowrap;
- font-variant-numeric: tabular-nums;
- }
- &__panel-body {
- flex: 1 1 auto;
- min-height: 0;
- padding: 14px;
- overflow-y: auto;
- overscroll-behavior: contain;
- box-sizing: border-box;
- }
- &__grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
- gap: 12px;
- }
- &__card {
- gap: 10px;
- padding: 14px;
- background: #fff;
- border: 1px solid #e7ebf3;
- border-radius: 12px;
- box-sizing: border-box;
- transition:
- border-color 0.15s ease,
- box-shadow 0.15s ease;
- &:hover {
- border-color: #bfcdfb;
- box-shadow: 0 4px 14px rgb(16 24 40 / 8%);
- transform: none;
- }
- }
- &__card-head {
- align-items: flex-start;
- justify-content: flex-start;
- gap: 10px;
- }
- &__card-icon {
- display: inline-flex;
- flex: 0 0 auto;
- align-items: center;
- justify-content: center;
- width: 34px;
- height: 34px;
- color: #2a50ec;
- background: rgb(42 80 236 / 8%);
- border-radius: 9px;
- &.is-embedding {
- color: #7c3aed;
- background: rgb(124 58 237 / 8%);
- }
- }
- &__card-heading {
- display: flex;
- flex: 1 1 auto;
- flex-direction: column;
- gap: 4px;
- min-width: 0;
- }
- &__name-row {
- display: flex;
- align-items: center;
- gap: 6px;
- min-width: 0;
- }
- &__name {
- display: block;
- min-width: 0;
- overflow: hidden;
- color: #111827;
- font-size: 14px;
- font-weight: 600;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- &__card-sub {
- overflow: hidden;
- color: #98a2b3;
- font-size: 11.5px;
- font-family: inherit;
- white-space: nowrap;
- text-overflow: ellipsis;
- font-variant-numeric: tabular-nums;
- }
- &__chip {
- display: inline-flex;
- align-items: center;
- flex: 0 0 auto;
- max-width: 220px;
- height: 20px;
- padding: 0 7px;
- overflow: hidden;
- color: #5b6577;
- font-size: 11px;
- font-weight: 500;
- line-height: 18px;
- white-space: nowrap;
- text-overflow: ellipsis;
- background: #f4f6fa;
- border: 1px solid #e7ebf3;
- border-radius: 6px;
- &.is-primary {
- color: #2a50ec;
- background: rgb(42 80 236 / 7%);
- border-color: rgb(42 80 236 / 18%);
- }
- }
- &__status {
- display: inline-flex;
- flex: 0 0 auto;
- align-items: center;
- gap: 5px;
- height: 22px;
- padding: 0 8px;
- color: #5b6577;
- font-size: 11px;
- font-weight: 500;
- white-space: nowrap;
- background: #f4f6fa;
- border: 1px solid #e7ebf3;
- border-radius: 6px;
- &::before {
- content: '';
- width: 6px;
- height: 6px;
- background: currentColor;
- border-radius: 50%;
- }
- &.is-on {
- color: #12805c;
- background: rgb(18 128 92 / 8%);
- border-color: rgb(18 128 92 / 20%);
- }
- &.is-warn {
- color: #b7791f;
- background: rgb(183 121 31 / 8%);
- border-color: rgb(183 121 31 / 20%);
- }
- &.is-off {
- color: #5b6577;
- background: #f4f6fa;
- border-color: #e7ebf3;
- }
- }
- &__card-meta {
- gap: 8px;
- min-width: 0;
- }
- &__meta-label {
- flex: 0 0 48px;
- color: #98a2b3;
- font-size: 11.5px;
- white-space: nowrap;
- }
- &__mono {
- min-width: 0;
- color: #5b6577;
- font-size: 11.5px;
- }
- &__card-ops {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-top: auto;
- padding-top: 10px;
- border-top: 1px solid #eff2f7;
- }
- &__ops-sp {
- flex: 1 1 auto;
- }
- &__op {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- height: 26px;
- padding: 0 10px;
- color: #5b6577;
- font-size: 11.5px;
- font-weight: 500;
- font-family: inherit;
- white-space: nowrap;
- background: #fff;
- border: 1px solid #e7ebf3;
- border-radius: 6px;
- cursor: pointer;
- transition:
- color 0.15s ease,
- background-color 0.15s ease,
- border-color 0.15s ease;
- &:hover:not(:disabled) {
- color: #2a50ec;
- background: #f8faff;
- border-color: #bfcdfb;
- }
- &:disabled {
- cursor: not-allowed;
- opacity: 0.55;
- }
- &.is-danger:hover:not(:disabled) {
- color: #e5484d;
- background: #fff5f5;
- border-color: rgb(229 72 77 / 35%);
- }
- }
- }
- @media (max-width: 1440px) {
- .ai-model__summary {
- display: none;
- }
- }
- @media (max-width: 1180px) {
- .ai-model__panel-desc {
- display: none;
- }
- }
- @media (max-width: 1120px) {
- .ai-model__tab .jeesite-icon {
- display: none !important;
- }
- }
- </style>
- <style lang="less">
- /* 新增/编辑模型弹框:与页面卡片统一的圆角、控件尺寸与按钮规范 */
- /*
- * 项目全局对 .ant-modal 有过高优先级覆盖(:not(...) 链),这里必须用 !important
- * 才能把圆角与头部配色统一到页面卡片规范。
- */
- .ai-model-modal .ant-modal-content {
- overflow: hidden;
- border-radius: 14px !important;
- padding: 0 !important;
- }
- .ai-model-modal .ant-modal-header {
- margin: 0 !important;
- padding: 14px 20px !important;
- background: #fff !important;
- border-bottom: 1px solid #eff2f7 !important;
- border-radius: 14px 14px 0 0 !important;
- }
- .ai-model-modal .ant-modal-title {
- color: #111827 !important;
- font-size: 15px !important;
- font-weight: 600 !important;
- }
- .ai-model-modal .ant-modal-close {
- top: 14px;
- right: 16px;
- }
- .ai-model-modal .ant-modal-body {
- padding: 16px 20px 8px !important;
- }
- .ai-model-modal .ant-form-item-label > label {
- color: #5b6577;
- font-size: 12px;
- }
- .ai-model-modal .ant-input,
- .ai-model-modal .ant-input-affix-wrapper,
- .ai-model-modal .ant-select-selector {
- min-height: 32px;
- border-color: #e7ebf3 !important;
- border-radius: 8px !important;
- box-shadow: none;
- }
- .ai-model-modal .ant-select-selector {
- padding: 0 10px !important;
- }
- .ai-model-modal .ant-input:focus,
- .ai-model-modal .ant-input-affix-wrapper-focused,
- .ai-model-modal .ant-select-focused .ant-select-selector {
- border-color: #2a50ec !important;
- box-shadow: 0 0 0 2px rgb(42 80 236 / 12%) !important;
- }
- </style>
|