Files
Yi.Framework/Yi.Ai.Vue3/src/pages/console/channel/index.vue
2026-01-24 17:28:12 +08:00

1000 lines
29 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 { AiAppDto, AiModelDto, AppShortcutDto } from '@/api/channel/types';
import { Delete, Edit, Plus, Refresh, View } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { onMounted, onUnmounted, ref } from 'vue';
import {
clearPremiumModelCache,
createApp,
createModel,
deleteApp,
deleteModel,
getAppList,
getAppShortcutList,
getModelList,
updateApp,
updateModel,
} from '@/api/channel';
// 移动端检测
const isMobile = ref(false);
function checkMobile() {
isMobile.value = window.innerWidth < 768;
}
// ==================== 应用管理 ====================
const appList = ref<AiAppDto[]>([]);
const appLoading = ref(false);
const selectedAppId = ref<string>('');
const selectedAppIds = ref<string[]>([]);
// 快捷号池列表
const shortcutList = ref<AppShortcutDto[]>([]);
const shortcutLoading = ref(false);
// 应用对话框
const appDialogVisible = ref(false);
const appDialogTitle = ref('');
const appForm = ref<Partial<AiAppDto>>({});
const selectedShortcutId = ref<string>('');
const appDetailDialogVisible = ref(false);
const appDetailData = ref<AiAppDto | null>(null);
// 批量应用号池对话框
const batchApplyDialogVisible = ref(false);
const batchApplyShortcutId = ref<string>('');
// 号池列表对话框
const poolListDialogVisible = ref(false);
// 获取应用列表
async function fetchAppList() {
appLoading.value = true;
try {
const res = await getAppList({
skipCount: 0,
maxResultCount: 100,
});
appList.value = res.data.items;
// 默认选中第一个应用
if (appList.value.length > 0 && !selectedAppId.value) {
selectedAppId.value = appList.value[0].id;
fetchModelList();
}
}
catch (error: any) {
ElMessage.error(error.message || '获取应用列表失败');
}
finally {
appLoading.value = false;
}
}
// 获取快捷号池列表
async function fetchShortcutList() {
shortcutLoading.value = true;
try {
const res = await getAppShortcutList();
shortcutList.value = res.data;
}
catch (error: any) {
ElMessage.error(error.message || '获取快捷号池列表失败');
}
finally {
shortcutLoading.value = false;
}
}
// 选择应用
function handleSelectApp(appId: string) {
selectedAppId.value = appId;
fetchModelList();
}
// 查看应用详情
function handleViewAppDetail(app: AiAppDto) {
appDetailData.value = app;
appDetailDialogVisible.value = true;
}
// 打开应用对话框
function openAppDialog(type: 'create' | 'edit', row?: AiAppDto) {
appDialogTitle.value = type === 'create' ? '创建应用' : '编辑应用';
selectedShortcutId.value = '';
if (type === 'create') {
appForm.value = {
name: '',
endpoint: '',
extraUrl: '',
apiKey: '',
orderNum: 0,
};
}
else {
appForm.value = { ...row };
}
appDialogVisible.value = true;
}
// 选择快捷号池
function handleSelectShortcut() {
const shortcut = shortcutList.value.find(s => s.id === selectedShortcutId.value);
if (shortcut) {
appForm.value = {
...appForm.value,
endpoint: shortcut.endpoint,
extraUrl: shortcut.extraUrl || '',
apiKey: shortcut.apiKey,
// 不修改名称和排序,保持应用原有的配置
};
ElMessage.success('已自动填入号池配置终结点和API Key');
}
}
// 打开批量应用号池对话框
function openBatchApplyDialog() {
if (selectedAppIds.value.length === 0) {
ElMessage.warning('请先选择要批量操作的应用');
return;
}
batchApplyShortcutId.value = '';
batchApplyDialogVisible.value = true;
}
// 批量应用号池
async function handleBatchApply() {
if (!batchApplyShortcutId.value) {
ElMessage.warning('请选择号池');
return;
}
const shortcut = shortcutList.value.find(s => s.id === batchApplyShortcutId.value);
if (!shortcut) {
ElMessage.error('未找到选择的号池');
return;
}
try {
// 批量更新应用只更新终结点和API Key不修改应用名称
const promises = selectedAppIds.value.map((appId) => {
// 获取当前应用信息,保留原有名称和排序
const currentApp = appList.value.find(a => a.id === appId);
return updateApp({
id: appId,
name: currentApp?.name || '', // 保持原有应用名称
endpoint: shortcut.endpoint,
extraUrl: shortcut.extraUrl,
apiKey: shortcut.apiKey,
orderNum: currentApp?.orderNum || 0, // 保持原有排序
});
});
await Promise.all(promises);
ElMessage.success(`成功应用到 ${selectedAppIds.value.length} 个应用终结点和API Key已更新`);
batchApplyDialogVisible.value = false;
selectedAppIds.value = [];
fetchAppList();
}
catch (error: any) {
ElMessage.error(error.message || '批量应用失败');
}
}
// 处理应用选择变化
function handleSelectionChange(selection: AiAppDto[]) {
selectedAppIds.value = selection.map(item => item.id);
}
// 切换应用选择
function toggleAppSelection(appId: string) {
const index = selectedAppIds.value.indexOf(appId);
if (index > -1) {
selectedAppIds.value.splice(index, 1);
}
else {
selectedAppIds.value.push(appId);
}
}
// 保存应用
async function saveApp() {
try {
if (appForm.value.id) {
await updateApp(appForm.value as any);
ElMessage.success('更新成功');
}
else {
await createApp(appForm.value as any);
ElMessage.success('创建成功');
}
appDialogVisible.value = false;
fetchAppList();
}
catch (error: any) {
ElMessage.error(error.message || '保存失败');
}
}
// 删除应用
async function handleDeleteApp(row: AiAppDto) {
try {
await ElMessageBox.confirm('确定要删除该应用吗?删除后该应用下的所有模型将无法使用。', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await deleteApp(row.id);
ElMessage.success('删除成功');
// 如果删除的是当前选中的应用,清空选中状态
if (selectedAppId.value === row.id) {
selectedAppId.value = '';
modelList.value = [];
}
fetchAppList();
}
catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败');
}
}
}
// ==================== 模型管理 ====================
const modelList = ref<AiModelDto[]>([]);
const modelLoading = ref(false);
const modelSearchKey = ref('');
const modelDialogVisible = ref(false);
const modelDialogTitle = ref('');
const modelForm = ref<Partial<AiModelDto>>({});
// 获取模型列表
async function fetchModelList() {
if (!selectedAppId.value) {
modelList.value = [];
return;
}
modelLoading.value = true;
try {
const res = await getModelList({
aiAppId: selectedAppId.value,
searchKey: modelSearchKey.value,
skipCount: 0,
maxResultCount: 100,
});
modelList.value = res.data.items;
}
catch (error: any) {
ElMessage.error(error.message || '获取模型列表失败');
}
finally {
modelLoading.value = false;
}
}
// 打开模型对话框
function openModelDialog(type: 'create' | 'edit', row?: AiModelDto) {
if (!selectedAppId.value) {
ElMessage.warning('请先选择一个应用');
return;
}
modelDialogTitle.value = type === 'create' ? '创建模型' : '编辑模型';
if (type === 'create') {
modelForm.value = {
handlerName: '',
modelId: '',
name: '',
description: '',
orderNum: 0,
aiAppId: selectedAppId.value,
extraInfo: '',
modelType: 0,
modelApiType: 0,
multiplier: 1,
multiplierShow: 1,
providerName: '',
iconUrl: '',
isPremium: false,
isEnabled: true,
};
}
else {
modelForm.value = { ...row };
}
modelDialogVisible.value = true;
}
// 保存模型
async function saveModel() {
try {
if (modelForm.value.id) {
await updateModel(modelForm.value as any);
ElMessage.success('更新成功');
}
else {
await createModel(modelForm.value as any);
ElMessage.success('创建成功');
}
modelDialogVisible.value = false;
fetchModelList();
}
catch (error: any) {
ElMessage.error(error.message || '保存失败');
}
}
// 删除模型
async function handleDeleteModel(row: AiModelDto) {
try {
await ElMessageBox.confirm('确定要删除该模型吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await deleteModel(row.id);
ElMessage.success('删除成功');
fetchModelList();
}
catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败');
}
}
}
// 清理尊享模型缓存
async function handleClearCache() {
try {
await clearPremiumModelCache();
ElMessage.success('缓存清理成功');
}
catch (error: any) {
ElMessage.error(error.message || '缓存清理失败');
}
}
// 初始化
onMounted(() => {
checkMobile();
window.addEventListener('resize', checkMobile);
fetchAppList();
fetchShortcutList();
});
onUnmounted(() => {
window.removeEventListener('resize', checkMobile);
});
</script>
<template>
<div class="channel-management">
<div class="channel-container" :class="{ 'mobile-view': isMobile }">
<!-- 左侧应用列表 -->
<div class="app-list-panel">
<div class="panel-header">
<h3>应用列表</h3>
<div class="header-actions">
<el-button
v-if="selectedAppIds.length > 0"
type="warning"
size="small"
@click="openBatchApplyDialog"
>
<span v-if="!isMobile">批量应用号池</span>
<span v-else>批量</span>
<template v-if="!isMobile">
({{ selectedAppIds.length }})
</template>
</el-button>
<el-button
type="info"
size="small"
@click="poolListDialogVisible = true"
>
<span v-if="!isMobile">查看号池列表</span>
<span v-else>号池</span>
</el-button>
<el-button type="primary" size="small" :icon="Plus" @click="openAppDialog('create')">
新建
</el-button>
</div>
</div>
<el-scrollbar class="app-list-scrollbar">
<div v-loading="appLoading" class="app-list">
<div
v-for="app in appList"
:key="app.id"
class="app-item"
:class="{ active: selectedAppId === app.id }"
>
<el-checkbox
:model-value="selectedAppIds.includes(app.id)"
@change="() => toggleAppSelection(app.id)"
@click.stop
/>
<div class="app-item-content" @click="handleSelectApp(app.id)">
<div class="app-name">
{{ app.name }}
</div>
<div class="app-actions">
<el-button
link
type="primary"
size="small"
:icon="isMobile ? undefined : View"
@click.stop="handleViewAppDetail(app)"
>
{{ isMobile ? '详情' : '' }}
</el-button>
<el-button
link
type="primary"
size="small"
:icon="isMobile ? undefined : Edit"
@click.stop="openAppDialog('edit', app)"
>
{{ isMobile ? '编辑' : '' }}
</el-button>
<el-button
link
type="danger"
size="small"
:icon="isMobile ? undefined : Delete"
@click.stop="handleDeleteApp(app)"
>
{{ isMobile ? '删除' : '' }}
</el-button>
</div>
</div>
</div>
<el-empty v-if="!appLoading && appList.length === 0" description="暂无应用" />
</div>
</el-scrollbar>
</div>
<!-- 右侧模型列表 -->
<div class="model-list-panel">
<div class="panel-header">
<h3>模型列表</h3>
<div class="header-actions">
<el-input
v-model="modelSearchKey"
:placeholder="isMobile ? '搜索' : '搜索模型'"
:style="isMobile ? 'width: 120px; margin-right: 8px' : 'width: 200px; margin-right: 10px'"
clearable
@keyup.enter="fetchModelList"
/>
<el-button type="warning" size="small" @click="handleClearCache">
{{ isMobile ? '' : '清理缓存' }}
</el-button>
<el-button type="primary" size="small" :icon="Plus" @click="openModelDialog('create')">
{{ isMobile ? '' : '新建' }}
</el-button>
<el-button size="small" :icon="Refresh" @click="fetchModelList">
{{ isMobile ? '' : '刷新' }}
</el-button>
</div>
</div>
<div v-if="!selectedAppId" class="empty-tip">
<el-empty :description="isMobile ? '请选择应用' : '请先选择左侧的应用'" />
</div>
<div v-else class="table-wrapper">
<el-table
v-loading="modelLoading"
:data="modelList"
border
stripe
:height="isMobile ? 'calc(100vh - 300px)' : 'calc(100vh - 220px)'"
>
<el-table-column prop="name" :label="isMobile ? '名称' : '模型名称'" min-width="120" />
<el-table-column v-if="!isMobile" prop="modelId" label="模型ID" min-width="180" show-overflow-tooltip />
<el-table-column v-if="!isMobile" prop="handlerName" label="处理名" min-width="100" />
<el-table-column v-if="!isMobile" prop="providerName" label="供应商" width="90" />
<el-table-column :label="isMobile ? '尊享' : '是否尊享'" width="80">
<template #default="{ row }">
<el-tag :type="row.isPremium ? 'warning' : 'info'" size="small">
{{ row.isPremium ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="isMobile ? '状态' : '是否启用'" width="80">
<template #default="{ row }">
<el-tag :type="row.isEnabled ? 'success' : 'danger'" size="small">
{{ row.isEnabled ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="!isMobile" prop="multiplierShow" label="显示倍率" width="90" />
<el-table-column v-if="!isMobile" prop="multiplier" label="倍率" width="80" />
<el-table-column v-if="!isMobile" prop="orderNum" label="排序" width="70" />
<el-table-column label="操作" :width="isMobile ? 120 : 150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openModelDialog('edit', row)">
{{ isMobile ? '' : '编辑' }}
</el-button>
<el-button link type="danger" size="small" @click="handleDeleteModel(row)">
{{ isMobile ? '' : '删除' }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<!-- 应用详情对话框 -->
<el-dialog v-model="appDetailDialogVisible" title="应用详情" :width="isMobile ? '90%' : '600px'">
<el-descriptions v-if="appDetailData" :column="isMobile ? 1 : 1" border>
<el-descriptions-item label="应用名称">
{{ appDetailData.name }}
</el-descriptions-item>
<el-descriptions-item label="终结点">
{{ appDetailData.endpoint }}
</el-descriptions-item>
<el-descriptions-item label="额外URL">
{{ appDetailData.extraUrl || '-' }}
</el-descriptions-item>
<el-descriptions-item label="API Key">
<el-input :model-value="appDetailData.apiKey" type="textarea" readonly />
</el-descriptions-item>
<el-descriptions-item label="排序">
{{ appDetailData.orderNum }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ appDetailData.creationTime }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
<!-- 应用编辑对话框 -->
<el-dialog v-model="appDialogVisible" :title="appDialogTitle" :width="isMobile ? '90%' : '650px'">
<el-form :model="appForm" :label-width="isMobile ? '80px' : '120px'">
<el-form-item label="选择号池">
<el-select
v-model="selectedShortcutId"
placeholder="选择号池自动填入配置"
style="width: 100%"
clearable
@change="handleSelectShortcut"
>
<el-option
v-for="shortcut in shortcutList"
:key="shortcut.id"
:label="shortcut.name"
:value="shortcut.id"
>
<div style="display: flex; justify-content: space-between; align-items: center">
<span>{{ shortcut.name }}</span>
<span style="font-size: 12px; color: #999; margin-left: 10px">{{ shortcut.endpoint }}</span>
</div>
</el-option>
</el-select>
<div style="font-size: 12px; color: #999; margin-top: 5px">
选择号池可自动填入终结点和 API Key
</div>
</el-form-item>
<el-divider />
<el-form-item label="应用名称" required>
<el-input v-model="appForm.name" placeholder="请输入应用名称" />
</el-form-item>
<el-form-item label="终结点" required>
<el-input v-model="appForm.endpoint" placeholder="请输入应用终结点URL" />
</el-form-item>
<el-form-item label="额外URL">
<el-input v-model="appForm.extraUrl" placeholder="请输入额外URL可选" />
</el-form-item>
<el-form-item label="API Key" required>
<el-input v-model="appForm.apiKey" type="textarea" placeholder="请输入API Key" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="appForm.orderNum" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="appDialogVisible = false">
取消
</el-button>
<el-button type="primary" @click="saveApp">
保存
</el-button>
</template>
</el-dialog>
<!-- 批量应用号池对话框 -->
<el-dialog v-model="batchApplyDialogVisible" title="批量应用号池" :width="isMobile ? '90%' : '700px'">
<el-alert
:title="`已选择 ${selectedAppIds.length} 个应用,将统一应用以下号池配置`"
type="info"
:closable="false"
style="margin-bottom: 15px"
/>
<el-form :label-width="isMobile ? '80px' : '100px'">
<el-form-item label="选中的应用">
<div style="max-height: 100px; overflow-y: auto">
<el-tag
v-for="appId in selectedAppIds"
:key="appId"
style="margin: 4px"
size="default"
>
{{ appList.find(a => a.id === appId)?.name || appId }}
</el-tag>
</div>
</el-form-item>
<el-form-item label="选择号池" required>
<el-select
v-model="batchApplyShortcutId"
placeholder="请选择号池"
style="width: 100%"
>
<el-option
v-for="shortcut in shortcutList"
:key="shortcut.id"
:label="shortcut.name"
:value="shortcut.id"
>
<div style="display: flex; justify-content: space-between; align-items: center">
<span>{{ shortcut.name }}</span>
<span style="font-size: 12px; color: #999; margin-left: 10px">{{ shortcut.endpoint }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item v-if="batchApplyShortcutId" label="号池详情">
<div
v-if="shortcutList.find(s => s.id === batchApplyShortcutId)"
style="background: #f5f7fa; padding: 15px; border-radius: 4px"
>
<div><strong>名称</strong>{{ shortcutList.find(s => s.id === batchApplyShortcutId)?.name }}</div>
<div style="margin-top: 8px">
<strong>终结点</strong>{{ shortcutList.find(s => s.id === batchApplyShortcutId)?.endpoint }}
</div>
<div style="margin-top: 8px">
<strong>API Key</strong>{{ shortcutList.find(s => s.id === batchApplyShortcutId)?.apiKey?.substring(0, 20) }}***
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="batchApplyDialogVisible = false">
取消
</el-button>
<el-button type="primary" @click="handleBatchApply">
确定应用
</el-button>
</template>
</el-dialog>
<!-- 号池列表对话框 -->
<el-dialog v-model="poolListDialogVisible" title="号池列表" :width="isMobile ? '95%' : '900px'">
<div class="pool-table-wrapper">
<el-table :data="shortcutList" border stripe>
<el-table-column prop="name" label="号池名称" :min-width="isMobile ? 120 : 180" />
<el-table-column prop="endpoint" label="终结点" :min-width="isMobile ? 180 : 250" show-overflow-tooltip />
<el-table-column v-if="!isMobile" prop="extraUrl" label="额外URL" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
{{ row.extraUrl || '-' }}
</template>
</el-table-column>
<el-table-column prop="apiKey" label="API Key" :min-width="isMobile ? 150 : 200" show-overflow-tooltip>
<template #default="{ row }">
{{ row.apiKey?.substring(0, isMobile ? 15 : 30) }}{{ row.apiKey?.length > (isMobile ? 15 : 30) ? '...' : '' }}
</template>
</el-table-column>
<el-table-column v-if="!isMobile" prop="orderNum" label="排序" width="80" />
<el-table-column v-if="!isMobile" prop="creationTime" label="创建时间" width="160" />
</el-table>
</div>
<template #footer>
<el-button type="primary" @click="poolListDialogVisible = false">
关闭
</el-button>
</template>
</el-dialog>
<!-- 模型编辑对话框 -->
<el-dialog v-model="modelDialogVisible" :title="modelDialogTitle" :width="isMobile ? '90%' : '700px'">
<el-form :model="modelForm" :label-width="isMobile ? '80px' : '120px'">
<el-form-item label="模型名称" required>
<el-input v-model="modelForm.name" placeholder="请输入模型名称" />
</el-form-item>
<el-form-item label="模型ID" required>
<el-input v-model="modelForm.modelId" placeholder="请输入模型ID" />
</el-form-item>
<el-form-item label="处理名" required>
<el-input v-model="modelForm.handlerName" placeholder="请输入处理名" />
</el-form-item>
<el-form-item label="供应商名称">
<el-input v-model="modelForm.providerName" placeholder="如OpenAI、Anthropic等" />
</el-form-item>
<el-form-item label="模型描述">
<el-input v-model="modelForm.description" type="textarea" placeholder="请输入模型描述" />
</el-form-item>
<el-form-item label="是否尊享模型">
<el-switch v-model="modelForm.isPremium" />
</el-form-item>
<el-form-item label="是否启用">
<el-switch v-model="modelForm.isEnabled" />
</el-form-item>
<el-form-item label="模型倍率">
<el-input-number v-model="modelForm.multiplier" :min="0" :step="0.1" />
</el-form-item>
<el-form-item label="显示倍率">
<el-input-number v-model="modelForm.multiplierShow" :min="0" :step="0.1" />
</el-form-item>
<el-form-item label="模型类型" required>
<el-select v-model="modelForm.modelType" placeholder="请选择模型类型">
<el-option label="聊天" :value="0" />
<el-option label="图片" :value="1" />
<el-option label="嵌入" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="API类型" required>
<el-select v-model="modelForm.modelApiType" placeholder="请选择API类型">
<el-option label="OpenAI" :value="0" />
<el-option label="Claude" :value="1" />
<el-option label="Response" :value="2" />
<el-option label="GenerateContent" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="图标URL">
<el-input v-model="modelForm.iconUrl" placeholder="请输入模型图标URL" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="modelForm.orderNum" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="modelDialogVisible = false">
取消
</el-button>
<el-button type="primary" @click="saveModel">
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.channel-management {
height: 100vh;
padding: 16px;
box-sizing: border-box;
background: #f5f7fa;
.channel-container {
display: flex;
gap: 16px;
height: 100%;
overflow: hidden;
&.mobile-view {
flex-direction: column;
overflow-y: auto;
}
}
.app-list-panel {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
min-width: 280px;
flex-shrink: 0;
.channel-container.mobile-view & {
width: 100%;
max-height: 50%;
}
}
.model-list-panel {
flex: 1;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
.channel-container.mobile-view & {
flex: none;
height: 50%;
}
}
.panel-header {
padding: 16px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
gap: 12px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
white-space: nowrap;
}
.header-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
}
.app-list-scrollbar {
flex: 1;
height: 0;
overflow: hidden;
:deep(.el-scrollbar__wrap) {
overflow-x: hidden;
}
}
.app-list {
padding: 12px;
}
.app-item {
padding: 12px;
margin-bottom: 8px;
border: 1px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.25s ease;
display: flex;
align-items: flex-start;
gap: 10px;
&:hover {
border-color: #409eff;
background: #f0f9ff;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
}
&.active {
border-color: #409eff;
background: #ecf5ff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
}
.app-item-content {
flex: 1;
min-width: 0;
.app-name {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: #303133;
word-break: break-word;
}
.app-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
}
}
.empty-tip {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.table-wrapper {
flex: 1;
overflow: hidden;
padding: 16px;
}
.pool-table-wrapper {
overflow-x: auto;
max-width: 100%;
}
}
// 移动端适配
@media (max-width: 768px) {
.channel-management {
padding: 8px;
.channel-container {
gap: 8px;
}
.panel-header {
padding: 12px;
flex-wrap: wrap;
h3 {
font-size: 14px;
}
.header-actions {
gap: 6px;
.el-button {
padding: 6px 10px;
font-size: 12px;
}
}
}
.app-list {
padding: 8px;
}
.app-item {
padding: 10px;
margin-bottom: 6px;
.app-item-content {
.app-name {
font-size: 13px;
margin-bottom: 6px;
}
.app-actions {
.el-button {
padding: 4px 6px;
font-size: 12px;
}
}
}
}
.table-wrapper {
padding: 8px;
}
}
}
// 小屏幕优化
@media (max-width: 480px) {
.channel-management {
.channel-container {
gap: 6px;
}
.app-list-panel,
.model-list-panel {
border-radius: 6px;
}
.panel-header {
padding: 10px;
}
.app-list {
padding: 6px;
}
.app-item {
padding: 8px;
}
}
}
</style>