feat(web): 插件前端全面增强 — 搜索/筛选/排序/详情页/条件表单/timeline 视图
- pluginData API: 支持 filter/search/sort_by/sort_order 参数 - plugins API: 新增 PluginFieldSchema/PluginEntitySchema/PluginPageSchema 类型 - PluginCRUDPage: 添加搜索框、筛选栏、视图切换(表格/时间线) - PluginCRUDPage: 添加详情 Drawer(Descriptions + 嵌套 CRUD) - PluginCRUDPage: 支持 visible_when 条件表单字段动态显示/隐藏 - PluginCRUDPage: 支持 compact 模式用于 detail 页面内嵌
This commit is contained in:
@@ -16,15 +16,32 @@ interface PaginatedDataResponse {
|
|||||||
total_pages: number;
|
total_pages: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PluginDataListOptions {
|
||||||
|
filter?: Record<string, string>;
|
||||||
|
search?: string;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_order?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
export async function listPluginData(
|
export async function listPluginData(
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
entity: string,
|
entity: string,
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 20,
|
pageSize = 20,
|
||||||
|
options?: PluginDataListOptions,
|
||||||
) {
|
) {
|
||||||
|
const params: Record<string, string> = {
|
||||||
|
page: String(page),
|
||||||
|
page_size: String(pageSize),
|
||||||
|
};
|
||||||
|
if (options?.filter) params.filter = JSON.stringify(options.filter);
|
||||||
|
if (options?.search) params.search = options.search;
|
||||||
|
if (options?.sort_by) params.sort_by = options.sort_by;
|
||||||
|
if (options?.sort_order) params.sort_order = options.sort_order;
|
||||||
|
|
||||||
const { data } = await client.get<{ success: boolean; data: PaginatedDataResponse }>(
|
const { data } = await client.get<{ success: boolean; data: PaginatedDataResponse }>(
|
||||||
`/plugins/${pluginId}/${entity}`,
|
`/plugins/${pluginId}/${entity}`,
|
||||||
{ params: { page, page_size: pageSize } },
|
{ params },
|
||||||
);
|
);
|
||||||
return data.data;
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,3 +119,44 @@ export async function getPluginSchema(id: string) {
|
|||||||
);
|
);
|
||||||
return data.data;
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Schema 类型定义 ──
|
||||||
|
|
||||||
|
export interface PluginFieldSchema {
|
||||||
|
name: string;
|
||||||
|
field_type: string;
|
||||||
|
required: boolean;
|
||||||
|
display_name?: string;
|
||||||
|
ui_widget?: string;
|
||||||
|
options?: { label: string; value: string }[];
|
||||||
|
searchable?: boolean;
|
||||||
|
filterable?: boolean;
|
||||||
|
sortable?: boolean;
|
||||||
|
visible_when?: string;
|
||||||
|
unique?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginEntitySchema {
|
||||||
|
name: string;
|
||||||
|
display_name: string;
|
||||||
|
fields: PluginFieldSchema[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginSchemaResponse {
|
||||||
|
entities: PluginEntitySchema[];
|
||||||
|
ui?: PluginUiSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginUiSchema {
|
||||||
|
pages: PluginPageSchema[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginPageSchema =
|
||||||
|
| { type: 'crud'; entity: string; label: string; icon?: string; enable_search?: boolean; enable_views?: string[] }
|
||||||
|
| { type: 'tree'; entity: string; label: string; icon?: string; id_field: string; parent_field: string; label_field: string }
|
||||||
|
| { type: 'detail'; entity: string; label: string; sections: PluginSectionSchema[] }
|
||||||
|
| { type: 'tabs'; label: string; icon?: string; tabs: PluginPageSchema[] };
|
||||||
|
|
||||||
|
export type PluginSectionSchema =
|
||||||
|
| { type: 'fields'; label: string; fields: string[] }
|
||||||
|
| { type: 'crud'; label: string; entity: string; filter_field?: string; enable_views?: string[] };
|
||||||
|
|||||||
@@ -14,81 +14,207 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
message,
|
message,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
|
Drawer,
|
||||||
|
Descriptions,
|
||||||
|
Segmented,
|
||||||
|
Timeline,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
listPluginData,
|
listPluginData,
|
||||||
createPluginData,
|
createPluginData,
|
||||||
updatePluginData,
|
updatePluginData,
|
||||||
deletePluginData,
|
deletePluginData,
|
||||||
|
PluginDataListOptions,
|
||||||
} from '../api/pluginData';
|
} from '../api/pluginData';
|
||||||
import { getPluginSchema } from '../api/plugins';
|
import {
|
||||||
|
getPluginSchema,
|
||||||
|
PluginFieldSchema,
|
||||||
|
PluginEntitySchema,
|
||||||
|
PluginPageSchema,
|
||||||
|
PluginSectionSchema,
|
||||||
|
} from '../api/plugins';
|
||||||
|
|
||||||
interface FieldDef {
|
const { Search } = Input;
|
||||||
name: string;
|
const { TextArea } = Input;
|
||||||
field_type: string;
|
|
||||||
required: boolean;
|
/** visible_when 表达式解析 */
|
||||||
display_name?: string;
|
function parseVisibleWhen(expression: string): { field: string; value: string } | null {
|
||||||
ui_widget?: string;
|
const regex = /^(\w+)\s*==\s*'([^']*)'$/;
|
||||||
options?: { label: string; value: string }[];
|
const match = expression.trim().match(regex);
|
||||||
|
if (!match) return null;
|
||||||
|
return { field: match[1], value: match[2] };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntitySchema {
|
/** 判断字段是否应该显示 */
|
||||||
name: string;
|
function shouldShowField(
|
||||||
display_name: string;
|
allValues: Record<string, unknown>,
|
||||||
fields: FieldDef[];
|
visibleWhen: string | undefined,
|
||||||
|
): boolean {
|
||||||
|
if (!visibleWhen) return true;
|
||||||
|
const parsed = parseVisibleWhen(visibleWhen);
|
||||||
|
if (!parsed) return true;
|
||||||
|
return String(allValues[parsed.field] ?? '') === parsed.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PluginCRUDPage() {
|
interface PluginCRUDPageProps {
|
||||||
const { pluginId, entityName } = useParams<{ pluginId: string; entityName: string }>();
|
/** 如果从 tabs/detail 页面内嵌使用,通过 props 传入配置 */
|
||||||
|
pluginIdOverride?: string;
|
||||||
|
entityOverride?: string;
|
||||||
|
filterField?: string;
|
||||||
|
filterValue?: string;
|
||||||
|
enableViews?: string[];
|
||||||
|
/** detail 页面内嵌时使用 compact 模式 */
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PluginCRUDPage({
|
||||||
|
pluginIdOverride,
|
||||||
|
entityOverride,
|
||||||
|
filterField,
|
||||||
|
filterValue,
|
||||||
|
enableViews: enableViewsProp,
|
||||||
|
compact,
|
||||||
|
}: PluginCRUDPageProps = {}) {
|
||||||
|
const routeParams = useParams<{ pluginId: string; entityName: string }>();
|
||||||
|
const pluginId = pluginIdOverride || routeParams.pluginId || '';
|
||||||
|
const entityName = entityOverride || routeParams.entityName || '';
|
||||||
|
|
||||||
const [records, setRecords] = useState<Record<string, unknown>[]>([]);
|
const [records, setRecords] = useState<Record<string, unknown>[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [fields, setFields] = useState<FieldDef[]>([]);
|
const [fields, setFields] = useState<PluginFieldSchema[]>([]);
|
||||||
const [displayName, setDisplayName] = useState(entityName || '');
|
const [displayName, setDisplayName] = useState(entityName || '');
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editRecord, setEditRecord] = useState<Record<string, unknown> | null>(null);
|
const [editRecord, setEditRecord] = useState<Record<string, unknown> | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const [formValues, setFormValues] = useState<Record<string, unknown>>({});
|
||||||
|
|
||||||
|
// 筛选/搜索/排序 state
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||||
|
const [sortBy, setSortBy] = useState<string | undefined>();
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||||
|
|
||||||
|
// 视图切换
|
||||||
|
const [viewMode, setViewMode] = useState<string>('table');
|
||||||
|
|
||||||
|
// 详情 Drawer
|
||||||
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
|
const [detailRecord, setDetailRecord] = useState<Record<string, unknown> | null>(null);
|
||||||
|
const [detailSections, setDetailSections] = useState<PluginSectionSchema[]>([]);
|
||||||
|
const [allEntities, setAllEntities] = useState<PluginEntitySchema[]>([]);
|
||||||
|
const [allPages, setAllPages] = useState<PluginPageSchema[]>([]);
|
||||||
|
|
||||||
|
// 从 fields 中提取 filterable 字段
|
||||||
|
const filterableFields = fields.filter((f) => f.filterable);
|
||||||
|
|
||||||
|
// 查找是否有 detail 页面
|
||||||
|
const hasDetailPage = allPages.some(
|
||||||
|
(p) => p.type === 'detail' && 'entity' in p && p.entity === entityName,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 可用视图
|
||||||
|
const enableViews = enableViewsProp ||
|
||||||
|
(() => {
|
||||||
|
const page = allPages.find(
|
||||||
|
(p) => p.type === 'crud' && 'entity' in p && p.entity === entityName,
|
||||||
|
);
|
||||||
|
return (page as { enable_views?: string[] })?.enable_views || ['table'];
|
||||||
|
})();
|
||||||
|
|
||||||
// 加载 schema
|
// 加载 schema
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pluginId) return;
|
if (!pluginId) return;
|
||||||
getPluginSchema(pluginId)
|
getPluginSchema(pluginId)
|
||||||
.then((schema) => {
|
.then((schema) => {
|
||||||
const entities = (schema as { entities?: EntitySchema[] }).entities || [];
|
const entities: PluginEntitySchema[] = (schema as { entities?: PluginEntitySchema[] }).entities || [];
|
||||||
|
setAllEntities(entities);
|
||||||
const entity = entities.find((e) => e.name === entityName);
|
const entity = entities.find((e) => e.name === entityName);
|
||||||
if (entity) {
|
if (entity) {
|
||||||
setFields(entity.fields);
|
setFields(entity.fields);
|
||||||
setDisplayName(entity.display_name || entityName || '');
|
setDisplayName(entity.display_name || entityName || '');
|
||||||
}
|
}
|
||||||
|
const ui = (schema as { ui?: { pages: PluginPageSchema[] } }).ui;
|
||||||
|
if (ui?.pages) {
|
||||||
|
setAllPages(ui.pages);
|
||||||
|
// 找到 detail 页面的 sections
|
||||||
|
const detailPage = ui.pages.find(
|
||||||
|
(p) => p.type === 'detail' && 'entity' in p && p.entity === entityName,
|
||||||
|
);
|
||||||
|
if (detailPage && 'sections' in detailPage) {
|
||||||
|
setDetailSections(detailPage.sections);
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// schema 加载失败时仍可使用
|
// schema 加载失败时仍可使用
|
||||||
});
|
});
|
||||||
}, [pluginId, entityName]);
|
}, [pluginId, entityName]);
|
||||||
|
|
||||||
const fetchData = useCallback(async (p = page) => {
|
const fetchData = useCallback(
|
||||||
if (!pluginId || !entityName) return;
|
async (p = page) => {
|
||||||
setLoading(true);
|
if (!pluginId || !entityName) return;
|
||||||
try {
|
setLoading(true);
|
||||||
const result = await listPluginData(pluginId, entityName, p);
|
try {
|
||||||
setRecords(result.data.map((r) => ({ ...r.data, _id: r.id, _version: r.version })));
|
const options: PluginDataListOptions = {};
|
||||||
setTotal(result.total);
|
// 自动添加 filterField 过滤(detail 页面内嵌 CRUD)
|
||||||
} catch {
|
const mergedFilters = { ...filters };
|
||||||
message.error('加载数据失败');
|
if (filterField && filterValue) {
|
||||||
}
|
mergedFilters[filterField] = filterValue;
|
||||||
setLoading(false);
|
}
|
||||||
}, [pluginId, entityName, page]);
|
if (Object.keys(mergedFilters).length > 0) {
|
||||||
|
options.filter = mergedFilters;
|
||||||
|
}
|
||||||
|
if (searchText) options.search = searchText;
|
||||||
|
if (sortBy) {
|
||||||
|
options.sort_by = sortBy;
|
||||||
|
options.sort_order = sortOrder;
|
||||||
|
}
|
||||||
|
const result = await listPluginData(pluginId, entityName, p, 20, options);
|
||||||
|
setRecords(
|
||||||
|
result.data.map((r) => ({ ...r.data, _id: r.id, _version: r.version })),
|
||||||
|
);
|
||||||
|
setTotal(result.total);
|
||||||
|
} catch {
|
||||||
|
message.error('加载数据失败');
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
[pluginId, entityName, page, filters, searchText, sortBy, sortOrder, filterField, filterValue],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [fetchData]);
|
}, [fetchData]);
|
||||||
|
|
||||||
|
// 筛选变化
|
||||||
|
const handleFilterChange = (fieldName: string, value: string | undefined) => {
|
||||||
|
const newFilters = { ...filters };
|
||||||
|
if (value) {
|
||||||
|
newFilters[fieldName] = value;
|
||||||
|
} else {
|
||||||
|
delete newFilters[fieldName];
|
||||||
|
}
|
||||||
|
setFilters(newFilters);
|
||||||
|
setPage(1);
|
||||||
|
// 直接触发重新查询
|
||||||
|
fetchData(1);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (values: Record<string, unknown>) => {
|
const handleSubmit = async (values: Record<string, unknown>) => {
|
||||||
if (!pluginId || !entityName) return;
|
if (!pluginId || !entityName) return;
|
||||||
// 去除内部字段
|
const { _id, _version, ...data } = values as Record<string, unknown> & {
|
||||||
const { _id, _version, ...data } = values as Record<string, unknown> & { _id?: string; _version?: number };
|
_id?: string;
|
||||||
|
_version?: number;
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (editRecord) {
|
if (editRecord) {
|
||||||
@@ -130,6 +256,7 @@ export default function PluginCRUDPage() {
|
|||||||
dataIndex: f.name,
|
dataIndex: f.name,
|
||||||
key: f.name,
|
key: f.name,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
|
sorter: f.sortable ? true : undefined,
|
||||||
render: (val: unknown) => {
|
render: (val: unknown) => {
|
||||||
if (typeof val === 'boolean') return val ? <Tag color="green">是</Tag> : <Tag>否</Tag>;
|
if (typeof val === 'boolean') return val ? <Tag color="green">是</Tag> : <Tag>否</Tag>;
|
||||||
return String(val ?? '-');
|
return String(val ?? '-');
|
||||||
@@ -138,15 +265,28 @@ export default function PluginCRUDPage() {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 150,
|
width: hasDetailPage ? 200 : 150,
|
||||||
render: (_: unknown, record: Record<string, unknown>) => (
|
render: (_: unknown, record: Record<string, unknown>) => (
|
||||||
<Space size="small">
|
<Space size="small">
|
||||||
|
{hasDetailPage && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setDetailRecord(record);
|
||||||
|
setDetailOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditRecord(record);
|
setEditRecord(record);
|
||||||
form.setFieldsValue(record);
|
form.setFieldsValue(record);
|
||||||
|
setFormValues(record);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -163,7 +303,7 @@ export default function PluginCRUDPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 动态生成表单字段
|
// 动态生成表单字段
|
||||||
const renderFormField = (field: FieldDef) => {
|
const renderFormField = (field: PluginFieldSchema) => {
|
||||||
const widget = field.ui_widget || field.field_type;
|
const widget = field.ui_widget || field.field_type;
|
||||||
switch (widget) {
|
switch (widget) {
|
||||||
case 'number':
|
case 'number':
|
||||||
@@ -186,71 +326,269 @@ export default function PluginCRUDPage() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
case 'textarea':
|
||||||
|
return <TextArea rows={3} />;
|
||||||
default:
|
default:
|
||||||
return <Input />;
|
return <Input />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// Timeline 视图渲染
|
||||||
<div style={{ padding: 24 }}>
|
const renderTimeline = () => {
|
||||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
const dateField = fields.find((f) => f.field_type === 'DateTime' || f.field_type === 'date');
|
||||||
<h2 style={{ margin: 0 }}>{displayName}</h2>
|
const titleField = fields.find((f) => f.searchable)?.name || fields[1]?.name;
|
||||||
<Space>
|
const contentField = fields.find((f) => f.ui_widget === 'textarea')?.name;
|
||||||
<Button
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
setEditRecord(null);
|
|
||||||
form.resetFields();
|
|
||||||
setModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
新增
|
|
||||||
</Button>
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => fetchData()}>
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table
|
return (
|
||||||
columns={columns}
|
<Timeline
|
||||||
dataSource={records}
|
items={records.map((record) => ({
|
||||||
rowKey="_id"
|
children: (
|
||||||
loading={loading}
|
<div>
|
||||||
pagination={{
|
{titleField && (
|
||||||
current: page,
|
<p>
|
||||||
total,
|
<strong>{String(record[titleField] ?? '-')}</strong>
|
||||||
pageSize: 20,
|
</p>
|
||||||
onChange: (p) => setPage(p),
|
)}
|
||||||
showTotal: (t) => `共 ${t} 条`,
|
{contentField && <p>{String(record[contentField] ?? '-')}</p>}
|
||||||
}}
|
{dateField && (
|
||||||
|
<p style={{ color: '#999', fontSize: 12 }}>
|
||||||
|
{String(record[dateField.name] ?? '-')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 详情 Drawer 渲染
|
||||||
|
const renderDetailDrawer = () => {
|
||||||
|
if (!detailRecord) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title={displayName + ' 详情'}
|
||||||
|
open={detailOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setDetailOpen(false);
|
||||||
|
setDetailRecord(null);
|
||||||
|
}}
|
||||||
|
width={640}
|
||||||
|
>
|
||||||
|
{detailSections.length > 0 ? (
|
||||||
|
detailSections.map((section, idx) => {
|
||||||
|
if (section.type === 'fields') {
|
||||||
|
return (
|
||||||
|
<div key={idx} style={{ marginBottom: 24 }}>
|
||||||
|
<h4>{section.label}</h4>
|
||||||
|
<Descriptions column={2} bordered size="small">
|
||||||
|
{section.fields.map((fieldName) => {
|
||||||
|
const fieldDef = fields.find((f) => f.name === fieldName);
|
||||||
|
const val = detailRecord[fieldName];
|
||||||
|
return (
|
||||||
|
<Descriptions.Item
|
||||||
|
key={fieldName}
|
||||||
|
label={fieldDef?.display_name || fieldName}
|
||||||
|
>
|
||||||
|
{typeof val === 'boolean' ? (
|
||||||
|
val ? (
|
||||||
|
<Tag color="green">是</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag>否</Tag>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
String(val ?? '-')
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Descriptions>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (section.type === 'crud') {
|
||||||
|
const secEntity = allEntities.find((e) => e.name === section.entity);
|
||||||
|
return (
|
||||||
|
<div key={idx} style={{ marginBottom: 24 }}>
|
||||||
|
<h4>{section.label}</h4>
|
||||||
|
{secEntity && (
|
||||||
|
<PluginCRUDPage
|
||||||
|
pluginIdOverride={pluginId}
|
||||||
|
entityOverride={section.entity}
|
||||||
|
filterField={section.filter_field}
|
||||||
|
filterValue={String(detailRecord._id ?? '')}
|
||||||
|
enableViews={section.enable_views}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
// 没有 sections 配置时,默认展示所有字段
|
||||||
|
<Descriptions column={2} bordered size="small">
|
||||||
|
{fields.map((field) => {
|
||||||
|
const val = detailRecord[field.name];
|
||||||
|
return (
|
||||||
|
<Descriptions.Item key={field.name} label={field.display_name || field.name}>
|
||||||
|
{typeof val === 'boolean' ? (
|
||||||
|
val ? (
|
||||||
|
<Tag color="green">是</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag>否</Tag>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
String(val ?? '-')
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Descriptions>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={compact ? { padding: 0 } : { padding: 24 }}>
|
||||||
|
{!compact && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 style={{ margin: 0 }}>{displayName}</h2>
|
||||||
|
<Space>
|
||||||
|
{enableViews.length > 1 && (
|
||||||
|
<Segmented
|
||||||
|
options={enableViews.map((v) => ({
|
||||||
|
label: v === 'table' ? '表格' : v === 'timeline' ? '时间线' : v,
|
||||||
|
value: v,
|
||||||
|
}))}
|
||||||
|
value={viewMode}
|
||||||
|
onChange={(val) => setViewMode(val as string)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setEditRecord(null);
|
||||||
|
form.resetFields();
|
||||||
|
setFormValues({});
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
新增
|
||||||
|
</Button>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => fetchData()}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 搜索和筛选栏 */}
|
||||||
|
{!compact && (
|
||||||
|
<Space style={{ marginBottom: 16 }} wrap>
|
||||||
|
{fields.some((f) => f.searchable) && (
|
||||||
|
<Search
|
||||||
|
placeholder="搜索..."
|
||||||
|
allowClear
|
||||||
|
style={{ width: 240 }}
|
||||||
|
onSearch={(value) => {
|
||||||
|
setSearchText(value);
|
||||||
|
setPage(1);
|
||||||
|
fetchData(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filterableFields.map((field) => (
|
||||||
|
<Select
|
||||||
|
key={field.name}
|
||||||
|
placeholder={field.display_name || field.name}
|
||||||
|
allowClear
|
||||||
|
style={{ width: 150 }}
|
||||||
|
options={field.options || []}
|
||||||
|
onChange={(value) => handleFilterChange(field.name, value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewMode === 'table' || enableViews.length <= 1 ? (
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={records}
|
||||||
|
rowKey="_id"
|
||||||
|
loading={loading}
|
||||||
|
size={compact ? 'small' : undefined}
|
||||||
|
pagination={
|
||||||
|
compact
|
||||||
|
? { pageSize: 5, showTotal: (t) => `共 ${t} 条` }
|
||||||
|
: {
|
||||||
|
current: page,
|
||||||
|
total,
|
||||||
|
pageSize: 20,
|
||||||
|
onChange: (p) => setPage(p),
|
||||||
|
showTotal: (t) => `共 ${t} 条`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : viewMode === 'timeline' ? (
|
||||||
|
renderTimeline()
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* 新增/编辑弹窗 */}
|
||||||
<Modal
|
<Modal
|
||||||
title={editRecord ? '编辑' : '新增'}
|
title={editRecord ? '编辑' : '新增'}
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
setEditRecord(null);
|
setEditRecord(null);
|
||||||
|
setFormValues({});
|
||||||
}}
|
}}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
<Form
|
||||||
{fields.map((field) => (
|
form={form}
|
||||||
<Form.Item
|
layout="vertical"
|
||||||
key={field.name}
|
onFinish={handleSubmit}
|
||||||
name={field.name}
|
onValuesChange={(_, allValues) => setFormValues(allValues)}
|
||||||
label={field.display_name || field.name}
|
>
|
||||||
rules={field.required ? [{ required: true, message: `请输入${field.display_name || field.name}` }] : []}
|
{fields.map((field) => {
|
||||||
valuePropName={field.field_type === 'boolean' ? 'checked' : 'value'}
|
// visible_when 条件显示
|
||||||
>
|
const visible = shouldShowField(formValues, field.visible_when);
|
||||||
{renderFormField(field)}
|
if (!visible) return null;
|
||||||
</Form.Item>
|
|
||||||
))}
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={field.name}
|
||||||
|
name={field.name}
|
||||||
|
label={field.display_name || field.name}
|
||||||
|
rules={
|
||||||
|
field.required
|
||||||
|
? [{ required: true, message: `请输入${field.display_name || field.name}` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
valuePropName={field.field_type === 'boolean' ? 'checked' : 'value'}
|
||||||
|
>
|
||||||
|
{renderFormField(field)}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 详情 Drawer */}
|
||||||
|
{renderDetailDrawer()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user