瀏覽代碼

场景需模特

kongwenhao 3 月之前
父節點
當前提交
09b21f2cd7

+ 5 - 0
frontend/src/apis/other.ts

@@ -7,5 +7,10 @@ export async function getCompanyTemplatesApi(){
     return GET('/api/ai_image/auto_photo/get_company_templates')
 }
 
+// 获取模特列表
+export async function getShoesModelTemplateApi(params: { status: number }){
+    return GET('/api/ai_image/main/shoes_model_template', params)
+}
+
 
 

+ 475 - 0
frontend/src/components/ModelGeneration/index.vue

@@ -0,0 +1,475 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="选择模特" width="1000px" :close-on-click-modal="false"
+    :close-on-press-escape="false"  custom-class="model-generation-dialog" @close="handleClose">
+    <div class="model-generation-container">
+      <!-- 主要内容区域 -->
+      <div class="main-content">
+
+        <!-- 左侧:女模特选择 -->
+        <div class="model-section">
+          <h2>女模特</h2>
+          <div class="model-display">
+            <el-image v-if="selectedFemaleModel" :src="selectedFemaleModel.image_url" :alt="selectedFemaleModel.name"
+              class="selected-model-image" lazy :preview-src-list="[selectedFemaleModel.image_url]" fit="cover" />
+            <div v-else class="placeholder-image">
+              <span>请在下方列表选择</span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 右侧:男模特选择 -->
+        <div class="model-section">
+          <h2>男模特</h2>
+          <div class="model-display">
+            <el-image v-if="selectedMaleModel" :src="selectedMaleModel.image_url" :alt="selectedMaleModel.name"
+              class="selected-model-image" lazy :preview-src-list="[selectedMaleModel.image_url]" fit="cover" />
+            <div v-else class="placeholder-image">
+              <span>请在下方列表选择</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 底部模特列表区域 -->
+      <div class="model-list-section">
+        <!-- 标签页切换 -->
+        <div class="tabs-container">
+          <div class="tab-item" :class="{ active: activeTab === 'female' }" @click="activeTab = 'female'">
+            女模特
+          </div>
+          <div class="tab-item" :class="{ active: activeTab === 'male' }" @click="activeTab = 'male'">
+            男模特
+          </div>
+        </div>
+
+        <!-- 模特网格列表 -->
+        <div class="model-grid">
+          <div v-for="model in currentModelList" :key="model.id" class="model-item"
+            :class="{ selected: isModelSelected(model) }" @click="selectModel(model)">
+            <el-image :src="model.image_url" :alt="model.name" class="model-thumbnail" lazy fit="cover" />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleCancel">取消</el-button>
+        <el-button type="primary" @click="handleConfirm" :disabled="!canConfirm">
+          确认
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getShoesModelTemplateApi } from '@/apis/other'
+
+// 定义组件的 props
+interface Props {
+  modelValue: boolean
+}
+
+const props = defineProps<Props>()
+
+// 定义组件的事件
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean]
+  confirm: [models: { female: any; male: any }]
+  cancel: []
+}>()
+
+// Dialog 显示状态
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 模特数据类型定义
+interface ModelData {
+  id: number
+  name: string
+  image_url: string
+  gender: 'male' | 'female'
+  keywords: string
+  status: number
+}
+
+// 当前激活的标签页
+const activeTab = ref<'female' | 'male'>('female')
+
+// 选中的女模特
+const selectedFemaleModel = ref<ModelData | null>(null)
+
+// 选中的男模特
+const selectedMaleModel = ref<ModelData | null>(null)
+
+// 女模特列表
+const femaleModels = ref<ModelData[]>([])
+
+// 男模特列表
+const maleModels = ref<ModelData[]>([])
+
+// 当前显示的模特列表
+const currentModelList = computed(() => {
+  return activeTab.value === 'female' ? femaleModels.value : maleModels.value
+})
+
+// 是否可以确认选择
+const canConfirm = computed(() => {
+  return selectedFemaleModel.value || selectedMaleModel.value
+})
+
+// 选择模特
+const selectModel = (model: ModelData) => {
+  if (model.keywords === '女性') {
+    selectedFemaleModel.value = model
+  } else {
+    selectedMaleModel.value = model
+  }
+}
+
+// 判断模特是否被选中
+const isModelSelected = (model: ModelData) => {
+  if (model.keywords === '女性') {
+    return selectedFemaleModel.value?.id === model.id
+  } else {
+    return selectedMaleModel.value?.id === model.id
+  }
+}
+
+// 确认选择
+const handleConfirm = () => {
+  // 只传递必要的数据字段,避免序列化问题
+  const selectedModels = {
+    female: selectedFemaleModel.value ? {
+      id: selectedFemaleModel.value.id,
+      name: selectedFemaleModel.value.name,
+      image_url: selectedFemaleModel.value.image_url,
+      gender: selectedFemaleModel.value.gender,
+      keywords: selectedFemaleModel.value.keywords,
+      status: selectedFemaleModel.value.status
+    } : null,
+    male: selectedMaleModel.value ? {
+      id: selectedMaleModel.value.id,
+      name: selectedMaleModel.value.name,
+      image_url: selectedMaleModel.value.image_url,
+      gender: selectedMaleModel.value.gender,
+      keywords: selectedMaleModel.value.keywords,
+      status: selectedMaleModel.value.status
+    } : null
+  }
+
+  console.log('选中的模特:', selectedModels)
+
+  // 通过事件将数据发送给父组件
+  emit('confirm', selectedModels)
+  dialogVisible.value = false
+}
+
+// 取消选择
+const handleCancel = () => {
+  emit('cancel')
+  dialogVisible.value = false
+}
+
+// 关闭弹窗
+const handleClose = () => {
+  emit('cancel')
+}
+
+// 获取模特列表
+const fetchModelList = async () => {
+  try {
+    const response = await getShoesModelTemplateApi({ status: 2 })
+    console.log(response)
+    if (response && response.data) {
+      // 根据性别分类模特
+      femaleModels.value = response.data.filter((model: ModelData) => model.keywords === '女性')
+      maleModels.value = response.data.filter((model: ModelData) => model.keywords === '男性')
+
+      // 预加载前几个图片以提高性能
+      setTimeout(() => {
+        preloadImages(femaleModels.value.slice(0, 10))
+        preloadImages(maleModels.value.slice(0, 10))
+      }, 100)
+    }
+  } catch (error) {
+    console.error('获取模特列表失败:', error)
+    ElMessage.error('获取模特列表失败')
+  }
+}
+
+// 预加载图片
+const preloadImages = (models: ModelData[]) => {
+  models.forEach(model => {
+    if (model.image_url) {
+      const img = new Image()
+      img.src = model.image_url
+    }
+  })
+}
+
+// 组件挂载时的初始化
+onMounted(() => {
+  console.log('模特生成页面已加载')
+  fetchModelList()
+})
+</script>
+
+<style lang="scss" scoped>
+.model-generation-container {
+  padding: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+.page-header {
+  text-align: center;
+  padding: 20px 0;
+  background: #ffffff;
+  border-bottom: 1px solid #e4e7ed;
+
+  h1 {
+    color: #303133;
+    margin: 0;
+    font-size: 20px;
+    font-weight: 500;
+  }
+}
+
+.main-content {
+  display: flex;
+  gap: 15px;
+  padding: 15px;
+  position: relative;
+  max-width: 50%;
+  margin: 0 auto;
+}
+
+.model-section {
+  flex: 1;
+  border-radius: 6px;
+  overflow: hidden;
+
+  h2 {
+    color: #303133;
+    margin: 0;
+    padding: 8px;
+    font-size: 13px;
+    font-weight: 550;
+    text-align: center;
+  }
+}
+
+.model-display {
+  width: 100%;
+  aspect-ratio: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  background: #f8f9fa;
+  border: 2px solid #e4e7ed;
+  border-radius: 4px;
+
+  .selected-model-image {
+    width: 100%;
+    height: 100%;
+
+    :deep(.el-image__inner) {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+  }
+
+  .placeholder-image {
+    color: #c0c4cc;
+    font-size: 11px;
+    text-align: center;
+
+    span {
+      color: #c0c4cc;
+      display: block;
+      line-height: 1.2;
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 15px;
+}
+
+
+
+.model-list-section {
+  margin: 2px 0px 0px 0;
+  overflow: hidden;
+}
+
+.tabs-container {
+  display: flex;
+  /* background: #f8f9fa; */
+  border-bottom: 1px solid #e4e7ed;
+  align-items: center;
+  justify-content: center;
+}
+
+.tab-item {
+  padding: 8px 16px;
+  cursor: pointer;
+  font-size: 13px;
+  font-weight: 550;
+  color: #606266;
+  border-bottom: 2px solid transparent;
+  transition: all 0.2s ease;
+  background: transparent;
+
+  &:hover {
+    color: #409eff;
+
+  }
+
+  &.active {
+    color: #409eff;
+    border-bottom-color: #409eff;
+  }
+}
+
+.model-grid {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  gap: 6px;
+  padding: 12px;
+  max-height: 320px;
+  overflow-y: auto;
+  margin-top: 8px;
+}
+
+.model-item {
+  aspect-ratio: 1;
+  border: 1px solid #e4e7ed;
+  border-radius: 3px;
+  overflow: hidden;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  background: #f8f9fa;
+
+  &:hover {
+    border-color: #409eff;
+    transform: scale(1.01);
+  }
+
+  &.selected {
+    border-color: #409eff;
+    border-width: 2px;
+    box-shadow: 0 0 0 1px #409eff;
+  }
+
+  .model-thumbnail {
+    width: 100%;
+    height: 100%;
+
+    :deep(.el-image__inner) {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+
+    :deep(.el-image__placeholder) {
+      background: #f8f9fa;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #c0c4cc;
+      font-size: 12px;
+    }
+
+    :deep(.el-image__error) {
+      background: #fef0f0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #f56c6c;
+      font-size: 12px;
+    }
+  }
+}
+
+// 滚动条样式
+.model-grid::-webkit-scrollbar {
+  width: 6px;
+}
+
+.model-grid::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.model-grid::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.model-grid::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .main-content {
+    flex-direction: column;
+    gap: 20px;
+    padding: 20px;
+  }
+
+  .model-grid {
+    grid-template-columns: repeat(4, 1fr);
+    gap: 10px;
+    padding: 15px;
+  }
+
+  .confirm-button-container {
+    position: static;
+    text-align: center;
+    margin-top: 20px;
+  }
+
+  .model-list-section {
+    margin: 0 20px 20px;
+  }
+}
+</style>
+
+<style lang="scss">
+.model-generation-dialog {
+  .el-dialog__body {
+    padding: 0;
+    background: #EAECED;
+  }
+
+ .el-dialog__footer {
+    padding: 0;
+    border-top: 1px solid #e4e7ed;
+    background: #fafafa;
+  }
+
+  .el-dialog {
+    border-radius: 8px;
+  }
+
+  .el-dialog__header {
+  }
+
+  .el-dialog__title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #303133;
+  }
+}</style>

+ 249 - 0
frontend/src/components/ScenePromptDialog/index.vue

@@ -0,0 +1,249 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="场景图生成" width="600px" :close-on-click-modal="false"
+    :close-on-press-escape="false" custom-class="scene-prompt-dialog" @close="handleClose">
+    <div class="scene-prompt-container">
+      <!-- 场景提示词输入区域 -->
+      <div class="input-section">
+        <div class="input-wrapper">
+          <el-input v-model="scenePrompt" type="textarea" :rows="8" placeholder="请输入场景提示词" class="scene-input"
+            resize="none" maxlength="500" show-word-limit />
+          <!-- AI帮我写按钮 -->
+          <div class="ai-help-button-container">
+            <el-button type="primary" size="small" @click="handleAIHelp" :loading="aiLoading" class="ai-help-button">
+              <el-icon class="ai-icon">
+                <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
+                  <path
+                    d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
+                    fill="currentColor" />
+                  <path
+                    d="M512 336c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
+                    fill="currentColor" />
+                </svg>
+              </el-icon>
+              AI帮我写
+            </el-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="primary" @click="handleConfirm" :disabled="!canConfirm" class="confirm-button">
+          确认
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+
+// 定义组件的 props
+interface Props {
+  modelValue: boolean
+}
+
+const props = defineProps<Props>()
+
+// 定义组件的事件
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean]
+  confirm: [prompt: string]
+  cancel: []
+}>()
+
+// Dialog 显示状态
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 场景提示词
+const scenePrompt = ref('')
+
+// AI 加载状态
+const aiLoading = ref(false)
+
+// 是否可以确认
+const canConfirm = computed(() => {
+  return scenePrompt.value.trim().length > 0
+})
+
+// AI 帮我写
+const handleAIHelp = async () => {
+  if (aiLoading.value) return
+
+  aiLoading.value = true
+  try {
+    // 这里可以调用 AI 接口生成场景提示词
+    // 模拟 AI 生成过程
+    await new Promise(resolve => setTimeout(resolve, 2000))
+
+    // 示例 AI 生成的提示词
+    const aiGeneratedPrompt = `一个现代化的办公场景,阳光透过落地窗洒在整洁的桌面上,桌上摆放着笔记本电脑、咖啡杯和几本专业书籍,背景是城市天际线,整体氛围专业而温馨。`
+
+    scenePrompt.value = aiGeneratedPrompt
+    ElMessage.success('AI 已为您生成场景提示词')
+  } catch (error) {
+    ElMessage.error('AI 生成失败,请重试')
+  } finally {
+    aiLoading.value = false
+  }
+}
+
+// 确认
+const handleConfirm = () => {
+  if (!canConfirm.value) return
+
+  emit('confirm', scenePrompt.value.trim())
+  dialogVisible.value = false
+  resetForm()
+}
+
+
+// 关闭弹窗
+const handleClose = () => {
+  emit('cancel')
+}
+
+// 重置表单
+const resetForm = () => {
+  scenePrompt.value = ''
+}
+</script>
+
+<style lang="scss" scoped>
+.scene-prompt-container {
+  padding: 20px;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  background: #EAECED;
+}
+
+.input-section {
+  position: relative;
+}
+
+.input-wrapper {
+  position: relative;
+  background: #f8f9fa;
+  border-radius: 8px;
+  border: 1px solid #e4e7ed;
+}
+
+.scene-input {
+  :deep(.el-textarea__inner) {
+    border: none;
+    background: transparent;
+    font-size: 16px;
+    line-height: 1.6;
+    color: #303133;
+    resize: none;
+
+    &::placeholder {
+      color: #c0c4cc;
+    }
+
+    &:focus {
+      box-shadow: none;
+    }
+  }
+}
+
+.ai-help-button-container {
+  position: absolute;
+  bottom: 4px;
+  right: 4px;
+}
+
+.ai-help-button {
+  background: linear-gradient(135deg, #409eff 0%, #67c23a 100%);
+  border: none;
+  border-radius: 20px;
+  padding: 8px 16px;
+  font-size: 13px;
+  font-weight: 500;
+  color: white;
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
+  transition: all 0.3s ease;
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
+  }
+
+  &:active {
+    transform: translateY(0);
+  }
+
+  .ai-icon {
+    margin-right: 6px;
+    font-size: 14px;
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: center;
+  gap: 16px;
+  padding: 14px 20px;
+}
+
+.confirm-button {
+  background: linear-gradient(135deg, #409eff 0%, #a855f7 100%);
+  border: none;
+  border-radius: 6px;
+  padding: 12px 32px;
+  font-size: 14px;
+  font-weight: 500;
+  color: white;
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
+  transition: all 0.3s ease;
+
+  &:hover {
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
+  }
+
+  &:active {
+    transform: translateY(0);
+  }
+
+  &:disabled {
+    background: #c0c4cc;
+    transform: none;
+    box-shadow: none;
+    cursor: not-allowed;
+  }
+}
+</style>
+
+<style lang="scss">
+.scene-prompt-dialog {
+  .el-dialog__body {
+    padding: 0;
+  }
+
+  .el-dialog__footer {
+    padding: 0 !important;
+  }
+
+  .el-dialog {
+    border-radius: 12px !important;
+    overflow: hidden;
+  }
+
+  .el-dialog__header {
+    background: #f8f9fa;
+  }
+
+  .el-dialog__title {
+    font-size: 18px !important;
+    font-weight: 600 !important;
+    color: #303133 !important;
+    text-align: center !important;
+  }
+}
+</style>

+ 125 - 6
frontend/src/views/Home/index.vue

@@ -18,12 +18,56 @@
       <div class="overlay-text">拍摄产品<br>并处理图像</div>
     </div>
 
-    <!-- 右侧图片区域 -->
-    <div class="image-container right-image" @click="goShot" v-log="{ describe: { action: '点击仅处理图像入口' } }">
-      <img src="@/assets/images/home/right.jpg" alt="仅处理图像" class="zoom-on-hover" />
-      <div class="overlay-text" style="line-height: 80px;">仅处理图像</div>
-    </div>
-  </div>
+         <!-- 右侧图片区域 -->
+     <div class="image-container right-image" @click="goShot" v-log="{ describe: { action: '点击仅处理图像入口' } }">
+       <img src="@/assets/images/home/right.jpg" alt="仅处理图像" class="zoom-on-hover" />
+       <div class="overlay-text" style="line-height: 80px;">仅处理图像</div>
+     </div>
+
+           <!-- 模特生成按钮 -->
+      <el-button type="info" @click="openModelDialog" class="model-generation-btn">
+        打开模特生成弹窗
+      </el-button>
+
+      <!-- 场景提示词按钮 -->
+      <el-button type="success" @click="openScenePromptDialog" class="scene-prompt-btn">
+        场景图生成
+      </el-button>
+
+     <!-- 显示选中的模特信息 -->
+     <div v-if="selectedModels" class="selected-models-info">
+       <h3>已选择的模特:</h3>
+       <div class="models-display">
+         <div v-if="selectedModels.female" class="model-info">
+           <strong>女模特:</strong>{{ selectedModels.female.image_url }}
+         </div>
+         <div v-if="selectedModels.male" class="model-info">
+           <strong>男模特:</strong>{{ selectedModels.male.image_url }}
+         </div>
+       </div>
+     </div>
+     <div class="scene-prompt-info">
+      <div class="scene-prompt-info-item">
+        <div class="scene-prompt-info-item-label">场景提示词:</div>
+        <div class="scene-prompt-info-item-value">{{ scenePrompt }}</div>
+      </div>
+     </div>
+
+     
+
+      <ModelGenerationDialog 
+         v-model="modelDialogVisible"
+         @confirm="handleModelSelection"
+         @cancel="modelDialogVisible = false"
+       />
+
+       <!-- 场景提示词弹窗 -->
+       <ScenePromptDialog 
+         v-model="scenePromptDialogVisible"
+         @confirm="handleScenePromptConfirm"
+         @cancel="scenePromptDialogVisible = false"
+       />
+   </div>
 </template>
 
 <script setup lang="ts">
@@ -31,13 +75,20 @@ import headerBar from "@/components/header-bar/index.vue";
 import { useRouter } from "vue-router";
 import configInfo from '@/stores/modules/config';
 import { ref, onMounted } from 'vue';
+import { ElMessage } from 'element-plus';
 import axios from 'axios';
 import client from "@/stores/modules/client";
 import icpList from '@/utils/ipc';
+import ModelGenerationDialog from '@/components/ModelGeneration/index.vue';
+import ScenePromptDialog from '@/components/ScenePromptDialog/index.vue';
 
 const configInfoStore = configInfo();
 const router = useRouter();
 const loading = ref(true);
+const selectedModels = ref<{ female: any; male: any } | null>(null);
+const modelDialogVisible = ref(false);
+const scenePromptDialogVisible = ref(false);
+const scenePrompt = ref('');
 
 
 import socket from "@/stores/modules/socket";
@@ -63,6 +114,30 @@ const goShot = () => {
     });
 };
 
+// 打开模特生成弹窗
+const openModelDialog = () => {
+    modelDialogVisible.value = true;
+};
+
+// 处理模特选择结果
+const handleModelSelection = (models: { female: any; male: any }) => {
+    selectedModels.value = models;
+    modelDialogVisible.value = false;
+    ElMessage.success('模特选择完成!');
+};
+
+// 打开场景提示词弹窗
+const openScenePromptDialog = () => {
+    scenePromptDialogVisible.value = true;
+};
+
+// 处理场景提示词确认
+const handleScenePromptConfirm = (prompt: string) => {
+    console.log('场景提示词:', prompt);
+    scenePrompt.value = prompt;
+    // 这里可以添加处理场景提示词的逻辑
+};
+
 // 健康检查函数
 const checkHealth = async () => {
     try {
@@ -185,4 +260,48 @@ onMounted(() => {
   min-height: 80px;
   min-width: 250px;
 }
+
+.model-generation-btn {
+  position: absolute;
+  top: 20px;
+  right: 20px;
+  z-index: 10;
+}
+
+.scene-prompt-btn {
+  position: absolute;
+  top: 70px;
+  right: 20px;
+  z-index: 10;
+}
+
+.selected-models-info {
+  position: absolute;
+  top: 20px;
+  left: 20px;
+  background: rgba(255, 255, 255, 0.9);
+  padding: 15px;
+  border-radius: 8px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
+  max-width: 300px;
+  z-index: 10;
+  
+  h3 {
+    margin: 0 0 10px 0;
+    color: #303133;
+    font-size: 16px;
+  }
+  
+  .models-display {
+    .model-info {
+      margin: 5px 0;
+      color: #606266;
+      font-size: 14px;
+      
+      strong {
+        color: #409eff;
+      }
+    }
+  }
+}
 </style>