From 9fa93f3d3d43234bb19c675a192cdc38b350eaaf Mon Sep 17 00:00:00 2001
From: ihuangxiaomin <1219549841@qq.com>
Date: Thu, 7 Nov 2024 17:27:52 +0800
Subject: [PATCH 1/3] feat: add icon-picker component

---
 packages/@core/base/icons/src/index.ts        |   7 +-
 packages/@core/base/icons/src/lucide.ts       |   2 +
 .../src/components/button/icon-button.vue     |   8 +-
 packages/effects/common-ui/package.json       |   1 +
 .../components/icon-picker/icon-picker.vue    | 148 ++++
 .../src/components/icon-picker/index.ts       |   1 +
 .../effects/common-ui/src/components/index.ts |   1 +
 packages/effects/hooks/src/index.ts           |   1 +
 packages/effects/hooks/src/use-pagination.ts  |  34 +
 .../demos/features/icons/icon-picker.vue      |  82 ++
 .../views/demos/features/icons/icons.data.ts  | 793 ++++++++++++++++++
 .../src/views/demos/features/icons/index.vue  |  16 +-
 pnpm-lock.yaml                                |   3 +
 13 files changed, 1094 insertions(+), 3 deletions(-)
 create mode 100644 packages/effects/common-ui/src/components/icon-picker/icon-picker.vue
 create mode 100644 packages/effects/common-ui/src/components/icon-picker/index.ts
 create mode 100644 packages/effects/hooks/src/use-pagination.ts
 create mode 100644 playground/src/views/demos/features/icons/icon-picker.vue
 create mode 100644 playground/src/views/demos/features/icons/icons.data.ts

diff --git a/packages/@core/base/icons/src/index.ts b/packages/@core/base/icons/src/index.ts
index 1e0fe56446f..85a47d27a2f 100644
--- a/packages/@core/base/icons/src/index.ts
+++ b/packages/@core/base/icons/src/index.ts
@@ -4,4 +4,9 @@ export * from './create-icon';
 export * from './lucide';
 
 export type { IconifyIcon as IconifyIconStructure } from '@iconify/vue';
-export { addCollection, addIcon, Icon as IconifyIcon } from '@iconify/vue';
+export {
+  addCollection,
+  addIcon,
+  Icon as IconifyIcon,
+  listIcons,
+} from '@iconify/vue';
diff --git a/packages/@core/base/icons/src/lucide.ts b/packages/@core/base/icons/src/lucide.ts
index 592bd73c647..152dc5cd205 100644
--- a/packages/@core/base/icons/src/lucide.ts
+++ b/packages/@core/base/icons/src/lucide.ts
@@ -27,6 +27,7 @@ export {
   FoldHorizontal,
   Fullscreen,
   Github,
+  Grip,
   Info,
   InspectionPanel,
   Languages,
@@ -40,6 +41,7 @@ export {
   Minimize,
   Minimize2,
   MoonStar,
+  Package2,
   Palette,
   PanelLeft,
   PanelRight,
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/button/icon-button.vue b/packages/@core/ui-kit/shadcn-ui/src/components/button/icon-button.vue
index 7c123a91dbd..ac626c1b4fa 100644
--- a/packages/@core/ui-kit/shadcn-ui/src/components/button/icon-button.vue
+++ b/packages/@core/ui-kit/shadcn-ui/src/components/button/icon-button.vue
@@ -14,6 +14,7 @@ interface Props extends VbenButtonProps {
   disabled?: boolean;
   onClick?: () => void;
   tooltip?: string;
+  tooltipDelayDuration?: number;
   tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
   variant?: ButtonVariants;
 }
@@ -21,6 +22,7 @@ interface Props extends VbenButtonProps {
 const props = withDefaults(defineProps<Props>(), {
   disabled: false,
   onClick: () => {},
+  tooltipDelayDuration: 200,
   tooltipSide: 'bottom',
   variant: 'icon',
 });
@@ -42,7 +44,11 @@ const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
     <slot></slot>
   </VbenButton>
 
-  <VbenTooltip v-else :side="tooltipSide">
+  <VbenTooltip
+    v-else
+    :delay-duration="tooltipDelayDuration"
+    :side="tooltipSide"
+  >
     <template #trigger>
       <VbenButton
         :class="cn('rounded-full', props.class)"
diff --git a/packages/effects/common-ui/package.json b/packages/effects/common-ui/package.json
index 9d18a0710e2..aa757d283d4 100644
--- a/packages/effects/common-ui/package.json
+++ b/packages/effects/common-ui/package.json
@@ -28,6 +28,7 @@
     "@vben/icons": "workspace:*",
     "@vben/locales": "workspace:*",
     "@vben/types": "workspace:*",
+    "@vben/hooks": "workspace:*",
     "@vueuse/core": "catalog:",
     "@vueuse/integrations": "catalog:",
     "qrcode": "catalog:",
diff --git a/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue b/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue
new file mode 100644
index 00000000000..6bbbc7559a6
--- /dev/null
+++ b/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue
@@ -0,0 +1,148 @@
+<script setup lang="ts">
+import { ref, watch, watchEffect } from 'vue';
+
+import { usePagination } from '@vben/hooks';
+import { Grip, Package2 } from '@vben/icons';
+import {
+  Button,
+  Pagination,
+  PaginationEllipsis,
+  PaginationFirst,
+  PaginationLast,
+  PaginationList,
+  PaginationListItem,
+  PaginationNext,
+  PaginationPrev,
+  VbenIcon,
+  VbenIconButton,
+  VbenPopover,
+} from '@vben-core/shadcn-ui';
+
+interface Props {
+  value?: string;
+  pageSize?: number;
+  /**
+   * 图标列表
+   */
+  icons?: string[];
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  value: '',
+  pageSize: 36,
+  icons: () => [],
+});
+
+const emit = defineEmits<{
+  change: [string];
+  'update:value': [string];
+}>();
+
+const currentSelect = ref('');
+const currentList = ref(props.icons);
+const refTrigger = ref<HTMLDivElement>();
+
+const { getPaginationList, getTotal, setCurrentPage } = usePagination(
+  currentList,
+  props.pageSize,
+);
+
+watchEffect(() => {
+  currentSelect.value = props.value;
+});
+
+watch(
+  () => currentSelect.value,
+  (v) => {
+    emit('update:value', v);
+    emit('change', v);
+  },
+);
+
+const handleClick = (icon: string) => {
+  currentSelect.value = icon;
+};
+
+const handlePageChange = (page: number) => {
+  setCurrentPage(page);
+};
+
+const changeOpenState = () => {
+  if (refTrigger.value) {
+    refTrigger.value.click();
+  }
+};
+
+defineExpose({ changeOpenState });
+</script>
+<template>
+  <VbenPopover
+    :content-props="{ align: 'end', alignOffset: -11, sideOffset: 8 }"
+    content-class="p-0 py-4"
+  >
+    <template #trigger>
+      <div ref="refTrigger">
+        <VbenIcon :icon="currentSelect || Grip" class="size-6" />
+      </div>
+    </template>
+
+    <div v-if="getPaginationList.length > 0">
+      <div class="grid max-h-[360px] w-full grid-cols-6 justify-items-center">
+        <VbenIconButton
+          v-for="(item, index) in getPaginationList"
+          :key="index"
+          :tooltip="item"
+          tooltip-side="top"
+          @click="handleClick(item)"
+        >
+          <VbenIcon :icon="item" />
+        </VbenIconButton>
+      </div>
+      <div v-if="getTotal >= pageSize" class="flex-center pt-1">
+        <Pagination
+          v-slot="{ page }"
+          :items-per-page="36"
+          :sibling-count="1"
+          :total="getTotal"
+          show-edges
+          @update:page="handlePageChange"
+        >
+          <PaginationList v-slot="{ items }" class="flex items-center gap-1">
+            <PaginationFirst class="size-5" />
+            <PaginationPrev class="size-5" />
+            <template v-for="(item, index) in items">
+              <PaginationListItem
+                v-if="item.type === 'page'"
+                :key="index"
+                :value="item.value"
+                as-child
+              >
+                <Button
+                  :variant="item.value === page ? 'default' : 'outline'"
+                  class="size-5 p-0 text-sm"
+                >
+                  {{ item.value }}
+                </Button>
+              </PaginationListItem>
+              <PaginationEllipsis
+                v-else
+                :key="item.type"
+                :index="index"
+                class="size-5"
+              />
+            </template>
+            <PaginationNext class="size-5" />
+            <PaginationLast class="size-5" />
+          </PaginationList>
+        </Pagination>
+      </div>
+    </div>
+
+    <template v-else>
+      <div class="flex-col-center text-muted-foreground min-h-[150px] w-full">
+        <Package2 />
+        <div>{{ $t('common.noData') }}</div>
+      </div>
+    </template>
+  </VbenPopover>
+</template>
diff --git a/packages/effects/common-ui/src/components/icon-picker/index.ts b/packages/effects/common-ui/src/components/icon-picker/index.ts
new file mode 100644
index 00000000000..3dabc86a927
--- /dev/null
+++ b/packages/effects/common-ui/src/components/icon-picker/index.ts
@@ -0,0 +1 @@
+export { default as IconPicker } from './icon-picker.vue';
diff --git a/packages/effects/common-ui/src/components/index.ts b/packages/effects/common-ui/src/components/index.ts
index 12d1caceb73..f5e3c7a9b49 100644
--- a/packages/effects/common-ui/src/components/index.ts
+++ b/packages/effects/common-ui/src/components/index.ts
@@ -1,5 +1,6 @@
 export * from './captcha';
 export * from './ellipsis-text';
+export * from './icon-picker';
 export * from './page';
 export * from '@vben-core/form-ui';
 export * from '@vben-core/popup-ui';
diff --git a/packages/effects/hooks/src/index.ts b/packages/effects/hooks/src/index.ts
index 865c903e2f4..51f640602fd 100644
--- a/packages/effects/hooks/src/index.ts
+++ b/packages/effects/hooks/src/index.ts
@@ -1,6 +1,7 @@
 export * from './use-app-config';
 export * from './use-content-maximize';
 export * from './use-design-tokens';
+export * from './use-pagination';
 export * from './use-refresh';
 export * from './use-tabs';
 export * from './use-watermark';
diff --git a/packages/effects/hooks/src/use-pagination.ts b/packages/effects/hooks/src/use-pagination.ts
new file mode 100644
index 00000000000..5422e87f268
--- /dev/null
+++ b/packages/effects/hooks/src/use-pagination.ts
@@ -0,0 +1,34 @@
+import type { Ref } from 'vue';
+import { computed, ref, unref } from 'vue';
+
+function pagination<T = any>(list: T[], pageNo: number, pageSize: number): T[] {
+  const offset = (pageNo - 1) * Number(pageSize);
+  const ret =
+    offset + Number(pageSize) >= list.length
+      ? list.slice(offset)
+      : list.slice(offset, offset + Number(pageSize));
+  return ret;
+}
+
+export function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {
+  const currentPage = ref(1);
+  const pageSizeRef = ref(pageSize);
+
+  const getPaginationList = computed(() => {
+    return pagination(unref(list), unref(currentPage), unref(pageSizeRef));
+  });
+
+  const getTotal = computed(() => {
+    return unref(list).length;
+  });
+
+  function setCurrentPage(page: number) {
+    currentPage.value = page;
+  }
+
+  function setPageSize(pageSize: number) {
+    pageSizeRef.value = pageSize;
+  }
+
+  return { setCurrentPage, getTotal, setPageSize, getPaginationList };
+}
diff --git a/playground/src/views/demos/features/icons/icon-picker.vue b/playground/src/views/demos/features/icons/icon-picker.vue
new file mode 100644
index 00000000000..6e05637f10e
--- /dev/null
+++ b/playground/src/views/demos/features/icons/icon-picker.vue
@@ -0,0 +1,82 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { IconPicker } from '@vben/common-ui';
+import { listIcons } from '@vben/icons';
+
+import { Input } from 'ant-design-vue';
+
+import iconsData from './icons.data';
+
+export interface Props {
+  allowClear?: boolean;
+  pageSize?: number;
+  /**
+   * 可以通过prefix获取系统中使用的图标集
+   */
+  prefix?: string;
+  readonly?: boolean;
+  value?: string;
+  width?: string;
+}
+
+// Don't inherit FormItem disabled、placeholder...
+defineOptions({
+  inheritAttrs: false,
+});
+
+const props = withDefaults(defineProps<Props>(), {
+  allowClear: true,
+  pageSize: 36,
+  prefix: '',
+  readonly: false,
+  value: '',
+  width: '100%',
+});
+
+const refIconPicker = ref();
+const currentSelect = ref('');
+
+function getIcons() {
+  if (props.prefix) {
+    return listIcons('', props.prefix);
+  } else {
+    const prefix = iconsData.prefix;
+    return iconsData.icons.map((icon) => `${prefix}:${icon}`);
+  }
+}
+
+const currentList = ref(getIcons());
+
+const triggerPopover = () => {
+  if (refIconPicker.value) {
+    refIconPicker.value.changeOpenState();
+  }
+};
+
+const handleChange = (icon: string) => {
+  currentSelect.value = icon;
+};
+</script>
+
+<template>
+  <Input
+    v-model:value="currentSelect"
+    :allow-clear="props.allowClear"
+    :readonly="props.readonly"
+    :style="{ width }"
+    class="cursor-pointer"
+    placeholder="点击选中图标"
+    @click="triggerPopover"
+  >
+    <template #addonAfter>
+      <IconPicker
+        ref="refIconPicker"
+        :icons="currentList"
+        :page-size="pageSize"
+        :value="currentSelect"
+        @change="handleChange"
+      />
+    </template>
+  </Input>
+</template>
diff --git a/playground/src/views/demos/features/icons/icons.data.ts b/playground/src/views/demos/features/icons/icons.data.ts
new file mode 100644
index 00000000000..833f051979b
--- /dev/null
+++ b/playground/src/views/demos/features/icons/icons.data.ts
@@ -0,0 +1,793 @@
+export default {
+  icons: [
+    'account-book-filled',
+    'account-book-outlined',
+    'account-book-twotone',
+    'aim-outlined',
+    'alert-filled',
+    'alert-outlined',
+    'alert-twotone',
+    'alibaba-outlined',
+    'align-center-outlined',
+    'align-left-outlined',
+    'align-right-outlined',
+    'alipay-circle-filled',
+    'alipay-circle-outlined',
+    'alipay-outlined',
+    'alipay-square-filled',
+    'aliwangwang-filled',
+    'aliwangwang-outlined',
+    'aliyun-outlined',
+    'amazon-circle-filled',
+    'amazon-outlined',
+    'amazon-square-filled',
+    'android-filled',
+    'android-outlined',
+    'ant-cloud-outlined',
+    'ant-design-outlined',
+    'apartment-outlined',
+    'api-filled',
+    'api-outlined',
+    'api-twotone',
+    'apple-filled',
+    'apple-outlined',
+    'appstore-add-outlined',
+    'appstore-filled',
+    'appstore-outlined',
+    'appstore-twotone',
+    'area-chart-outlined',
+    'arrow-down-outlined',
+    'arrow-left-outlined',
+    'arrow-right-outlined',
+    'arrow-up-outlined',
+    'arrows-alt-outlined',
+    'audio-filled',
+    'audio-muted-outlined',
+    'audio-outlined',
+    'audio-twotone',
+    'audit-outlined',
+    'backward-filled',
+    'backward-outlined',
+    'bank-filled',
+    'bank-outlined',
+    'bank-twotone',
+    'bar-chart-outlined',
+    'barcode-outlined',
+    'bars-outlined',
+    'behance-circle-filled',
+    'behance-outlined',
+    'behance-square-filled',
+    'behance-square-outlined',
+    'bell-filled',
+    'bell-outlined',
+    'bell-twotone',
+    'bg-colors-outlined',
+    'block-outlined',
+    'bold-outlined',
+    'book-filled',
+    'book-outlined',
+    'book-twotone',
+    'border-bottom-outlined',
+    'border-horizontal-outlined',
+    'border-inner-outlined',
+    'border-left-outlined',
+    'border-outer-outlined',
+    'border-outlined',
+    'border-right-outlined',
+    'border-top-outlined',
+    'border-verticle-outlined',
+    'borderless-table-outlined',
+    'box-plot-filled',
+    'box-plot-outlined',
+    'box-plot-twotone',
+    'branches-outlined',
+    'bug-filled',
+    'bug-outlined',
+    'bug-twotone',
+    'build-filled',
+    'build-outlined',
+    'build-twotone',
+    'bulb-filled',
+    'bulb-outlined',
+    'bulb-twotone',
+    'calculator-filled',
+    'calculator-outlined',
+    'calculator-twotone',
+    'calendar-filled',
+    'calendar-outlined',
+    'calendar-twotone',
+    'camera-filled',
+    'camera-outlined',
+    'camera-twotone',
+    'car-filled',
+    'car-outlined',
+    'car-twotone',
+    'caret-down-filled',
+    'caret-down-outlined',
+    'caret-left-filled',
+    'caret-left-outlined',
+    'caret-right-filled',
+    'caret-right-outlined',
+    'caret-up-filled',
+    'caret-up-outlined',
+    'carry-out-filled',
+    'carry-out-outlined',
+    'carry-out-twotone',
+    'check-circle-filled',
+    'check-circle-outlined',
+    'check-circle-twotone',
+    'check-outlined',
+    'check-square-filled',
+    'check-square-outlined',
+    'check-square-twotone',
+    'chrome-filled',
+    'chrome-outlined',
+    'ci-circle-filled',
+    'ci-circle-outlined',
+    'ci-circle-twotone',
+    'ci-outlined',
+    'ci-twotone',
+    'clear-outlined',
+    'clock-circle-filled',
+    'clock-circle-outlined',
+    'clock-circle-twotone',
+    'close-circle-filled',
+    'close-circle-outlined',
+    'close-circle-twotone',
+    'close-outlined',
+    'close-square-filled',
+    'close-square-outlined',
+    'close-square-twotone',
+    'cloud-download-outlined',
+    'cloud-filled',
+    'cloud-outlined',
+    'cloud-server-outlined',
+    'cloud-sync-outlined',
+    'cloud-twotone',
+    'cloud-upload-outlined',
+    'cluster-outlined',
+    'code-filled',
+    'code-outlined',
+    'code-sandbox-circle-filled',
+    'code-sandbox-outlined',
+    'code-sandbox-square-filled',
+    'code-twotone',
+    'codepen-circle-filled',
+    'codepen-circle-outlined',
+    'codepen-outlined',
+    'codepen-square-filled',
+    'coffee-outlined',
+    'column-height-outlined',
+    'column-width-outlined',
+    'comment-outlined',
+    'compass-filled',
+    'compass-outlined',
+    'compass-twotone',
+    'compress-outlined',
+    'console-sql-outlined',
+    'contacts-filled',
+    'contacts-outlined',
+    'contacts-twotone',
+    'container-filled',
+    'container-outlined',
+    'container-twotone',
+    'control-filled',
+    'control-outlined',
+    'control-twotone',
+    'copy-filled',
+    'copy-outlined',
+    'copy-twotone',
+    'copyright-circle-filled',
+    'copyright-circle-outlined',
+    'copyright-circle-twotone',
+    'copyright-outlined',
+    'copyright-twotone',
+    'credit-card-filled',
+    'credit-card-outlined',
+    'credit-card-twotone',
+    'crown-filled',
+    'crown-outlined',
+    'crown-twotone',
+    'customer-service-filled',
+    'customer-service-outlined',
+    'customer-service-twotone',
+    'dash-outlined',
+    'dashboard-filled',
+    'dashboard-outlined',
+    'dashboard-twotone',
+    'database-filled',
+    'database-outlined',
+    'database-twotone',
+    'delete-column-outlined',
+    'delete-filled',
+    'delete-outlined',
+    'delete-row-outlined',
+    'delete-twotone',
+    'delivered-procedure-outlined',
+    'deployment-unit-outlined',
+    'desktop-outlined',
+    'diff-filled',
+    'diff-outlined',
+    'diff-twotone',
+    'dingding-outlined',
+    'dingtalk-circle-filled',
+    'dingtalk-outlined',
+    'dingtalk-square-filled',
+    'disconnect-outlined',
+    'dislike-filled',
+    'dislike-outlined',
+    'dislike-twotone',
+    'dollar-circle-filled',
+    'dollar-circle-outlined',
+    'dollar-circle-twotone',
+    'dollar-outlined',
+    'dollar-twotone',
+    'dot-chart-outlined',
+    'double-left-outlined',
+    'double-right-outlined',
+    'down-circle-filled',
+    'down-circle-outlined',
+    'down-circle-twotone',
+    'down-outlined',
+    'down-square-filled',
+    'down-square-outlined',
+    'down-square-twotone',
+    'download-outlined',
+    'drag-outlined',
+    'dribbble-circle-filled',
+    'dribbble-outlined',
+    'dribbble-square-filled',
+    'dribbble-square-outlined',
+    'dropbox-circle-filled',
+    'dropbox-outlined',
+    'dropbox-square-filled',
+    'edit-filled',
+    'edit-outlined',
+    'edit-twotone',
+    'ellipsis-outlined',
+    'enter-outlined',
+    'environment-filled',
+    'environment-outlined',
+    'environment-twotone',
+    'euro-circle-filled',
+    'euro-circle-outlined',
+    'euro-circle-twotone',
+    'euro-outlined',
+    'euro-twotone',
+    'exception-outlined',
+    'exclamation-circle-filled',
+    'exclamation-circle-outlined',
+    'exclamation-circle-twotone',
+    'exclamation-outlined',
+    'expand-alt-outlined',
+    'expand-outlined',
+    'experiment-filled',
+    'experiment-outlined',
+    'experiment-twotone',
+    'export-outlined',
+    'eye-filled',
+    'eye-invisible-filled',
+    'eye-invisible-outlined',
+    'eye-invisible-twotone',
+    'eye-outlined',
+    'eye-twotone',
+    'facebook-filled',
+    'facebook-outlined',
+    'fall-outlined',
+    'fast-backward-filled',
+    'fast-backward-outlined',
+    'fast-forward-filled',
+    'fast-forward-outlined',
+    'field-binary-outlined',
+    'field-number-outlined',
+    'field-string-outlined',
+    'field-time-outlined',
+    'file-add-filled',
+    'file-add-outlined',
+    'file-add-twotone',
+    'file-done-outlined',
+    'file-excel-filled',
+    'file-excel-outlined',
+    'file-excel-twotone',
+    'file-exclamation-filled',
+    'file-exclamation-outlined',
+    'file-exclamation-twotone',
+    'file-filled',
+    'file-gif-outlined',
+    'file-image-filled',
+    'file-image-outlined',
+    'file-image-twotone',
+    'file-jpg-outlined',
+    'file-markdown-filled',
+    'file-markdown-outlined',
+    'file-markdown-twotone',
+    'file-outlined',
+    'file-pdf-filled',
+    'file-pdf-outlined',
+    'file-pdf-twotone',
+    'file-ppt-filled',
+    'file-ppt-outlined',
+    'file-ppt-twotone',
+    'file-protect-outlined',
+    'file-search-outlined',
+    'file-sync-outlined',
+    'file-text-filled',
+    'file-text-outlined',
+    'file-text-twotone',
+    'file-twotone',
+    'file-unknown-filled',
+    'file-unknown-outlined',
+    'file-unknown-twotone',
+    'file-word-filled',
+    'file-word-outlined',
+    'file-word-twotone',
+    'file-zip-filled',
+    'file-zip-outlined',
+    'file-zip-twotone',
+    'filter-filled',
+    'filter-outlined',
+    'filter-twotone',
+    'fire-filled',
+    'fire-outlined',
+    'fire-twotone',
+    'flag-filled',
+    'flag-outlined',
+    'flag-twotone',
+    'folder-add-filled',
+    'folder-add-outlined',
+    'folder-add-twotone',
+    'folder-filled',
+    'folder-open-filled',
+    'folder-open-outlined',
+    'folder-open-twotone',
+    'folder-outlined',
+    'folder-twotone',
+    'folder-view-outlined',
+    'font-colors-outlined',
+    'font-size-outlined',
+    'fork-outlined',
+    'form-outlined',
+    'format-painter-filled',
+    'format-painter-outlined',
+    'forward-filled',
+    'forward-outlined',
+    'frown-filled',
+    'frown-outlined',
+    'frown-twotone',
+    'fullscreen-exit-outlined',
+    'fullscreen-outlined',
+    'function-outlined',
+    'fund-filled',
+    'fund-outlined',
+    'fund-projection-screen-outlined',
+    'fund-twotone',
+    'fund-view-outlined',
+    'funnel-plot-filled',
+    'funnel-plot-outlined',
+    'funnel-plot-twotone',
+    'gateway-outlined',
+    'gif-outlined',
+    'gift-filled',
+    'gift-outlined',
+    'gift-twotone',
+    'github-filled',
+    'github-outlined',
+    'gitlab-filled',
+    'gitlab-outlined',
+    'global-outlined',
+    'gold-filled',
+    'gold-outlined',
+    'gold-twotone',
+    'golden-filled',
+    'google-circle-filled',
+    'google-outlined',
+    'google-plus-circle-filled',
+    'google-plus-outlined',
+    'google-plus-square-filled',
+    'google-square-filled',
+    'group-outlined',
+    'hdd-filled',
+    'hdd-outlined',
+    'hdd-twotone',
+    'heart-filled',
+    'heart-outlined',
+    'heart-twotone',
+    'heat-map-outlined',
+    'highlight-filled',
+    'highlight-outlined',
+    'highlight-twotone',
+    'history-outlined',
+    'home-filled',
+    'home-outlined',
+    'home-twotone',
+    'hourglass-filled',
+    'hourglass-outlined',
+    'hourglass-twotone',
+    'html5-filled',
+    'html5-outlined',
+    'html5-twotone',
+    'idcard-filled',
+    'idcard-outlined',
+    'idcard-twotone',
+    'ie-circle-filled',
+    'ie-outlined',
+    'ie-square-filled',
+    'import-outlined',
+    'inbox-outlined',
+    'info-circle-filled',
+    'info-circle-outlined',
+    'info-circle-twotone',
+    'info-outlined',
+    'insert-row-above-outlined',
+    'insert-row-below-outlined',
+    'insert-row-left-outlined',
+    'insert-row-right-outlined',
+    'instagram-filled',
+    'instagram-outlined',
+    'insurance-filled',
+    'insurance-outlined',
+    'insurance-twotone',
+    'interaction-filled',
+    'interaction-outlined',
+    'interaction-twotone',
+    'issues-close-outlined',
+    'italic-outlined',
+    'key-outlined',
+    'laptop-outlined',
+    'layout-filled',
+    'layout-outlined',
+    'layout-twotone',
+    'left-circle-filled',
+    'left-circle-outlined',
+    'left-circle-twotone',
+    'left-outlined',
+    'left-square-filled',
+    'left-square-outlined',
+    'left-square-twotone',
+    'like-filled',
+    'like-outlined',
+    'like-twotone',
+    'line-chart-outlined',
+    'line-height-outlined',
+    'line-outlined',
+    'link-outlined',
+    'linkedin-filled',
+    'linkedin-outlined',
+    'loading-3-quarters-outlined',
+    'loading-outlined',
+    'lock-filled',
+    'lock-outlined',
+    'lock-twotone',
+    'login-outlined',
+    'logout-outlined',
+    'mac-command-filled',
+    'mac-command-outlined',
+    'mail-filled',
+    'mail-outlined',
+    'mail-twotone',
+    'man-outlined',
+    'medicine-box-filled',
+    'medicine-box-outlined',
+    'medicine-box-twotone',
+    'medium-circle-filled',
+    'medium-outlined',
+    'medium-square-filled',
+    'medium-workmark-outlined',
+    'meh-filled',
+    'meh-outlined',
+    'meh-twotone',
+    'menu-fold-outlined',
+    'menu-outlined',
+    'menu-unfold-outlined',
+    'merge-cells-outlined',
+    'message-filled',
+    'message-outlined',
+    'message-twotone',
+    'minus-circle-filled',
+    'minus-circle-outlined',
+    'minus-circle-twotone',
+    'minus-outlined',
+    'minus-square-filled',
+    'minus-square-outlined',
+    'minus-square-twotone',
+    'mobile-filled',
+    'mobile-outlined',
+    'mobile-twotone',
+    'money-collect-filled',
+    'money-collect-outlined',
+    'money-collect-twotone',
+    'monitor-outlined',
+    'more-outlined',
+    'node-collapse-outlined',
+    'node-expand-outlined',
+    'node-index-outlined',
+    'notification-filled',
+    'notification-outlined',
+    'notification-twotone',
+    'number-outlined',
+    'one-to-one-outlined',
+    'ordered-list-outlined',
+    'paper-clip-outlined',
+    'partition-outlined',
+    'pause-circle-filled',
+    'pause-circle-outlined',
+    'pause-circle-twotone',
+    'pause-outlined',
+    'pay-circle-filled',
+    'pay-circle-outlined',
+    'percentage-outlined',
+    'phone-filled',
+    'phone-outlined',
+    'phone-twotone',
+    'pic-center-outlined',
+    'pic-left-outlined',
+    'pic-right-outlined',
+    'picture-filled',
+    'picture-outlined',
+    'picture-twotone',
+    'pie-chart-filled',
+    'pie-chart-outlined',
+    'pie-chart-twotone',
+    'play-circle-filled',
+    'play-circle-outlined',
+    'play-circle-twotone',
+    'play-square-filled',
+    'play-square-outlined',
+    'play-square-twotone',
+    'plus-circle-filled',
+    'plus-circle-outlined',
+    'plus-circle-twotone',
+    'plus-outlined',
+    'plus-square-filled',
+    'plus-square-outlined',
+    'plus-square-twotone',
+    'pound-circle-filled',
+    'pound-circle-outlined',
+    'pound-circle-twotone',
+    'pound-outlined',
+    'poweroff-outlined',
+    'printer-filled',
+    'printer-outlined',
+    'printer-twotone',
+    'profile-filled',
+    'profile-outlined',
+    'profile-twotone',
+    'project-filled',
+    'project-outlined',
+    'project-twotone',
+    'property-safety-filled',
+    'property-safety-outlined',
+    'property-safety-twotone',
+    'pull-request-outlined',
+    'pushpin-filled',
+    'pushpin-outlined',
+    'pushpin-twotone',
+    'qq-circle-filled',
+    'qq-outlined',
+    'qq-square-filled',
+    'qrcode-outlined',
+    'question-circle-filled',
+    'question-circle-outlined',
+    'question-circle-twotone',
+    'question-outlined',
+    'radar-chart-outlined',
+    'radius-bottomleft-outlined',
+    'radius-bottomright-outlined',
+    'radius-setting-outlined',
+    'radius-upleft-outlined',
+    'radius-upright-outlined',
+    'read-filled',
+    'read-outlined',
+    'reconciliation-filled',
+    'reconciliation-outlined',
+    'reconciliation-twotone',
+    'red-envelope-filled',
+    'red-envelope-outlined',
+    'red-envelope-twotone',
+    'reddit-circle-filled',
+    'reddit-outlined',
+    'reddit-square-filled',
+    'redo-outlined',
+    'reload-outlined',
+    'rest-filled',
+    'rest-outlined',
+    'rest-twotone',
+    'retweet-outlined',
+    'right-circle-filled',
+    'right-circle-outlined',
+    'right-circle-twotone',
+    'right-outlined',
+    'right-square-filled',
+    'right-square-outlined',
+    'right-square-twotone',
+    'rise-outlined',
+    'robot-filled',
+    'robot-outlined',
+    'rocket-filled',
+    'rocket-outlined',
+    'rocket-twotone',
+    'rollback-outlined',
+    'rotate-left-outlined',
+    'rotate-right-outlined',
+    'safety-certificate-filled',
+    'safety-certificate-outlined',
+    'safety-certificate-twotone',
+    'safety-outlined',
+    'save-filled',
+    'save-outlined',
+    'save-twotone',
+    'scan-outlined',
+    'schedule-filled',
+    'schedule-outlined',
+    'schedule-twotone',
+    'scissor-outlined',
+    'search-outlined',
+    'security-scan-filled',
+    'security-scan-outlined',
+    'security-scan-twotone',
+    'select-outlined',
+    'send-outlined',
+    'setting-filled',
+    'setting-outlined',
+    'setting-twotone',
+    'shake-outlined',
+    'share-alt-outlined',
+    'shop-filled',
+    'shop-outlined',
+    'shop-twotone',
+    'shopping-cart-outlined',
+    'shopping-filled',
+    'shopping-outlined',
+    'shopping-twotone',
+    'shrink-outlined',
+    'signal-filled',
+    'sisternode-outlined',
+    'sketch-circle-filled',
+    'sketch-outlined',
+    'sketch-square-filled',
+    'skin-filled',
+    'skin-outlined',
+    'skin-twotone',
+    'skype-filled',
+    'skype-outlined',
+    'slack-circle-filled',
+    'slack-outlined',
+    'slack-square-filled',
+    'slack-square-outlined',
+    'sliders-filled',
+    'sliders-outlined',
+    'sliders-twotone',
+    'small-dash-outlined',
+    'smile-filled',
+    'smile-outlined',
+    'smile-twotone',
+    'snippets-filled',
+    'snippets-outlined',
+    'snippets-twotone',
+    'solution-outlined',
+    'sort-ascending-outlined',
+    'sort-descending-outlined',
+    'sound-filled',
+    'sound-outlined',
+    'sound-twotone',
+    'split-cells-outlined',
+    'star-filled',
+    'star-outlined',
+    'star-twotone',
+    'step-backward-filled',
+    'step-backward-outlined',
+    'step-forward-filled',
+    'step-forward-outlined',
+    'stock-outlined',
+    'stop-filled',
+    'stop-outlined',
+    'stop-twotone',
+    'strikethrough-outlined',
+    'subnode-outlined',
+    'swap-left-outlined',
+    'swap-outlined',
+    'swap-right-outlined',
+    'switcher-filled',
+    'switcher-outlined',
+    'switcher-twotone',
+    'sync-outlined',
+    'table-outlined',
+    'tablet-filled',
+    'tablet-outlined',
+    'tablet-twotone',
+    'tag-filled',
+    'tag-outlined',
+    'tag-twotone',
+    'tags-filled',
+    'tags-outlined',
+    'tags-twotone',
+    'taobao-circle-filled',
+    'taobao-circle-outlined',
+    'taobao-outlined',
+    'taobao-square-filled',
+    'team-outlined',
+    'thunderbolt-filled',
+    'thunderbolt-outlined',
+    'thunderbolt-twotone',
+    'to-top-outlined',
+    'tool-filled',
+    'tool-outlined',
+    'tool-twotone',
+    'trademark-circle-filled',
+    'trademark-circle-outlined',
+    'trademark-circle-twotone',
+    'trademark-outlined',
+    'transaction-outlined',
+    'translation-outlined',
+    'trophy-filled',
+    'trophy-outlined',
+    'trophy-twotone',
+    'twitter-circle-filled',
+    'twitter-outlined',
+    'twitter-square-filled',
+    'underline-outlined',
+    'undo-outlined',
+    'ungroup-outlined',
+    'unlock-filled',
+    'unlock-outlined',
+    'unlock-twotone',
+    'unordered-list-outlined',
+    'up-circle-filled',
+    'up-circle-outlined',
+    'up-circle-twotone',
+    'up-outlined',
+    'up-square-filled',
+    'up-square-outlined',
+    'up-square-twotone',
+    'upload-outlined',
+    'usb-filled',
+    'usb-outlined',
+    'usb-twotone',
+    'user-add-outlined',
+    'user-delete-outlined',
+    'user-outlined',
+    'user-switch-outlined',
+    'usergroup-add-outlined',
+    'usergroup-delete-outlined',
+    'verified-outlined',
+    'vertical-align-bottom-outlined',
+    'vertical-align-middle-outlined',
+    'vertical-align-top-outlined',
+    'vertical-left-outlined',
+    'vertical-right-outlined',
+    'video-camera-add-outlined',
+    'video-camera-filled',
+    'video-camera-outlined',
+    'video-camera-twotone',
+    'wallet-filled',
+    'wallet-outlined',
+    'wallet-twotone',
+    'warning-filled',
+    'warning-outlined',
+    'warning-twotone',
+    'wechat-filled',
+    'wechat-outlined',
+    'weibo-circle-filled',
+    'weibo-circle-outlined',
+    'weibo-outlined',
+    'weibo-square-filled',
+    'weibo-square-outlined',
+    'whats-app-outlined',
+    'wifi-outlined',
+    'windows-filled',
+    'windows-outlined',
+    'woman-outlined',
+    'yahoo-filled',
+    'yahoo-outlined',
+    'youtube-filled',
+    'youtube-outlined',
+    'yuque-filled',
+    'yuque-outlined',
+    'zhihu-circle-filled',
+    'zhihu-outlined',
+    'zhihu-square-filled',
+    'zoom-in-outlined',
+    'zoom-out-outlined',
+  ],
+  prefix: 'ant-design',
+};
diff --git a/playground/src/views/demos/features/icons/index.vue b/playground/src/views/demos/features/icons/index.vue
index 0e5c28b2349..ce958b16df9 100644
--- a/playground/src/views/demos/features/icons/index.vue
+++ b/playground/src/views/demos/features/icons/index.vue
@@ -17,6 +17,8 @@ import {
 } from '@vben/icons';
 
 import { Card } from 'ant-design-vue';
+
+import IconPicker from './icon-picker.vue';
 </script>
 
 <template>
@@ -45,7 +47,7 @@ import { Card } from 'ant-design-vue';
       </div>
     </Card>
 
-    <Card title="Svg Icons">
+    <Card class="mb-5" title="Svg Icons">
       <div class="flex items-center gap-5">
         <SvgAvatar1Icon class="size-8" />
         <SvgAvatar2Icon class="size-8 text-red-500" />
@@ -57,5 +59,17 @@ import { Card } from 'ant-design-vue';
         <SvgDownloadIcon class="size-8" />
       </div>
     </Card>
+
+    <Card class="mb-5" title="图标选择器(Iconify)">
+      <div class="flex items-center gap-5">
+        <IconPicker width="300px" />
+      </div>
+    </Card>
+
+    <Card title="图标选择器(Svg)">
+      <div class="flex items-center gap-5">
+        <IconPicker prefix="svg" width="300px" />
+      </div>
+    </Card>
   </Page>
 </template>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d341e8bbddc..c4ee2ec6bee 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1472,6 +1472,9 @@ importers:
       '@vben/constants':
         specifier: workspace:*
         version: link:../../constants
+      '@vben/hooks':
+        specifier: workspace:*
+        version: link:../hooks
       '@vben/icons':
         specifier: workspace:*
         version: link:../../icons

From 2a631de845e57af2aeb6ede5d1aa2b5a8a0f7819 Mon Sep 17 00:00:00 2001
From: ihuangxiaomin <1219549841@qq.com>
Date: Thu, 7 Nov 2024 18:12:59 +0800
Subject: [PATCH 2/3] fix: resolve conversations

---
 packages/effects/common-ui/package.json       |  2 +-
 .../components/icon-picker/icon-picker.vue    |  8 +++++
 packages/effects/hooks/src/use-pagination.ts  | 27 ++++++++++++++--
 .../demos/features/icons/icon-picker.vue      | 31 +++++++++++--------
 4 files changed, 52 insertions(+), 16 deletions(-)

diff --git a/packages/effects/common-ui/package.json b/packages/effects/common-ui/package.json
index aa757d283d4..766b370e8cc 100644
--- a/packages/effects/common-ui/package.json
+++ b/packages/effects/common-ui/package.json
@@ -27,8 +27,8 @@
     "@vben/constants": "workspace:*",
     "@vben/icons": "workspace:*",
     "@vben/locales": "workspace:*",
-    "@vben/types": "workspace:*",
     "@vben/hooks": "workspace:*",
+    "@vben/types": "workspace:*",
     "@vueuse/core": "catalog:",
     "@vueuse/integrations": "catalog:",
     "qrcode": "catalog:",
diff --git a/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue b/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue
index 6bbbc7559a6..2d4f2eb137d 100644
--- a/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue
+++ b/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue
@@ -42,6 +42,14 @@ const currentSelect = ref('');
 const currentList = ref(props.icons);
 const refTrigger = ref<HTMLDivElement>();
 
+watch(
+  () => props.icons,
+  (newIcons) => {
+    currentList.value = newIcons;
+  },
+  { immediate: true },
+);
+
 const { getPaginationList, getTotal, setCurrentPage } = usePagination(
   currentList,
   props.pageSize,
diff --git a/packages/effects/hooks/src/use-pagination.ts b/packages/effects/hooks/src/use-pagination.ts
index 5422e87f268..ba3e269333e 100644
--- a/packages/effects/hooks/src/use-pagination.ts
+++ b/packages/effects/hooks/src/use-pagination.ts
@@ -1,12 +1,23 @@
 import type { Ref } from 'vue';
 import { computed, ref, unref } from 'vue';
 
+/**
+ * Paginates an array of items
+ * @param list The array to paginate
+ * @param pageNo The current page number (1-based)
+ * @param pageSize Number of items per page
+ * @returns Paginated array slice
+ * @throws {Error} If pageNo or pageSize are invalid
+ */
 function pagination<T = any>(list: T[], pageNo: number, pageSize: number): T[] {
+  if (pageNo < 1) throw new Error('Page number must be positive');
+  if (pageSize < 1) throw new Error('Page size must be positive');
+
   const offset = (pageNo - 1) * Number(pageSize);
   const ret =
-    offset + Number(pageSize) >= list.length
+    offset + pageSize >= list.length
       ? list.slice(offset)
-      : list.slice(offset, offset + Number(pageSize));
+      : list.slice(offset, offset + pageSize);
   return ret;
 }
 
@@ -14,6 +25,10 @@ export function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {
   const currentPage = ref(1);
   const pageSizeRef = ref(pageSize);
 
+  const totalPages = computed(() =>
+    Math.ceil(unref(list).length / unref(pageSizeRef)),
+  );
+
   const getPaginationList = computed(() => {
     return pagination(unref(list), unref(currentPage), unref(pageSizeRef));
   });
@@ -23,11 +38,19 @@ export function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {
   });
 
   function setCurrentPage(page: number) {
+    if (page < 1 || page > unref(totalPages)) {
+      throw new Error('Invalid page number');
+    }
     currentPage.value = page;
   }
 
   function setPageSize(pageSize: number) {
+    if (pageSize < 1) {
+      throw new Error('Page size must be positive');
+    }
     pageSizeRef.value = pageSize;
+    // Reset to first page to prevent invalid state
+    currentPage.value = 1;
   }
 
   return { setCurrentPage, getTotal, setPageSize, getPaginationList };
diff --git a/playground/src/views/demos/features/icons/icon-picker.vue b/playground/src/views/demos/features/icons/icon-picker.vue
index 6e05637f10e..699ebec6733 100644
--- a/playground/src/views/demos/features/icons/icon-picker.vue
+++ b/playground/src/views/demos/features/icons/icon-picker.vue
@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
 
 import { IconPicker } from '@vben/common-ui';
 import { listIcons } from '@vben/icons';
@@ -37,21 +37,26 @@ const props = withDefaults(defineProps<Props>(), {
 const refIconPicker = ref();
 const currentSelect = ref('');
 
-function getIcons() {
-  if (props.prefix) {
-    return listIcons('', props.prefix);
-  } else {
-    const prefix = iconsData.prefix;
-    return iconsData.icons.map((icon) => `${prefix}:${icon}`);
+const currentList = computed(() => {
+  try {
+    if (props.prefix) {
+      const icons = listIcons('', props.prefix);
+      if (icons.length === 0) {
+        console.warn(`No icons found for prefix: ${props.prefix}`);
+      }
+      return icons;
+    } else {
+      const prefix = iconsData.prefix;
+      return iconsData.icons.map((icon) => `${prefix}:${icon}`);
+    }
+  } catch (error) {
+    console.error('Failed to load icons:', error);
+    return [];
   }
-}
-
-const currentList = ref(getIcons());
+});
 
 const triggerPopover = () => {
-  if (refIconPicker.value) {
-    refIconPicker.value.changeOpenState();
-  }
+  refIconPicker.value?.changeOpenState?.();
 };
 
 const handleChange = (icon: string) => {

From 32df443ad8eceb9952b16e7dbce060a010cd6938 Mon Sep 17 00:00:00 2001
From: ihuangxiaomin <1219549841@qq.com>
Date: Thu, 7 Nov 2024 18:19:29 +0800
Subject: [PATCH 3/3] refactor: resort @vben/hooks

---
 packages/effects/common-ui/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/effects/common-ui/package.json b/packages/effects/common-ui/package.json
index 766b370e8cc..d741cbbcd1e 100644
--- a/packages/effects/common-ui/package.json
+++ b/packages/effects/common-ui/package.json
@@ -25,9 +25,9 @@
     "@vben-core/shadcn-ui": "workspace:*",
     "@vben-core/shared": "workspace:*",
     "@vben/constants": "workspace:*",
+    "@vben/hooks": "workspace:*",
     "@vben/icons": "workspace:*",
     "@vben/locales": "workspace:*",
-    "@vben/hooks": "workspace:*",
     "@vben/types": "workspace:*",
     "@vueuse/core": "catalog:",
     "@vueuse/integrations": "catalog:",