boss 1 gün önce
ebeveyn
işleme
45beb67a09

+ 84 - 0
src/api/admin/aiModel.js

@@ -0,0 +1,84 @@
+import request from '@/utils/request'
+
+const BASE = '/admin/aiModel'
+const SCENE_BASE = '/admin/aiScene'
+
+// ─── 模型管理 ───
+
+export function listAiModel() {
+  return request({ url: BASE + '/list', method: 'get' })
+}
+
+export function getAiModel(id) {
+  return request({ url: BASE + '/' + id, method: 'get' })
+}
+
+export function addAiModel(data) {
+  return request({ url: BASE, method: 'post', data })
+}
+
+export function updateAiModel(id, data) {
+  return request({ url: BASE + '/' + id, method: 'put', data })
+}
+
+export function deleteAiModel(id) {
+  return request({ url: BASE + '/' + id, method: 'delete' })
+}
+
+export function batchSortAiModel(sortList) {
+  return request({ url: BASE + '/batchSort', method: 'put', data: sortList })
+}
+
+export function testAiModel(id) {
+  return request({ url: BASE + '/test/' + id, method: 'post' })
+}
+
+export function refreshAiModel() {
+  return request({ url: BASE + '/refresh', method: 'post' })
+}
+
+// ─── 场景管理 ───
+
+export function listAiScene() {
+  return request({ url: SCENE_BASE + '/list', method: 'get' })
+}
+
+export function getAiScene(sceneCode) {
+  return request({ url: SCENE_BASE + '/' + sceneCode, method: 'get' })
+}
+
+export function updateAiScene(sceneCode, data) {
+  return request({ url: SCENE_BASE + '/' + sceneCode, method: 'put', data })
+}
+
+export function updateSceneThreshold(sceneCode, qualityThreshold) {
+  return request({ url: SCENE_BASE + '/' + sceneCode + '/threshold', method: 'put', data: { qualityThreshold } })
+}
+
+export function getSceneModels(sceneCode) {
+  return request({ url: SCENE_BASE + '/' + sceneCode + '/models', method: 'get' })
+}
+
+export function getSceneEnabledModels(sceneCode) {
+  return request({ url: SCENE_BASE + '/' + sceneCode + '/enabledModels', method: 'get' })
+}
+
+export function addSceneModel(sceneCode, data) {
+  return request({ url: SCENE_BASE + '/' + sceneCode + '/models', method: 'post', data })
+}
+
+export function deleteSceneModel(sceneCode, id) {
+  return request({ url: SCENE_BASE + '/' + sceneCode + '/models/' + id, method: 'delete' })
+}
+
+export function clearSceneModels(sceneCode) {
+  return request({ url: SCENE_BASE + '/' + sceneCode + '/models', method: 'delete' })
+}
+
+export function updateSceneModel(sceneCode, id, data) {
+  return request({ url: SCENE_BASE + '/' + sceneCode + '/models/' + id, method: 'put', data })
+}
+
+export function refreshAiScene() {
+  return request({ url: SCENE_BASE + '/refresh', method: 'post' })
+}

+ 528 - 0
src/views/admin/aiModel/index.vue

@@ -0,0 +1,528 @@
+<template>
+  <div class="app-container">
+    <el-card>
+      <div slot="header" class="card-header">
+        <span>AI模型配置管理</span>
+        <div>
+          <el-button type="primary" size="small" @click="handleAddModel" v-if="activeTab === 'models'">
+            <i class="el-icon-plus"></i>新增模型
+          </el-button>
+          <el-button type="success" size="small" @click="handleRefresh">
+            <i class="el-icon-refresh"></i>刷新缓存
+          </el-button>
+        </div>
+      </div>
+
+      <el-alert
+        title="统一模型配置说明"
+        description="所有文本AI模型统一在此管理,按sort_order排序优先级(越小越优先)。每个场景可独立配置使用哪些模型及流水线顺序。FastGPT、TTS语音、豆包视觉及图像/语音相关模型不在此处管理。"
+        type="info" show-icon :closable="false" style="margin-bottom: 20px"
+      />
+
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+        <!-- ===== 模型列表 Tab ===== -->
+        <el-tab-pane label="模型列表" name="models">
+          <el-table :data="modelList" v-loading="modelLoading" border size="small" row-key="id"
+            style="width:100%">
+            <el-table-column label="排序" width="70" align="center">
+              <template slot-scope="{ row, $index }">
+                <span class="sort-handle" style="cursor: move; font-size: 18px;">☰</span>
+                <span style="margin-left: 4px;">{{ $index + 1 }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="modelName" label="模型名称" min-width="120" />
+            <el-table-column prop="providerCode" label="供应商" width="90" />
+            <el-table-column prop="modelIdentifier" label="模型标识" min-width="160" show-overflow-tooltip />
+            <el-table-column prop="apiEndpoint" label="API地址" min-width="200" show-overflow-tooltip />
+            <el-table-column prop="maxTokens" label="MaxToken" width="90" align="center" />
+            <el-table-column prop="temperature" label="温度" width="70" align="center" />
+            <el-table-column prop="sortOrder" label="排序号" width="70" align="center" />
+            <el-table-column prop="status" label="状态" width="70" align="center">
+              <template slot-scope="{ row }">
+                <el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
+                  {{ row.status === 1 ? '启用' : '禁用' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="280" fixed="right">
+              <template slot-scope="{ row }">
+                <el-button type="primary" size="mini" @click="handleEditModel(row)">编辑</el-button>
+                <el-button type="success" size="mini" @click="handleTestModel(row)">测试</el-button>
+                <el-popconfirm title="确定删除该模型吗?" @confirm="handleDeleteModel(row)">
+                  <el-button slot="reference" type="danger" size="mini">删除</el-button>
+                </el-popconfirm>
+              </template>
+            </el-table-column>
+          </el-table>
+
+          <div style="margin-top: 12px; text-align: right;">
+            <el-button type="warning" size="small" @click="handleSaveSort" :disabled="!sortChanged">
+              <i class="el-icon-sort"></i>保存排序
+            </el-button>
+          </div>
+        </el-tab-pane>
+
+        <!-- ===== 场景配置 Tab ===== -->
+        <el-tab-pane label="场景配置" name="scenes">
+          <el-row :gutter="16">
+            <!-- 左侧:场景列表 -->
+            <el-col :span="8">
+              <el-card shadow="never" class="scene-list-card">
+                <div slot="header" class="sub-header">使用场景</div>
+                <div v-loading="sceneLoading">
+                  <div v-for="scene in sceneList" :key="scene.sceneCode"
+                    class="scene-item"
+                    :class="{ active: selectedScene === scene.sceneCode }"
+                    @click="selectScene(scene)">
+                    <div class="scene-name">
+                      {{ scene.sceneName }}
+                      <el-tag size="mini" :type="scene.sceneType === 'multi_pipeline' ? 'warning' : 'info'" style="margin-left: 6px;">
+                        {{ scene.sceneType === 'multi_pipeline' ? '多模型' : '单模型' }}
+                      </el-tag>
+                    </div>
+                    <div class="scene-code">{{ scene.sceneCode }}</div>
+                  </div>
+                </div>
+              </el-card>
+            </el-col>
+
+            <!-- 右侧:场景详情 -->
+            <el-col :span="16">
+              <el-card shadow="never" v-if="selectedSceneInfo">
+                <div slot="header" class="sub-header">
+                  {{ selectedSceneInfo.sceneName }} - 模型配置
+                  <el-tag size="small" :type="selectedSceneInfo.status === 1 ? 'success' : 'danger'" style="margin-left: 8px;">
+                    {{ selectedSceneInfo.status === 1 ? '启用' : '禁用' }}
+                  </el-tag>
+                </div>
+
+                <!-- 场景基础信息 -->
+                <el-form :model="sceneForm" label-width="100px" size="small" inline>
+                  <el-form-item label="场景编码">{{ selectedSceneInfo.sceneCode }}</el-form-item>
+                  <el-form-item label="场景类型">
+                    {{ selectedSceneInfo.sceneType === 'multi_pipeline' ? '多模型流水线' : '单模型' }}
+                  </el-form-item>
+                  <el-form-item label="流水线类型" v-if="selectedSceneInfo.sceneType === 'multi_pipeline'">
+                    {{ selectedSceneInfo.pipelineType === 'scoring' ? '质量评分链' : selectedSceneInfo.pipelineType === 'sequential' ? '顺序调用' : '-' }}
+                  </el-form-item>
+                  <el-form-item label="质量阈值" v-if="selectedSceneInfo.pipelineType === 'scoring'">
+                    <el-input-number v-model="sceneForm.qualityThreshold" :min="60" :max="160" :step="10" size="mini" style="width: 100px;" />
+                    <el-button type="primary" size="mini" @click="saveThreshold" style="margin-left: 8px;">保存</el-button>
+                  </el-form-item>
+                </el-form>
+
+                <el-divider />
+
+                <!-- 场景关联模型列表 -->
+                <div style="margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
+                  <span>关联模型流水线(按 pipeline_order 排序)</span>
+                  <el-button type="primary" size="mini" @click="handleBindModel">
+                    <i class="el-icon-plus"></i>绑定模型
+                  </el-button>
+                </div>
+
+                <el-table :data="sceneModelList" v-loading="sceneModelLoading" border size="small"
+                  row-key="id" style="width:100%">
+                  <el-table-column label="顺序" width="60" align="center">
+                    <template slot-scope="{ $index }">
+                      <span class="sort-handle" style="cursor: move;">☰</span>
+                      <span style="margin-left: 4px;">{{ $index + 1 }}</span>
+                    </template>
+                  </el-table-column>
+                  <el-table-column label="模型名称" min-width="120">
+                    <template slot-scope="{ row }">
+                      {{ row.model ? row.model.modelName : '模型#' + row.modelId }}
+                    </template>
+                  </el-table-column>
+                  <el-table-column label="供应商" width="80">
+                    <template slot-scope="{ row }">
+                      {{ row.model ? row.model.providerCode : '-' }}
+                    </template>
+                  </el-table-column>
+                  <el-table-column label="流水线角色" width="110" align="center">
+                    <template slot-scope="{ row }">
+                      <el-tag size="mini" :type="row.role === 'scorer' ? 'warning' : 'success'">
+                        {{ row.role === 'scorer' ? '评分者' : '生成者' }}
+                      </el-tag>
+                    </template>
+                  </el-table-column>
+                  <el-table-column prop="pipelineOrder" label="顺序号" width="70" align="center" />
+                  <el-table-column prop="sortWeight" label="权重" width="60" align="center" />
+                  <el-table-column label="操作" width="120" fixed="right">
+                    <template slot-scope="{ row }">
+                      <el-button type="text" size="mini" @click="handleEditSceneModel(row)">编辑</el-button>
+                      <el-popconfirm title="确定移除此关联?" @confirm="handleRemoveSceneModel(row)">
+                        <el-button slot="reference" type="text" size="mini" style="color: #F56C6C;">移除</el-button>
+                      </el-popconfirm>
+                    </template>
+                  </el-table-column>
+                </el-table>
+                <div v-if="sceneModelList.length === 0" style="text-align: center; color: #999; padding: 40px 0;">
+                  暂未绑定模型,请点击"绑定模型"添加
+                </div>
+              </el-card>
+
+              <el-empty v-else description="请从左侧选择一个场景" />
+            </el-col>
+          </el-row>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+
+    <!-- 模型编辑弹窗 -->
+    <el-dialog :title="modelDialogTitle" :visible.sync="modelDialogVisible" width="560px" @closed="resetModelForm">
+      <el-form :model="modelForm" :rules="modelRules" ref="modelFormRef" label-width="120px">
+        <el-form-item label="模型名称" prop="modelName">
+          <el-input v-model="modelForm.modelName" placeholder="如:豆包Pro 32K" />
+        </el-form-item>
+        <el-form-item label="供应商" prop="providerCode">
+          <el-select v-model="modelForm.providerCode" placeholder="请选择" style="width: 100%">
+            <el-option label="豆包(Doubao)" value="doubao" />
+            <el-option label="通义千问(Qwen)" value="qwen" />
+            <el-option label="元宝(Yuanbao)" value="yuanbao" />
+            <el-option label="DeepSeek" value="deepseek" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="模型标识" prop="modelIdentifier">
+          <el-input v-model="modelForm.modelIdentifier" placeholder="如:doubao-1-5-pro-32k" />
+        </el-form-item>
+        <el-form-item label="API地址" prop="apiEndpoint">
+          <el-input v-model="modelForm.apiEndpoint" placeholder="API Endpoint" />
+        </el-form-item>
+        <el-form-item label="API Key" prop="apiKey">
+          <el-input v-model="modelForm.apiKey" placeholder="API Key" show-password />
+        </el-form-item>
+        <el-form-item label="最大Token">
+          <el-input-number v-model="modelForm.maxTokens" :min="512" :max="131072" :step="512" style="width: 180px;" />
+        </el-form-item>
+        <el-form-item label="温度参数">
+          <el-slider v-model="modelForm.temperature" :min="0" :max="2" :step="0.1" show-input style="width: 200px;" />
+        </el-form-item>
+        <el-form-item label="排序号">
+          <el-input-number v-model="modelForm.sortOrder" :min="0" :max="9999" style="width: 180px;" />
+          <span style="color: #909399; font-size: 12px; margin-left: 8px;">越小越优先</span>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-switch v-model="modelForm.status" :active-value="1" :inactive-value="0" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="modelDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmitModel">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 场景绑定模型弹窗 -->
+    <el-dialog title="绑定模型到场景" :visible.sync="bindDialogVisible" width="480px">
+      <el-form label-width="100px" size="small">
+        <el-form-item label="选择模型">
+          <el-select v-model="bindForm.modelId" placeholder="请选择模型" style="width: 100%" filterable>
+            <el-option v-for="m in modelList" :key="m.id"
+              :label="m.modelName + ' (' + m.providerCode + ')'" :value="m.id"
+              :disabled="m.status !== 1" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="流水线角色">
+          <el-radio-group v-model="bindForm.role">
+            <el-radio label="generator">生成者</el-radio>
+            <el-radio label="scorer">评分者</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="流水线顺序">
+          <el-input-number v-model="bindForm.pipelineOrder" :min="0" :max="99" size="small" />
+        </el-form-item>
+        <el-form-item label="权重">
+          <el-input-number v-model="bindForm.sortWeight" :min="0" :max="99" size="small" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="bindDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleConfirmBind">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 场景模型编辑弹窗 -->
+    <el-dialog title="编辑场景模型" :visible.sync="editSceneModelDialog" width="420px">
+      <el-form label-width="100px" size="small">
+        <el-form-item label="流水线角色">
+          <el-radio-group v-model="editSceneModelForm.role">
+            <el-radio label="generator">生成者</el-radio>
+            <el-radio label="scorer">评分者</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="流水线顺序">
+          <el-input-number v-model="editSceneModelForm.pipelineOrder" :min="0" :max="99" size="small" />
+        </el-form-item>
+        <el-form-item label="权重">
+          <el-input-number v-model="editSceneModelForm.sortWeight" :min="0" :max="99" size="small" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="editSceneModelDialog = false">取消</el-button>
+        <el-button type="primary" @click="handleUpdateSceneModel">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listAiModel, getAiModel, addAiModel, updateAiModel, deleteAiModel,
+  batchSortAiModel, testAiModel, refreshAiModel,
+  listAiScene, updateAiScene, updateSceneThreshold,
+  getSceneModels, addSceneModel, deleteSceneModel, updateSceneModel, refreshAiScene
+} from '@/api/admin/aiModel'
+
+export default {
+  name: 'AiModelConfig',
+  data() {
+    return {
+      activeTab: 'models',
+      // 模型
+      modelLoading: false,
+      modelList: [],
+      modelDialogVisible: false,
+      modelDialogTitle: '',
+      modelForm: {
+        id: null, modelName: '', providerCode: '', modelIdentifier: '',
+        apiEndpoint: '', apiKey: '', maxTokens: 4096, temperature: 0.7,
+        sortOrder: 0, status: 1
+      },
+      modelRules: {
+        modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
+        providerCode: [{ required: true, message: '请选择供应商', trigger: 'change' }],
+        modelIdentifier: [{ required: true, message: '请输入模型标识', trigger: 'blur' }],
+        apiEndpoint: [{ required: true, message: '请输入API地址', trigger: 'blur' }],
+        apiKey: [{ required: true, message: '请输入API Key', trigger: 'blur' }]
+      },
+      sortChanged: false,
+      // 场景
+      sceneLoading: false,
+      sceneList: [],
+      selectedScene: '',
+      selectedSceneInfo: null,
+      sceneForm: { qualityThreshold: 120 },
+      sceneModelLoading: false,
+      sceneModelList: [],
+      bindDialogVisible: false,
+      bindForm: { modelId: null, role: 'generator', pipelineOrder: 0, sortWeight: 0 },
+      editSceneModelDialog: false,
+      editSceneModelForm: { id: null, role: 'generator', pipelineOrder: 0, sortWeight: 0 }
+    }
+  },
+  created() {
+    this.loadModels()
+    this.loadScenes()
+  },
+  methods: {
+    // ─── 模型 CRUD ───
+    async loadModels() {
+      this.modelLoading = true
+      try {
+        const res = await listAiModel()
+        this.modelList = res.data || []
+      } catch (e) {
+        this.$message.error('获取模型列表失败')
+      } finally {
+        this.modelLoading = false
+      }
+    },
+    handleAddModel() {
+      this.modelDialogTitle = '新增模型'
+      this.modelForm = {
+        id: null, modelName: '', providerCode: '', modelIdentifier: '',
+        apiEndpoint: '', apiKey: '', maxTokens: 4096, temperature: 0.7,
+        sortOrder: 0, status: 1
+      }
+      this.modelDialogVisible = true
+      this.$nextTick(() => { this.$refs.modelFormRef && this.$refs.modelFormRef.resetFields() })
+    },
+    handleEditModel(row) {
+      this.modelDialogTitle = '编辑模型'
+      this.modelForm = { ...row }
+      this.modelDialogVisible = true
+    },
+    async handleSubmitModel() {
+      try {
+        await this.$refs.modelFormRef.validate()
+        if (this.modelForm.id) {
+          await updateAiModel(this.modelForm.id, this.modelForm)
+          this.$message.success('修改成功')
+        } else {
+          await addAiModel(this.modelForm)
+          this.$message.success('新增成功')
+        }
+        this.modelDialogVisible = false
+        await this.loadModels()
+      } catch (e) {
+        if (e !== false) this.$message.error('操作失败')
+      }
+    },
+    async handleDeleteModel(row) {
+      try {
+        await deleteAiModel(row.id)
+        this.$message.success('删除成功')
+        await this.loadModels()
+      } catch (e) {
+        this.$message.error('删除失败')
+      }
+    },
+    async handleTestModel(row) {
+      try {
+        const res = await testAiModel(row.id)
+        if (res.code === 200 && res.data) {
+          const result = res.data
+          if (result.success) {
+            this.$message.success('连接成功!响应:' + (result.response || 'OK'))
+          } else {
+            this.$message.error('连接失败:' + (result.error || '未知错误'))
+          }
+        } else {
+          this.$message.error('测试失败')
+        }
+      } catch (e) {
+        this.$message.error('测试请求失败')
+      }
+    },
+    handleSaveSort() {
+      const sortList = this.modelList.map((m, i) => ({ id: m.id, sortOrder: i }))
+      batchSortAiModel(sortList).then(() => {
+        this.$message.success('排序已保存')
+        this.sortChanged = false
+        this.loadModels()
+      }).catch(() => {
+        this.$message.error('保存排序失败')
+      })
+    },
+    resetModelForm() {
+      this.modelForm = {
+        id: null, modelName: '', providerCode: '', modelIdentifier: '',
+        apiEndpoint: '', apiKey: '', maxTokens: 4096, temperature: 0.7,
+        sortOrder: 0, status: 1
+      }
+    },
+
+    // ─── 场景 ───
+    async loadScenes() {
+      this.sceneLoading = true
+      try {
+        const res = await listAiScene()
+        this.sceneList = res.data || []
+      } catch (e) {
+        this.$message.error('获取场景列表失败')
+      } finally {
+        this.sceneLoading = false
+      }
+    },
+    selectScene(scene) {
+      this.selectedScene = scene.sceneCode
+      this.selectedSceneInfo = scene
+      this.sceneForm.qualityThreshold = scene.qualityThreshold || 120
+      this.loadSceneModels(scene.sceneCode)
+    },
+    async loadSceneModels(sceneCode) {
+      this.sceneModelLoading = true
+      try {
+        const res = await getSceneModels(sceneCode)
+        this.sceneModelList = res.data || []
+      } catch (e) {
+        this.$message.error('获取场景模型列表失败')
+      } finally {
+        this.sceneModelLoading = false
+      }
+    },
+    async saveThreshold() {
+      try {
+        await updateSceneThreshold(this.selectedScene, this.sceneForm.qualityThreshold)
+        this.$message.success('阈值已更新')
+        this.selectedSceneInfo.qualityThreshold = this.sceneForm.qualityThreshold
+      } catch (e) {
+        this.$message.error('更新失败')
+      }
+    },
+    handleBindModel() {
+      this.bindForm = { modelId: null, role: 'generator', pipelineOrder: this.sceneModelList.length, sortWeight: 0 }
+      this.bindDialogVisible = true
+    },
+    async handleConfirmBind() {
+      if (!this.bindForm.modelId) {
+        this.$message.warning('请选择模型')
+        return
+      }
+      try {
+        await addSceneModel(this.selectedScene, this.bindForm)
+        this.$message.success('绑定成功')
+        this.bindDialogVisible = false
+        this.loadSceneModels(this.selectedScene)
+      } catch (e) {
+        this.$message.error('绑定失败')
+      }
+    },
+    handleEditSceneModel(row) {
+      this.editSceneModelForm = {
+        id: row.id,
+        role: row.role || 'generator',
+        pipelineOrder: row.pipelineOrder || 0,
+        sortWeight: row.sortWeight || 0
+      }
+      this.editSceneModelDialog = true
+    },
+    async handleUpdateSceneModel() {
+      try {
+        await updateSceneModel(this.selectedScene, this.editSceneModelForm.id, this.editSceneModelForm)
+        this.$message.success('更新成功')
+        this.editSceneModelDialog = false
+        this.loadSceneModels(this.selectedScene)
+      } catch (e) {
+        this.$message.error('更新失败')
+      }
+    },
+    async handleRemoveSceneModel(row) {
+      try {
+        await deleteSceneModel(this.selectedScene, row.id)
+        this.$message.success('已移除')
+        this.loadSceneModels(this.selectedScene)
+      } catch (e) {
+        this.$message.error('移除失败')
+      }
+    },
+
+    // ─── 通用 ───
+    handleTabClick() {
+      if (this.activeTab === 'models') this.loadModels()
+      if (this.activeTab === 'scenes') this.loadScenes()
+    },
+    async handleRefresh() {
+      try {
+        await refreshAiModel()
+        await refreshAiScene()
+        this.$message.success('缓存已刷新')
+        if (this.activeTab === 'models') this.loadModels()
+        if (this.activeTab === 'scenes') this.loadScenes()
+      } catch (e) {
+        this.$message.error('刷新失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.sub-header { font-weight: 600; font-size: 14px; }
+.scene-list-card { max-height: 600px; overflow-y: auto; }
+.scene-item {
+  padding: 10px 12px;
+  border: 1px solid #EBEEF5;
+  border-radius: 4px;
+  margin-bottom: 8px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+.scene-item:hover { border-color: #409EFF; background: #ECF5FF; }
+.scene-item.active { border-color: #409EFF; background: #ECF5FF; }
+.scene-name { font-weight: 500; font-size: 13px; }
+.scene-code { color: #909399; font-size: 11px; margin-top: 2px; }
+.sort-handle { cursor: move; color: #909399; display: inline-block; vertical-align: middle; }
+.sort-handle:hover { color: #409EFF; }
+</style>

+ 1 - 1
src/views/admin/dashboard/index.vue

@@ -66,7 +66,7 @@ export default {
       quickLinks: [
         { title: '代理管理', path: '/admin/proxy', icon: 'el-icon-user', color: '#3b82f6' },
         { title: '租户管理', path: '/admin/company', icon: 'el-icon-office-building', color: '#10b981' },
-        { title: '模型配置', path: '/admin/aiProvider', icon: 'el-icon-cpu', color: '#8b5cf6' },
+        { title: 'AI模型配置', path: '/admin/aiModel', icon: 'el-icon-cpu', color: '#8b5cf6' },
         { title: '消费记录', path: '/admin/consumeRecord', icon: 'el-icon-coin', color: '#f59e0b' }
       ]
     }

+ 6 - 3
src/views/admin/menu.js

@@ -78,12 +78,15 @@ const adminRoutes = {
     { path: 'storeOrder', component: () => import('@/views/admin/storeOrder/index'), name: 'AdminStoreOrder', meta: { title: '销售订单' } },
     { path: 'article', component: () => import('@/views/admin/article/index'), name: 'AdminArticle', meta: { title: '文章管理' } },
 
-    // 12. 其他管理
+    // 12. AI模型管理(统一配置)
+    { path: 'aiModel', component: () => import('@/views/admin/aiModel/index'), name: 'AdminAiModel', meta: { title: 'AI模型配置' } },
+    { path: 'aiProvider', component: () => import('@/views/admin/aiProvider/index'), name: 'AdminAiProvider', meta: { title: '大模型管理(旧)' } },
+
+    // 13. 其他管理
     { path: 'ipadServer', component: () => import('@/views/admin/ipadServer/index'), name: 'AdminIpadServer', meta: { title: 'Ipad服务器' } },
     { path: 'keywordManage', component: () => import('@/views/admin/keywordManage/index'), name: 'AdminKeywordManage', meta: { title: '关键词管理' } },
-    { path: 'textModel', component: () => import('@/views/admin/textModel/index'), name: 'AdminTextModel', meta: { title: '文本模型配置' } },
 
-    // 13. Lobster 引擎(挂在 /admin 下,避免 /workflow 代理到 8006 导致 401)
+    // 14. Lobster 引擎(挂在 /admin 下,避免 /workflow 代理到 8006 导致 401)
     { path: 'workflowGenerate', component: () => import('@/views/lobster/workflow-generate/index'), name: 'AdminWorkflowGenerate', meta: { title: 'AI生成工作流' } },
     { path: 'salesCorpus', component: () => import('@/views/lobster/sales-corpus/index'), name: 'AdminSalesCorpus', meta: { title: '销冠语料学习' } }
   ]