first commit

This commit is contained in:
liu
2026-07-13 15:23:29 +08:00
commit 50885a98c8
473 changed files with 33772 additions and 0 deletions
@@ -0,0 +1,158 @@
import React, { useState, useMemo, useCallback } from 'react';
import { Select, Modal, Tabs } from 'antd';
import type { TabsProps } from 'antd';
import { categories, type CategoriesKeys } from '@/utils/iconFields';
import IconFont from '@/components/IconFont';
import { useTranslation } from 'react-i18next';
/**
* 图标选择器组件属性
*/
export interface IconSelectProps {
value?: string;
onChange?: (value: string | null) => void;
placeholder?: string;
disabled?: boolean;
readonly?: boolean;
}
/**
* 图标选择器组件
* 基于 Ant Design Select + Modal + Tabs 封装
*
* @example
* <IconSelect
* value={icon}
* onChange={setIcon}
* placeholder="请选择图标"
* />
*/
const IconSelect: React.FC<IconSelectProps> = ({
value,
onChange,
placeholder,
disabled = false,
readonly = false,
}) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [selectedIcon, setSelectedIcon] = useState<string | undefined>(value);
// 同步外部 value
React.useEffect(() => {
setSelectedIcon(value);
}, [value]);
// 处理图标选择
const handleIconClick = useCallback((icon: string) => {
setSelectedIcon(icon);
onChange?.(icon);
setOpen(false);
}, [onChange]);
// 处理清空
const handleClear = useCallback(() => {
setSelectedIcon(undefined);
onChange?.(null);
}, [onChange]);
// 图标列表组件
const IconsList = useCallback(({ type }: { type: CategoriesKeys }) => {
return (
<div className="flex flex-wrap max-h-[400px] overflow-auto gap-2">
{categories[type].map((item) => (
<div
className="cursor-pointer p-2 border border-gray-300 rounded hover:border-blue-500 hover:bg-blue-50 transition-all flex items-center justify-center"
key={item}
onClick={() => handleIconClick(item)}
title={item}
>
<IconFont name={item} style={{ fontSize: 20 }} />
</div>
))}
</div>
);
}, [handleIconClick]);
// Tabs 配置
const tabItems: TabsProps['items'] = useMemo(() => [
{
key: 'use',
label: t('xin.form.iconSelector.tabs.use'),
children: <IconsList type="useIcons" />,
},
{
key: 'suggestion',
label: t('xin.form.iconSelector.tabs.suggestion'),
children: <IconsList type="suggestionIcons" />,
},
{
key: 'direction',
label: t('xin.form.iconSelector.tabs.direction'),
children: <IconsList type="directionIcons" />,
},
{
key: 'editor',
label: t('xin.form.iconSelector.tabs.editor'),
children: <IconsList type="editorIcons" />,
},
{
key: 'data',
label: t('xin.form.iconSelector.tabs.data'),
children: <IconsList type="dataIcons" />,
},
{
key: 'logo',
label: t('xin.form.iconSelector.tabs.logo'),
children: <IconsList type="logoIcons" />,
},
{
key: 'other',
label: t('xin.form.iconSelector.tabs.other'),
children: <IconsList type="otherIcons" />,
},
], [IconsList, t]);
// Select 选项
const selectOptions = useMemo(() => {
if (!selectedIcon) return [];
return [{
label: (
<div className="flex items-center gap-2">
<IconFont name={selectedIcon} />
<span>{selectedIcon}</span>
</div>
),
value: selectedIcon,
}];
}, [selectedIcon]);
return (
<>
<Select
value={selectedIcon}
placeholder={placeholder || t('xin.form.iconSelector.placeholder')}
disabled={disabled}
open={false}
onClick={() => !disabled && !readonly && setOpen(true)}
options={selectOptions}
style={{ width: '100%' }}
allowClear
onClear={handleClear}
suffixIcon={selectedIcon ? <IconFont name={selectedIcon} /> : undefined}
/>
<Modal
title={t('xin.form.iconSelector.modal.title')}
open={open}
onCancel={() => setOpen(false)}
footer={null}
width={800}
>
<Tabs defaultActiveKey="use" items={tabItems} />
</Modal>
</>
);
};
export default IconSelect;
@@ -0,0 +1,33 @@
/**
* 图标选择器组件属性
*/
export interface IconSelectProps {
/**
* 选中的图标名称
*/
value?: string;
/**
* 值变化回调
* @param value 选中的图标名称或 null(清空时)
*/
onChange?: (value: string | null) => void;
/**
* 占位符文本
* @default '请选择图标'
*/
placeholder?: string;
/**
* 是否禁用
* @default false
*/
disabled?: boolean;
/**
* 是否只读(只读模式下不能打开选择弹窗)
* @default false
*/
readonly?: boolean;
}