master
LiuJiaNan 2025-10-22 14:43:42 +08:00
commit d7ccfda275
36 changed files with 2331 additions and 0 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/src/test/
/target/
.idea
/node_modules
*.local
package-lock.json

42
.npmignore Normal file
View File

@ -0,0 +1,42 @@
# 开发相关文件
.eslintrc.cjs
.eslintignore
.prettierrc.cjs
.gitignore
vite.config.js
vitest.config.js
tsconfig.json
jsconfig.json
# 构建和缓存目录
node_modules/
dist/
.vite/
.cache/
# 开发工具配置
.vscode/
.idea/
.editorconfig
# 日志文件
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# 测试相关
coverage/
.nyc_output/
test/
tests/
__tests__/
*.test.js
*.test.ts
*.spec.js
*.spec.ts
# 其他
.DS_Store
.env*
!.env.example

14
README.md Normal file
View File

@ -0,0 +1,14 @@
# zy-react-library
## 📦 安装
```bash
# yarn
yarn add zy-react-library
```
## 📄 更新日志
### v1.0.0 (2025-10-22)
- 🎉 初始版本发布

31
components/FormBuilder/FormBuilder.d.ts vendored Normal file
View File

@ -0,0 +1,31 @@
import type { FormInstance, FormProps } from "antd/es/form";
import type { Gutter } from "antd/es/grid/row";
import type { FC } from "react";
import type { FormOption, FormValues } from "./FormItemsRenderer";
/**
* FormBuilder
*/
export interface FormBuilderProps extends Omit<FormProps, "form"> {
/** 表单初始值 */
values?: FormValues;
/** 表单配置项数组 */
options: FormOption[];
/** 栅格间距,默认 24 */
gutter?: Gutter | [Gutter, Gutter];
/** 占据栅格列数,默认 12 */
span?: number | string;
/** 表单实例(通过 Form.useForm() 创建) */
form: FormInstance;
/** 自动生成必填规则,默认 true */
useAutoGenerateRequired?: boolean;
/** 表单提交回调 */
onFinish?: (values: any) => void;
}
/**
*
*/
declare const FormBuilder: FC<FormBuilderProps>;
export default FormBuilder;

View File

@ -0,0 +1,41 @@
import { Form, Row } from "antd";
import FormItemsRenderer from "./FormItemsRenderer";
/**
* 表单构建器组件
*/
const FormBuilder = (props) => {
const {
values,
options,
gutter = 24,
span = 12,
labelCol = { span: 4 },
onFinish,
useAutoGenerateRequired = true,
...restProps
} = props;
return (
<Form
labelCol={labelCol}
scrollToFirstError
wrapperCol={{ span: 24 - labelCol.span }}
onFinish={onFinish}
initialValues={values}
{...restProps}
>
<Row gutter={gutter}>
<FormItemsRenderer
options={options}
span={span}
useAutoGenerateRequired={useAutoGenerateRequired}
/>
</Row>
</Form>
);
};
FormBuilder.displayName = "FormBuilder";
export default FormBuilder;

View File

@ -0,0 +1,107 @@
import type { ColProps } from "antd/es/col";
import type { FormItemProps, Rule } from "antd/es/form";
import type { NamePath } from "rc-field-form/lib/interface";
import type { FC, ReactNode } from "react";
import type { FORM_ITEM_RENDER_ENUM } from "../../enum/formItemRender";
/**
*
*/
export type FormItemRenderType
= | (typeof FORM_ITEM_RENDER_ENUM)[keyof typeof FORM_ITEM_RENDER_ENUM]
| ((props: any) => ReactNode);
/**
*
*/
export interface OptionItem {
/** 值字段 */
value?: any;
/** 标签字段 */
label?: string;
/** 字典ID */
dictionariesId?: any;
/** ID字段 */
id?: any;
/** 名称字段 */
name?: string;
[key: string]: any;
}
/**
*
*/
export interface itemsFieldConfig {
/** 值字段的键名,默认为 'value' */
valueKey?: string;
/** 标签字段的键名,默认为 'label' */
labelKey?: string;
}
/**
*
*/
export type FormValues = Record<string, any>;
/**
*
*/
export interface FormOption {
/** 表单项字段名 */
name?: string | string[];
/** 表单项标签 */
label?: ReactNode;
/** 渲染类型 */
render?: FormItemRenderType;
/** 占据栅格列数,默认 12 */
span?: number | string;
/** 是否必填,默认 true支持函数动态计算 */
required?: boolean | ((formValues: FormValues) => boolean);
/** 验证规则 */
rules?: Rule | Rule[];
/** 占位符文本,默认会根据传入的 render 类型自动判断(请选择、请输入)和 label 组合 */
placeholder?: ReactNode;
/** 提示信息,传入将在 label 右侧生成图标展示 tooltip */
tip?: ReactNode;
/** 是否隐藏,默认 false支持函数动态计算 */
hidden?: boolean | ((formValues: FormValues) => boolean);
/** 是否自定义渲染,默认 false将不生成 Col 和 Form.Item 仅生效 render、items、itemsField、componentProps */
customizeRender?: boolean;
/** 选项数据(用于 select、radio、checkbox */
items?: OptionItem[];
/** 字段键配置 */
itemsField?: itemsFieldConfig;
/** 传递给表单控件的属性,支持函数动态计算 */
componentProps?: Record<string, any> | ((formValues: FormValues) => Record<string, any>);
/** 传递给 Form.Item 的属性,支持函数动态计算 */
formItemProps?: FormItemProps | ((formValues: FormValues) => FormItemProps);
/** label 栅格配置 */
labelCol?: ColProps;
/** wrapper 栅格配置 */
wrapperCol?: ColProps;
/** 是否应该更新(用于表单联动) */
shouldUpdate?: boolean | ((prevValues: FormValues, nextValues: FormValues, info: { source?: string }) => boolean);
/** 依赖字段(用于表单联动) */
dependencies?: NamePath[];
}
/**
* FormItemsRenderer
*/
export interface FormItemsRendererProps {
/** 表单配置项数组 */
options: FormOption[];
/** 默认栅格占据列数,默认 12 */
span?: number;
/** 是否折叠仅显示前3项默认 false */
collapse?: boolean;
/** 自动生成必填规则,默认 true */
useAutoGenerateRequired?: boolean;
}
/**
*
*/
declare const FormItemsRenderer: FC<FormItemsRendererProps>;
export default FormItemsRenderer;

View File

@ -0,0 +1,345 @@
import { InfoCircleOutlined } from "@ant-design/icons";
import {
Checkbox,
Col,
DatePicker,
Divider,
Form,
Input,
InputNumber,
Radio,
Select,
Tooltip,
} from "antd";
import dayjs from "dayjs";
import { FORM_ITEM_RENDER_ENUM } from "../../enum/formItemRender";
const { TextArea } = Input;
const { RangePicker } = DatePicker;
/**
* 表单项渲染器组件
*/
const FormItemsRenderer = ({
options,
span = 12,
collapse = false,
useAutoGenerateRequired = true,
}) => {
const form = Form.useFormInstance();
// 获取传给组件的属性
const getComponentProps = (option) => {
return typeof option.componentProps === "function"
? option.componentProps(form.getFieldsValue())
: (option.componentProps || {});
};
// 获取传给formItem的属性
const getFormItemProps = (option) => {
const formItemProps = typeof option.formItemProps === "function"
? option.formItemProps(form.getFieldsValue())
: (option.formItemProps || {});
// 为日期组件添加特殊处理
if ([
FORM_ITEM_RENDER_ENUM.DATE,
FORM_ITEM_RENDER_ENUM.DATE_MONTH,
FORM_ITEM_RENDER_ENUM.DATE_YEAR,
FORM_ITEM_RENDER_ENUM.DATETIME,
].includes(option.render)) {
formItemProps.getValueFromEvent = (_, dateString) => dateString;
formItemProps.getValueProps = value => ({ value: value ? dayjs(value) : undefined });
}
// 为日期周组件添加特殊处理
if ([
FORM_ITEM_RENDER_ENUM.DATE_WEEK,
].includes(option.render)) {
formItemProps.getValueFromEvent = (_, dateString) => dateString;
formItemProps.getValueProps = value => ({ value: value ? dayjs(value, "YYYY-wo") : undefined });
}
// 为日期范围组件添加特殊处理
if ([
FORM_ITEM_RENDER_ENUM.DATE_RANGE,
FORM_ITEM_RENDER_ENUM.DATETIME_RANGE,
].includes(option.render)) {
formItemProps.getValueFromEvent = (_, dateString) => dateString;
formItemProps.getValueProps = value => ({ value: Array.isArray(value) ? value.map(v => v ? dayjs(v) : undefined) : undefined });
}
return formItemProps;
};
// 获取items里的value和label字段key
const getItemsFieldKey = (option) => {
return {
valueKey: option?.itemsField?.valueKey || "value",
labelKey: option?.itemsField?.labelKey || "label",
};
};
// 获取验证规则
const getRules = (option) => {
if (!useAutoGenerateRequired)
return option.rules ? (Array.isArray(option.rules) ? option.rules : [option.rules]) : [];
if (option.render === FORM_ITEM_RENDER_ENUM.DIVIDER)
return [];
// 支持动态计算 required
const required = typeof option.required === "function"
? option.required(form.getFieldsValue())
: (option.required || true);
if (required) {
const isBlurTrigger = !option.render || [
FORM_ITEM_RENDER_ENUM.INPUT,
FORM_ITEM_RENDER_ENUM.TEXTAREA,
FORM_ITEM_RENDER_ENUM.INPUT_NUMBER,
FORM_ITEM_RENDER_ENUM.NUMBER,
].includes(option.render);
const rules = [
{
required: true,
message: `${isBlurTrigger ? "请输入" : "请选择"}${option.label}`,
},
];
if (option.rules) {
if (Array.isArray(option.rules)) {
rules.push(...option.rules);
}
else {
rules.push(option.rules);
}
}
return rules;
}
return option.rules ? (Array.isArray(option.rules) ? option.rules : [option.rules]) : [];
};
// 渲染表单控件
const renderFormControl = (option) => {
const componentProps = getComponentProps(option);
const itemsFieldKey = getItemsFieldKey(option);
/** @type {string | Function} */
const render = option.render || FORM_ITEM_RENDER_ENUM.INPUT;
const placeholder = option.placeholder || `${render === FORM_ITEM_RENDER_ENUM.SELECT || render === FORM_ITEM_RENDER_ENUM.RADIO || render === FORM_ITEM_RENDER_ENUM.CHECKBOX ? "选择" : "输入"}${option.label}`;
switch (render) {
case FORM_ITEM_RENDER_ENUM.INPUT:
return <Input placeholder={placeholder} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.TEXTAREA:
return <TextArea placeholder={placeholder} rows={3} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.INPUT_NUMBER:
case FORM_ITEM_RENDER_ENUM.NUMBER:
return <InputNumber placeholder={placeholder} style={{ width: "100%" }} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.SELECT:
return (
<Select placeholder={placeholder} {...componentProps}>
{(option.items || []).map((item) => {
const value = item[itemsFieldKey.valueKey] ?? item.dictionariesId ?? item.id;
const label = item[itemsFieldKey.labelKey] ?? item.name;
return (
<Select.Option key={value} value={value}>
{label}
</Select.Option>
);
})}
</Select>
);
case FORM_ITEM_RENDER_ENUM.RADIO:
return (
<Radio.Group {...componentProps}>
{(option.items || []).map((item) => {
const value = item[itemsFieldKey.valueKey] ?? item.dictionariesId ?? item.id;
const label = item[itemsFieldKey.labelKey] ?? item.name;
return (
<Radio key={value} value={value}>
{label}
</Radio>
);
})}
</Radio.Group>
);
case FORM_ITEM_RENDER_ENUM.CHECKBOX:
return (
<Checkbox.Group {...componentProps}>
{(option.items || []).map((item) => {
const value = item[itemsFieldKey.valueKey] ?? item.dictionariesId ?? item.id;
const label = item[itemsFieldKey.labelKey] ?? item.name;
return (
<Checkbox key={value} value={value}>
{label}
</Checkbox>
);
})}
</Checkbox.Group>
);
case FORM_ITEM_RENDER_ENUM.DATE:
return <DatePicker placeholder={placeholder} format="YYYY-MM-DD" style={{ width: "100%" }} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.DATE_MONTH:
return <DatePicker picker="month" placeholder={placeholder} format="YYYY-MM" style={{ width: "100%" }} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.DATE_YEAR:
return <DatePicker picker="year" placeholder={placeholder} format="YYYY" style={{ width: "100%" }} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.DATE_WEEK:
return <DatePicker picker="week" placeholder={placeholder} format="YYYY-wo" style={{ width: "100%" }} {...componentProps} />;
case FORM_ITEM_RENDER_ENUM.DATE_RANGE:
return (
<RangePicker
placeholder={[`请选择开始${option.label}`, `请选择结束${option.label}`]}
format="YYYY-MM-DD"
style={{ width: "100%" }}
{...componentProps}
/>
);
case FORM_ITEM_RENDER_ENUM.DATETIME:
return (
<DatePicker
showTime
placeholder={placeholder}
format="YYYY-MM-DD HH:mm:ss"
style={{ width: "100%" }}
{...componentProps}
/>
);
case FORM_ITEM_RENDER_ENUM.DATETIME_RANGE:
return (
<RangePicker
showTime
placeholder={[`请选择开始${option.label}`, `请选择结束${option.label}`]}
format="YYYY-MM-DD HH:mm:ss"
style={{ width: "100%" }}
{...componentProps}
/>
);
case FORM_ITEM_RENDER_ENUM.DIVIDER:
return null;
default:
// 支持传入自定义组件
if (typeof render === "function") {
const CustomComponent = render;
return <CustomComponent {...componentProps} />;
}
return <Input placeholder={placeholder} {...componentProps} />;
}
};
// 渲染 label带提示
const renderLabel = (option) => {
if (!option.tip)
return option.label;
return (
<>
{option.label}
<Tooltip title={option.tip}>
<InfoCircleOutlined style={{ marginLeft: 4, fontSize: 12 }} />
</Tooltip>
</>
);
};
return (
<>
{options.map((option, index) => {
const itemSpan = option.render === FORM_ITEM_RENDER_ENUM.DIVIDER ? 24 : option.span ?? span;
// 使用 style 控制显示/隐藏
const style = collapse && index >= 3 ? { display: "none" } : undefined;
// 如果是分割线
if (option.render === FORM_ITEM_RENDER_ENUM.DIVIDER) {
return (
<Col key={option.name || index} span={itemSpan} style={style}>
<Divider orientation="left">{option.label}</Divider>
</Col>
);
}
// 如果配置了 shouldUpdate 或 dependencies使用 Form.Item 的联动机制
if (option.shouldUpdate || option.dependencies || option?.componentProps?.shouldUpdate || option?.componentProps?.dependencies) {
return (
option.customizeRender
? (renderFormControl(option))
: (
<Col key={option.name || index} span={itemSpan} style={style}>
<Form.Item
noStyle
shouldUpdate={option.shouldUpdate || option?.componentProps?.shouldUpdate}
dependencies={option.dependencies || option?.componentProps?.dependencies}
>
{(form) => {
// 支持动态计算 hidden
const hidden = typeof option.hidden === "function"
? option.hidden(form.getFieldsValue())
: (option.hidden || false);
if (hidden)
return null;
return (
<Form.Item
name={option.name}
label={renderLabel(option)}
rules={getRules(option)}
labelCol={option.labelCol}
wrapperCol={option.wrapperCol}
{...getFormItemProps(option)}
>
{renderFormControl(option)}
</Form.Item>
);
}}
</Form.Item>
</Col>
)
);
}
// 普通表单项(静态配置)
if (option.hidden)
return null;
return (
option.customizeRender
? (renderFormControl(option))
: (
<Col key={option.name || index} span={itemSpan} style={style}>
<Form.Item
name={option.name}
label={renderLabel(option)}
rules={getRules(option)}
labelCol={option.labelCol}
wrapperCol={option.wrapperCol}
{...getFormItemProps(option)}
>
{renderFormControl(option)}
</Form.Item>
</Col>
)
);
})}
</>
);
};
FormItemsRenderer.displayName = "FormItemsRenderer";
export default FormItemsRenderer;

3
components/FormBuilder/index.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import FormBuilder from "./FormBuilder";
export default FormBuilder;

View File

@ -0,0 +1,3 @@
import FormBuilder from "./FormBuilder";
export default FormBuilder;

12
components/PreviewImg/index.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
import type { FC } from "react";
export interface PreviewImgProps {
/** 图片列表 */
files: any[];
/** 图片地址的key默认 filePath */
fileUrlKey?: string;
}
declare const PreviewImg: FC<PreviewImgProps>;
export default PreviewImg;

View File

@ -0,0 +1,26 @@
import { Image } from "antd";
import { getFileUrl } from "../../utils/index";
const PreviewImg = (props) => {
const { files = [], fileUrlKey = "filePath" } = props;
const fileUrl = getFileUrl();
return (
<Image.PreviewGroup>
{
files.filter(Boolean).map((item, index) => (
<Image
key={item[fileUrlKey] || item}
src={item[fileUrlKey] ? fileUrl + item[fileUrlKey] : fileUrl + item}
style={{ marginLeft: index > 0 ? 10 : 0 }}
width={100}
height={100}
alt=""
/>
))
}
</Image.PreviewGroup>
);
};
export default PreviewImg;

41
components/Search/index.d.ts vendored Normal file
View File

@ -0,0 +1,41 @@
import type { FormInstance, FormProps } from "antd/es/form";
import type { FC, ReactNode } from "react";
import type { FormOption } from "../FormBuilder/FormItemsRenderer";
type FormValues = Record<string, any>;
/**
* Search
*/
export interface SearchProps extends Omit<FormProps, "form" | "onFinish"> {
/** 表单配置项数组 */
options: FormOption[];
/** 表单值 */
values?: FormValues;
/** 搜索和重置都会触发的回调 */
onFinish?: (values: FormValues, type: "submit" | "reset") => void;
/** 搜索回调 */
onSubmit?: (values: FormValues) => void;
/** 重置回调 */
onReset?: (values: FormValues) => void;
/** 搜索按钮文本,默认"搜索" */
searchText?: string;
/** 重置按钮文本,默认"重置" */
resetText?: string;
/** 是否显示搜索按钮,默认 true */
showSearchButton?: boolean;
/** 是否显示重置按钮,默认 true */
showResetButton?: boolean;
/** 额外的底部按钮组 */
extraButtons?: ReactNode;
/** 表单实例(通过 Form.useForm() 创建) */
form: FormInstance;
}
/**
*
* /4/
*/
declare const Search: FC<SearchProps>;
export default Search;

125
components/Search/index.js Normal file
View File

@ -0,0 +1,125 @@
import { DownOutlined, UpOutlined } from "@ant-design/icons";
import { Button, Col, Form, Row } from "antd";
import { useEffect, useRef, useState } from "react";
import FormItemsRenderer from "../FormBuilder/FormItemsRenderer";
/**
* 搜索表单组件
*/
const Search = (props) => {
const {
labelCol = { span: 4 },
options = [],
values,
onFinish,
onSubmit,
onReset,
searchText = "搜索",
resetText = "重置",
showSearchButton = true,
showResetButton = true,
extraButtons,
form,
...restProps
} = props;
const [collapse, setCollapse] = useState(true);
const [span, setSpan] = useState(6);
const [showCollapseButton, setShowCollapseButton] = useState(false);
const classNameRef = useRef(`search-${Date.now()}`);
// 计算是否需要显示展开/收起按钮
useEffect(() => {
if (!options || options.length === 0)
return;
const calculateLayout = () => {
const colEl = document.querySelectorAll(
`.${classNameRef.current}>.${window.process.env.app.antd["ant-prefix"]}-col`,
);
const colElLength = colEl.length;
const excludeLast = colElLength - (extraButtons ? 2 : 1);
const spanMap = { 0: 24, 1: 18, 2: 12, 3: 6 };
setSpan(spanMap[excludeLast % 4] || 6);
setShowCollapseButton(excludeLast > 3);
};
// 延迟执行以确保 DOM 已渲染
setTimeout(calculateLayout, 0);
}, [options, extraButtons]);
// 处理表单提交
const handleSubmit = () => {
const values = form.getFieldsValue();
onFinish?.(values, "submit");
onSubmit?.(values);
};
// 处理重置
const handleReset = () => {
form.resetFields();
const values = form.getFieldsValue();
onFinish?.(values, "reset");
onReset?.(values);
};
// 切换展开/收起
const toggleCollapse = () => {
setCollapse(!collapse);
};
return (
<Form
form={form}
labelCol={labelCol}
initialValues={values}
{...restProps}
>
<Row className={classNameRef.current}>
<FormItemsRenderer
options={options}
span={6}
collapse={collapse}
useAutoGenerateRequired={false}
/>
<Col span={showCollapseButton ? (collapse ? 6 : span) : span}>
<Form.Item label=" " colon={false} style={{ textAlign: "right" }}>
{showSearchButton && (
<Button type="primary" onClick={handleSubmit}>
{searchText}
</Button>
)}
{showResetButton && (
<Button style={{ marginLeft: 8 }} onClick={handleReset}>
{resetText}
</Button>
)}
{showCollapseButton && (
<Button
type="link"
icon={collapse ? <DownOutlined /> : <UpOutlined />}
onClick={toggleCollapse}
style={{ marginLeft: 8 }}
>
{collapse ? "展开" : "收起"}
</Button>
)}
</Form.Item>
</Col>
{extraButtons && (
<Col span={24}>
<Form.Item label=" " colon={false} labelCol={{ span: 0 }}>
{extraButtons}
</Form.Item>
</Col>
)}
</Row>
</Form>
);
};
Search.displayName = "Search";
export default Search;

23
components/Table/index.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
import type { ProTableProps } from "@ant-design/pro-table";
import type { TableProps } from "antd";
import type { FC } from "react";
/**
* TablePro
*/
export type TableProProps<DataSource, U, ValueType> = Omit<TableProps, 'columns'> & ProTableProps<DataSource, U, ValueType> & {
/** 当一个路由下存在多个表格的情况下 需要给每一个表格设置一个唯一存储索引 若没有设置则使用默认索引,请注意缓存数据会被覆盖 */
storeIndex?: string;
/** 是否禁用内容区滚动,默认 false */
disabledResizer?: boolean;
/** 是否显示索引列,默认 true */
showIndex?: boolean;
/** 是否使用居中布局,默认 true */
useAlignCenter?: boolean;
}
/**
*
*/
declare const TablePro: <DataSource, U, ValueType = "text">(props: TableProProps<DataSource, U, ValueType>) => ReturnType<FC>;
export default TablePro;

19
components/Table/index.js Normal file
View File

@ -0,0 +1,19 @@
import Table from "@cqsjjb/jjb-react-admin-component/Table";
import { getIndexColumn } from "../../utils/index";
function TablePro(props) {
const {
columns = [],
showIndex = true,
useAlignCenter = true,
...restProps
} = props;
const storeIndex = props.storeIndex || `${window.process.env.app.antd["ant-prefix"]}_${Math.random().toString(36).substring(2)}`;
function calcColumns() {
showIndex && columns.unshift(getIndexColumn(props.pagination));
return columns.map(item => ({ ...item, align: useAlignCenter ? "center" : "left" }));
}
return <Table storeIndex={storeIndex} columns={calcColumns()} {...restProps} />;
}
export default TablePro;

View File

@ -0,0 +1,6 @@
import type { FC } from "react";
import type { PreviewImgProps } from "../PreviewImg";
declare const TooltipPreviewImg: FC<PreviewImgProps>;
export default TooltipPreviewImg;

View File

@ -0,0 +1,22 @@
import { Tag, Tooltip } from "antd";
import PreviewImg from "~/components/PreviewImg";
const TooltipPreviewImg = (props) => {
const { files = [], fileUrlKey = "filePath" } = props;
const renderContent = () => {
return (
files.length > 0
? <PreviewImg files={files} fileUrlKey={fileUrlKey} />
: <span>暂无图片</span>
);
};
return (
<Tooltip placement="top" title={renderContent()}>
<Tag>预览</Tag>
</Tooltip>
);
};
export default TooltipPreviewImg;

28
components/Upload/index.d.ts vendored Normal file
View File

@ -0,0 +1,28 @@
import type { UploadProps as AntUploadProps, UploadFile } from "antd/es/upload";
import type { FC, ReactNode } from "react";
/**
* Upload
*/
export interface UploadProps extends Omit<AntUploadProps, "fileList"> {
/** 文件列表 */
value?: UploadFile[];
/** 图片分辨率限制,如 "1920*1080" */
ratio?: `${number}*${number}`;
/** 是否显示提示,默认 true */
showTip?: boolean;
/** 文件大小限制单位MB默认 0不限制 */
size?: number;
/** 自定义提示内容 */
tipContent?: ReactNode;
/** listType 为 text 时上传按钮文本,默认 "点击选择文件上传" */
uploadButtonText?: string;
}
/**
*
*
*/
declare const Upload: FC<UploadProps>;
export default Upload;

161
components/Upload/index.js Normal file
View File

@ -0,0 +1,161 @@
import { PlusOutlined } from "@ant-design/icons";
import { Upload as AntUpload, Button, message, Modal } from "antd";
import { useState } from "react";
/**
* 文件上传组件
*/
const Upload = (props) => {
const {
value = [],
onChange,
onPreview,
maxCount = 1,
listType = "text",
accept = "",
ratio = "",
showTip = true,
multiple = true,
size = 0,
tipContent,
uploadButtonText = "点击选择文件上传",
...restProps
} = props;
const [previewVisible, setPreviewVisible] = useState(false);
const [previewImage, setPreviewImage] = useState("");
// 生成提示信息
const getTipText = () => {
if (tipContent)
return tipContent;
const tips = [
`最多上传${maxCount}个文件`,
accept
? `并且只能上传${accept
.replace(/\./g, "")
.split(",")
.join("、")}格式的文件`
: "可以上传任意格式的文件",
size ? `文件大小不能超过${size}M` : "",
ratio ? `只能上传${ratio}分辨率的图片` : "",
].filter(Boolean);
return `${tips.join("")}`;
};
const handleBeforeUpload = () => {
return false;
};
// 文件状态改变
const handleChange = ({ file, fileList }) => {
const acceptList = accept ? accept.split(",") : [];
const ratioArr = ratio ? ratio.split("*") : [];
const suffix = file.name.substring(
file.name.lastIndexOf("."),
file.name.length,
);
const maxSize = size * 1024 * 1024;
// 验证文件格式
if (acceptList.length > 0 && !acceptList.includes(suffix)) {
message.warning(`只能上传${accept}格式的文件`);
return;
}
// 验证文件大小
if (maxSize && file.size > maxSize) {
message.warning(`文件大小不能超过${size}M`);
return;
}
// 验证图片分辨率
if (ratioArr.length === 2 && file.type?.startsWith("image/")) {
const img = new Image();
img.src = file.url || file.thumbUrl;
img.onload = () => {
if (img.width !== +ratioArr[0] || img.height !== +ratioArr[1]) {
message.warning(`只能上传${ratio}分辨率的图片`);
const filtered = fileList.filter(item => item.uid !== file.uid);
onChange?.(filtered);
return;
}
onChange?.(fileList);
};
}
else {
onChange?.(fileList);
}
};
// 预览文件
const handlePreview = (file) => {
if (["picture-card", "picture-circle", "picture"].includes(listType)) {
setPreviewImage(file.url || file.thumbUrl);
setPreviewVisible(true);
}
onPreview?.(file);
};
// 关闭预览
const handleCancel = () => {
setPreviewVisible(false);
};
// 上传按钮
const uploadButton
= ["picture-card", "picture-circle", "picture"].includes(listType)
? (
<div>
<PlusOutlined style={{ fontSize: 32 }} />
</div>
)
: (
<Button type="primary">{uploadButtonText}</Button>
);
return (
<>
<AntUpload
fileList={value}
multiple={multiple}
maxCount={maxCount}
listType={listType}
accept={accept}
onChange={handleChange}
onPreview={handlePreview}
beforeUpload={handleBeforeUpload}
{...restProps}
>
{value.length >= maxCount ? null : uploadButton}
</AntUpload>
{
showTip
? (tipContent || getTipText()) && (
<div style={{ marginTop: 10, color: "#ff4d4f" }}>
{tipContent || getTipText()}
</div>
)
: null
}
<Modal
open={previewVisible}
title="查看图片"
footer={null}
onCancel={handleCancel}
>
<img
alt="preview"
style={{ width: "100%", objectFit: "scale-down" }}
src={previewImage}
/>
</Modal>
</>
);
};
Upload.displayName = "Upload";
export default Upload;

35
enum/formItemRender/index.d.ts vendored Normal file
View File

@ -0,0 +1,35 @@
/**
*
*/
export declare const FORM_ITEM_RENDER_ENUM: {
/** 映射为 antd Input */
INPUT: "input";
/** 映射为 antd Input.TextArea */
TEXTAREA: "textarea";
/** 映射为 antd InputNumber */
INPUT_NUMBER: "inputNumber";
/** 映射为 antd InputNumber */
NUMBER: "number";
/** 映射为 antd Select */
SELECT: "select";
/** 映射为 antd Radio.Group */
RADIO: "radio";
/** 映射为 antd Checkbox.Group */
CHECKBOX: "checkbox";
/** 映射为 antd DatePicker日期格式为YYYY-MM-DD */
DATE: "date";
/** 映射为 antd DatePicker.MonthPicker日期格式为YYYY-MM */
DATE_MONTH: "dateMonth";
/** 映射为 antd DatePicker.YearPicker日期格式为YYYY */
DATE_YEAR: "dateYear";
/** 映射为 antd DatePicker.WeekPicker日期格式为YYYY-wo */
DATE_WEEK: "dateWeek";
/** 映射为 antd DatePicker.RangePicker日期格式为YYYY-MM-DD */
DATE_RANGE: "dateRange";
/** 映射为 antd DatePicker日期格式为YYYY-MM-DD HH:mm:ss */
DATETIME: "datetime";
/** 映射为 antd DatePicker.RangePicker日期格式为YYYY-MM-DD HH:mm:ss */
DATETIME_RANGE: "datetimeRange";
/** 映射为 antd Divider */
DIVIDER: "divider";
};

View File

@ -0,0 +1,35 @@
/**
* 表单项类型枚举
*/
export const FORM_ITEM_RENDER_ENUM = {
/** 映射为 antd Input */
INPUT: "input",
/** 映射为 antd Input.TextArea */
TEXTAREA: "textarea",
/** 映射为 antd InputNumber */
INPUT_NUMBER: "inputNumber",
/** 映射为 antd InputNumber */
NUMBER: "number",
/** 映射为 antd Select */
SELECT: "select",
/** 映射为 antd Radio.Group */
RADIO: "radio",
/** 映射为 antd Checkbox.Group */
CHECKBOX: "checkbox",
/** 映射为 antd DatePicker日期格式为YYYY-MM-DD */
DATE: "date",
/** 映射为 antd DatePicker.MonthPicker日期格式为YYYY-MM */
DATE_MONTH: "dateMonth",
/** 映射为 antd DatePicker.YearPicker日期格式为YYYY */
DATE_YEAR: "dateYear",
/** 映射为 antd DatePicker.WeekPicker日期格式为YYYY-wo */
DATE_WEEK: "dateWeek",
/** 映射为 antd DatePicker.RangePicker日期格式为YYYY-MM-DD */
DATE_RANGE: "dateRange",
/** 映射为 antd DatePicker日期格式为YYYY-MM-DD HH:mm:ss */
DATETIME: "datetime",
/** 映射为 antd DatePicker.RangePicker日期格式为YYYY-MM-DD HH:mm:ss */
DATETIME_RANGE: "datetimeRange",
/** 映射为 antd Divider */
DIVIDER: "divider",
};

16
hooks/useDownloadBlob/index.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
interface UseDownloadBlobOptions {
/** 下载文件的自定义文件名(不含后缀),默认为当前时间戳 */
name?: string;
/** Blob 对象的 MIME 类型,默认为 Excel 类型 */
type?: string;
/** 请求时携带的查询参数对象 */
params?: Record<string, any>;
}
/**
* Blob
*/
export default function useDownloadBlob(
url: string,
options?: UseDownloadBlobOptions
): Promise<any>;

View File

@ -0,0 +1,52 @@
import { message } from "antd";
import dayjs from "dayjs";
import { getFileUrl } from "../../utils/index.js";
/**
* 下载Blob流文件
*/
export default function useDownloadBlob(
url,
options = { name: "", type: "", params: {} },
) {
const fileUrl = getFileUrl();
return new Promise((resolve, reject) => {
const finalUrl = !url.includes(fileUrl) ? fileUrl + url : url;
Object.entries(options.params).forEach(([key, value]) => {
finalUrl.searchParams.append(key, value);
});
fetch(finalUrl, {
method: "GET",
mode: "cors",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.blob();
})
.then((blob) => {
const finalBlob = new Blob([blob], {
type: options.type || "application/vnd.ms-excel",
});
const downloadElement = document.createElement("a");
const href = window.URL.createObjectURL(finalBlob);
downloadElement.style.display = "none";
downloadElement.href = href;
downloadElement.download
= options.name || dayjs().format("YYYY-MM-DD HH:mm:ss");
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
resolve({ data: finalBlob });
})
.catch((err) => {
message.error("导出失败");
reject(err);
});
});
}

4
hooks/useDownloadFile/index.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
/**
*
*/
export default function useDownloadFile(url: string, name?: string): void;

View File

@ -0,0 +1,36 @@
import { message, Modal } from "antd";
import { getFileName, getFileSuffix, getFileUrl } from "../../utils/index.js";
/**
* 下载文件
*/
export default function useDownloadFile(url, name) {
if (!url)
throw new Error("没有下载地址");
Modal.confirm({ title: "提示", content: "确定要下载此文件吗?", onOk: () => {
const fileUrl = getFileUrl();
if (name) {
if (!getFileSuffix(url))
name = name + getFileSuffix(url);
}
else {
name = getFileName(url);
}
fetch(!url.includes(fileUrl) ? fileUrl + url : url)
.then(res => res.blob())
.then((blob) => {
const a = document.createElement("a");
document.body.appendChild(a);
a.style.display = "none";
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = `${name}`;
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
})
.catch(() => {
message.error("下载失败");
});
} });
}

View File

@ -0,0 +1,18 @@
/**
* useIsExistenceDuplicateSelection
*/
interface UseIsExistenceDuplicateSelectionOptions<T> {
/** 需要检查重复项的目标数组 */
data: T[];
/** 用于去重判断的对象属性名 */
key: keyof T;
/** 可选的错误提示信息 */
message?: string;
}
/**
*
*/
export default function useIsExistenceDuplicateSelection<T>(
options: UseIsExistenceDuplicateSelectionOptions<T>
): Promise<void>;

View File

@ -0,0 +1,18 @@
import { message as antdMessage } from "antd";
import { uniqBy } from "lodash-es";
/**
* 检查数组中是否存在重复项
*/
export default function useIsExistenceDuplicateSelection(options) {
const { data, key, message = "存在重复项,请勿重复选择" } = options;
return new Promise((resolve, reject) => {
if (uniqBy(data, key).length !== data.length) {
antdMessage.error(message);
reject(new Error(message));
}
else {
resolve();
}
});
}

75
hooks/useTable/index.d.ts vendored Normal file
View File

@ -0,0 +1,75 @@
import type { AntdTableOptions, AntdTableResult, Data, Params, Service } from "ahooks/lib/useAntdTable/types";
import type { FormInstance } from "antd/es/form";
type FormValues = Record<string, any>;
/**
* useTable
*/
export interface UseTableOptions<TData extends Data, TParams extends Params> extends Omit<AntdTableOptions<TData, TParams>, "defaultParams" | "form"> {
/** 是否使用分页,默认是 */
usePagination?: boolean;
/** 默认分页参数,默认 { current: 1; pageSize: 10 } */
defaultPagination?: { current: number; pageSize: number };
/** 是否使用存储查询条件,默认是 */
useStorageQueryCriteria?: boolean;
/** 额外参数 */
params?: FormValues | (() => FormValues);
/** 表单数据转换函数,在每次请求之前调用,接收当前搜索的表单项,要求返回一个对象 */
transform?: (formData: FormValues) => FormValues;
/** 回调函数 */
callback?: (list: any[], data: any) => void;
/** 表单实例(通过 Form.useForm() 创建) */
form?: FormInstance;
}
/**
*
*/
export interface BasePaginationConfig {
/** 当前页码 */
current: number;
/** 每页数量 */
pageSize: number;
/** 总数 */
total: number;
}
/**
*
*/
export interface ExtendedPaginationConfig extends BasePaginationConfig {
/** 显示快速跳转 */
showQuickJumper: boolean;
/** 显示页码选择器 */
showSizeChanger: boolean;
}
/**
* useTable
*/
export interface UseTableResult<TData extends Data, TParams extends Params> extends AntdTableResult<TData, TParams> {
/** 表格属性 */
tableProps: {
/** 表格数据 */
dataSource: TData["list"];
/** 表格加载状态 */
loading: boolean;
/** 表格改变 */
onChange: (pagination: any, filters?: any, sorter?: any) => void;
/** 分页属性 */
pagination: false | ExtendedPaginationConfig;
[key: string]: any;
};
/** 查询方法,等于直接调用 search.submit */
getData: () => void;
}
/**
* useTable ahooks useAntdTable
*/
declare function useTable<TData extends Data, TParams extends Params>(
service: Service<TData, TParams>,
options?: UseTableOptions<TData, TParams>
): UseTableResult<TData, TParams>;
export default useTable;

162
hooks/useTable/index.js Normal file
View File

@ -0,0 +1,162 @@
import { tools } from "@cqsjjb/jjb-common-lib";
import { useAntdTable } from "ahooks";
const { query } = tools.router;
/**
* 获取数据
*/
function getService(service, getExtraParams = {}, transform) {
// 获取额外的参数
const extraParams = (typeof getExtraParams === "function" ? getExtraParams() : getExtraParams) || {};
// 获取数据
return async ({ current, pageSize }, formData = {}) => {
// 如果提供了 transform 函数,则在请求之前调用它
let transformedFormData = formData;
if (typeof transform === "function") {
const transformResult = transform(formData);
// 如果 transform 函数有返回值则将其与原始表单数据合并transform 的优先级更高
if (transformResult && typeof transformResult === "object") {
transformedFormData = { ...formData, ...transformResult };
}
}
// 发起请求
const res = await service({
pageIndex: current,
pageSize,
...transformedFormData,
...extraParams,
});
// 返回数据
return {
list: res.data || [],
total: res.totalCount || 0,
...res,
};
};
}
/**
* 将搜索表单项和分页参数保存到 URL
*/
function setQuery(searchForm, pagination) {
// 将对象转换为键值对字符串格式
const getJoinString = (data) => {
const keys = [];
const values = [];
Object.entries(data).forEach(([key, value]) => {
if (value) {
keys.push(key);
if (Array.isArray(value)) {
// 数组值使用方括号包裹,并用竖线分隔
values.push(`[${value.join("|")}]`);
}
else {
values.push(value);
}
}
});
return { keys: keys.join(","), values: values.join(",") };
};
// 获取搜索表单和分页数据的键值对字符串
const searchFormData = getJoinString(searchForm);
const paginationData = getJoinString(pagination);
// 将数据存储到 URL 查询参数中
query.searchFormKeys = searchFormData.keys;
query.searchFormValues = searchFormData.values;
query.paginationKeys = paginationData.keys;
query.paginationValues = paginationData.values;
}
/**
* URL 中获取查询参数
*/
function getQuery(keysStr, valuesStr) {
// 将键值字符串分割为数组
const keys = keysStr ? keysStr.split(",") : [];
const values = valuesStr ? valuesStr.split(",") : [];
// 构建结果对象
const resultMap = {};
keys.forEach((key, index) => {
if (values[index]) {
// 处理数组值(方括号包裹的值)
if (values[index].startsWith("[") && values[index].endsWith("]")) {
const arrayContent = values[index].substring(1, values[index].length - 1);
resultMap[key] = arrayContent ? arrayContent.split("|") : [];
}
else {
// 处理普通值
resultMap[key] = values[index];
}
}
});
return resultMap;
}
/**
* 自定义 useTable继承 ahooks useAntdTable根据需求进行扩展
*/
function useTable(service, options) {
// 获取额外参数和转换函数
const { params: extraParams, transform, ...restOptions } = options || {};
// 获取配置项
const {
useStorageQueryCriteria = true,
usePagination = true,
defaultType = "advance",
defaultCurrent = 1,
defaultPageSize = 10,
defaultPagination = { current: defaultCurrent, pageSize: defaultPageSize },
...restRestOptions
} = restOptions;
// 获取存储的查询条件
const storageQueryCriteriaSearchForm = useStorageQueryCriteria ? getQuery(query.searchFormKeys, query.searchFormValues) : {};
const storageQueryCriteriaPagination = useStorageQueryCriteria && usePagination ? getQuery(query.paginationKeys, query.paginationValues) : {};
// 确定实际使用的搜索表单和分页参数
const actualSearchForm = Object.keys(storageQueryCriteriaSearchForm).length > 0 ? storageQueryCriteriaSearchForm : {};
/** @type {{current: number, pageSize: number}} */
const actualPagination = usePagination ? Object.keys(storageQueryCriteriaPagination).length > 0 ? storageQueryCriteriaPagination : defaultPagination : {};
// 调用 ahooks 的 useAntdTable
const res = useAntdTable(
getService(service, extraParams, transform),
{
...restRestOptions,
defaultParams: [actualPagination, actualSearchForm],
defaultType,
onSuccess: (data, params) => {
// 执行成功回调,为了保留 ahooks 的 onSuccess 回调
restOptions.onSuccess && restOptions.onSuccess(data, params);
// 存储查询条件和分页到 URL
useStorageQueryCriteria && setQuery(
params[1] ?? {},
usePagination
? { current: res.tableProps.pagination.current, pageSize: res.tableProps.pagination.pageSize }
: {},
);
},
},
);
// 执行回调函数
restOptions.callback && restOptions.callback(res?.data?.list || [], res?.data || {});
// 返回结果
return {
...res,
tableProps: {
...res.tableProps,
pagination: usePagination ? { ...res.tableProps.pagination, showQuickJumper: true, showSizeChanger: true } : false,
},
getData: res.search.submit,
};
}
export default useTable;

6
npm Normal file
View File

@ -0,0 +1,6 @@
# npm地址
https://www.npmjs.com/package/zy-react-library
# npm账号
liujianan15703339975
Ljn15703339975.

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "zy-react-library",
"private": false,
"version": "1.0.0",
"type": "module",
"description": "",
"author": "LiuJiaNan",
"license": "MIT",
"files": [
"components",
"enum",
"hooks",
"regular",
"utils",
"README.md"
],
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"postinstall": "echo 'Thanks for using our component library!'"
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@ant-design/pro-components": "^2.8.10",
"@cqsjjb/jjb-common-lib": "latest",
"ahooks": "^3.9.5",
"antd": "^5.27.6",
"dayjs": "^1.11.18",
"lodash-es": "^4.17.21",
"react": "^18.3.1"
}
}

49
regular/index.d.ts vendored Normal file
View File

@ -0,0 +1,49 @@
/**
* 86
*/
export const PHONE: RegExp;
/**
*
*/
export const UNIFIED_SOCIAL_CREDIT_CODE: RegExp;
/**
* 1518
*/
export const ID_NUMBER: RegExp;
/**
*
*/
export const MOBILE_PHONE: RegExp;
/**
*
*/
export const FLOATING_POINT_NUMBER: RegExp;
/**
*
*/
export const TWO_DECIMAL_PLACES: RegExp;
/**
*
*/
export const ONE_DECIMAL_PLACES: RegExp;
/**
*
*/
export const LICENSE_PLATE_NUMBER: RegExp;
/**
* 8
*/
export const STRONG_PASSWORD: RegExp;
/**
* HTML
*/
export const HTML_TAG: RegExp;

56
regular/index.js Normal file
View File

@ -0,0 +1,56 @@
/**
* 匹配中国手机号码可包含国家代码86支持各种运营商号段
*/
export const PHONE
= /^(?:(?:\+|00)86)?1(?:3\d|4[5-7|9]|5[0-3|5-9]|6[5-7]|7[0-8]|8\d|9[1|89])\d{8}$/;
/**
* 匹配中国大陆的统一社会信用代码
*/
export const UNIFIED_SOCIAL_CREDIT_CODE
= /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/;
/**
* 匹配中国大陆的身份证号码包括15位和18位号码并验证最后一位校验码
*/
export const ID_NUMBER
= /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[12]\d|30|31)\d{3}[\dX]$/i;
/**
* 匹配中国大陆的移动电话号码不包含国家代码
*/
export const MOBILE_PHONE
= /^(13\d|14[579]|15[0-3,5-9]|166|17[0135-8]|18\d|19[89])\d{8}$/;
/**
* 匹配浮点数允许整数一位或两位小数以及零的情况
*/
export const FLOATING_POINT_NUMBER
= /(^[1-9](\d+)?(\.\d{1,2})?$)|(^(0)$)|(^\d\.\d(\d)?$)/;
/**
* 两位小数
*/
export const TWO_DECIMAL_PLACES = /^\d+\.\d{2}$/;
/**
* 一位小数非必须
*/
export const ONE_DECIMAL_PLACES = /^\d+(\.\d?)?$/;
/**
* 匹配中国大陆的车牌号码
*/
export const LICENSE_PLATE_NUMBER
= /^([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z][A-Z][A-Z0-9]{4}[A-Z0-9挂学警港澳])$/;
/**
* 匹配强密码要求至少8个字符包含大小写字母数字和特殊字符
*/
export const STRONG_PASSWORD
= /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d]).{8,}$/;
/**
* 匹配完整的HTML标签包括开始标签和结束标签
*/
export const HTML_TAG = /<[^>]*>/g;

265
utils/index.d.ts vendored Normal file
View File

@ -0,0 +1,265 @@
import type { BasePaginationConfig } from "../hooks/useTable";
// 定义 getDataType 函数可能返回的所有类型
type DataType
= | "String"
| "Number"
| "Boolean"
| "Symbol"
| "Undefined"
| "Null"
| "Object"
| "Array"
| "Function"
| "Date"
| "RegExp"
| "Error"
| "Map"
| "Set"
| "WeakMap"
| "WeakSet"
| "ArrayBuffer"
| "DataView"
| "Promise"
| "Generator"
| "GeneratorFunction"
| "AsyncFunction"
| "Arguments"
| "Math"
| "JSON"
| "Window"
| "HTMLDocument"
| string; // 允许其他可能的类型
// 为 findCharIndex 函数定义接口类型
interface FindCharIndexOptions {
/** 查找的字符串 */
str: string;
/** 查找的字符 */
char: string;
/** 第几次出现 */
num: number;
}
// 为 paging 函数定义接口类型
interface PagingOptions {
/** 分页的数组 */
list: any[];
/** 当前页 */
currentPage: number | string;
/** 每页条数 */
pageSize: number | string;
}
// 为 addingPrefixToFile 函数定义接口类型
interface AddingPrefixToFileOptions {
/** 附件路径字段名 */
pathKey?: string;
/** 附件名称字段名 */
nameKey?: string;
/** 附件id字段名 */
idKey?: string;
}
// 为 getLabelName 函数定义接口类型
interface GetLabelNameOptions {
/** 状态 */
status: number | string;
/** 翻译的数组 */
list: any[];
/** id字段名 */
idKey?: string;
/** name字段名 */
nameKey?: string;
}
// 为 getSelectAppointItemList 函数定义接口类型
interface GetSelectAppointItemListOptions {
/** 获取的数组 */
list: any[];
/** 获取的值 */
value: any[];
/** 获取的id字段名 */
idKey?: string;
}
// 为 listTransTree 函数定义接口类型
interface ListTransTreeOptions {
/** 需要转换的json */
json: any[];
/** id字段 */
idKey: string;
/** 父级id字段 */
parentIdKey: string;
/** 子级字段 */
childrenKey: string;
}
// 为 isEmptyToWhether 函数定义接口类型
interface IsEmptyToWhetherOptions {
/** 真值时显示的文本 */
yesText?: string;
/** 假值时显示的文本 */
noText?: string;
/** 判断为真的值 */
yesValue?: string | number;
}
/**
*
*/
export function serialNumber(
pagination: BasePaginationConfig,
index: number
): number;
/**
*
*/
export function toArrayString(value: string): Array<string>;
/**
*
*/
export function interceptTheSuffix(name: string, suffix: string): boolean;
/**
* base64
*/
export function image2Base64(imgUrl: string): Promise<string>;
/**
* base64 (File)
*/
export function image2Base642(file: File): Promise<string>;
/**
* 访
*/
export function checkImgExists(imgUrl: string): Promise<any>;
/**
*
*/
export function getDataType(data: any): DataType;
/**
*
*/
export function ArrayDeduplication<T extends number | string>(arr: T[]): T[];
/**
*
*/
export function arrayObjectDeduplication<T>(arr: T[], key: string): T[];
/**
*
*/
export function findCharIndex(options: FindCharIndexOptions): number;
/**
*
*/
export function randoms(min: number, max: number): number;
/**
*
*/
export function numFormat(num: number | string): string;
/**
*
*/
export function isEmpty(value: any): boolean;
/**
* url
*/
export function getUrlParam(key: string): string;
/**
*
*/
export function paging<T>(options: PagingOptions): T[];
/**
*
*/
export function getFileSuffix(name: string): string;
/**
*
*/
export function getFileName(name: string): string;
/**
* txt
*/
export function readTxtDocument(filePah: string): Promise<string>;
/**
*
*/
export function secondConversion(second: string | number): string;
/**
*
*/
export function addingPrefixToFile<T extends Record<string, any>>(
list: T[],
options?: AddingPrefixToFileOptions
): (T & { url: string; name: string; imgFilesId: any })[];
/**
*
*/
export function getLabelName<T>(options: GetLabelNameOptions): string | undefined;
/**
*
*/
export function calculateFileSize(size: number | string): string;
/**
*
*/
export function idCardGetDateAndGender(idCard: string): { sex: "1" | "0"; date: string };
/**
* select
*/
export function getSelectAppointItemList<T>(options: GetSelectAppointItemListOptions): T[];
/**
* json
*/
export function listTransTree<T>(options: ListTransTreeOptions): T[];
/**
* "是"/"否"
*/
export function isEmptyToWhether(
value: any,
options?: IsEmptyToWhetherOptions
): string;
/**
* guid
*/
export function createGuid(len?: number): string;
/**
*
*/
export function getIndexColumn(pagination: false | BasePaginationConfig): {
title: string;
key: string;
width: number;
render: (...args: any[]) => number;
};
/**
* url
*/
export function getFileUrl(): string;

402
utils/index.js Normal file
View File

@ -0,0 +1,402 @@
import { ID_NUMBER } from "../regular";
/**
* 计算序号
*/
export function serialNumber(pagination, index) {
return (pagination.current - 1) * pagination.pageSize + (index + 1);
}
/**
* 字符串数组转数组
*/
export function toArrayString(value) {
// eslint-disable-next-line no-eval
return value ? eval(value).map(String) : [];
}
/**
* 判断文件后缀名是否符合
*/
export function interceptTheSuffix(name, suffix) {
return (
name.substring(name.lastIndexOf("."), name.length).toLowerCase()
=== suffix.toLowerCase()
);
}
/**
* 图片转base64
*/
export function image2Base64(imgUrl) {
return new Promise((resolve) => {
const img = new Image();
img.src = imgUrl;
img.crossOrigin = "Anonymous";
img.onload = function () {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
const ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase();
resolve(canvas.toDataURL(`image/${ext}`));
};
});
}
/**
图片转base64 (File对象版本)
*/
export function image2Base642(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
resolve(e.target.result); // 返回 base64
};
reader.onerror = (error) => {
reject(error); // 处理错误
};
});
}
/**
* 判断图片是否可访问成功
*/
export function checkImgExists(imgUrl) {
return new Promise((resolve, reject) => {
const ImgObj = new Image();
ImgObj.src = imgUrl;
ImgObj.onload = function (res) {
resolve(res);
};
ImgObj.onerror = function (err) {
reject(err);
};
});
}
/**
* 获取数据类型
*/
export function getDataType(data) {
return Object.prototype.toString.call(data).slice(8, -1);
}
/**
* 数组去重
*/
export function ArrayDeduplication(arr) {
return [...new Set(arr)];
}
/**
* 数组对象去重
*/
export function arrayObjectDeduplication(arr, key) {
const obj = {};
arr = arr.reduce((previousValue, currentValue) => {
if (!obj[currentValue[key]]) {
obj[currentValue[key]] = true;
previousValue.push(currentValue);
}
return previousValue;
}, []);
return arr;
}
/**
* 查找字符串中指定的值第几次出现的位置
*/
export function findCharIndex(options) {
const { str, char, num } = options;
let index = str.indexOf(char);
if (index === -1)
return -1;
for (let i = 0; i < num - 1; i++) {
index = str.indexOf(char, index + 1);
if (index === -1)
return -1;
}
return index;
}
/**
* 生成指定两个值之间的随机数
*/
export function randoms(min, max) {
return Math.random() * (max - min + 1) + min;
}
/**
* 千位分隔符
*/
export function numFormat(num) {
if (num) {
const numArr = num.toString().split(".");
const arr = numArr[0].split("").reverse();
let res = [];
for (let i = 0; i < arr.length; i++) {
if (i % 3 === 0 && i !== 0) {
res.push(",");
}
res.push(arr[i]);
}
res.reverse();
if (numArr[1]) {
res = res.join("").concat(`.${numArr[1]}`);
}
else {
res = res.join("");
}
return res;
}
}
/**
* 验证是否为空
*/
export function isEmpty(value) {
return (
value === undefined
|| value === null
|| (typeof value === "object" && Object.keys(value).length === 0)
|| (typeof value === "string" && value.trim().length === 0)
);
}
/**
* 获取url参数
*/
export function getUrlParam(key) {
const reg = new RegExp(`(^|&)${key}=([^&]*)(&|$)`);
const r = window.location.search.substr(1).match(reg);
if (r != null)
return decodeURI(r[2]);
return "";
}
/**
* 数据分页
*/
export function paging(options) {
const { list, currentPage, pageSize } = options;
return list.filter((item, index) => {
return (
index < +currentPage * +pageSize
&& index >= (+currentPage - 1) * +pageSize
);
});
}
/**
* 获取文件后缀
*/
export function getFileSuffix(name) {
return name.substring(name.lastIndexOf(".") + 1);
}
/**
* 获取文件名称
*/
export function getFileName(name) {
if (!name)
return "";
return name.substring(name.lastIndexOf("/") + 1);
}
/**
* 读取txt文档
*/
export function readTxtDocument(filePah) {
return new Promise((resolve) => {
const FILE_URL = getFileUrl();
const file_url = FILE_URL + filePah;
const xhr = new XMLHttpRequest();
xhr.open("get", file_url, true);
xhr.responseType = "blob";
xhr.onload = function (event) {
const reader = new FileReader();
reader.readAsText(event.target.response, "GB2312");
reader.onload = function () {
resolve(reader.result);
};
};
xhr.send();
});
}
/**
* 将秒转换成时分秒
*/
export function secondConversion(second) {
if (!second)
return 0;
const h = Number.parseInt(second / 60 / 60, 10);
const m = Number.parseInt((second / 60) % 60, 10);
const s = Number.parseInt(second % 60, 10);
if (h) {
return `${h}小时${m}分钟${s}`;
}
else {
if (m) {
return `${m}分钟${s}`;
}
else {
return `${s}`;
}
}
}
/**
* 附件添加前缀
*/
export function addingPrefixToFile(list, options = {}) {
if (!list)
return [];
const {
pathKey = "filePath",
nameKey = "fileName",
idKey = "imgFilesId",
} = options;
const FILE_URL = getFileUrl();
for (let i = 0; i < list.length; i++) {
list[i].url = FILE_URL + list[i][pathKey];
list[i].name = list[i][nameKey] || getFileName(list[i][pathKey]);
list[i].imgFilesId = list[i][idKey];
}
return list;
}
/**
* 翻译状态
*/
export function getLabelName(options) {
const { status, list, idKey = "id", nameKey = "name" } = options;
for (let i = 0; i < list.length; i++) {
if (status?.toString() === list[i][idKey]?.toString()) {
return list[i][nameKey];
}
}
}
/**
* 计算文件大小
*/
export function calculateFileSize(size) {
return size > 1024
? `${(`${size / 1024}`).substring(0, (`${size / 1024}`).lastIndexOf(".") + 3)
}MB`
: `${size}KB`;
}
/**
* 根据身份证号获取出生日期和性别
*/
export function idCardGetDateAndGender(idCard) {
let sex = "";
let date = "";
if (ID_NUMBER.test(idCard)) {
const org_birthday = idCard.substring(6, 14);
const org_gender = idCard.substring(16, 17);
const birthday
= `${org_birthday.substring(0, 4)
}-${
org_birthday.substring(4, 6)
}-${
org_birthday.substring(6, 8)}`;
const birthdays = new Date(birthday.replace(/-/g, "/"));
const Month = birthdays.getMonth() + 1;
let MonthDate;
const DayDate = birthdays.getDate();
let Day;
if (Month < 10)
MonthDate = `0${Month}`;
else MonthDate = Month;
if (DayDate < 10)
Day = `0${DayDate}`;
else Day = DayDate;
sex = org_gender % 2 === 1 ? "1" : "0";
date = `${birthdays.getFullYear()}-${MonthDate}-${Day}`;
}
return { sex, date };
}
/**
* 获取select中指定项组成的数组
*/
export function getSelectAppointItemList(options) {
const { list, value, idKey = "id" } = options;
return list.filter(item => value.includes(item[idKey]));
}
/**
* json转换为树形结构
*/
export function listTransTree(options) {
const { json, idKey, parentIdKey, childrenKey } = options;
const r = [];
const hash = {};
let i = 0;
let j = 0;
const len = json.length;
for (; i < len; i++) {
hash[json[i][idKey]] = json[i];
}
for (; j < len; j++) {
const aVal = json[j];
const hashVP = hash[aVal[parentIdKey]];
if (hashVP) {
!hashVP[childrenKey] && (hashVP[childrenKey] = []);
hashVP[childrenKey].push(aVal);
}
else {
r.push(aVal);
}
}
return r;
}
/**
* 将值转换为"是"/"否"显示文本
*/
export function isEmptyToWhether(value, options = {}) {
const { yesText = "是", noText = "否", yesValue = "1" } = options;
return !isEmpty(value)
? value.toString() === yesValue.toString()
? yesText
: noText
: "";
}
/**
* 生成指定长度的guid
*/
export function createGuid(len = 32) {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < len; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 获取序号列
*/
export function getIndexColumn(pagination) {
return {
title: "序号",
key: "index",
width: 70,
render: (_, __, index) => pagination === false ? index + 1 : serialNumber(pagination, index),
};
}
/**
* 获取文件url
*/
export function getFileUrl() {
return process.env.app["fileUrl"];
}