This commit is contained in:
梁朝伟 2023-03-07 19:24:29 +08:00
parent dfe01079ea
commit 0a5c5de102
11 changed files with 569 additions and 0 deletions

View File

@ -9,6 +9,12 @@
"lib/**/*"
],
"dependencies": {
"@ant-design/cssinjs": "^1.6.1",
"@ant-design/icons": "^5.0.1",
"@icon-park/react": "^1.4.2",
"antd": "^5.3.0",
"antd-mobile": "^5.28.1",
"antd-mobile-icons": "^0.3.0",
"debounce": "^1.2.1",
"format-message-parse": "^6.2.4",
"history": "^5.3.0",
@ -16,6 +22,7 @@
"oak-common-aspect": "file:../oak-common-aspect",
"oak-domain": "file:../oak-domain",
"oak-memory-tree-store": "file:../oak-memory-tree-store",
"react-scripts": "^5.0.1",
"rmc-pull-to-refresh": "^1.0.13",
"url": "^0.11.0",
"uuid": "^8.3.2"

View File

@ -0,0 +1,9 @@
{
"component": true,
"usingComponents": {
"l-dialog": "../../miniprogram_npm/lin-ui/dialog/index",
"l-button": "../../miniprogram_npm/lin-ui/button/index",
"popover": "../../miniprogram_npm/popover/popover",
"popover-item": "../../miniprogram_npm/popover/popover-item"
}
}

View File

@ -0,0 +1,24 @@
.panel-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 20rpx 10rpx;
}
.more {
white-space: nowrap;
font-size: 28rpx;
color: #000;
padding: 10rpx;
}
.btn-container {
display: flex;
flex-direction: row;
justify-content: flex-end;
flex-wrap: wrap;
width: 100%;
gap: 16rpx;
}

View File

@ -0,0 +1,27 @@
export default OakComponent({
properties: {
entity: String,
actions: {
type: Array,
value: [],
},
items: {
type: Array,
value: [],
},
mode: {
type: String,
value: 'cell',
},
column: {
type: Number,
value: 3,
},
},
data: {
},
lifetimes: {
},
methods: {
},
});

View File

@ -0,0 +1,31 @@
<block wx:if="{{ newItems && newItems.length > 0 }}">
<view class="panel-container">
<block wx:if="{{ moreItems && moreItems.length > 0 }}">
<view id='more' class="more" catchtap="handleMoreClick">
更多
</view>
<popover id='popover'>
<block wx:for="{{ moreItems }}" wx:key="index">
<popover-item hasline catchtap='handleClick' data-type="popover" data-item="{{ item }}">{{ item.text }}</popover-item>
</block>
</popover>
</block>
<view class="btn-container">
<block wx:for="{{ newItems }}" wx:key="index">
<l-button plain="{{true}}" shape="{{ item.buttonProps.shape || 'square' }}" catchtap="handleClick" data-item="{{ item }}">
{{item.text}}
</l-button>
</block>
</view>
</view>
<l-dialog id="my-action-btn-dialog" bind:linconfirm="linconfirm" bind:lincancel="lincancel" />
</block>

View File

@ -0,0 +1,16 @@
.panelContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 10px 5px;
}
.more {
white-space: nowrap;
font-size: 14px;
color: #000;
padding: 5px;
}

View File

@ -0,0 +1,253 @@
import React from 'react';
import {
Space,
Button,
Modal,
ButtonProps,
SpaceProps,
Dropdown,
Typography,
} from 'antd';
import { WebComponentProps } from '../../types/Page';
import { EntityDict } from 'oak-domain/lib/types/Entity';
import Style from './web.module.less';
const { confirm } = Modal;
type Item = {
icon?: string | React.ReactNode;
label?: string;
action?: string;
auth: boolean;
type?: 'a' | 'button';
index?: number;
alerted?: boolean;
alertTitle?: string;
alertContent?: string;
confirmText?: string;
cancelText?: string;
render?: React.ReactNode;
beforeAction?: (item: Item) => boolean | Promise<boolean>;
afterAction?: (item: Item) => void;
onClick?: (item: Item) => void | Promise<void>;
buttonProps?: Omit<ButtonProps, 'onClick'>;
filter?: () => boolean;
};
function ItemComponent(
props: Item & {
onClick: () => void | Promise<void>;
text: string;
}
) {
const { type, buttonProps, render, onClick, text } = props;
if (type === 'button') {
return (
<Button {...buttonProps} onClick={onClick}>
{text}
</Button>
);
}
if (render) {
return <div onClick={onClick}>{render}</div>;
}
return <a onClick={onClick}>{text}</a>;
}
export default function Render(
props: WebComponentProps<
EntityDict,
keyof EntityDict,
false,
{
entity: string;
actions: string[];
items: Item[];
spaceProps: SpaceProps;
mode: 'cell' | 'table-cell';
column: 3;
},
{
getActionName: (action?: string) => string;
}
>
) {
const { methods, data } = props;
const { t, getActionName } = methods;
const {
items,
oakLegalActions,
spaceProps,
entity,
mode = 'cell',
column,
} = data;
const getItems = () => {
const items2 = items
.filter((ele) => {
const { action, filter } = ele;
const authResult =
!action ||
(action &&
oakLegalActions?.includes(
action as EntityDict[keyof EntityDict]['Action']
));
const filterResult =
ele.hasOwnProperty('filter') && filter ? filter() : true;
return authResult && filterResult;
})
.map((ele, index: number) => {
const { label, action } = ele;
let text: string = '';
if (label) {
text = label;
} else {
text = getActionName(action);
}
let onClick = async () => {
if (ele.onClick) {
ele.onClick(ele);
return;
}
if (ele.beforeAction) {
const r = await ele.beforeAction(ele);
if (!r) {
return;
}
}
await methods.execute(
action as EntityDict[keyof EntityDict]['Action']
);
if (ele.afterAction) {
ele.afterAction(ele);
}
};
if (ele.alerted) {
onClick = async () => {
let content = '';
if (action) {
const text = getActionName(action);
content = `确认${text}该数据`;
}
confirm({
title: ele.alertTitle,
content: ele.alertContent || content,
okText: ele.confirmText || '确定',
cancelText: ele.cancelText || '取消',
onOk: async () => {
if (ele.onClick) {
ele.onClick(ele);
return;
}
if (ele.beforeAction) {
const r = await ele.beforeAction(ele);
if (!r) {
return;
}
}
await methods.execute(
ele.action as EntityDict[keyof EntityDict]['Action']
);
if (ele.afterAction) {
ele.afterAction(ele);
}
},
});
};
}
return Object.assign(ele, {
text: text,
onClick2: onClick,
});
});
let newItems = items2;
let moreItems: Item[] = [];
if (column && items2.length > column) {
newItems = [...items2].splice(0, column);
moreItems = [...items2].splice(column, items2.length);
}
return {
newItems,
moreItems,
};
};
const { newItems, moreItems } = getItems();
if (!newItems || newItems.length === 0) {
return null;
}
if (mode === 'table-cell') {
return (
<Space {...spaceProps}>
{newItems?.map((ele, index: number) => {
return (
<ItemComponent
{...ele}
onClick={ele.onClick2}
text={ele.text}
/>
);
})}
{moreItems && moreItems.length > 0 && (
<Dropdown
menu={{
items: moreItems.map((ele: any, index) => ({
label: ele.text as string,
key: index,
})),
onClick: (e: any) => {
const item = moreItems[e.key] as any;
item.onClick2();
},
}}
placement="top"
arrow
>
<a onClick={(e) => e.preventDefault()}></a>
</Dropdown>
)}
</Space>
);
}
return (
<div className={Style.panelContainer}>
{moreItems && moreItems.length > 0 && (
<Dropdown
menu={{
items: moreItems.map((ele: any, index) => ({
label: ele.text,
key: index,
})),
onClick: (e: any) => {
const item = moreItems[e.key] as any;
item.onClick2();
},
}}
arrow
>
<Typography className={Style.more}></Typography>
</Dropdown>
)}
<Space {...spaceProps}>
{newItems?.map((ele, index: number) => {
return (
<ItemComponent
type="button"
{...ele}
onClick={ele.onClick2}
text={ele.text}
/>
);
})}
</Space>
</div>
);
}

View File

@ -0,0 +1,151 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Table, Tag, TableProps } from 'antd';
import type { ColumnsType, ColumnType, ColumnGroupType } from 'antd/es/table';
import assert from 'assert';
import {useFeatures} from '../../platforms/web/features/index';
import { getAttributes } from '../../utils/usefulFn';
import { get } from 'oak-domain/lib/utils/lodash';
import dayjs from 'dayjs';
import { ActionBtnPanelProps } from '@oak-general-business/types/actionBtnPanel';
import ActionBtnPanel from '@oak-general-business/components/func/actionBtnPanel';
type SelfColumn = {
path?: string;
}
type Column = SelfColumn & ColumnType<any>;
type Props = {
entity: string;
data: any[];
columns: (Column | string)[];
actionBtnProps?: (row: any) => ActionBtnPanelProps;
tableProps?: TableProps<any>;
}
type RenderCellProps = {
content: any;
entity: string;
path: string;
attr: string;
attrType: string;
}
function decodeTitle(entity: string, attr: string) {
const { t } = useTranslation();
if (attr === ('$$createAt$$' || '$$updateAt$$')) {
return t(`common:${attr}`)
}
return t(`${entity}:attr.${attr}`)
}
// 解析路径, 获取属性类型、属性值、以及实体名称
function Fn(entity: string, path: string) {
let _entity = entity;
let attr: string;
assert(!path.includes('['), '数组索引不需要携带[],请使用arr.0.value')
const features = useFeatures();
const dataSchema = features.cache.getSchema();
if (!path.includes('.')) {
attr = path;
}
else {
const strs = path.split('.');
// 最后一个肯定是属性
attr = strs.pop()!;
// 倒数第二个可能是类名可能是索引
_entity = strs.pop()!;
// 判断是否是数组索引
if (!Number.isNaN(Number(_entity))) {
_entity = strs.pop()!.split('$')[0];
}
}
const attributes = getAttributes(dataSchema[_entity as keyof typeof dataSchema].attributes);
const attribute = attributes[attr];
return {
entity: _entity,
attr,
attribute,
}
}
function RenderCell(props: RenderCellProps) {
const { content, entity, path, attr, attrType } = props;
const value = get(content, path);
const { t } = useTranslation();
const feature = useFeatures();
const colorDict = feature.style.getColorDict();
if (!value) {
return (<div>--</div>);
}
// 属性类型是enum要使用标签
else if (attrType === 'enum') {
return (
<Tag color={colorDict[entity][attr][String(value)]} >
{t(`${entity}:v.${attr}.${value}`)}
</Tag>
)
}
else if (attrType === 'datetime') {
return <div>{dayjs(value).format('YYYY-MM-DD HH:mm')}</div>
}
return (
<div>{value}</div>
)
}
function List(props: Props) {
const { data, columns, entity, actionBtnProps, tableProps } = props;
const { t } = useTranslation();
const tableColumns: ColumnsType<any> = columns.map((ele) => {
let title: string = '';
let render: (value: any, row: any) => React.ReactNode = () => <></>;
let path: string | undefined;
if (typeof ele === 'string') {
path = ele;
}
else {
path = ele.path;
}
const { entity: useEntity, attr, attribute } = Fn(entity, path!) || {};
title = decodeTitle(useEntity, attr);
render = (value, row) => (
<RenderCell entity={entity} content={row} path={path!} attr={attr} attrType={attribute.type} />
);
const column = {
align: 'center',
title,
dataIndex: typeof ele === 'string' ? ele : ele.dataIndex,
render,
};
// 类型如果是枚举类型那么它的宽度一般不超过160
// if (attribute?.type === 'enum') {
// Object.assign(column, {width: 160})
// }
return Object.assign(column, typeof ele !== 'string' && ele);
}) as ColumnsType<any>;
if (tableColumns && tableColumns) {
tableColumns.unshift({title: '序号', width: 100, render(value, record, index) {
return index + 1;
},})
}
if (actionBtnProps) {
tableColumns.push({
fixed: 'right',
align: 'right',
title: '操作',
key: 'operation',
width: 100,
render: (value, row) => (
<ActionBtnPanel {...actionBtnProps(row)} />
)
})
}
return (
<Table dataSource={data} scroll={{ x: 2200 }} columns={tableColumns} ></Table>
);
}
export default List;

20
src/utils/usefulFn.ts Normal file
View File

@ -0,0 +1,20 @@
export function getAttributes(attributes: Record<string, any>) {
return Object.assign({}, attributes, {
id: {
type: 'char',
},
$$createAt$$: {
type: 'datetime',
},
$$updateAt$$: {
type: 'datetime',
},
$$deleteAt$$: {
type: 'datetime',
},
$$seq$$: {
type: 'datetime',
},
});
}

23
typings/polyfill.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
import {
MakeOakComponent,
} from '../src/types/Page';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { EntityDict, Aspect } from 'oak-domain/lib/types';
import { Feature } from '../src/types/Feature';
import { AsyncContext } from "oak-domain/lib/store/AsyncRowStore";
import { SyncContext } from "oak-domain/lib/store/SyncRowStore";
type ED = EntityDict & BaseEntityDict;
type Cxt = AsyncContext<ED>;
type FrontCxt = SyncContext<ED>;
type AD = Record<string, Aspect<ED, Cxt>>;
type FD = Record<string, Feature>
declare global {
const OakComponent: MakeOakComponent<
ED,
Cxt,
FrontCxt,
AD,
FD
>;
}
export {};

8
typings/react.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="react-scripts" />
declare module '*.module.less' {
const classes: {
readonly [key: string]: string;
};
export default classes;
}