index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. <template>
  2. <el-dialog v-model="dialogVisible" title="选择模特" width="1000px" :close-on-click-modal="false"
  3. :close-on-press-escape="false" custom-class="model-generation-dialog" @close="handleClose">
  4. <div class="model-generation-container">
  5. <!-- 主要内容区域 -->
  6. <div class="main-content">
  7. <!-- 左侧:女模特选择 -->
  8. <div class="model-section">
  9. <h2>女模特</h2>
  10. <div class="model-display">
  11. <el-image v-if="selectedFemaleModel" :src="selectedFemaleModel.image_url" :alt="selectedFemaleModel.name"
  12. class="selected-model-image" lazy :preview-src-list="[selectedFemaleModel.image_url]" fit="cover" />
  13. <div v-else class="placeholder-image">
  14. <span>请在下方列表选择</span>
  15. </div>
  16. </div>
  17. </div>
  18. <!-- 右侧:男模特选择 -->
  19. <div class="model-section">
  20. <h2>男模特</h2>
  21. <div class="model-display">
  22. <el-image v-if="selectedMaleModel" :src="selectedMaleModel.image_url" :alt="selectedMaleModel.name"
  23. class="selected-model-image" lazy :preview-src-list="[selectedMaleModel.image_url]" fit="cover" />
  24. <div v-else class="placeholder-image">
  25. <span>请在下方列表选择</span>
  26. </div>
  27. </div>
  28. </div>
  29. </div>
  30. <!-- 底部模特列表区域 -->
  31. <div class="model-list-section">
  32. <!-- 标签页切换 -->
  33. <div class="tabs-container">
  34. <div class="tab-item" :class="{ active: activeTab === 'female' }" @click="switchTab('female')">
  35. 女模特
  36. </div>
  37. <div class="tab-item" :class="{ active: activeTab === 'male' }" @click="switchTab('male')">
  38. 男模特
  39. </div>
  40. </div>
  41. <!-- 模特网格列表 -->
  42. <div class="model-grid">
  43. <div v-for="model in currentModelList" :key="model.id" class="model-item"
  44. :class="{ selected: isModelSelected(model) }" @click="selectModel(model)">
  45. <el-image :src="model.image_url" :alt="model.name" class="model-thumbnail" lazy fit="cover" />
  46. </div>
  47. </div>
  48. </div>
  49. </div>
  50. <template #footer>
  51. <div class="dialog-footer">
  52. <el-button @click="handleCancel">取消</el-button>
  53. <el-button type="primary" @click="handleConfirm" :disabled="!canConfirm">
  54. 确认
  55. </el-button>
  56. </div>
  57. </template>
  58. </el-dialog>
  59. </template>
  60. <script setup lang="ts">
  61. import { ref, reactive, computed, onMounted, nextTick } from 'vue'
  62. import { ElMessage } from 'element-plus'
  63. import { getShoesModelTemplateApi } from '@/apis/other'
  64. // 定义组件的 props
  65. interface Props {
  66. modelValue: boolean
  67. }
  68. const props = defineProps<Props>()
  69. // 定义组件的事件
  70. const emit = defineEmits<{
  71. 'update:modelValue': [value: boolean]
  72. confirm: [models: { female: any; male: any }]
  73. cancel: []
  74. }>()
  75. // Dialog 显示状态
  76. const dialogVisible = computed({
  77. get: () => props.modelValue,
  78. set: (value) => emit('update:modelValue', value)
  79. })
  80. // 模特数据类型定义
  81. interface ModelData {
  82. id: number
  83. name: string
  84. image_url: string
  85. gender: 'male' | 'female'
  86. keywords: string
  87. status: number
  88. }
  89. // 当前激活的标签页
  90. const activeTab = ref<'female' | 'male'>('female')
  91. // 选中的女模特
  92. const selectedFemaleModel = ref<ModelData | null>(null)
  93. // 选中的男模特
  94. const selectedMaleModel = ref<ModelData | null>(null)
  95. // 女模特列表
  96. const femaleModels = ref<ModelData[]>([])
  97. // 男模特列表
  98. const maleModels = ref<ModelData[]>([])
  99. // 当前显示的模特列表
  100. const currentModelList = computed(() => {
  101. return activeTab.value === 'female' ? femaleModels.value : maleModels.value
  102. })
  103. // 是否可以确认选择
  104. const canConfirm = computed(() => {
  105. return selectedFemaleModel.value || selectedMaleModel.value
  106. })
  107. // 选择模特
  108. const selectModel = (model: ModelData) => {
  109. if (model.keywords === '女性') {
  110. selectedFemaleModel.value = model
  111. } else {
  112. selectedMaleModel.value = model
  113. }
  114. }
  115. // 判断模特是否被选中
  116. const isModelSelected = (model: ModelData) => {
  117. if (model.keywords === '女性') {
  118. return selectedFemaleModel.value?.id === model.id
  119. } else {
  120. return selectedMaleModel.value?.id === model.id
  121. }
  122. }
  123. // 切换标签页并滚动到顶部
  124. const switchTab = (tab: 'female' | 'male') => {
  125. activeTab.value = tab
  126. // 使用 nextTick 确保 DOM 更新后再滚动
  127. nextTick(() => {
  128. const modelGrid = document.querySelector('.model-grid') as HTMLElement
  129. if (modelGrid) {
  130. modelGrid.scrollTop = 0
  131. }
  132. })
  133. }
  134. // 确认选择
  135. const handleConfirm = () => {
  136. // 只传递必要的数据字段,避免序列化问题
  137. const selectedModels = {
  138. female: selectedFemaleModel.value ? {
  139. id: selectedFemaleModel.value.id,
  140. name: selectedFemaleModel.value.name,
  141. image_url: selectedFemaleModel.value.image_url,
  142. gender: selectedFemaleModel.value.gender,
  143. keywords: selectedFemaleModel.value.keywords,
  144. status: selectedFemaleModel.value.status
  145. } : null,
  146. male: selectedMaleModel.value ? {
  147. id: selectedMaleModel.value.id,
  148. name: selectedMaleModel.value.name,
  149. image_url: selectedMaleModel.value.image_url,
  150. gender: selectedMaleModel.value.gender,
  151. keywords: selectedMaleModel.value.keywords,
  152. status: selectedMaleModel.value.status
  153. } : null
  154. }
  155. console.log('选中的模特:', selectedModels)
  156. // 通过事件将数据发送给父组件
  157. emit('confirm', selectedModels)
  158. dialogVisible.value = false
  159. }
  160. // 取消选择
  161. const handleCancel = () => {
  162. emit('cancel')
  163. dialogVisible.value = false
  164. }
  165. // 关闭弹窗
  166. const handleClose = () => {
  167. emit('cancel')
  168. }
  169. // 获取模特列表
  170. const fetchModelList = async () => {
  171. try {
  172. const response = await getShoesModelTemplateApi({ status: 2 })
  173. console.log(response)
  174. if (response && response.data) {
  175. // 根据性别分类模特
  176. femaleModels.value = response.data.filter((model: ModelData) => model.keywords === '女性')
  177. maleModels.value = response.data.filter((model: ModelData) => model.keywords === '男性')
  178. // 预加载前几个图片以提高性能
  179. setTimeout(() => {
  180. preloadImages(femaleModels.value.slice(0, 10))
  181. preloadImages(maleModels.value.slice(0, 10))
  182. }, 100)
  183. }
  184. } catch (error) {
  185. console.error('获取模特列表失败:', error)
  186. ElMessage.error('获取模特列表失败')
  187. }
  188. }
  189. // 预加载图片
  190. const preloadImages = (models: ModelData[]) => {
  191. models.forEach(model => {
  192. if (model.image_url) {
  193. const img = new Image()
  194. img.src = model.image_url
  195. }
  196. })
  197. }
  198. // 组件挂载时的初始化
  199. onMounted(() => {
  200. console.log('模特生成页面已加载')
  201. fetchModelList()
  202. })
  203. </script>
  204. <style lang="scss" scoped>
  205. .model-generation-container {
  206. padding: 0;
  207. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  208. }
  209. .page-header {
  210. text-align: center;
  211. padding: 20px 0;
  212. background: #ffffff;
  213. border-bottom: 1px solid #e4e7ed;
  214. h1 {
  215. color: #303133;
  216. margin: 0;
  217. font-size: 20px;
  218. font-weight: 500;
  219. }
  220. }
  221. .main-content {
  222. display: flex;
  223. gap: 15px;
  224. padding: 15px;
  225. position: relative;
  226. max-width: 50%;
  227. margin: 0 auto;
  228. }
  229. .model-section {
  230. flex: 1;
  231. border-radius: 6px;
  232. overflow: hidden;
  233. h2 {
  234. color: #303133;
  235. margin: 0;
  236. padding: 8px;
  237. font-size: 13px;
  238. font-weight: 550;
  239. text-align: center;
  240. }
  241. }
  242. .model-display {
  243. width: 100%;
  244. aspect-ratio: 1;
  245. display: flex;
  246. align-items: center;
  247. justify-content: center;
  248. overflow: hidden;
  249. background: #f8f9fa;
  250. border: 2px solid #e4e7ed;
  251. border-radius: 4px;
  252. .selected-model-image {
  253. width: 100%;
  254. height: 100%;
  255. :deep(.el-image__inner) {
  256. width: 100%;
  257. height: 100%;
  258. object-fit: cover;
  259. }
  260. }
  261. .placeholder-image {
  262. color: #c0c4cc;
  263. font-size: 11px;
  264. text-align: center;
  265. span {
  266. color: #c0c4cc;
  267. display: block;
  268. line-height: 1.2;
  269. }
  270. }
  271. }
  272. .dialog-footer {
  273. display: flex;
  274. justify-content: flex-end;
  275. gap: 12px;
  276. padding: 15px;
  277. }
  278. .model-list-section {
  279. margin: 2px 0px 0px 0;
  280. overflow: hidden;
  281. }
  282. .tabs-container {
  283. display: flex;
  284. /* background: #f8f9fa; */
  285. border-bottom: 1px solid #e4e7ed;
  286. align-items: center;
  287. justify-content: center;
  288. }
  289. .tab-item {
  290. padding: 8px 16px;
  291. cursor: pointer;
  292. font-size: 13px;
  293. font-weight: 550;
  294. color: #606266;
  295. border-bottom: 2px solid transparent;
  296. transition: all 0.2s ease;
  297. background: transparent;
  298. &:hover {
  299. color: #409eff;
  300. }
  301. &.active {
  302. color: #409eff;
  303. border-bottom-color: #409eff;
  304. }
  305. }
  306. .model-grid {
  307. display: grid;
  308. grid-template-columns: repeat(5, 1fr);
  309. gap: 6px;
  310. padding: 12px;
  311. max-height: 320px;
  312. overflow-y: auto;
  313. margin-top: 8px;
  314. }
  315. .model-item {
  316. aspect-ratio: 1;
  317. border: 1px solid #e4e7ed;
  318. border-radius: 3px;
  319. overflow: hidden;
  320. cursor: pointer;
  321. transition: all 0.2s ease;
  322. background: #f8f9fa;
  323. &:hover {
  324. border-color: #409eff;
  325. transform: scale(1.01);
  326. }
  327. &.selected {
  328. border-color: #409eff;
  329. border-width: 2px;
  330. box-shadow: 0 0 0 1px #409eff;
  331. }
  332. .model-thumbnail {
  333. width: 100%;
  334. height: 100%;
  335. :deep(.el-image__inner) {
  336. width: 100%;
  337. height: 100%;
  338. object-fit: cover;
  339. }
  340. :deep(.el-image__placeholder) {
  341. background: #f8f9fa;
  342. display: flex;
  343. align-items: center;
  344. justify-content: center;
  345. color: #c0c4cc;
  346. font-size: 12px;
  347. }
  348. :deep(.el-image__error) {
  349. background: #fef0f0;
  350. display: flex;
  351. align-items: center;
  352. justify-content: center;
  353. color: #f56c6c;
  354. font-size: 12px;
  355. }
  356. }
  357. }
  358. // 滚动条样式
  359. .model-grid::-webkit-scrollbar {
  360. width: 6px;
  361. }
  362. .model-grid::-webkit-scrollbar-track {
  363. background: #f1f1f1;
  364. border-radius: 3px;
  365. }
  366. .model-grid::-webkit-scrollbar-thumb {
  367. background: #c1c1c1;
  368. border-radius: 3px;
  369. }
  370. .model-grid::-webkit-scrollbar-thumb:hover {
  371. background: #a8a8a8;
  372. }
  373. // 响应式设计
  374. @media (max-width: 768px) {
  375. .main-content {
  376. flex-direction: column;
  377. gap: 20px;
  378. padding: 20px;
  379. }
  380. .model-grid {
  381. grid-template-columns: repeat(4, 1fr);
  382. gap: 10px;
  383. padding: 15px;
  384. }
  385. .confirm-button-container {
  386. position: static;
  387. text-align: center;
  388. margin-top: 20px;
  389. }
  390. .model-list-section {
  391. margin: 0 20px 20px;
  392. }
  393. }
  394. </style>
  395. <style lang="scss">
  396. .model-generation-dialog {
  397. .el-dialog__body {
  398. padding: 0;
  399. background: #EAECED;
  400. }
  401. .el-dialog__footer {
  402. padding: 0;
  403. border-top: 1px solid #e4e7ed;
  404. background: #fafafa;
  405. }
  406. .el-dialog {
  407. border-radius: 8px;
  408. }
  409. .el-dialog__header {
  410. }
  411. .el-dialog__title {
  412. font-size: 18px;
  413. font-weight: 600;
  414. color: #303133;
  415. }
  416. }</style>