mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-02-02 18:28:40 +08:00
Merge branch 'vbenjs:main' into main
This commit is contained in:
commit
f9d35c2654
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { VNode } from 'vue';
|
import type { VNode } from 'vue';
|
||||||
|
|
||||||
import { computed, h, ref, watch, watchEffect } from 'vue';
|
import { computed, ref, watch, watchEffect } from 'vue';
|
||||||
|
|
||||||
import { usePagination } from '@vben/hooks';
|
import { usePagination } from '@vben/hooks';
|
||||||
import { EmptyIcon, Grip, listIcons } from '@vben/icons';
|
import { EmptyIcon, Grip, listIcons } from '@vben/icons';
|
||||||
@ -9,6 +9,7 @@ import { $t } from '@vben/locales';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Input,
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationEllipsis,
|
PaginationEllipsis,
|
||||||
PaginationFirst,
|
PaginationFirst,
|
||||||
@ -22,11 +23,16 @@ import {
|
|||||||
VbenPopover,
|
VbenPopover,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { refDebounced } from '@vueuse/core';
|
import { refDebounced, watchDebounced } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { fetchIconsData } from './icons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
/** 图标集的名字 */
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
|
/** 是否自动请求API以获得图标集的数据.提供prefix时有效 */
|
||||||
|
autoFetchApi?: boolean;
|
||||||
/**
|
/**
|
||||||
* 图标列表
|
* 图标列表
|
||||||
*/
|
*/
|
||||||
@ -39,16 +45,19 @@ interface Props {
|
|||||||
modelValueProp?: string;
|
modelValueProp?: string;
|
||||||
/** 图标样式 */
|
/** 图标样式 */
|
||||||
iconClass?: string;
|
iconClass?: string;
|
||||||
|
type?: 'icon' | 'input';
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
prefix: 'ant-design',
|
prefix: 'ant-design',
|
||||||
pageSize: 36,
|
pageSize: 36,
|
||||||
icons: () => [],
|
icons: () => [],
|
||||||
inputComponent: () => h('div'),
|
|
||||||
iconSlot: 'default',
|
iconSlot: 'default',
|
||||||
iconClass: 'size-4',
|
iconClass: 'size-4',
|
||||||
modelValueProp: 'value',
|
autoFetchApi: true,
|
||||||
|
modelValueProp: 'modelValue',
|
||||||
|
inputComponent: undefined,
|
||||||
|
type: 'input',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -62,9 +71,28 @@ const currentSelect = ref('');
|
|||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const keyword = ref('');
|
const keyword = ref('');
|
||||||
const keywordDebounce = refDebounced(keyword, 300);
|
const keywordDebounce = refDebounced(keyword, 300);
|
||||||
|
const innerIcons = ref<string[]>([]);
|
||||||
|
|
||||||
|
watchDebounced(
|
||||||
|
() => props.prefix,
|
||||||
|
async (prefix) => {
|
||||||
|
if (prefix && prefix !== 'svg' && props.autoFetchApi) {
|
||||||
|
innerIcons.value = await fetchIconsData(prefix);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, debounce: 500, maxWait: 1000 },
|
||||||
|
);
|
||||||
|
|
||||||
const currentList = computed(() => {
|
const currentList = computed(() => {
|
||||||
try {
|
try {
|
||||||
if (props.prefix) {
|
if (props.prefix) {
|
||||||
|
if (
|
||||||
|
props.prefix !== 'svg' &&
|
||||||
|
props.autoFetchApi &&
|
||||||
|
props.icons.length === 0
|
||||||
|
) {
|
||||||
|
return innerIcons.value;
|
||||||
|
}
|
||||||
const icons = listIcons('', props.prefix);
|
const icons = listIcons('', props.prefix);
|
||||||
if (icons.length === 0) {
|
if (icons.length === 0) {
|
||||||
console.warn(`No icons found for prefix: ${props.prefix}`);
|
console.warn(`No icons found for prefix: ${props.prefix}`);
|
||||||
@ -146,18 +174,61 @@ defineExpose({ toggleOpenState, open, close });
|
|||||||
content-class="p-0 pt-3"
|
content-class="p-0 pt-3"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<component
|
<template v-if="props.type === 'input'">
|
||||||
:is="inputComponent"
|
<component
|
||||||
:[modelValueProp]="currentSelect"
|
v-if="props.inputComponent"
|
||||||
:placeholder="$t('ui.iconPicker.placeholder')"
|
:is="inputComponent"
|
||||||
>
|
:[modelValueProp]="currentSelect"
|
||||||
<template #[iconSlot]>
|
:placeholder="$t('ui.iconPicker.placeholder')"
|
||||||
<VbenIcon :icon="currentSelect || Grip" class="size-4" />
|
role="combobox"
|
||||||
</template>
|
:aria-label="$t('ui.iconPicker.placeholder')"
|
||||||
</component>
|
aria-expanded="visible"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<template #[iconSlot]>
|
||||||
|
<VbenIcon
|
||||||
|
:icon="currentSelect || Grip"
|
||||||
|
class="size-4"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</component>
|
||||||
|
<div class="relative w-full" v-else>
|
||||||
|
<Input
|
||||||
|
v-bind="$attrs"
|
||||||
|
v-model="currentSelect"
|
||||||
|
:placeholder="$t('ui.iconPicker.placeholder')"
|
||||||
|
class="h-8 w-full pr-8"
|
||||||
|
role="combobox"
|
||||||
|
:aria-label="$t('ui.iconPicker.placeholder')"
|
||||||
|
aria-expanded="visible"
|
||||||
|
/>
|
||||||
|
<VbenIcon
|
||||||
|
:icon="currentSelect || Grip"
|
||||||
|
class="absolute right-1 top-1 size-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<VbenIcon
|
||||||
|
:icon="currentSelect || Grip"
|
||||||
|
v-else
|
||||||
|
class="size-4"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div class="mb-2 flex w-full">
|
<div class="mb-2 flex w-full">
|
||||||
<component :is="inputComponent" v-bind="searchInputProps" />
|
<component
|
||||||
|
v-if="inputComponent"
|
||||||
|
:is="inputComponent"
|
||||||
|
v-bind="searchInputProps"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-else
|
||||||
|
class="mx-2 h-8 w-full"
|
||||||
|
:placeholder="$t('ui.iconPicker.search')"
|
||||||
|
v-model="keyword"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="paginationList.length > 0">
|
<template v-if="paginationList.length > 0">
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一个缓存对象,在不刷新页面时,无需重复请求远程接口
|
||||||
|
*/
|
||||||
|
export const ICONS_MAP: Recordable<string[]> = {};
|
||||||
|
|
||||||
|
interface IconifyResponse {
|
||||||
|
prefix: string;
|
||||||
|
total: number;
|
||||||
|
title: string;
|
||||||
|
uncategorized?: string[];
|
||||||
|
categories?: Recordable<string[]>;
|
||||||
|
aliases?: Recordable<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PENDING_REQUESTS: Recordable<Promise<string[]>> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过Iconify接口获取图标集数据。
|
||||||
|
* 同一时间多个图标选择器同时请求同一个图标集时,实际上只会发起一次请求(所有请求共享同一份结果)。
|
||||||
|
* 请求结果会被缓存,刷新页面前同一个图标集不会再次请求
|
||||||
|
* @param prefix 图标集名称
|
||||||
|
* @returns 图标集中包含的所有图标名称
|
||||||
|
*/
|
||||||
|
export async function fetchIconsData(prefix: string): Promise<string[]> {
|
||||||
|
if (Reflect.has(ICONS_MAP, prefix) && ICONS_MAP[prefix]) {
|
||||||
|
return ICONS_MAP[prefix];
|
||||||
|
}
|
||||||
|
if (Reflect.has(PENDING_REQUESTS, prefix) && PENDING_REQUESTS[prefix]) {
|
||||||
|
return PENDING_REQUESTS[prefix];
|
||||||
|
}
|
||||||
|
PENDING_REQUESTS[prefix] = (async () => {
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 1000 * 10);
|
||||||
|
const response: IconifyResponse = await fetch(
|
||||||
|
`https://api.iconify.design/collection?prefix=${prefix}`,
|
||||||
|
{ signal: controller.signal },
|
||||||
|
).then((res) => res.json());
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
const list = response.uncategorized || [];
|
||||||
|
if (response.categories) {
|
||||||
|
for (const category in response.categories) {
|
||||||
|
list.push(...(response.categories[category] || []));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ICONS_MAP[prefix] = list.map((v) => `${prefix}:${v}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch icons for prefix ${prefix}:`, error);
|
||||||
|
return [] as string[];
|
||||||
|
}
|
||||||
|
return ICONS_MAP[prefix];
|
||||||
|
})();
|
||||||
|
return PENDING_REQUESTS[prefix];
|
||||||
|
}
|
@ -20,7 +20,10 @@ import {
|
|||||||
|
|
||||||
import { Card, Input } from 'ant-design-vue';
|
import { Card, Input } from 'ant-design-vue';
|
||||||
|
|
||||||
const iconValue = ref('ant-design:trademark-outlined');
|
const iconValue1 = ref('ant-design:trademark-outlined');
|
||||||
|
const iconValue2 = ref('svg:avatar-1');
|
||||||
|
const iconValue3 = ref('mdi:alien-outline');
|
||||||
|
const iconValue4 = ref('mdi-light:book-multiple');
|
||||||
|
|
||||||
const inputComponent = h(Input);
|
const inputComponent = h(Input);
|
||||||
</script>
|
</script>
|
||||||
@ -78,26 +81,32 @@ const inputComponent = h(Input);
|
|||||||
<Card class="mb-5" title="图标选择器">
|
<Card class="mb-5" title="图标选择器">
|
||||||
<div class="mb-5 flex items-center gap-5">
|
<div class="mb-5 flex items-center gap-5">
|
||||||
<span>原始样式(Iconify):</span>
|
<span>原始样式(Iconify):</span>
|
||||||
<IconPicker class="w-[200px]" />
|
<IconPicker v-model="iconValue1" class="w-[200px]" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-5 flex items-center gap-5">
|
<div class="mb-5 flex items-center gap-5">
|
||||||
<span>原始样式(svg):</span>
|
<span>原始样式(svg):</span>
|
||||||
<IconPicker class="w-[200px]" prefix="svg" />
|
<IconPicker v-model="iconValue2" class="w-[200px]" prefix="svg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-5 flex items-center gap-5">
|
<div class="mb-5 flex items-center gap-5">
|
||||||
<span>使用Input:</span>
|
<span>自定义Input:</span>
|
||||||
<IconPicker :input-component="inputComponent" icon-slot="addonAfter" />
|
<IconPicker
|
||||||
|
:input-component="inputComponent"
|
||||||
|
v-model="iconValue3"
|
||||||
|
icon-slot="addonAfter"
|
||||||
|
model-value-prop="value"
|
||||||
|
prefix="mdi"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-5">
|
<div class="flex items-center gap-5">
|
||||||
<span>可手动输入,只能点击图标打开弹窗:</span>
|
<span>显示为一个Icon:</span>
|
||||||
<Input
|
<Input
|
||||||
v-model:value="iconValue"
|
v-model:value="iconValue4"
|
||||||
allow-clear
|
allow-clear
|
||||||
placeholder="点击这里选择图标"
|
placeholder="点击这里选择图标"
|
||||||
style="width: 300px"
|
style="width: 300px"
|
||||||
>
|
>
|
||||||
<template #addonAfter>
|
<template #addonAfter>
|
||||||
<IconPicker v-model="iconValue" class="w-[200px]" />
|
<IconPicker v-model="iconValue4" prefix="mdi-light" type="icon" />
|
||||||
</template>
|
</template>
|
||||||
</Input>
|
</Input>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user