feat: tagEditor
Some checks failed
coverage CI / build (push) Has been cancelled

This commit is contained in:
2025-09-24 11:09:02 +08:00
parent 67b9a74212
commit 12495b6d85
11 changed files with 461 additions and 87 deletions

View File

@@ -4,8 +4,8 @@ NODE_ENV=development
VITE_DEV=true VITE_DEV=true
# 请求路径 # 请求路径
# VITE_BASE_URL='http://114.132.60.20:48080' VITE_BASE_URL='http://114.132.60.20:48080'
VITE_BASE_URL='http://192.168.1.114:48080' # 理君 # VITE_BASE_URL='http://192.168.1.114:48080' # 理君
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务 # 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务

View File

@@ -4,30 +4,12 @@ import { PlusOutlined } from '@ant-design/icons';
import { import {
type ActionType, type ActionType,
type ParamsType, type ParamsType,
ProColumns,
ProTable, ProTable,
TableDropdown,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Button, Space } from 'antd'; import { Button, Space } from 'antd';
import React, { import React, { forwardRef, useCallback, useMemo, useState } from 'react';
act, import { formatPaginationTotal } from '@/utils/antd/tableHelpers';
forwardRef, import type { BaseRecord, EnhancedProTableProps } from './types';
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import {
buildTableDropdownMenuItems,
formatPaginationTotal,
handleTableDropdownSelect,
} from '@/utils/antd/tableHelpers';
import {
type BaseRecord,
type EnhancedProTableProps,
TableAction,
ToolbarAction,
} from './types';
function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>( function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
props: EnhancedProTableProps<T, U>, props: EnhancedProTableProps<T, U>,
@@ -36,21 +18,22 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
const { const {
columns, columns,
request, request,
actions = [], // actions = [],
permissions = [], // permissions = [],
checkPermission = () => true, checkPermission = () => true,
toolbarActions, toolbarActions,
showIndex = true, showIndex = true,
showSelection = true, showSelection = true,
showActions = true, // showActions = true,
maxActionCount = 2, // maxActionCount = 2,
onAdd, // onAdd,
onEdit, // onEdit,
onDelete, // onDelete,
onView, // onView,
onExport, // onExport,
customToolbarRender, // customToolbarRender,
customActionRender, customActionRender,
rowKey,
...restProps ...restProps
} = props; } = props;
@@ -91,14 +74,13 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
[showSelection], [showSelection],
); );
const toolBarRender = useCallback( const toolBarRender = useCallback(() =>
( // action: ActionType | undefined,
action: ActionType | undefined, // rows: {
rows: { // selectedRowKeys?: (string | number)[] | undefined;
selectedRowKeys?: (string | number)[] | undefined; // selectedRows?: T[] | undefined;
selectedRows?: T[] | undefined; // }
}, {
) => {
const toolbarElements = const toolbarElements =
toolbarActions?.map((action) => { toolbarActions?.map((action) => {
return ( return (
@@ -125,22 +107,19 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
// </Button>, // </Button>,
// ]; // ];
return toolbarElements; return toolbarElements;
}, }, [toolbarActions]);
[toolbarActions],
);
return ( return (
<ProTable<T, U> <ProTable<T, U>
{...restProps} {...restProps}
columns={columns} columns={columns}
actionRef={ref} actionRef={ref}
request={request} request={request}
rowKey="id" rowKey={rowKey || 'id'}
rowSelection={rowSelection} rowSelection={rowSelection}
toolBarRender={toolBarRender} toolBarRender={toolBarRender}
manualRequest={false} manualRequest={false}
showSorterTooltip showSorterTooltip
tableAlertRender={tableAlertRender} tableAlertRender={tableAlertRender}
// tableAlertOptionRender={tableAlertOptionRender}
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
search={{ search={{
labelWidth: 'auto', labelWidth: 'auto',

View File

@@ -1,20 +1,19 @@
// components/EnhancedProTable/types.ts // components/EnhancedProTable/types.ts
import { import type {
ProTableProps,
ProColumns,
ActionType, ActionType,
} from "@ant-design/pro-components"; ProColumns,
import { ButtonProps } from "antd"; ProTableProps,
import React from "react"; } from '@ant-design/pro-components';
import type { ButtonProps } from 'antd';
import React from 'react';
export interface BaseRecord { export interface BaseRecord {
id: number;
[key: string]: any; [key: string]: any;
} }
export interface ApiResponse<T> { export interface ApiResponse<T> {
data: T[]; data: T[];
total: number; total?: number;
success: boolean; success: boolean;
message?: string; message?: string;
} }
@@ -32,7 +31,7 @@ export interface TableAction<T = any> {
key: string; key: string;
label: string; label: string;
icon?: React.ReactNode; icon?: React.ReactNode;
type?: ButtonProps["type"]; type?: ButtonProps['type'];
danger?: boolean; danger?: boolean;
disabled?: (record: T) => boolean; disabled?: (record: T) => boolean;
visible?: (record: T) => boolean; visible?: (record: T) => boolean;
@@ -44,7 +43,7 @@ export interface ToolbarAction {
key: string; key: string;
label: string; label: string;
icon?: React.ReactNode; icon?: React.ReactNode;
type?: ButtonProps["type"]; type?: ButtonProps['type'];
danger?: boolean; danger?: boolean;
disabled?: boolean; disabled?: boolean;
onClick: (selectedKeys?: React.Key[], selectedRows?: any[]) => void; onClick: (selectedKeys?: React.Key[], selectedRows?: any[]) => void;
@@ -53,10 +52,10 @@ export interface ToolbarAction {
} }
export interface EnhancedProTableProps<T extends BaseRecord, U = any> export interface EnhancedProTableProps<T extends BaseRecord, U = any>
extends Omit<ProTableProps<T, U>, "columns" | "request" | "toolBarRender"> { extends Omit<ProTableProps<T, U>, 'columns' | 'request' | 'toolBarRender'> {
columns: ProColumns<T>[]; columns: ProColumns<T>[];
request?: ( request?: (
params: U & { current?: number; pageSize?: number } params: U & { current?: number; pageSize?: number },
) => Promise<ApiResponse<T>>; ) => Promise<ApiResponse<T>>;
loading?: boolean; // 添加 loading 属性 loading?: boolean; // 添加 loading 属性
actions?: TableAction<T>[]; actions?: TableAction<T>[];
@@ -73,11 +72,11 @@ export interface EnhancedProTableProps<T extends BaseRecord, U = any>
onView?: (record: T) => void; onView?: (record: T) => void;
onExport?: (selectedRows: T[]) => void; onExport?: (selectedRows: T[]) => void;
customToolbarRender?: ( customToolbarRender?: (
defaultActions: React.ReactNode[] defaultActions: React.ReactNode[],
) => React.ReactNode[]; ) => React.ReactNode[];
customActionRender?: ( customActionRender?: (
record: T, record: T,
defaultActions: React.ReactNode[] defaultActions: React.ReactNode[],
) => React.ReactNode[]; ) => React.ReactNode[];
} }

View File

@@ -0,0 +1,105 @@
// src/components/TagEditor/index.tsx
import { PlusOutlined } from '@ant-design/icons';
import type { TagProps } from 'antd';
import { Input, Space, Tag } from 'antd';
import React, { useMemo, useState } from 'react';
interface TagEditorProps {
value?: string[];
onChange?: (value: string[]) => void;
placeholder?: string;
maxCount?: number;
tagProps?: TagProps;
disabled?: boolean;
}
const TagEditor: React.FC<TagEditorProps> = ({
value = [],
onChange,
placeholder = '请输入标签',
maxCount,
tagProps,
disabled = false,
}) => {
// const [tags, setTags] = useState<string[]>(value);
const [inputVisible, setInputVisible] = useState(false);
const [inputValue, setInputValue] = useState('');
const tags = useMemo(() => {
return value || [];
}, [value]);
const handleClose = (removedTag: string) => {
const newTags = tags.filter((tag) => tag !== removedTag);
onChange?.(newTags);
};
const showInput = () => {
setInputVisible(true);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
if (inputValue && !tags.includes(inputValue)) {
const newTags = [...tags, inputValue];
onChange?.(newTags);
}
setInputVisible(false);
setInputValue('');
};
const handleInputBlur = () => {
handleInputConfirm();
};
const canAddMore = !maxCount || tags.length < maxCount;
return (
<Space wrap>
{tags.map((tag) => (
<Tag
key={tag}
closable={!disabled}
onClose={() => handleClose(tag)}
{...tagProps}
>
{tag}
</Tag>
))}
{inputVisible ? (
<Input
type="text"
size="small"
style={{ width: 110 }}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
onPressEnter={handleInputConfirm}
placeholder={placeholder}
autoFocus
/>
) : (
canAddMore &&
!disabled && (
<Tag
onClick={showInput}
style={{
background: '#fff',
borderStyle: 'dashed',
cursor: 'pointer',
}}
>
<PlusOutlined />
</Tag>
)
)}
</Space>
);
};
export default TagEditor;

View File

@@ -1 +1,5 @@
export const formStatusType = { create: '创建', update: '编辑', test: '测试' }; export const formStatusType: { [key: string]: string } = {
create: '创建',
update: '编辑',
test: '测试',
};

View File

@@ -0,0 +1,155 @@
import type {
ProColumns,
ProFormColumnsType,
} from '@ant-design/pro-components';
import dayjs from 'dayjs';
import TagEditor from '@/components/TagEditor';
export const baseTenantColumns: ProColumns<API.CategoryDO>[] = [
{
title: '类目名称',
dataIndex: 'categoryName',
width: 100,
},
{
title: '类目ID',
dataIndex: 'categoryId',
},
{
title: '类目层级',
dataIndex: 'grade',
hideInSearch: true, // 在搜索表单中隐藏
},
{
title: '父级类目',
dataIndex: 'parentName',
hideInSearch: true, // 在搜索表单中隐藏
},
{
title: '排序权重',
dataIndex: 'sort',
hideInSearch: true, // 在搜索表单中隐藏
},
{
title: '状态',
dataIndex: 'status',
valueType: 'switch',
hideInSearch: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
valueType: 'dateRange',
hideInSearch: true, // 在搜索表单中隐藏
render: (_, record: API.CategoryDO) =>
dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
];
export const formColumns = (data: {
type: string;
grade: number;
}): ProFormColumnsType[] => [
{
title: '类目',
dataIndex: 'grade',
valueType: 'radio',
fieldProps: {
value: data.grade || 1,
options: [
{ label: '一级类目', value: 1 },
{ label: '二级类目', value: 2 },
{ label: '三级类目', value: 3 },
],
disabled: data.type === 'create',
},
},
{
title: '类目名称',
dataIndex: 'username',
formItemProps: {
rules: [
{
required: true,
message: '请输入用户名',
},
],
},
},
{
title: '排序权重',
dataIndex: 'sort',
valueType: 'digit',
},
{
title: '类目描述',
dataIndex: 'description',
valueType: 'textarea',
renderFormItem: () => {
return 1;
},
},
{
title: '关联父级',
dataIndex: 'parentId',
valueType: 'select',
hideInForm: data.grade - 1 === 0,
},
{
title: '类目icon',
dataIndex: 'icon',
},
{
title: '类目标签',
dataIndex: 'tages',
renderFormItem: () => {
return (
<TagEditor
placeholder="输入标签名称"
maxCount={10}
tagProps={{
color: 'blue',
}}
/>
);
},
},
{
title: '类目状态',
dataIndex: 'status',
hideInForm: data.type === 'create',
},
{
title: '类目ID',
dataIndex: 'categoryId',
hideInForm: data.type === 'create',
},
{
title: '创建时间',
dataIndex: 'createTime',
valueType: 'dateTime',
hideInForm: data.type === 'create',
},
{
title: '创建人',
dataIndex: 'creator',
hideInForm: data.type === 'create',
},
{
title: '更新时间',
dataIndex: 'updateTime',
valueType: 'dateTime',
hideInForm: data.type === 'create',
},
{
title: '更新人',
dataIndex: 'updator',
hideInForm: data.type === 'create',
},
];
// {
// title: "模板内容",
// dataIndex: "content",
// valueType: "textarea",
// },

View File

@@ -0,0 +1,132 @@
import { PlusOutlined } from '@ant-design/icons';
import {
type ActionType,
type ProColumns,
ProCoreActionType,
} from '@ant-design/pro-components';
import type { TabsProps } from 'antd';
import { Tabs } from 'antd';
import { useCallback, useRef, useState } from 'react';
import ConfigurableDrawerForm, {
type ConfigurableDrawerFormRef,
} from '@/components/DrawerForm';
import EnhancedProTable from '@/components/EnhancedProTable';
import type { ToolbarAction } from '@/components/EnhancedProTable/types';
import { formStatusType } from '@/constants';
import { getCategoryList } from '@/services/prodApi/category';
import { baseTenantColumns, formColumns } from './config';
const ProdCategory = () => {
const tableRef = useRef<ActionType>(null);
const [type, setType] = useState<'create' | 'update' | 'test'>('create');
const [grade, setGrade] = useState<string>('');
const configurableDrawerRef = useRef<ConfigurableDrawerFormRef>(null);
const onChange = useCallback(
(key: string) => {
setGrade(key);
},
[grade],
);
const onFetch = async (params: API.getProductCategoryCategoryListParams) => {
const data = await getCategoryList({
...params,
grade: grade ? Number(grade) : undefined,
});
return {
data: data,
success: true,
};
};
const handleAdd = () => {
setType('create');
configurableDrawerRef.current?.open();
};
const toolbarActions: ToolbarAction[] = [
{
key: 'add',
label: '新建',
type: 'primary',
icon: <PlusOutlined />,
onClick: handleAdd,
},
];
const handleEdit = async (row: API.CategoryDO) => {
setType('update');
configurableDrawerRef.current?.open(row);
};
const handleSubmit = async (values: API.CategoryDO) => {
console.log('values', values);
// const success = await handleAdd(values as API.CategoryDO);
// if (success) {
// handleClose();
// }
return true;
};
const actionColumns: ProColumns<API.CategoryDO> = {
title: '操作',
dataIndex: 'option',
valueType: 'option',
fixed: 'right',
width: 120,
render: (_text: React.ReactNode, record: API.CategoryDO, _: number) => [
<a key="edit" onClick={() => handleEdit(record)}>
</a>,
<a key="detail"></a>,
],
};
const columns = [...baseTenantColumns, actionColumns];
const renderChildren = () => {
return (
<>
<EnhancedProTable<API.CategoryDO>
ref={tableRef}
columns={columns}
rowKey="categoryId"
request={onFetch}
toolbarActions={toolbarActions}
headerTitle="短信渠道"
showIndex={false}
showSelection={false}
/>
<ConfigurableDrawerForm
ref={configurableDrawerRef}
title={formStatusType[type]}
columns={formColumns({ grade: Number(grade), type })}
onSubmit={handleSubmit}
/>
</>
);
};
const items: TabsProps['items'] = [
{
key: '',
label: '全部分类',
children: renderChildren(),
},
{
key: '3',
label: '三级分类',
children: renderChildren(),
},
{
key: '2',
label: '二级分类',
children: renderChildren(),
},
{
key: '1',
label: '一级分类',
children: renderChildren(),
},
];
return <Tabs defaultActiveKey={grade} items={items} onChange={onChange} />;
};
export default ProdCategory;

View File

@@ -0,0 +1,5 @@
const ProdList = () => {
return <div>ProdList</div>;
};
export default ProdList;

View File

@@ -1,6 +1,5 @@
import type { import type {
ProColumns, ProColumns,
ProDescriptionsItemProps,
ProFormColumnsType, ProFormColumnsType,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -160,7 +159,7 @@ export const formColumns = (): ProFormColumnsType[] => [
}, },
]; ];
export const formTestColumns = (type: string): ProFormColumnsType[] => [ export const formTestColumns = (): ProFormColumnsType[] => [
{ {
title: '模板内容', title: '模板内容',
dataIndex: 'content', dataIndex: 'content',

View File

@@ -15,18 +15,17 @@ import { formStatusType } from '@/constants';
import { import {
createMailAccount, createMailAccount,
deleteMailAccount, deleteMailAccount,
getMailAccount,
getMailAccountPage, getMailAccountPage,
type MailAccountVO, type MailAccountVO,
updateMailAccount, updateMailAccount,
} from '@/services/system/message/mail/account'; } from '@/services/system/message/mail/account';
import { baseTenantColumns, formColumns, formTestColumns } from './config'; import { baseTenantColumns, formColumns } from './config';
const SyStemMessageSmsTemplate = () => { const SyStemMessageSmsTemplate = () => {
const tableRef = useRef<ActionType>(null); const tableRef = useRef<ActionType>(null);
const configurableDrawerRef = useRef<ConfigurableDrawerFormRef>(null); const configurableDrawerRef = useRef<ConfigurableDrawerFormRef>(null);
const [type, setType] = useState<'create' | 'update' | 'test'>('create'); const [type, setType] = useState<'create' | 'update' | 'test'>('create');
const testRef = useRef<ConfigurableDrawerFormRef>(null); // const testRef = useRef<ConfigurableDrawerFormRef>(null);
const [currentItem, setCurrentItem] = useState<MailAccountVO>(); const [currentItem, setCurrentItem] = useState<MailAccountVO>();
const onFetch = async ( const onFetch = async (
params: MailAccountVO & { params: MailAccountVO & {
@@ -46,11 +45,11 @@ const SyStemMessageSmsTemplate = () => {
}; };
}; };
const handleSend = async (record: MailAccountVO) => { // const handleSend = async (record: MailAccountVO) => {
setType('test'); // setType("test");
setCurrentItem(record); // setCurrentItem(record);
testRef.current?.open(record); // testRef.current?.open(record);
}; // };
const handleEdit = (record: MailAccountVO) => { const handleEdit = (record: MailAccountVO) => {
setType('update'); setType('update');
@@ -68,10 +67,10 @@ const SyStemMessageSmsTemplate = () => {
if (type === 'create') { if (type === 'create') {
await createMailAccount(values); await createMailAccount(values);
message.success('创建成功'); message.success('创建成功');
} else { } else if (currentItem?.id) {
await updateMailAccount({ await updateMailAccount({
...values, ...values,
id: currentItem!.id, id: currentItem.id,
}); });
message.success('编辑成功'); message.success('编辑成功');
} }
@@ -97,7 +96,7 @@ const SyStemMessageSmsTemplate = () => {
fixed: 'right', fixed: 'right',
width: 120, width: 120,
render: ( render: (
text: React.ReactNode, _text: React.ReactNode,
record: MailAccountVO, record: MailAccountVO,
_: number, _: number,
action: ProCoreActionType | undefined, action: ProCoreActionType | undefined,

View File

@@ -3,25 +3,22 @@
import { request } from "@umijs/max"; import { request } from "@umijs/max";
/** 获取菜单页面的表 GET /product/category/categoryList */ /** 获取菜单页面的表 GET /product/category/categoryList */
export async function getProductCategoryCategoryList( export async function getCategoryList(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.getProductCategoryCategoryListParams, params: API.getProductCategoryCategoryListParams,
options?: { [key: string]: any } options?: { [key: string]: any }
) { ) {
return request<API.CommonResultListCategoryDto>( return request("/product/category/categoryList", {
"/product/category/categoryList", method: "GET",
{ params: {
method: "GET", ...params,
params: { },
...params, ...(options || {}),
}, });
...(options || {}),
}
);
} }
/** 创建产品类目 创建产品类目 POST /product/category/create */ /** 创建产品类目 创建产品类目 POST /product/category/create */
export async function postProductCategoryCreate( export async function postCategoryCreate(
body: API.CategorySaveReqVO, body: API.CategorySaveReqVO,
options?: { [key: string]: any } options?: { [key: string]: any }
) { ) {
@@ -36,7 +33,7 @@ export async function postProductCategoryCreate(
} }
/** 更新产品类目 更新产品类目 PUT /product/category/update */ /** 更新产品类目 更新产品类目 PUT /product/category/update */
export async function putProductCategoryUpdate( export async function putCategoryUpdate(
body: API.CategorySaveReqVO, body: API.CategorySaveReqVO,
options?: { [key: string]: any } options?: { [key: string]: any }
) { ) {