index.vue 13 KB

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