feat: kanban view
This commit is contained in:
parent
eba121b998
commit
330d08ab35
@ -23,6 +23,7 @@
|
||||
"dependencies": {
|
||||
"@ant-design/pro-layout": "^5.0.12",
|
||||
"@formily/antd-components": "^1.3.6",
|
||||
"@lourenci/react-kanban": "^2.1.0",
|
||||
"@types/react-big-calendar": "^0.24.8",
|
||||
"@umijs/preset-react": "1.x",
|
||||
"@umijs/test": "^3.2.23",
|
||||
|
@ -8,14 +8,14 @@ import set from 'lodash/set';
|
||||
|
||||
export function Create(props) {
|
||||
const { size, onFinish, schema = {}, associatedKey, ...restProps } = props;
|
||||
const { title, viewName, transform } = schema;
|
||||
const { title, pageTitle, viewName, transform, componentProps = {} } = schema;
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size={size}
|
||||
onClick={() => {
|
||||
Drawer.open({
|
||||
title: title,
|
||||
title: pageTitle || title,
|
||||
content: ({resolve, closeWithConfirm}) => (
|
||||
<div>
|
||||
<View
|
||||
@ -53,6 +53,7 @@ export function Create(props) {
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
type={'primary'}
|
||||
{...componentProps}
|
||||
>{ title }</Button>
|
||||
</>
|
||||
)
|
||||
@ -157,9 +158,9 @@ export function Add(props) {
|
||||
|
||||
export function Destroy(props) {
|
||||
const { size, schema = {}, onFinish } = props;
|
||||
const { title } = schema;
|
||||
const { title, componentProps = {} } = schema;
|
||||
return (
|
||||
<Popconfirm title="确认删除吗?" onConfirm={async () => {
|
||||
<Popconfirm title="确认删除吗?" onConfirm={async (e) => {
|
||||
onFinish && await onFinish();
|
||||
}}>
|
||||
<Button
|
||||
@ -167,6 +168,7 @@ export function Destroy(props) {
|
||||
danger
|
||||
type={'ghost'}
|
||||
icon={<DeleteOutlined />}
|
||||
{...componentProps}
|
||||
>{ title }</Button>
|
||||
</Popconfirm>
|
||||
)
|
||||
|
@ -90,7 +90,7 @@ export function fields2properties(fields = [], options: any = {}) {
|
||||
const actions = createFormActions();
|
||||
|
||||
export function Form(props: any) {
|
||||
const { onValueChange, onReset, __parent, noRequest = false, onFinish, onDraft, resolve, data: record = {}, associatedKey, schema = {} } = props;
|
||||
const { initialValues = {}, onValueChange, onReset, __parent, noRequest = false, onFinish, onDraft, resolve, data: record = {}, associatedKey, schema = {} } = props;
|
||||
console.log({ noRequest, record, associatedKey, __parent });
|
||||
const { statusable, resourceName, rowKey = 'id', fields = [], appends = [], associationField = {} } = schema;
|
||||
|
||||
@ -116,6 +116,7 @@ export function Form(props: any) {
|
||||
layout={'vertical'}
|
||||
initialValues={{
|
||||
...data,
|
||||
...initialValues,
|
||||
associatedKey,
|
||||
resourceKey,
|
||||
}}
|
||||
|
@ -0,0 +1,301 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRequest, useHistory } from 'umi';
|
||||
import api from '@/api-client';
|
||||
import { Descriptions, Popconfirm, Card, Button } from 'antd';
|
||||
import Board, { moveCard } from "@lourenci/react-kanban";
|
||||
import "@lourenci/react-kanban/dist/styles.css";
|
||||
import { PlusOutlined, CloseOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import Drawer from '@/components/pages/AdminLoader/Drawer';
|
||||
import { Actions, Create, Destroy } from '../../Actions';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import Field from '@/components/views/Field';
|
||||
import { Details, DetailsPage } from '../Table';
|
||||
import merge from 'lodash/merge';
|
||||
import { Spin } from '@nocobase/client';
|
||||
import './style.less';
|
||||
|
||||
export function Kanban(props: any) {
|
||||
const { defaultFilter, associatedKey, schema = {} } = props;
|
||||
const {
|
||||
filter: schemaFilter,
|
||||
sort,
|
||||
appends,
|
||||
groupField = {},
|
||||
resourceName,
|
||||
createViewName = 'form',
|
||||
disableCardDrag = false,
|
||||
// allowAddCard = false,
|
||||
// allowRemoveCard = false,
|
||||
labelField,
|
||||
actions = [],
|
||||
fields = [],
|
||||
detailsOpenMode,
|
||||
details = [],
|
||||
rowKey = 'id',
|
||||
} = schema;
|
||||
const { dataSource = [] } = groupField;
|
||||
console.log({groupField, dataSource})
|
||||
|
||||
const paginated = false;
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const { data, loading, mutate, refresh, run, params } = useRequest((params = {}, ...args) => {
|
||||
const { current, pageSize, sorter, filter, ...restParams } = params;
|
||||
console.log('paramsparamsparamsparamsparams', params, args);
|
||||
return api.resource(resourceName).list({
|
||||
associatedKey,
|
||||
page: paginated ? current : 1,
|
||||
perPage: paginated ? pageSize : -1,
|
||||
sorter,
|
||||
sort: [`${groupField.name}_sort`],
|
||||
'fields[appends]': appends,
|
||||
// filter,
|
||||
// ...actionDefaultParams,
|
||||
filter: {
|
||||
and: [
|
||||
defaultFilter,
|
||||
schemaFilter,
|
||||
filter,
|
||||
// __parent ? {
|
||||
// collection_name: __parent,
|
||||
// } : null,
|
||||
].filter(obj => obj && Object.keys(obj).length)
|
||||
}
|
||||
// ...args2,
|
||||
})
|
||||
.then(({data = [], meta = {}}) => {
|
||||
return {
|
||||
data: {
|
||||
list: data,
|
||||
total: meta.count||data.length,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, {
|
||||
paginated,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Spin/>;
|
||||
}
|
||||
|
||||
const columns = dataSource.map(group => {
|
||||
return {
|
||||
id: group.value,
|
||||
title: group.label,
|
||||
cards: filter(data?.list, (item) => {
|
||||
return get(item, groupField.name) === group.value;
|
||||
}),
|
||||
};
|
||||
});
|
||||
console.log({columns});
|
||||
|
||||
const bodyStyle: any = {};
|
||||
|
||||
if (fields.length === 0) {
|
||||
bodyStyle.padding = 0;
|
||||
}
|
||||
|
||||
const createAction = actions.find(action => {
|
||||
return action.type === 'create';
|
||||
});
|
||||
|
||||
let allowAddCard = !!props.allowAddCard;
|
||||
|
||||
if (createAction) {
|
||||
allowAddCard = true;
|
||||
}
|
||||
|
||||
let allowRemoveCard = !!props.allowRemoveCard;
|
||||
const destroyAction = actions.find(action => {
|
||||
return action.type === 'destroy';
|
||||
});
|
||||
if (destroyAction) {
|
||||
allowRemoveCard = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'noco-kanban-view'}>
|
||||
<Actions
|
||||
associatedKey={associatedKey}
|
||||
style={{ marginBottom: 14 }}
|
||||
actions={actions.filter(action => !['create', 'destroy'].includes(action.type))}
|
||||
onTrigger={{
|
||||
async filter(values) {
|
||||
const items = values.filter.and || values.filter.or;
|
||||
// @ts-ignore
|
||||
run({...params[0], filter: values.filter});
|
||||
// refresh();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Board
|
||||
allowRemoveLane
|
||||
allowRenameColumn
|
||||
allowRemoveCard={allowRemoveCard}
|
||||
disableColumnDrag
|
||||
disableCardDrag={disableCardDrag}
|
||||
onLaneRemove={console.log}
|
||||
renderCard={(data, {dragging, removeCard}) => {
|
||||
const openDetails = (e) => {
|
||||
if (!detailsOpenMode || !details.length) {
|
||||
return;
|
||||
}
|
||||
if (detailsOpenMode === 'window') {
|
||||
const paths = history.location.pathname.split('/');
|
||||
history.push(`/admin/${paths[2]}/${data[rowKey]}/0`);
|
||||
} else {
|
||||
Drawer.open({
|
||||
headerStyle: details.length > 1 ? {
|
||||
paddingBottom: 0,
|
||||
borderBottom: 0,
|
||||
// paddingTop: 16,
|
||||
// marginBottom: -4,
|
||||
} : {},
|
||||
title: details.length > 1 ? undefined : data[labelField],
|
||||
content: ({resolve, closeWithConfirm}) => (
|
||||
<div>
|
||||
<Details
|
||||
associatedKey={associatedKey}
|
||||
resourceName={resourceName}
|
||||
onFinish={async () => {
|
||||
await refresh();
|
||||
resolve();
|
||||
}}
|
||||
onValueChange={() => {
|
||||
closeWithConfirm && closeWithConfirm(true);
|
||||
}}
|
||||
onDraft={async () => {
|
||||
await refresh();
|
||||
resolve();
|
||||
}}
|
||||
onReset={resolve}
|
||||
onDataChange={async () => {
|
||||
await refresh();
|
||||
}}
|
||||
data={data}
|
||||
resolve={resolve}
|
||||
items={details}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
hoverable
|
||||
size={'small'}
|
||||
style={{background: '#fff', minWidth: 250, maxWidth: 250, marginBottom: 12}}
|
||||
bordered={false}
|
||||
title={(
|
||||
<div onClick={openDetails}>
|
||||
{data[labelField]}
|
||||
</div>
|
||||
)}
|
||||
bodyStyle={bodyStyle}
|
||||
extra={
|
||||
allowRemoveCard && (
|
||||
<Destroy
|
||||
schema={merge(destroyAction, {
|
||||
title: null,
|
||||
componentProps: {
|
||||
danger: false,
|
||||
type: 'text',
|
||||
size: 'small',
|
||||
},
|
||||
})}
|
||||
onFinish={async () => {
|
||||
await api.resource(resourceName).destroy({
|
||||
associatedKey,
|
||||
filter: {
|
||||
[`${rowKey}.in`]: [data[rowKey]],
|
||||
},
|
||||
});
|
||||
await refresh();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div onClick={openDetails}>
|
||||
{fields.length > 0 && (
|
||||
<Descriptions column={1} size={'small'} layout={'vertical'}>
|
||||
{fields.map((field: any) => {
|
||||
return (
|
||||
<Descriptions.Item label={field.title||field.name}>
|
||||
<Field data={data} viewType={'descriptions'} schema={field} value={get(data, field.name)}/>
|
||||
</Descriptions.Item>
|
||||
)
|
||||
})}
|
||||
</Descriptions>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}}
|
||||
onCardDragEnd={async ({ columns }, card, source, destination) => {
|
||||
await api.resource(resourceName).update({
|
||||
associatedKey,
|
||||
resourceKey: card[rowKey],
|
||||
values: {
|
||||
[groupField.name]: destination.toColumnId,
|
||||
},
|
||||
});
|
||||
const destColumn = columns.find( column => column.id === destination.toColumnId);
|
||||
const targetIndex = get(destColumn, ['cards', destination.toPosition+1, rowKey]);
|
||||
console.log({targetIndex, card, destination})
|
||||
await api.resource(resourceName).sort({
|
||||
associatedKey,
|
||||
resourceKey: card[rowKey],
|
||||
values: {
|
||||
field: `${groupField.name}_sort`,
|
||||
sticky: destination.toPosition === 0,
|
||||
target: {
|
||||
[rowKey]: targetIndex,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
initialBoard={{columns}}
|
||||
// allowAddCard={{ on: "bottom" }}
|
||||
allowAddCard={false}
|
||||
// onNewCardConfirm={draftCard => ({
|
||||
// id: new Date().getTime(),
|
||||
// ...draftCard
|
||||
// })}
|
||||
renderColumnHeader={({ title, id }, { addCard }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="react-kanban-column-header"><span>{ title }</span></div>
|
||||
{allowAddCard && <Create
|
||||
onFinish={(values: any) => {
|
||||
refresh();
|
||||
}}
|
||||
initialValues={{
|
||||
[groupField.name]: id,
|
||||
}}
|
||||
schema={merge(createAction, {
|
||||
title: null,
|
||||
pageTitle: '新增',
|
||||
componentProps: {
|
||||
type: 'text',
|
||||
block: true,
|
||||
className: 'noco-card-adder-button',
|
||||
style: {
|
||||
marginBottom: 10,
|
||||
border: 0,
|
||||
background: '#fff',
|
||||
},
|
||||
},
|
||||
})}/>}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
onCardNew={console.log}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
.react-kanban-board {
|
||||
padding: 0;
|
||||
margin: -24px -36px;
|
||||
background-color: #eff2f5;
|
||||
.react-kanban-column {
|
||||
background-color: #eff2f5;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-column-header {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.noco-card-adder-button:hover {
|
||||
background: rgba(255, 255, 255, .4) !important;
|
||||
}
|
||||
|
||||
.noco-kanban-view {
|
||||
.action-buttons {
|
||||
margin: -24px -24px 0 !important;
|
||||
background-color: #eff2f5;
|
||||
padding-bottom: 40px;
|
||||
.ant-btn {
|
||||
border: 0;
|
||||
}
|
||||
.filter-action-button {
|
||||
margin-left: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
.action-buttons + .react-kanban-board {
|
||||
margin-top: 0;
|
||||
}
|
||||
.ant-descriptions-item-label {
|
||||
width: auto;
|
||||
}
|
||||
.ant-descriptions-item {
|
||||
// padding-bottom: 4px;
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ import pathToRegexp from 'path-to-regexp'
|
||||
export const icon = <LoadingOutlined style={{ fontSize: 36 }} spin />;
|
||||
|
||||
export function Details(props) {
|
||||
const { __parent, associatedKey, resourceName, onFinish, onReset, onDataChange, data, items = [], resolve, onValueChange } = props;
|
||||
const { __parent, associatedKey, resourceName, onFinish, onDraft, onReset, onDataChange, data, items = [], resolve, onValueChange } = props;
|
||||
if (!items || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@ -48,6 +48,7 @@ export function Details(props) {
|
||||
}
|
||||
return (
|
||||
<View
|
||||
onDraft={onDraft}
|
||||
onValueChange={onValueChange}
|
||||
__parent={__parent}
|
||||
associatedKey={associatedKey} onReset={onReset} onFinish={onFinish} onDataChange={onDataChange} data={data} viewName={viewName}/>
|
||||
|
@ -21,6 +21,7 @@ import { FilterForm } from './FilterForm';
|
||||
import { SubTable } from './SubTable';
|
||||
import { Wysiwyg } from './Wysiwyg';
|
||||
import { Calendar } from './Calendar';
|
||||
import { Kanban } from './Kanban';
|
||||
|
||||
const VIEWS = new Map();
|
||||
|
||||
@ -66,5 +67,6 @@ registerView('descriptions', Descriptions);
|
||||
registerView('association', Association);
|
||||
registerView('wysiwyg', Wysiwyg);
|
||||
registerView('calendar', Calendar);
|
||||
registerView('kanban', Kanban);
|
||||
|
||||
export default View;
|
||||
|
@ -374,6 +374,16 @@ export const getInfo = async (ctx: actions.Context, next) => {
|
||||
data.sort = [];
|
||||
}
|
||||
|
||||
if (data.type === 'kanban' && data.groupField) {
|
||||
const groupField = await Field.findOne({
|
||||
where: {
|
||||
name: data.groupField,
|
||||
collection_name: data.collection_name,
|
||||
}
|
||||
});
|
||||
data.groupField = groupField;
|
||||
}
|
||||
|
||||
ctx.body = data;
|
||||
await next();
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ export default {
|
||||
name: 'type',
|
||||
title: '操作类型',
|
||||
dataSource: [
|
||||
{ label: '过滤', value: 'filter' },
|
||||
{ label: '筛选', value: 'filter' },
|
||||
{ label: '新增', value: 'create' },
|
||||
{ label: '编辑', value: 'update' },
|
||||
{ label: '删除', value: 'destroy' },
|
||||
|
@ -86,10 +86,12 @@ export default async function (options = {}) {
|
||||
const createDetailsViews = async (model, options) => {
|
||||
const data = model.get();
|
||||
const View = database.getModel('views_v2');
|
||||
const tableDetials = _.get(data, 'x-table-props.details') || [];
|
||||
if (tableDetials.length) {
|
||||
const types = ['table', 'calendar', 'kanban'];
|
||||
for (const type of types) {
|
||||
const items = _.get(data, `x-${type}-props.details`) || [];
|
||||
if (items.length) {
|
||||
const details = [];
|
||||
for (const item of tableDetials) {
|
||||
for (const item of items) {
|
||||
if (item.view) {
|
||||
if (!item.view.id) {
|
||||
const view = await View.create(item.view);
|
||||
@ -97,9 +99,11 @@ export default async function (options = {}) {
|
||||
item.view.id = view.id;
|
||||
} else {
|
||||
const view = await View.findByPk(item.view.id);
|
||||
if (view) {
|
||||
await view.update(item.view);
|
||||
await view.updateAssociations(item.view);
|
||||
}
|
||||
}
|
||||
const view = await View.findOne(View.parseApiJson({
|
||||
filter: {
|
||||
id: item.view.id,
|
||||
@ -115,44 +119,51 @@ export default async function (options = {}) {
|
||||
}
|
||||
details.push(item);
|
||||
}
|
||||
model.set('options.x-table-props.details', details);
|
||||
model.set(`options.x-${type}-props.details`, details);
|
||||
}
|
||||
const calendarDetials = _.get(data, 'x-calendar-props.details') || [];
|
||||
if (calendarDetials.length) {
|
||||
const details = [];
|
||||
for (const item of calendarDetials) {
|
||||
if (item.view) {
|
||||
if (!item.view.id) {
|
||||
const view = await View.create(item.view);
|
||||
await view.updateAssociations(item.view);
|
||||
item.view.id = view.id;
|
||||
} else {
|
||||
const view = await View.findByPk(item.view.id);
|
||||
await view.update(item.view);
|
||||
await view.updateAssociations(item.view);
|
||||
}
|
||||
const view = await View.findOne(View.parseApiJson({
|
||||
filter: {
|
||||
id: item.view.id,
|
||||
},
|
||||
fields: {
|
||||
appends: ['collection', 'targetField', 'targetView'],
|
||||
},
|
||||
}));
|
||||
if (view) {
|
||||
console.log({view});
|
||||
item.view = view.toJSON();
|
||||
}
|
||||
}
|
||||
details.push(item);
|
||||
}
|
||||
model.set('options.x-calendar-props.details', details);
|
||||
}
|
||||
};
|
||||
|
||||
database.getModel('views_v2').addHook('beforeCreate', createDetailsViews);
|
||||
database.getModel('views_v2').addHook('beforeUpdate', createDetailsViews);
|
||||
|
||||
database.getModel('views_v2').addHook('beforeSave', async (model, options) => {
|
||||
const data = model.get();
|
||||
if (data.type !== 'kanban') {
|
||||
return;
|
||||
}
|
||||
let groupField = _.get(data, `x-kanban-props.groupField`);
|
||||
if (!groupField) {
|
||||
return;
|
||||
}
|
||||
if (typeof groupField === 'object' && groupField.name) {
|
||||
groupField = groupField.name;
|
||||
}
|
||||
const Field = database.getModel('fields');
|
||||
let field = await Field.findOne({
|
||||
where: {
|
||||
name: `${groupField}_sort`,
|
||||
collection_name: data.collection_name,
|
||||
},
|
||||
});
|
||||
if (field) {
|
||||
return;
|
||||
}
|
||||
await Field.create({
|
||||
interface: 'sort',
|
||||
type: 'sort',
|
||||
name: `${groupField}_sort`,
|
||||
// TODO: 不支持相关数据
|
||||
collection_name: data.collection_name,
|
||||
scope: [groupField],
|
||||
title: '看板分组排序',
|
||||
developerMode: true,
|
||||
component: {
|
||||
type: 'sort',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
database.getModel('menus').addHook('beforeSave', async (model, options) => {
|
||||
const { transaction } = options;
|
||||
// console.log('beforeSave', model.get('views'));
|
||||
|
@ -304,6 +304,15 @@ export const calendar = {
|
||||
},
|
||||
},
|
||||
info3: {
|
||||
interface: 'description',
|
||||
type: 'virtual',
|
||||
title: '操作按钮配置',
|
||||
component: {
|
||||
type: 'description',
|
||||
},
|
||||
},
|
||||
actions,
|
||||
info4: {
|
||||
interface: 'description',
|
||||
type: 'virtual',
|
||||
title: '单条数据页面配置',
|
||||
@ -319,6 +328,95 @@ export const calendar = {
|
||||
},
|
||||
};
|
||||
|
||||
export const kanban = {
|
||||
title: '看板',
|
||||
options: {
|
||||
},
|
||||
properties: {
|
||||
info1: {
|
||||
interface: 'description',
|
||||
type: 'virtual',
|
||||
title: '看板配置',
|
||||
component: {
|
||||
type: 'description',
|
||||
},
|
||||
},
|
||||
labelField: {
|
||||
interface: 'select',
|
||||
type: 'virtual',
|
||||
title: '作为单条数据标题的字段',
|
||||
name: 'labelField',
|
||||
required: true,
|
||||
component: {
|
||||
type: 'remoteSelect',
|
||||
resourceName: 'collections.fields',
|
||||
labelField: 'title',
|
||||
valueField: 'name',
|
||||
filter: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
groupField: {
|
||||
interface: 'select',
|
||||
type: 'virtual',
|
||||
title: '看板分组字段',
|
||||
name: 'groupField',
|
||||
required: true,
|
||||
component: {
|
||||
type: 'remoteSelect',
|
||||
resourceName: 'collections.fields',
|
||||
labelField: 'title',
|
||||
valueField: 'name',
|
||||
filter: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
...fields,
|
||||
title: '显示在看板里的字段'
|
||||
},
|
||||
info2: {
|
||||
interface: 'description',
|
||||
type: 'virtual',
|
||||
title: '数据配置',
|
||||
component: {
|
||||
type: 'description',
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
interface: 'json',
|
||||
type: 'virtual',
|
||||
title: '只显示符合以下条件的数据',
|
||||
mode: 'replace',
|
||||
defaultValue: {},
|
||||
component: {
|
||||
type: 'filter',
|
||||
},
|
||||
},
|
||||
info3: {
|
||||
interface: 'description',
|
||||
type: 'virtual',
|
||||
title: '操作按钮配置',
|
||||
component: {
|
||||
type: 'description',
|
||||
},
|
||||
},
|
||||
actions,
|
||||
info4: {
|
||||
interface: 'description',
|
||||
type: 'virtual',
|
||||
title: '单条数据页面配置',
|
||||
component: {
|
||||
type: 'description',
|
||||
},
|
||||
},
|
||||
detailsOpenMode,
|
||||
details,
|
||||
},
|
||||
};
|
||||
|
||||
export const wysiwyg = {
|
||||
title: '富文本',
|
||||
options: {
|
||||
|
Loading…
Reference in New Issue
Block a user