Files
ai-agent-admin/web/apps/web-ele/src/components/zq-form/zq-icon-picker/zq-icon-picker.vue
T
2026-06-08 18:14:59 +08:00

324 lines
7.4 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.
<script setup lang="ts">
import { computed, ref, watch, watchEffect } from 'vue';
import { Grip, IconifyIcon, listIcons } from '@vben/icons';
import { $t } from '@vben/locales';
import { ElEmpty, ElInput, ElPagination, ElScrollbar } from 'element-plus';
import { ZqDialog } from '#/components/zq-dialog';
import { fetchIconsData } from './icons';
interface Props {
pageSize?: number;
/** 图标集的名字 */
prefix?: string;
/** 是否自动请求API以获得图标集的数据.提供prefix时有效 */
autoFetchApi?: boolean;
/** 图标列表 */
icons?: string[];
/** 图标样式 */
iconClass?: string;
/** 弹窗标题 */
dialogTitle?: string;
/** 弹窗宽度 */
dialogWidth?: number | string;
/** 是否禁用 */
disabled?: boolean;
/** 占位符 */
placeholder?: string;
}
const props = withDefaults(defineProps<Props>(), {
prefix: 'ant-design',
pageSize: 54,
icons: () => [],
iconClass: 'size-4',
autoFetchApi: true,
dialogTitle: undefined,
dialogWidth: '600px',
disabled: false,
placeholder: undefined,
});
const emit = defineEmits<{
change: [string];
}>();
const modelValue = defineModel({ default: '', type: String });
const dialogVisible = ref(false);
const currentSelect = ref('');
const currentPage = ref(1);
const keyword = ref('');
const innerIcons = ref<string[]>([]);
const searchKeyword = ref('');
let searchTimer: null | ReturnType<typeof setTimeout> = null;
watch(searchKeyword, (val) => {
if (searchTimer) clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
keyword.value = val;
currentPage.value = 1;
}, 300);
});
watch(
() => props.prefix,
async (prefix) => {
if (prefix && prefix !== 'svg' && props.autoFetchApi) {
innerIcons.value = await fetchIconsData(prefix);
}
},
{ immediate: true },
);
const currentList = computed(() => {
try {
if (props.icons.length > 0) {
return props.icons;
}
if (props.prefix) {
if (props.prefix !== 'svg' && props.autoFetchApi) {
return innerIcons.value;
}
return listIcons('', props.prefix);
}
return props.icons;
} catch {
return [];
}
});
const filteredList = computed(() => {
if (!keyword.value) return currentList.value;
return currentList.value.filter((item) => item.includes(keyword.value));
});
const total = computed(() => filteredList.value.length);
const paginationList = computed(() => {
const start = (currentPage.value - 1) * props.pageSize;
return filteredList.value.slice(start, start + props.pageSize);
});
watchEffect(() => {
currentSelect.value = modelValue.value;
});
watch(
() => currentSelect.value,
(v) => {
emit('change', v);
},
);
function openDialog() {
if (props.disabled) return;
dialogVisible.value = true;
}
function handleClick(icon: string) {
currentSelect.value = icon;
}
function handleConfirm() {
modelValue.value = currentSelect.value;
dialogVisible.value = false;
}
function handleDialogOpen() {
currentSelect.value = modelValue.value;
keyword.value = '';
searchKeyword.value = '';
currentPage.value = 1;
}
function handlePageChange(page: number) {
currentPage.value = page;
}
const computedTitle = computed(
() => props.dialogTitle ?? $t('ui.iconPicker.placeholder'),
);
defineExpose({ openDialog });
</script>
<template>
<div class="zq-icon-picker" :class="{ 'is-disabled': disabled }">
<!-- 触发器带图标预览的输入框 -->
<div class="zq-icon-picker-trigger" @click="openDialog">
<ElInput
:model-value="currentSelect"
:placeholder="placeholder ?? $t('ui.iconPicker.placeholder')"
:disabled="disabled"
readonly
class="zq-icon-picker-el-input"
>
<template #prefix>
<IconifyIcon
v-if="currentSelect"
:icon="currentSelect"
class="h-4 w-4"
/>
<component
:is="Grip"
v-else
class="h-4 w-4 text-[var(--el-text-color-placeholder)]"
/>
</template>
</ElInput>
</div>
<!-- 弹窗 -->
<ZqDialog
v-model="dialogVisible"
:title="computedTitle"
:width="dialogWidth"
:show-footer="true"
:confirm-text="$t('common.confirm')"
:cancel-text="$t('common.cancel')"
:destroy-on-close="false"
:show-fullscreen-button="false"
@opened="handleDialogOpen"
@confirm="handleConfirm"
>
<div class="zq-icon-picker-dialog-content">
<!-- 搜索栏 -->
<div class="zq-icon-picker-search">
<ElInput
v-model="searchKeyword"
:placeholder="$t('ui.iconPicker.search')"
clearable
class="w-full"
/>
</div>
<!-- 图标网格 -->
<template v-if="paginationList.length > 0">
<ElScrollbar class="zq-icon-picker-scrollbar">
<div class="zq-icon-picker-grid">
<button
v-for="(item, index) in paginationList"
:key="index"
class="zq-icon-picker-grid-item"
:class="{ 'is-active': currentSelect === item }"
:title="item"
type="button"
@click="handleClick(item)"
>
<IconifyIcon :icon="item" class="h-5 w-5" aria-hidden="true" />
</button>
</div>
</ElScrollbar>
<!-- 分页 -->
<div v-if="total > pageSize" class="zq-icon-picker-pagination">
<ElPagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
small
@current-change="handlePageChange"
/>
</div>
</template>
<!-- 空状态 -->
<template v-else>
<ElEmpty :description="$t('common.noData')" class="py-8" />
</template>
</div>
</ZqDialog>
</div>
</template>
<style lang="scss" scoped>
.zq-icon-picker {
width: 100%;
&.is-disabled {
cursor: not-allowed;
opacity: 0.6;
.zq-icon-picker-trigger {
pointer-events: none;
}
}
&-trigger {
width: 100%;
cursor: pointer;
}
&-el-input {
:deep(.el-input__wrapper) {
cursor: pointer;
}
:deep(.el-input__inner) {
cursor: pointer;
}
}
&-dialog-content {
display: flex;
flex-direction: column;
gap: 12px;
}
&-search {
padding: 0 2px;
}
&-scrollbar {
max-height: 360px;
}
&-grid {
display: grid;
grid-template-columns: repeat(9, 1fr);
gap: 4px;
padding: 4px 2px;
}
&-grid-item {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
aspect-ratio: 1;
padding: 8px;
cursor: pointer;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
transition: all 0.15s ease;
color: var(--el-text-color-regular);
&:hover {
background-color: var(--el-fill-color-light);
color: var(--el-color-primary);
border-color: var(--el-color-primary-light-7);
}
&.is-active {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border-color: var(--el-color-primary-light-5);
}
}
&-pagination {
display: flex;
justify-content: center;
padding-top: 4px;
border-top: 1px solid var(--el-border-color-lighter);
}
}
</style>