mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 06:55:26 +00:00
246 lines
6.2 KiB
Vue
246 lines
6.2 KiB
Vue
<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>
|