Files
yys-editor/src/components/common/GenericImageSelector.vue

246 lines
6.2 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>
<el-dialog v-model="show" :title="config.title">
<span v-if="config.currentItem">
当前选择{{ config.currentItem[config.itemRender.labelField] }}
</span>
<div v-if="config.allowUserAssetUpload" class="user-asset-actions">
<input
ref="uploadInputRef"
type="file"
accept="image/*"
class="hidden-input"
@change="handleUploadAsset"
/>
<el-button size="small" type="primary" @click="triggerUpload">上传我的素材</el-button>
</div>
<!-- 搜索框 -->
<div v-if="config.searchable !== false" style="display: flex; align-items: center;">
<el-input
v-model="searchText"
placeholder="请输入内容"
style="width: 200px; margin-right: 10px;"
/>
</div>
<!-- Tab分组 -->
<el-tabs
v-model="activeTab"
type="card"
class="demo-tabs"
>
<el-tab-pane
v-for="group in config.groups"
:key="group.name"
:label="group.label"
:name="group.name"
>
<div style="max-height: 600px; overflow-y: auto;">
<el-space wrap size="large">
<div
v-for="item in filteredItems(group)"
:key="item.id || item[config.itemRender.labelField]"
style="display: flex; flex-direction: column; justify-content: center"
>
<el-button
class="selector-button"
:style="`width: ${imageSize}px; height: ${imageSize}px;`"
@click="handleSelect(item)"
>
<span
class="selector-image-frame"
:style="`width: ${imageSize - 1}px; height: ${imageSize - 1}px; background-image: url('${getItemImageUrl(item)}');`"
/>
</el-button>
<span style="text-align: center; display: block;">
{{ item[config.itemRender.labelField] }}
</span>
<el-button
v-if="item.__userAsset"
type="danger"
text
size="small"
@click.stop="removeUserAsset(item)"
>
删除
</el-button>
</div>
</el-space>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import type { SelectorConfig, GroupConfig } from '@/types/selector'
import { resolveAssetUrl } from '@/utils/assetUrl'
import { createCustomAssetFromFile, listCustomAssets, subscribeCustomAssetStore } from '@/utils/customAssets'
const props = defineProps<{
config: SelectorConfig
modelValue: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
'select': [item: any]
}>()
const show = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const searchText = ref('')
const activeTab = ref('ALL')
const imageSize = computed(() => props.config.itemRender.imageSize || 100)
const imageField = computed(() => props.config.itemRender.imageField)
const uploadInputRef = ref<HTMLInputElement | null>(null)
const dataSource = ref<any[]>([])
let unsubscribeCustomAssets: (() => void) | null = null
const refreshDataSource = () => {
const source = Array.isArray(props.config.dataSource) ? props.config.dataSource : []
const staticAssets = source.filter((item) => !item?.__userAsset)
const library = props.config.assetLibrary
if (!library) {
dataSource.value = [...source]
return
}
const customAssets = listCustomAssets(library)
dataSource.value = [...staticAssets, ...customAssets]
}
watch(
() => [props.config.dataSource, props.config.assetLibrary],
() => {
refreshDataSource()
},
{ immediate: true, deep: true }
)
watch(
() => props.modelValue,
(visible) => {
if (visible) {
refreshDataSource()
}
}
)
onMounted(() => {
unsubscribeCustomAssets = subscribeCustomAssetStore(() => {
if (props.modelValue) {
refreshDataSource()
}
})
})
onBeforeUnmount(() => {
unsubscribeCustomAssets?.()
unsubscribeCustomAssets = null
})
// 过滤逻辑
const filteredItems = (group: GroupConfig) => {
let items = dataSource.value
// 分组过滤
if (group.name !== 'ALL') {
if (group.filter) {
items = items.filter(group.filter)
} else if (!props.config.groupField) {
items = []
} else {
items = items.filter(item =>
item[props.config.groupField]?.toLowerCase() === group.name.toLowerCase()
)
}
}
// 搜索过滤
if (searchText.value.trim()) {
const searchFields = props.config.searchFields || [props.config.itemRender.labelField]
items = items.filter(item =>
searchFields.some(field =>
item[field]?.toLowerCase().includes(searchText.value.toLowerCase())
)
)
}
return items
}
const handleSelect = (item: any) => {
const field = imageField.value
const normalizedItem = {
...item,
[field]: resolveAssetUrl(item?.[field])
}
emit('select', normalizedItem)
searchText.value = ''
activeTab.value = 'ALL'
}
const getItemImageUrl = (item: any) => resolveAssetUrl(item?.[imageField.value]) as string
const triggerUpload = () => {
uploadInputRef.value?.click()
}
const handleUploadAsset = async (event: Event) => {
const target = event.target as HTMLInputElement | null
const file = target?.files?.[0]
if (!file || !props.config.assetLibrary) {
if (target) {
target.value = ''
}
return
}
try {
const createdAsset = await createCustomAssetFromFile(props.config.assetLibrary, file)
props.config.onUserAssetUploaded?.(createdAsset)
refreshDataSource()
} finally {
if (target) {
target.value = ''
}
}
}
const removeUserAsset = (item: any) => {
props.config.onDeleteUserAsset?.(item)
refreshDataSource()
}
</script>
<style scoped>
.user-asset-actions {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.hidden-input {
display: none;
}
.selector-button {
padding: 0;
}
.selector-image-frame {
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
</style>