Files
aida_front/src/component/common/SelectImages.vue
2025-11-11 10:13:59 +08:00

783 lines
16 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.
<template>
<div v-if="showPanel" class="image-list-overlay" @click.self="showPanel = false">
<div class="image-list-modal">
<div class="modal-header">
<h3>{{ $t(navTypeList(t).library.label) }}</h3>
<button class="close-btn" @click="showPanel = false">&times;</button>
</div>
<div class="modal-content">
<!-- 分类标签 -->
<div v-if="showCategories" class="image-categories">
<div
v-for="category in categories"
:key="category"
:class="['category-btn', { active: selectedCategory === category }]"
@click="handleChangeCategory(category)"
>
{{ category }}
</div>
</div>
<!-- 图片网格 -->
<div class="image-grid" @scroll="handleScroll">
<div
v-for="(item, index) in list"
:key="index"
class="image-item"
@click="handleImageClick(item)"
@dblclick="handleImageDoubleClick(item)"
>
<div class="image-wrapper">
<img
v-lazy="item.url"
:alt="item.name"
@error="handleImageError"
loading="lazy"
/>
<div class="image-overlay">
<span class="image-name">{{ item.name }}</span>
</div>
</div>
<img class="selected-icon image-select" src="@/assets/images/icon/selected.png" v-show="selectList.includes(item.url)">
<!-- <div class="" v-show="selectList.includes(item.url)">
<i class="fi fi-sr-check-circle"></i>
</div> -->
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading && list.length > 0" class="loading-state">
<div class="loading-spinner"></div>
</div>
<!-- 空状态 -->
<div v-if="list.length === 0 && !loading" class="empty-state">
<div class="empty-icon">📷</div>
<p>{{ $t('Canvas.NoPicture') }}</p>
</div>
</div>
<div class="modal-footer">
<div class="image-count">
{{ $t('Canvas.general') }} {{ total }}
{{ $t('Canvas.PicturesInTotal') }}
</div>
<div class="image-submit gallery_btn" @click="confirm">
{{ $t('Canvas.confirm') }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
ref,
computed,
defineProps,
defineEmits,
defineExpose,
onMounted,
onUnmounted,
nextTick,
watch
} from 'vue'
import { useI18n } from 'vue-i18n'
import { Https } from '@/tool/https'
import { navTypeList } from '@/tool/listData'
const { t } = useI18n()
// Props
const props = defineProps({
api: {
type: String,
default: ''
},
isLibrary: {
type: Boolean,
default: false
},
radio: {
type: Boolean,
default: false
},
fullData: {
type: Boolean,
default: false
},
libraryType: {
type: String,
default: ''
}
})
// Emits
const emits = defineEmits(['select'])
const libraryTypeList = ref([
...navTypeList(t).library.list.filter(item => item.value !== 'MyBrand')
])
// 根据传入的libraryType参数确定默认选中的分类
const getDefaultCategory = () => {
if (props.libraryType) {
// 如果传入了libraryType查找匹配的category
const matchedCategory = libraryTypeList.value.find(
item => item.value === props.libraryType
)
return matchedCategory ? matchedCategory.label : libraryTypeList.value[0]?.label || ''
}
// 如果没有传入参数,选择第一个
return libraryTypeList.value[0]?.label || ''
}
const showPanel = ref(false)
const selectedCategory = ref(getDefaultCategory())
const selectList = ref([])
const list = ref([])
const currentPage = ref(1)
const hasMore = ref(true)
const loading = ref(false)
const pageSize = ref(10)
const total = ref(0)
// 计算属性:获取所有分类
const categories = computed(() => {
if (props.isLibrary) {
return libraryTypeList.value.map(item => item.label)
} else {
return []
}
})
// 计算属性:是否显示分类选择器
const showCategories = computed(() => {
return !props.libraryType // 如果没有传入libraryType参数则显示分类选择器
})
// 新增API请求函数
const fetchImages = async (
page = 1,
category = selectedCategory.value,
reset = false
) => {
if (!props.api) return
loading.value = true
const type = libraryTypeList.value.find(item => item.label === category)?.value
const params = {
classificationIdList: [],
level1Type: props.libraryType || type,
level2Type: '',
page,
ageGroup: '',
modelSex: '',
pictureName: '',
size: pageSize.value,
intersection: 1
}
const res = await Https.axiosPost(props.api, params)
loading.value = false
if (res && res.content) {
const newData = res.content
total.value = res.total
if (reset) {
list.value = newData
currentPage.value = 1
} else {
list.value = [...list.value, ...newData]
}
// 判断是否还有更多数据
hasMore.value = page < res.pages
currentPage.value = page
if (currentPage.value === 1 && hasMore.value) {
loadMore()
}
}
}
// 新增:无限滚动处理
const handleScroll = event => {
const { scrollTop, scrollHeight, clientHeight } = event.target
const threshold = 100 // 距离底部100px时触发加载
if (
scrollHeight - scrollTop - clientHeight < threshold &&
hasMore.value &&
!loading.value
) {
loadMore()
}
}
// 新增:加载更多
const loadMore = () => {
if (!loading.value && hasMore.value) {
const nextPage = currentPage.value + 1
fetchImages(nextPage, selectedCategory.value, false)
}
}
// 检查是否需要自动加载更多数据
const checkAndLoadMore = () => {
const scrollContainer = document.querySelector('.image-list-content')
if (!scrollContainer) return
const { scrollHeight, clientHeight } = scrollContainer
// 如果内容高度小于等于容器高度,且还有更多数据,自动加载
if (scrollHeight <= clientHeight && hasMore.value && !loading.value) {
console.log('内容不够滚动,自动加载更多数据')
loadMore()
}
}
// 新增:重置并加载
const resetAndLoad = (category = selectedCategory.value) => {
list.value = []
hasMore.value = true
currentPage.value = 0
// console.log('默认选择----',getDefaultCategory())
selectedCategory.value =category || getDefaultCategory()
fetchImages(1, category, true)
// 检查是否需要自动加载更多数据
nextTick(() => {
checkAndLoadMore()
})
}
watch(
() => showPanel.value,
val => {
if (val) {
resetAndLoad()
}
}
)
// 处理图片点击
const handleImageClick = item => {
const isSelected = selectList.value.includes(item.url)
if (props.radio) {
// 单选模式:选中当前图片或取消选中
selectList.value = isSelected ? [] : [item.url]
} else {
// 多选模式:切换选中状态
if (isSelected) {
selectList.value = selectList.value.filter(url => url !== item.url)
} else {
selectList.value.push(item.url)
}
}
}
// 处理图片双击
const handleImageDoubleClick = item => {
selectList.value = [item.url]
confirm()
}
// 处理分类切换
const handleChangeCategory = category => {
// console.log('handleChangeCategory',category)
selectedCategory.value = category
// 如果提供了API则重新加载数据
if (props.api) {
resetAndLoad(category)
}
}
// 处理图片加载错误
const handleImageError = event => {
event.target.src =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWbvueJh+WKoOi9veWksei0pe+8jOivt+ajgOafpeWbvueJh+i3r+W+hDwvdGV4dD48L3N2Zz4='
event.target.alt = '图片加载失败'
}
const confirm = () => {
// console.log('selectList.value',selectList.value,'list.value',list.value)
let emitData = null
if (props.fullData) {
const selected = list.value.filter(item => selectList.value.includes(item.url))
emitData = props.radio ? selected[0] : selected
} else {
emitData = selectList.value
}
emits('select', emitData)
selectList.value = []
showPanel.value = false
}
const init = () => {
showPanel.value = true
}
// 暴露给父组件的方法
defineExpose({
resetAndLoad,
fetchImages,
init
})
</script>
<style scoped lang="less">
// .image-list-container {
// position: relative;
// display: inline-block;
// }
.image-list-trigger {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 6px;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s ease;
color: #666;
&:hover {
background-color: rgba(66, 133, 244, 0.1);
border-color: rgba(66, 133, 244, 0.2);
color: #4285f4;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
}
}
/* 弹窗遮罩层 */
.image-list-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1001;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 弹窗主体 */
.image-list-modal {
background-color: #fff;
border-radius: 12px;
width: 90%;
max-width: 1200px;
height: 85vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
animation: modalSlideUp 0.3s ease;
}
@keyframes modalSlideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 弹窗头部 */
.modal-header {
padding: 16px 20px;
background-color: rgba(255, 255, 255, 0.8);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
h3 {
margin: 0;
font-size: 18px;
color: #333;
font-weight: 600;
}
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
line-height: 1;
opacity: 0.7;
transition: all 0.2s;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
opacity: 1;
color: #333;
transform: scale(1.1);
}
}
/* 弹窗内容 */
.modal-content {
padding: 20px;
// overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
background-color: #fff;
-webkit-overflow-scrolling: touch;
}
/* 分类标签 */
.image-categories {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 12px;
flex-wrap: wrap;
scrollbar-width: thin;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
}
.category-btn {
padding: 8px 16px;
border-radius: 6px;
background-color: rgba(0, 0, 0, 0.03);
border: 1px solid rgba(0, 0, 0, 0.05);
color: #333;
cursor: pointer;
font-size: 14px;
white-space: nowrap;
transition: all 0.2s;
height: 36px;
display: flex;
align-items: center;
font-weight: 500;
&.active {
background-color: rgba(66, 133, 244, 0.1);
border-color: rgba(66, 133, 244, 0.2);
color: #4285f4;
}
&:hover:not(.active) {
background-color: rgba(0, 0, 0, 0.05);
transform: translateY(-1px);
}
}
/* 图片网格 */
.image-grid {
display: grid;
overflow-y: auto;
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
gap: 16px;
min-height: 22rem;
max-height: 50rem;
padding-bottom: 2rem;
@media screen and (max-width: 768px) {
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
gap: 12px;
}
@media screen and (max-width: 48rem) {
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
gap: 10px;
}
}
.image-item {
cursor: pointer;
border-radius: 8px;
// overflow: hidden;
transition: all 0.2s ease;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.05);
position: relative;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
border-color: rgba(66, 133, 244, 0.2);
.image-overlay {
opacity: 1;
}
}
}
.image-wrapper {
position: relative;
width: 100%;
height: 22rem;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: contain;
transition: transform 0.2s ease;
}
&:hover img {
transform: scale(1.05);
}
}
.image-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
color: white;
padding: 16px 12px 12px;
opacity: 0;
transition: opacity 0.2s ease;
}
.image-select {
position: absolute;
bottom: -1.25rem;
right: -1.25rem;
z-index: 2;
// transform: translate(50%, 50%);
// i {
// font-size: 2.5rem;
// }
&.selected-icon{
width:2.5rem;
height: 2.5rem;
}
}
.image-name {
font-size: 14px;
font-weight: 500;
line-height: 1.2;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
min-height: 300px;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
p {
font-size: 16px;
margin: 0;
}
}
/* 错误状态 */
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #e74c3c;
min-height: 300px;
.error-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.7;
}
p {
font-size: 16px;
margin: 0 0 16px 0;
}
.retry-btn {
padding: 8px 16px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
&:hover {
background-color: #c0392b;
}
}
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f3f3f3;
border-top: 3px solid #4285f4;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
p {
font-size: 14px;
margin: 0;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 没有更多数据状态 */
.no-more-state {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #999;
font-size: 14px;
border-top: 1px solid #f0f0f0;
margin-top: 20px;
}
/* 弹窗底部 */
.modal-footer {
padding: 16px 20px;
background-color: rgba(255, 255, 255, 0.8);
border-top: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
> .image-submit{
font-size: 1.2rem;
line-height: 4rem;
}
}
.image-count {
font-size: 14px;
color: #666;
font-weight: 500;
}
/* 响应式设计 */
@media screen and (max-width: 1024px) {
.image-list-modal {
width: 95%;
max-width: 900px;
}
.modal-content {
padding: 16px;
}
.modal-header,
.modal-footer {
padding: 14px 16px;
}
}
@media screen and (max-width: 640px) {
.image-list-modal {
width: 98%;
margin: 10px;
max-height: 90vh;
}
.modal-content {
padding: 12px;
gap: 16px;
}
.modal-header h3 {
font-size: 16px;
}
.image-categories {
gap: 8px;
padding-bottom: 8px;
}
.category-btn {
padding: 6px 12px;
font-size: 13px;
height: 32px;
}
}
</style>