Files
aida_front/src/component/common/SelectImages.vue

763 lines
16 KiB
Vue
Raw Normal View History

2025-09-22 15:30:26 +08:00
<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">
2025-09-22 15:30:26 +08:00
<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)"
>
<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>
<div class="image-select" 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>
<p>加载中...</p>
</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: ''
2025-09-22 15:30:26 +08:00
}
})
// 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 || ''
}
2025-09-22 15:30:26 +08:00
const showPanel = ref(false)
const selectedCategory = ref(getDefaultCategory())
2025-09-22 15:30:26 +08:00
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)
2025-09-22 15:30:26 +08:00
} else {
return []
}
})
// 计算属性:是否显示分类选择器
const showCategories = computed(() => {
return !props.libraryType // 如果没有传入libraryType参数则显示分类选择器
})
2025-09-22 15:30:26 +08:00
// 新增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
2025-09-22 15:30:26 +08:00
const params = {
classificationIdList: [],
level1Type: props.libraryType || type,
2025-09-22 15:30:26 +08:00
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 = getDefaultCategory()
2025-09-22 15:30:26 +08:00
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]
2025-09-22 15:30:26 +08:00
} else {
// 多选模式:切换选中状态
if (isSelected) {
selectList.value = selectList.value.filter(url => url !== item.url)
} else {
selectList.value.push(item.url)
}
2025-09-22 15:30:26 +08:00
}
}
// 处理分类切换
const 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)
2025-09-22 15:30:26 +08:00
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: 20rem;
max-height: 50rem;
@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: 0;
right: 0;
z-index: 2;
transform: translate(50%, 50%);
i {
font-size: 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-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>