init
commit
d7ccfda275
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
/src/test/
|
||||
/target/
|
||||
.idea
|
||||
|
||||
/node_modules
|
||||
*.local
|
||||
package-lock.json
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# zy-react-library
|
||||
|
||||
## 📦 安装
|
||||
|
||||
```bash
|
||||
# yarn
|
||||
yarn add zy-react-library
|
||||
```
|
||||
|
||||
## 📄 更新日志
|
||||
|
||||
### v1.0.0 (2025-10-22)
|
||||
|
||||
- 🎉 初始版本发布
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import FormBuilder from "./FormBuilder";
|
||||
|
||||
export default FormBuilder;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import FormBuilder from "./FormBuilder";
|
||||
|
||||
export default FormBuilder;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { FC } from "react";
|
||||
import type { PreviewImgProps } from "../PreviewImg";
|
||||
|
||||
declare const TooltipPreviewImg: FC<PreviewImgProps>;
|
||||
|
||||
export default TooltipPreviewImg;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
};
|
||||
|
|
@ -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",
|
||||
};
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
export default function useDownloadFile(url: string, name?: string): void;
|
||||
|
|
@ -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("下载失败");
|
||||
});
|
||||
} });
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# npm地址
|
||||
https://www.npmjs.com/package/zy-react-library
|
||||
|
||||
# npm账号
|
||||
liujianan15703339975
|
||||
Ljn15703339975.
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* 匹配中国手机号码,可包含国家代码86,支持各种运营商号段。
|
||||
*/
|
||||
export const PHONE: RegExp;
|
||||
|
||||
/**
|
||||
* 匹配中国大陆的统一社会信用代码。
|
||||
*/
|
||||
export const UNIFIED_SOCIAL_CREDIT_CODE: RegExp;
|
||||
|
||||
/**
|
||||
* 匹配中国大陆的身份证号码,包括15位和18位号码,并验证最后一位校验码。
|
||||
*/
|
||||
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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"];
|
||||
}
|
||||
Loading…
Reference in New Issue