refactor: excel 部分重构,优化弹窗体验 (#598)

Reviewed-on: daoyoucloud/tachycode#598
Co-authored-by: bai.jingfeng <bai.jingfeng@foxmail.com>
Co-committed-by: bai.jingfeng <bai.jingfeng@foxmail.com>
This commit is contained in:
bai.jingfeng 2024-04-15 05:55:39 +08:00 committed by sealday
parent bcec8a2414
commit a87d95210c
32 changed files with 904 additions and 865 deletions

View File

@ -1,53 +0,0 @@
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import Spreadsheet from './x-sheet';
import { css } from '@nocobase/client';
import { demoData } from './x-sheet/demo';
export type SheetRef = {
getData: () => any;
};
export type SheetProps = {
data?: any;
style?: React.CSSProperties;
};
const Sheet = forwardRef<SheetRef, SheetProps>(({ data }, ref) => {
const workbookRef = useRef<Spreadsheet>(null);
const containerRef = useRef(null);
useImperativeHandle(ref, () => ({
getData,
}));
const getData = () => {
if (!workbookRef.current) {
throw new Error('Workbook is not initialized');
}
return workbookRef.current.getData();
};
useEffect(() => {
const workbook = new Spreadsheet('#sheet', {
view: {
height: () => containerRef.current.offsetHeight,
width: () => containerRef.current.offsetWidth,
},
}).loadData(demoData);
workbookRef.current = workbook;
}, [data]);
return (
<div
ref={containerRef}
id="sheet"
className={css`
height: 800px;
`}
/>
);
});
Sheet.displayName = 'Sheet';
export default Sheet;

View File

@ -0,0 +1,40 @@
import { connect, mapProps } from '@nocobase/schema';
import { Button } from 'antd';
import React, { useRef, useState } from 'react';
import ModalFullScreen from '../modal-full-screen/ModalFullScreen';
import Sheet, { SheetRef } from './Sheet';
const ExcelFileComponent = (props) => {
const { title, disabled, value, onChange } = props;
const ref = useRef<SheetRef>();
const [isOpenModal, setIsOpenModal] = useState(false);
const showModal = () => {
setIsOpenModal(true);
};
const hideModal = () => {
setIsOpenModal(false);
};
const handleOk = () => {
const data = ref.current.getData();
onChange(data);
setIsOpenModal(false);
};
return (
<>
<Button type="primary" onClick={showModal}>
{disabled ? '查看' : '编辑'}
</Button>
<ModalFullScreen title={title ?? 'Excel'} open={isOpenModal} onOk={handleOk} onCancel={hideModal}>
<Sheet ref={ref} data={value || []} />
</ModalFullScreen>
</>
);
};
export const ExcelFile = connect(
ExcelFileComponent,
mapProps((props) => ({
...props,
})),
);

View File

@ -0,0 +1,63 @@
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import Spreadsheet from '../x-sheet';
import { ClassNamesArg, css, cx } from '@nocobase/client';
export type SheetRef = {
getData: () => any;
};
export type SheetProps = {
data?: any;
className?: ClassNamesArg;
};
const ExcelSheet = forwardRef<SheetRef, SheetProps>(({ data, className }, ref) => {
const workbookRef = useRef<Spreadsheet>(null);
const containerRef = useRef(null);
const getData = () => {
if (!workbookRef.current) {
throw new Error('Workbook is not initialized');
}
return workbookRef.current.getData();
};
useImperativeHandle(ref, () => ({
getData,
}));
useEffect(() => {
if (containerRef.current) {
const workbook = new Spreadsheet(containerRef.current, {
view: {
height: () => containerRef.current?.offsetHeight,
width: () => containerRef.current?.offsetWidth,
},
});
workbook.loadData(data);
workbookRef.current = workbook;
return () => {
workbook.dispose();
};
}
}, [data]);
return (
<div
ref={containerRef}
id="sheet988"
className={cx(
css`
width: 100%;
height: 100%;
`,
className,
)}
/>
);
});
ExcelSheet.displayName = 'ExcelSheet';
export default ExcelSheet;

View File

@ -1,5 +1,5 @@
export * from './PDFViewer';
export * from './Sheet';
export * from './excel-table/Sheet';
export * from './SignatureInput';
export * from './SignaturePad';
export * from './custom-components/CustomAssociatedField';
@ -11,3 +11,4 @@ export * from './fields/Expression';
export * from './system/MobileLink';
export * from './system/Notifications';
export * from './system/OnlineUserProvider';
export * from './excel-table/ExcelFile';

View File

@ -0,0 +1,46 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { cx, css, ClassNamesArg } from '@nocobase/client';
import { ModalHeader } from './ModalFullScreenChild';
interface ModalFullScreenProps {
className?: ClassNamesArg;
title?: string;
open?: boolean;
destroyOnClose?: boolean;
children?: any;
onOk?: () => void;
onCancel?: () => void;
}
const ModalFullScreen = (props: ModalFullScreenProps) => {
const { className, title, open, children, onOk, onCancel } = props;
if (!open) {
return null;
}
return ReactDOM.createPortal(
<div
className={cx(
css`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: ${open ? 'flex' : 'none'};
flex-direction: column;
`,
className,
)}
>
<ModalHeader title={title} onOk={onOk} onCancel={onCancel} />
{children}
</div>,
document.body,
);
};
ModalFullScreen.displayName = 'ModalFullScreen';
export default ModalFullScreen;

View File

@ -0,0 +1,51 @@
import React from 'react';
import { css } from '@nocobase/client';
import { Button } from 'antd';
export const ModalHeader = (props) => {
const { title, onOk, onCancel } = props;
return (
<div
className={css`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 50px;
height: 50px;
background-color: #e9e9e9;
`}
>
<div
className={css`
font-size: 16px;
font-weight: bold;
color: #333333;
`}
>
{title}
</div>
<div
className={css`
display: flex;
flex-direction: row;
justify-content: space-around;
`}
>
<Button
className={css`
display: flex;
flex-direction: row;
justify-content: space-around;
margin-right: 20px;
`}
type="primary"
onClick={onOk}
>
</Button>
<Button onClick={onCancel}></Button>
</div>
</div>
);
};

View File

@ -1,11 +0,0 @@
/* eslint no-bitwise: "off" */
/*
v: int value
digit: bit len of v
flag: true or false
*/
const bitmap = (v, digit, flag) => {
const b = 1 << digit;
return flag ? (v | b) : (v ^ b);
};
export default bitmap;

View File

@ -0,0 +1,12 @@
/**
*
* @param v int value
* @param digit bit len of v
* @param flag true or false
* @returns
*/
const bitmap = (v: number, digit: number, flag: boolean) => {
const b = 1 << digit;
return flag ? v | b : v ^ b;
};
export default bitmap;

View File

@ -1,39 +0,0 @@
// src: include chars: [0-9], +, -, *, /
// // 9+(3-1)*3+10/2 => 9 3 1-3*+ 10 2/+
const infix2suffix = (src) => {
const operatorStack = [];
const stack = [];
for (let i = 0; i < src.length; i += 1) {
const c = src.charAt(i);
if (c !== ' ') {
if (c >= '0' && c <= '9') {
stack.push(c);
} else if (c === ')') {
let c1 = operatorStack.pop();
while (c1 !== '(') {
stack.push(c1);
c1 = operatorStack.pop();
}
} else {
// priority: */ > +-
if (operatorStack.length > 0 && (c === '+' || c === '-')) {
const last = operatorStack[operatorStack.length - 1];
if (last === '*' || last === '/') {
while (operatorStack.length > 0) {
stack.push(operatorStack.pop());
}
}
}
operatorStack.push(c);
}
}
}
while (operatorStack.length > 0) {
stack.push(operatorStack.pop());
}
return stack;
};
export default {
infix2suffix,
};

View File

@ -1,145 +1,37 @@
/* global window */
function dpr() {
return window.devicePixelRatio || 1;
import { DrawBox } from './DrawBox';
import { npxLine } from './utils';
import { drawFontLine } from './utils';
import { npx } from './utils';
import { thinLineWidth } from './utils';
import { dpr } from './utils';
interface Attr {
align?: 'left' | 'center' | 'right';
valign?: 'top' | 'middle' | 'bottom';
color?: string;
strike?: boolean;
font?: {
name: string;
size: number;
bold: boolean;
italic: boolean;
};
underline?: unknown;
}
function thinLineWidth() {
return dpr() - 0.5;
}
function npx(px) {
return parseInt(px * dpr(), 10);
}
function npxLine(px) {
const n = npx(px);
return n > 0 ? n - 0.5 : 0.5;
}
class DrawBox {
constructor(x, y, w, h, padding = 0) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.padding = padding;
this.bgcolor = '#ffffff';
// border: [width, style, color]
this.borderTop = null;
this.borderRight = null;
this.borderBottom = null;
this.borderLeft = null;
}
setBorders({
top, bottom, left, right,
}) {
if (top) this.borderTop = top;
if (right) this.borderRight = right;
if (bottom) this.borderBottom = bottom;
if (left) this.borderLeft = left;
}
innerWidth() {
return this.width - (this.padding * 2) - 2;
}
innerHeight() {
return this.height - (this.padding * 2) - 2;
}
textx(align) {
const { width, padding } = this;
let { x } = this;
if (align === 'left') {
x += padding;
} else if (align === 'center') {
x += width / 2;
} else if (align === 'right') {
x += width - padding;
}
return x;
}
texty(align, h) {
const { height, padding } = this;
let { y } = this;
if (align === 'top') {
y += padding;
} else if (align === 'middle') {
y += height / 2 - h / 2;
} else if (align === 'bottom') {
y += height - padding - h;
}
return y;
}
topxys() {
const { x, y, width } = this;
return [[x, y], [x + width, y]];
}
rightxys() {
const {
x, y, width, height,
} = this;
return [[x + width, y], [x + width, y + height]];
}
bottomxys() {
const {
x, y, width, height,
} = this;
return [[x, y + height], [x + width, y + height]];
}
leftxys() {
const {
x, y, height,
} = this;
return [[x, y], [x, y + height]];
}
}
function drawFontLine(type, tx, ty, align, valign, blheight, blwidth) {
const floffset = { x: 0, y: 0 };
if (type === 'underline') {
if (valign === 'bottom') {
floffset.y = 0;
} else if (valign === 'top') {
floffset.y = -(blheight + 2);
} else {
floffset.y = -blheight / 2;
}
} else if (type === 'strike') {
if (valign === 'bottom') {
floffset.y = blheight / 2;
} else if (valign === 'top') {
floffset.y = -((blheight / 2) + 2);
}
}
if (align === 'center') {
floffset.x = blwidth / 2;
} else if (align === 'right') {
floffset.x = blwidth;
}
this.line(
[tx - floffset.x, ty - floffset.y],
[tx - floffset.x + blwidth, ty - floffset.y],
);
}
class Draw {
constructor(el, width, height) {
this.el = el;
export class Draw {
private ctx: CanvasRenderingContext2D;
constructor(
private el: HTMLCanvasElement,
width: number,
height: number,
) {
this.ctx = el.getContext('2d');
this.resize(width, height);
this.ctx.scale(dpr(), dpr());
}
resize(width, height) {
// console.log('dpr:', dpr);
resize(width: number, height: number) {
this.el.style.width = `${width}px`;
this.el.style.height = `${height}px`;
this.el.width = npx(width);
@ -152,7 +44,7 @@ class Draw {
return this;
}
attr(options) {
attr(options: unknown) {
Object.assign(this.ctx, options);
return this;
}
@ -173,27 +65,27 @@ class Draw {
return this;
}
translate(x, y) {
translate(x: number, y: number) {
this.ctx.translate(npx(x), npx(y));
return this;
}
scale(x, y) {
scale(x: number, y: number) {
this.ctx.scale(x, y);
return this;
}
clearRect(x, y, w, h) {
clearRect(x: number, y: number, w: number, h: number) {
this.ctx.clearRect(x, y, w, h);
return this;
}
fillRect(x, y, w, h) {
fillRect(x: number, y: number, w: number, h: number) {
this.ctx.fillRect(npx(x) - 0.5, npx(y) - 0.5, npx(w), npx(h));
return this;
}
fillText(text, x, y) {
fillText(text: string, x: number, y: number) {
this.ctx.fillText(text, npx(x), npx(y));
return this;
}
@ -215,11 +107,9 @@ class Draw {
}
textWrap: text wrapping
*/
text(mtxt, box, attr = {}, textWrap = true) {
text(mtxt: string, box: DrawBox, attr: Attr = {}, textWrap = true) {
const { ctx } = this;
const {
align, valign, font, color, strike, underline,
} = attr;
const { align, valign, font, color, strike, underline } = attr;
const tx = box.textx(align);
ctx.save();
ctx.beginPath();
@ -269,9 +159,9 @@ class Draw {
return this;
}
border(style, color) {
border(style: string, color: string) {
const { ctx } = this;
ctx.lineWidth = thinLineWidth;
ctx.lineWidth = thinLineWidth();
ctx.strokeStyle = color;
// console.log('style:', style);
if (style === 'medium') {
@ -303,16 +193,13 @@ class Draw {
return this;
}
strokeBorders(box) {
strokeBorders(box: DrawBox) {
const { ctx } = this;
ctx.save();
// border
const {
borderTop, borderRight, borderBottom, borderLeft,
} = box;
const { borderTop, borderRight, borderBottom, borderLeft } = box;
if (borderTop) {
this.border(...borderTop);
// console.log('box.topxys:', box.topxys());
this.line(...box.topxys());
}
if (borderRight) {
@ -332,9 +219,7 @@ class Draw {
dropdown(box) {
const { ctx } = this;
const {
x, y, width, height,
} = box;
const { x, y, width, height } = box;
const sx = x + width - 15;
const sy = y + height - 15;
ctx.save();
@ -380,9 +265,7 @@ class Draw {
rect(box, dtextcb) {
const { ctx } = this;
const {
x, y, width, height, bgcolor,
} = box;
const { x, y, width, height, bgcolor } = box;
ctx.save();
ctx.beginPath();
ctx.fillStyle = bgcolor || '#fff';
@ -393,11 +276,3 @@ class Draw {
ctx.restore();
}
}
export default {};
export {
Draw,
DrawBox,
thinLineWidth,
npx,
};

View File

@ -0,0 +1,100 @@
type Border = [string, string];
export class DrawBox {
private width: number;
private height: number;
private bgcolor: string;
borderTop?: Border;
borderRight?: Border;
borderBottom?: Border;
borderLeft?: Border;
constructor(
private x: number,
private y: number,
w: number,
h: number,
private padding = 0,
) {
this.width = w;
this.height = h;
this.bgcolor = '#ffffff';
// border: [width, style, color]
this.borderTop = null;
this.borderRight = null;
this.borderBottom = null;
this.borderLeft = null;
}
setBorders({ top, bottom, left, right }) {
if (top) this.borderTop = top;
if (right) this.borderRight = right;
if (bottom) this.borderBottom = bottom;
if (left) this.borderLeft = left;
}
innerWidth() {
return this.width - this.padding * 2 - 2;
}
innerHeight() {
return this.height - this.padding * 2 - 2;
}
textx(align) {
const { width, padding } = this;
let { x } = this;
if (align === 'left') {
x += padding;
} else if (align === 'center') {
x += width / 2;
} else if (align === 'right') {
x += width - padding;
}
return x;
}
texty(align, h) {
const { height, padding } = this;
let { y } = this;
if (align === 'top') {
y += padding;
} else if (align === 'middle') {
y += height / 2 - h / 2;
} else if (align === 'bottom') {
y += height - padding - h;
}
return y;
}
topxys() {
const { x, y, width } = this;
return [
[x, y],
[x + width, y],
];
}
rightxys() {
const { x, y, width, height } = this;
return [
[x + width, y],
[x + width, y + height],
];
}
bottomxys() {
const { x, y, width, height } = this;
return [
[x, y + height],
[x + width, y + height],
];
}
leftxys() {
const { x, y, height } = this;
return [
[x, y],
[x, y + height],
];
}
}

View File

@ -1,92 +0,0 @@
class Draw {
constructor(el) {
this.el = el;
this.ctx = el.getContext('2d');
}
clear() {
const { width, height } = this.el;
this.ctx.clearRect(0, 0, width, height);
return this;
}
attr(m) {
Object.assign(this.ctx, m);
return this;
}
save() {
this.ctx.save();
this.ctx.beginPath();
return this;
}
restore() {
this.ctx.restore();
return this;
}
beginPath() {
this.ctx.beginPath();
return this;
}
closePath() {
this.ctx.closePath();
return this;
}
measureText(text) {
return this.ctx.measureText(text);
}
rect(x, y, width, height) {
this.ctx.rect(x, y, width, height);
return this;
}
scale(x, y) {
this.ctx.scale(x, y);
return this;
}
rotate(angle) {
this.ctx.rotate(angle);
return this;
}
translate(x, y) {
this.ctx.translate(x, y);
return this;
}
transform(a, b, c, d, e) {
this.ctx.transform(a, b, c, d, e);
return this;
}
fillRect(x, y, w, h) {
this.ctx.fillRect(x, y, w, h);
return this;
}
strokeRect(x, y, w, h) {
this.ctx.strokeRect(x, y, w, h);
return this;
}
fillText(text, x, y, maxWidth) {
this.ctx.fillText(text, x, y, maxWidth);
return this;
}
strokeText(text, x, y, maxWidth) {
this.ctx.strokeText(text, x, y, maxWidth);
return this;
}
}
export default {};
export {
Draw,
};

View File

@ -0,0 +1,3 @@
export { DrawBox } from './DrawBox';
export { thinLineWidth, dpr, npx } from './utils';
export { Draw } from './Draw';

View File

@ -0,0 +1,49 @@
export function dpr() {
return window.devicePixelRatio ?? 1;
}
export function thinLineWidth() {
return dpr() - 0.5;
}
export function npx(px: number | string) {
if (typeof px === 'string') {
return parseInt(px, 10) * dpr();
}
return px * dpr();
}
export function npxLine(px) {
const n = npx(px);
return n > 0 ? n - 0.5 : 0.5;
}
export function drawFontLine(
type: 'underline' | 'strike',
tx: number,
ty: number,
align: 'center' | 'right',
valign: 'bottom' | 'top',
blheight: number,
blwidth: number,
) {
const floffset = { x: 0, y: 0 };
if (type === 'underline') {
if (valign === 'bottom') {
floffset.y = 0;
} else if (valign === 'top') {
floffset.y = -(blheight + 2);
} else {
floffset.y = -blheight / 2;
}
} else if (type === 'strike') {
if (valign === 'bottom') {
floffset.y = blheight / 2;
} else if (valign === 'top') {
floffset.y = -(blheight / 2 + 2);
}
}
if (align === 'center') {
floffset.x = blwidth / 2;
} else if (align === 'right') {
floffset.x = blwidth;
}
this.line([tx - floffset.x, ty - floffset.y], [tx - floffset.x + blwidth, ty - floffset.y]);
}

View File

@ -16,23 +16,23 @@ class DropdownMore extends Dropdown {
}
reset(items) {
const eles = items.map((it, i) => h('div', `${cssPrefix}-item`)
const eles = items.map((it, i) =>
h('div', `${cssPrefix}-item`)
.css('width', '150px')
.css('font-weight', 'normal')
.on('click', () => {
this.contentClick(i);
this.hide();
})
.child(it));
.child(it),
);
this.setContentChildren(...eles);
}
setTitle() {}
}
const menuItems = [
{ key: 'delete', title: tf('contextmenu.deleteSheet') },
];
const menuItems = [{ key: 'delete', title: tf('contextmenu.deleteSheet') }];
function buildMenuItem(item) {
return h('div', `${cssPrefix}-item`)
@ -44,7 +44,7 @@ function buildMenuItem(item) {
}
function buildMenu() {
return menuItems.map(it => buildMenuItem.call(this, it));
return menuItems.map((it) => buildMenuItem.call(this, it));
}
class ContextMenu {
@ -71,10 +71,7 @@ class ContextMenu {
}
export default class Bottombar {
constructor(addFunc = () => {},
swapFunc = () => {},
deleteFunc = () => {},
updateFunc = () => {}) {
constructor(addFunc = () => {}, swapFunc = (index) => {}, deleteFunc = () => {}, updateFunc = (index, value) => {}) {
this.swapFunc = swapFunc;
this.updateFunc = updateFunc;
this.dataNames = [];
@ -88,35 +85,38 @@ export default class Bottombar {
this.contextMenu.itemClick = deleteFunc;
this.el = h('div', `${cssPrefix}-bottombar`).children(
this.contextMenu.el,
this.menuEl = h('ul', `${cssPrefix}-menu`).child(
(this.menuEl = h('ul', `${cssPrefix}-menu`).child(
h('li', '').children(
new Icon('add').on('click', () => {
addFunc();
}),
h('span', '').child(this.moreEl),
),
),
)),
);
}
addItem(name, active, options) {
this.dataNames.push(name);
const item = h('li', active ? 'active' : '').child(name);
item.on('click', () => {
item
.on('click', () => {
this.clickSwap2(item);
}).on('contextmenu', (evt) => {
})
.on('contextmenu', (evt) => {
if (options.mode === 'read') return;
const { offsetLeft, offsetHeight } = evt.target;
this.contextMenu.setOffset({ left: offsetLeft, bottom: offsetHeight + 1 });
this.deleteEl = item;
}).on('dblclick', () => {
})
.on('dblclick', () => {
if (options.mode === 'read') return;
const v = item.html();
const input = new FormInput('auto', '');
input.val(v);
input.input.on('blur', ({ target }) => {
const { value } = target;
const nindex = this.dataNames.findIndex(it => it === v);
const nindex = this.dataNames.findIndex((it) => it === v);
this.renameItem(nindex, value);
/*
this.dataNames.splice(nindex, 1, value);
@ -155,7 +155,7 @@ export default class Bottombar {
deleteItem() {
const { activeEl, deleteEl } = this;
if (this.items.length > 1) {
const index = this.items.findIndex(it => it === deleteEl);
const index = this.items.findIndex((it) => it === deleteEl);
this.items.splice(index, 1);
this.dataNames.splice(index, 1);
this.menuEl.removeChild(deleteEl.el);
@ -172,7 +172,7 @@ export default class Bottombar {
}
clickSwap2(item) {
const index = this.items.findIndex(it => it === item);
const index = this.items.findIndex((it) => it === item);
this.clickSwap(item);
this.activeEl.toggle();
this.swapFunc(index);

View File

@ -1,8 +1,8 @@
import { Element } from './element';
import { HComponent } from './element';
import { cssPrefix } from '../config';
import { t } from '../locale/locale';
export default class Button extends Element {
export default class Button extends HComponent {
// type: primary
constructor(title, type = '') {
super('div', `${cssPrefix}-button ${type}`);

View File

@ -1,8 +1,8 @@
import { Element, h } from './element';
import { HComponent, h } from './element';
import { bindClickoutside, unbindClickoutside } from './event';
import { cssPrefix } from '../config';
export default class Dropdown extends Element {
export default class Dropdown extends HComponent {
constructor(title, width, showArrow, placement, ...children) {
super('div', `${cssPrefix}-dropdown ${placement}`);
this.title = title;
@ -13,24 +13,22 @@ export default class Dropdown extends Element {
} else if (showArrow) {
this.title.addClass('arrow-left');
}
this.contentEl = h('div', `${cssPrefix}-dropdown-content`)
.css('width', width)
.hide();
this.contentEl = h('div', `${cssPrefix}-dropdown-content`).css('width', width).hide();
this.setContentChildren(...children);
this.headerEl = h('div', `${cssPrefix}-dropdown-header`);
this.headerEl.on('click', () => {
this.headerEl
.on('click', () => {
if (this.contentEl.css('display') !== 'block') {
this.show();
} else {
this.hide();
}
}).children(
})
.children(
this.title,
showArrow ? h('div', `${cssPrefix}-icon arrow-right`).child(
h('div', `${cssPrefix}-icon-img arrow-down`),
) : '',
showArrow ? h('div', `${cssPrefix}-icon arrow-right`).child(h('div', `${cssPrefix}-icon-img arrow-down`)) : '',
);
this.children(this.headerEl, this.contentEl);
}

View File

@ -1,5 +1,6 @@
import _ from 'lodash';
class Element {
private el: HTMLElement;
el: HTMLElement;
private _data: Record<string, any>;
constructor(tag: HTMLElement | string, className = '') {
if (typeof tag === 'string') {
@ -23,7 +24,7 @@ class Element {
const [fen, ...oen] = eventNames.split('.');
const eventName = fen;
this.el.addEventListener(eventName, (evt) => {
console.log('excel debug', eventName, eventNames, oen, evt);
// console.log('excel debug', eventName, eventNames, oen, evt);
handler(evt);
for (let i = 0; i < oen.length; i += 1) {
const k = oen[i];
@ -58,7 +59,7 @@ class Element {
};
}
scroll(v?) {
scroll = _.throttle((v?) => {
const { el } = this;
if (v !== undefined) {
if (v.left !== undefined) {
@ -69,7 +70,7 @@ class Element {
}
}
return { left: el.scrollLeft, top: el.scrollTop };
}
}, 10);
box() {
return this.el.getBoundingClientRect();
@ -240,4 +241,4 @@ class Element {
const h = (tag, className = '') => new Element(tag, className);
export { Element, h };
export { Element as HComponent, h };

View File

@ -91,16 +91,12 @@ export function createEventEmitter() {
function on(eventName, callback) {
const push = () => {
const currentListener = listeners.get(eventName);
return (Array.isArray(currentListener)
&& currentListener.push(callback))
|| false;
return (Array.isArray(currentListener) && currentListener.push(callback)) || false;
};
const create = () => listeners.set(eventName, [].concat(callback));
return (listeners.has(eventName)
&& push())
|| create();
return (listeners.has(eventName) && push()) || create();
}
function fire(eventName, args) {
@ -109,22 +105,22 @@ export function createEventEmitter() {
for (const callback of currentListener) callback.call(null, ...args);
};
return listeners.has(eventName)
&& exec();
return listeners.has(eventName) && exec();
}
function removeListener(eventName, callback) {
const remove = () => {
const currentListener = listeners.get(eventName);
const idx = currentListener.indexOf(callback);
return (idx >= 0)
&& currentListener.splice(idx, 1)
&& listeners.get(eventName).length === 0
&& listeners.delete(eventName);
return (
idx >= 0 &&
currentListener.splice(idx, 1) &&
listeners.get(eventName).length === 0 &&
listeners.delete(eventName)
);
};
return listeners.has(eventName)
&& remove();
return listeners.has(eventName) && remove();
}
function once(eventName, callback) {

View File

@ -1,7 +1,7 @@
import { Element, h } from './element';
import { HComponent, h } from './element';
import { cssPrefix } from '../config';
export default class Icon extends Element {
export default class Icon extends HComponent {
constructor(name) {
super('div', `${cssPrefix}-icon`);
this.iconNameEl = h('div', `${cssPrefix}-icon-img ${name}`);

View File

@ -2,7 +2,7 @@
import { h } from './element';
import { cssPrefix } from '../config';
import Button from './button';
import { Draw } from '../canvas/draw';
import { Draw } from '../canvas';
import { renderCell } from './table';
import { t } from '../locale/locale';
@ -16,7 +16,7 @@ const PAGER_SIZES = [
['A3', 11.69, 16.54],
['A4', 8.27, 11.69],
['A5', 5.83, 8.27],
['B4', 9.84, 13.90],
['B4', 9.84, 13.9],
['B5', 6.93, 9.84],
];
@ -68,8 +68,7 @@ export default class Print {
this.data = data;
this.el = h('div', `${cssPrefix}-print`)
.children(
h('div', `${cssPrefix}-print-bar`)
.children(
h('div', `${cssPrefix}-print-bar`).children(
h('div', '-title').child('Print settings'),
h('div', '-right').children(
h('div', `${cssPrefix}-buttons`).children(
@ -78,27 +77,37 @@ export default class Print {
),
),
),
h('div', `${cssPrefix}-print-content`)
.children(
this.contentEl = h('div', '-content'),
h('div', `${cssPrefix}-print-content`).children(
(this.contentEl = h('div', '-content')),
h('div', '-sider').child(
h('form', '').children(
h('fieldset', '').children(
h('label', '').child(`${t('print.size')}`),
h('select', '').children(
...PAGER_SIZES.map((it, index) => h('option', '').attr('value', index).child(`${it[0]} ( ${it[1]}''x${it[2]}'' )`)),
).on('change', pagerSizeChange.bind(this)),
h('select', '')
.children(
...PAGER_SIZES.map((it, index) =>
h('option', '').attr('value', index).child(`${it[0]} ( ${it[1]}''x${it[2]}'' )`),
),
)
.on('change', pagerSizeChange.bind(this)),
),
h('fieldset', '').children(
h('label', '').child(`${t('print.orientation')}`),
h('select', '').children(
...PAGER_ORIENTATIONS.map((it, index) => h('option', '').attr('value', index).child(`${t('print.orientations')[index]}`)),
).on('change', pagerOrientationChange.bind(this)),
h('select', '')
.children(
...PAGER_ORIENTATIONS.map((it, index) =>
h('option', '')
.attr('value', index)
.child(`${t('print.orientations')[index]}`),
),
)
.on('change', pagerOrientationChange.bind(this)),
),
),
),
),
).hide();
)
.hide();
}
resetData(data) {

View File

@ -1,9 +1,9 @@
import { h, Element } from './element';
import { h, HComponent } from './element';
import { cssPrefix } from '../config';
export default class Scrollbar {
private el: Element;
private contentEl: Element;
private el: HComponent;
private contentEl: HComponent;
private _moveFn: (type: string, handler: (e: Event) => void) => void;
constructor(private vertical: 'vertical' | 'horizontal') {
this._moveFn = null;

View File

@ -4,15 +4,13 @@ import _cell from '../core/cell';
import { formulam } from '../core/formula';
import { formatm } from '../core/format';
import {
Draw, DrawBox, thinLineWidth, npx,
} from '../canvas/draw';
import { Draw, DrawBox, thinLineWidth, npx } from '../canvas';
// gobal var
const cellPaddingWidth = 5;
const tableFixedHeaderCleanStyle = { fillStyle: '#f4f5f8' };
const tableGridStyle = {
fillStyle: '#fff',
lineWidth: thinLineWidth,
lineWidth: thinLineWidth(),
strokeStyle: '#e6e6e6',
};
function tableFixedHeaderStyle() {
@ -27,9 +25,7 @@ function tableFixedHeaderStyle() {
}
function getDrawBox(data, rindex, cindex, yoffset = 0) {
const {
left, top, width, height,
} = data.cellRect(rindex, cindex);
const { left, top, width, height } = data.cellRect(rindex, cindex);
return new DrawBox(left, top + yoffset, width, height, cellPaddingWidth);
}
/*
@ -76,7 +72,7 @@ export function renderCell(draw, data, rindex, cindex, yoffset = 0) {
// render text
let cellText = '';
if (!data.settings.evalPaused) {
cellText = _cell.render(cell.text || '', formulam, (y, x) => (data.getCellTextOrDefault(x, y)));
cellText = _cell.render(cell.text || '', formulam, (y, x) => data.getCellTextOrDefault(x, y));
} else {
cellText = cell.text || '';
}
@ -87,14 +83,19 @@ export function renderCell(draw, data, rindex, cindex, yoffset = 0) {
const font = Object.assign({}, style.font);
font.size = getFontSizePxByPt(font.size);
// console.log('style:', style);
draw.text(cellText, dbox, {
draw.text(
cellText,
dbox,
{
align: style.align,
valign: style.valign,
font,
color: style.color,
strike: style.strike,
underline: style.underline,
}, style.textwrap);
},
style.textwrap,
);
// error
const error = data.validations.getError(rindex, cindex);
if (error) {
@ -125,8 +126,7 @@ function renderAutofilter(viewRange) {
function renderContent(viewRange, fw, fh, tx, ty) {
const { draw, data } = this;
draw.save();
draw.translate(fw, fh)
.translate(tx, ty);
draw.translate(fw, fh).translate(tx, ty);
const { exceptRowSet } = data;
// const exceptRows = Array.from(exceptRowSet);
@ -143,12 +143,14 @@ function renderContent(viewRange, fw, fh, tx, ty) {
// 1 render cell
draw.save();
draw.translate(0, -exceptRowTotalHeight);
viewRange.each((ri, ci) => {
viewRange.each(
(ri, ci) => {
renderCell(draw, data, ri, ci);
}, ri => filteredTranslateFunc(ri));
},
(ri) => filteredTranslateFunc(ri),
);
draw.restore();
// 2 render mergeCell
const rset = new Set();
draw.save();
@ -173,8 +175,7 @@ function renderContent(viewRange, fw, fh, tx, ty) {
function renderSelectedHeaderCell(x, y, w, h) {
const { draw } = this;
draw.save();
draw.attr({ fillStyle: 'rgba(75, 137, 255, 0.08)' })
.fillRect(x, y, w, h);
draw.attr({ fillStyle: 'rgba(75, 137, 255, 0.08)' }).fillRect(x, y, w, h);
draw.restore();
}
@ -197,9 +198,7 @@ function renderFixedHeaders(type, viewRange, w, h, tx, ty) {
if (type === 'all' || type === 'left') draw.fillRect(0, nty, w, sumHeight);
if (type === 'all' || type === 'top') draw.fillRect(ntx, 0, sumWidth, h);
const {
sri, sci, eri, eci,
} = data.selector.range;
const { sri, sci, eri, eci } = data.selector.range;
// console.log(data.selectIndexes);
// draw text
// text font, align...
@ -213,7 +212,7 @@ function renderFixedHeaders(type, viewRange, w, h, tx, ty) {
if (sri <= ii && ii < eri + 1) {
renderSelectedHeaderCell.call(this, 0, y, w, rowHeight);
}
draw.fillText(ii + 1, w / 2, y + (rowHeight / 2));
draw.fillText(ii + 1, w / 2, y + rowHeight / 2);
if (i > 0 && data.rows.isHide(i - 1)) {
draw.save();
draw.attr({ strokeStyle: '#c6c6c6' });
@ -233,7 +232,7 @@ function renderFixedHeaders(type, viewRange, w, h, tx, ty) {
if (sci <= ii && ii < eci + 1) {
renderSelectedHeaderCell.call(this, x, 0, colWidth, h);
}
draw.fillText(stringAt(ii), x + (colWidth / 2), h / 2);
draw.fillText(stringAt(ii), x + colWidth / 2, h / 2);
if (i > 0 && data.cols.isHide(i - 1)) {
draw.save();
draw.attr({ strokeStyle: '#c6c6c6' });
@ -251,20 +250,16 @@ function renderFixedLeftTopCell(fw, fh) {
const { draw } = this;
draw.save();
// left-top-cell
draw.attr({ fillStyle: '#f4f5f8' })
.fillRect(0, 0, fw, fh);
draw.attr({ fillStyle: '#f4f5f8' }).fillRect(0, 0, fw, fh);
draw.restore();
}
function renderContentGrid({
sri, sci, eri, eci, w, h,
}, fw, fh, tx, ty) {
function renderContentGrid({ sri, sci, eri, eci, w, h }, fw, fh, tx, ty) {
const { draw, data } = this;
const { settings } = data;
draw.save();
draw.attr(tableGridStyle)
.translate(fw + tx, fh + ty);
draw.attr(tableGridStyle).translate(fw + tx, fh + ty);
// const sumWidth = cols.sumWidth(sci, eci + 1);
// const sumHeight = rows.sumHeight(sri, eri + 1);
// console.log('sumWidth:', sumWidth);
@ -290,9 +285,7 @@ function renderFreezeHighlightLine(fw, fh, ftw, fth) {
const { draw, data } = this;
const twidth = data.viewWidth() - fw;
const theight = data.viewHeight() - fh;
draw.save()
.translate(fw, fh)
.attr({ strokeStyle: 'rgba(75, 137, 255, .6)' });
draw.save().translate(fw, fh).attr({ strokeStyle: 'rgba(75, 137, 255, .6)' });
draw.line([0, fth], [twidth, fth]);
draw.line([ftw, 0], [ftw, theight]);
draw.restore();

View File

@ -1,201 +0,0 @@
export interface ExtendToolbarOption {
tip?: string;
el?: HTMLElement;
icon?: string;
onClick?: (data: object, sheet: object) => void
}
export interface Options {
mode?: 'edit' | 'read';
showToolbar?: boolean;
showGrid?: boolean;
showContextmenu?: boolean;
showBottomBar?: boolean;
extendToolbar?: {
left?: ExtendToolbarOption[],
right?: ExtendToolbarOption[],
};
autoFocus?: boolean;
view?: {
height: () => number;
width: () => number;
};
row?: {
len: number;
height: number;
};
col?: {
len: number;
width: number;
indexWidth: number;
minWidth: number;
};
style?: {
bgcolor: string;
align: 'left' | 'center' | 'right';
valign: 'top' | 'middle' | 'bottom';
textwrap: boolean;
strike: boolean;
underline: boolean;
color: string;
font: {
name: 'Helvetica';
size: number;
bold: boolean;
italic: false;
};
};
}
export type CELL_SELECTED = 'cell-selected';
export type CELLS_SELECTED = 'cells-selected';
export type CELL_EDITED = 'cell-edited';
export type CellMerge = [number, number];
export interface SpreadsheetEventHandler {
(
envt: CELL_SELECTED,
callback: (cell: Cell, rowIndex: number, colIndex: number) => void
): void;
(
envt: CELLS_SELECTED,
callback: (
cell: Cell,
parameters: { sri: number; sci: number; eri: number; eci: number }
) => void
): void;
(
evnt: CELL_EDITED,
callback: (text: string, rowIndex: number, colIndex: number) => void
): void;
}
export interface ColProperties {
width?: number;
}
/**
* Data for representing a cell
*/
export interface CellData {
text: string;
style?: number;
merge?: CellMerge;
}
/**
* Data for representing a row
*/
export interface RowData {
cells: {
[key: number]: CellData;
}
}
/**
* Data for representing a sheet
*/
export interface SheetData {
name?: string;
freeze?: string;
styles?: CellStyle[];
merges?: string[];
cols?: {
len?: number;
[key: number]: ColProperties;
};
rows?: {
[key: number]: RowData
};
}
/**
* Data for representing a spreadsheet
*/
export interface SpreadsheetData {
[index: number]: SheetData;
}
export interface CellStyle {
align?: 'left' | 'center' | 'right';
valign?: 'top' | 'middle' | 'bottom';
font?: {
bold?: boolean;
}
bgcolor?: string;
textwrap?: boolean;
color?: string;
border?: {
top?: string[];
right?: string[];
bottom?: string[];
left?: string[];
};
}
export interface Editor {}
export interface Element {}
export interface Row {}
export interface Table {}
export interface Cell {}
export interface Sheet {}
export default class Spreadsheet {
constructor(container: string | HTMLElement, opts?: Options);
on: SpreadsheetEventHandler;
/**
* retrieve cell
* @param rowIndex {number} row index
* @param colIndex {number} column index
* @param sheetIndex {number} sheet iindex
*/
cell(rowIndex: number, colIndex: number, sheetIndex: number): Cell;
/**
* retrieve cell style
* @param rowIndex
* @param colIndex
* @param sheetIndex
*/
cellStyle(
rowIndex: number,
colIndex: number,
sheetIndex: number
): CellStyle;
/**
* get/set cell text
* @param rowIndex
* @param colIndex
* @param text
* @param sheetIndex
*/
cellText(
rowIndex: number,
colIndex: number,
text: string,
sheetIndex?: number
): this;
/**
* remove current sheet
*/
deleteSheet(): void;
/**s
* load data
* @param json
*/
loadData(json: Record<string, any>): this;
/**
* get data
*/
getData(): Record<string, any>;
/**
* bind handler to change event, including data change and user actions
* @param callback
*/
change(callback: (json: Record<string, any>) => void): this;
/**
* set locale
* @param lang
* @param message
*/
static locale(lang: string, message: object): void;
}

View File

@ -1,131 +0,0 @@
/* global window, document */
import { h } from './component/element';
import DataProxy from './core/data_proxy';
import Sheet from './component/sheet';
import Bottombar from './component/bottombar';
import { cssPrefix } from './config';
import { locale } from './locale/locale';
import './index.less';
class Spreadsheet {
constructor(selectors, options = {}) {
let targetEl = selectors;
this.options = { showBottomBar: true, ...options };
this.sheetIndex = 1;
this.datas = [];
if (typeof selectors === 'string') {
targetEl = document.querySelector(selectors);
}
this.bottombar = this.options.showBottomBar ? new Bottombar(() => {
if (this.options.mode === 'read') return;
const d = this.addSheet();
this.sheet.resetData(d);
}, (index) => {
const d = this.datas[index];
this.sheet.resetData(d);
}, () => {
this.deleteSheet();
}, (index, value) => {
this.datas[index].name = value;
this.sheet.trigger('change');
}) : null;
this.data = this.addSheet();
const rootEl = h('div', `${cssPrefix}`)
.on('contextmenu', evt => evt.preventDefault());
// create canvas element
targetEl.appendChild(rootEl.el);
this.sheet = new Sheet(rootEl, this.data);
if (this.bottombar !== null) {
rootEl.child(this.bottombar.el);
}
}
addSheet(name, active = true) {
const n = name || `sheet${this.sheetIndex}`;
const d = new DataProxy(n, this.options);
d.change = (...args) => {
this.sheet.trigger('change', ...args);
};
this.datas.push(d);
// console.log('d:', n, d, this.datas);
if (this.bottombar !== null) {
this.bottombar.addItem(n, active, this.options);
}
this.sheetIndex += 1;
return d;
}
deleteSheet() {
if (this.bottombar === null) return;
const [oldIndex, nindex] = this.bottombar.deleteItem();
if (oldIndex >= 0) {
this.datas.splice(oldIndex, 1);
if (nindex >= 0) this.sheet.resetData(this.datas[nindex]);
this.sheet.trigger('change');
}
}
loadData(data) {
const ds = Array.isArray(data) ? data : [data];
if (this.bottombar !== null) {
this.bottombar.clear();
}
this.datas = [];
if (ds.length > 0) {
for (let i = 0; i < ds.length; i += 1) {
const it = ds[i];
const nd = this.addSheet(it.name, i === 0);
nd.setData(it);
if (i === 0) {
this.sheet.resetData(nd);
}
}
}
return this;
}
getData() {
return this.datas.map(it => it.getData());
}
cellText(ri, ci, text, sheetIndex = 0) {
this.datas[sheetIndex].setCellText(ri, ci, text, 'finished');
return this;
}
cell(ri, ci, sheetIndex = 0) {
return this.datas[sheetIndex].getCell(ri, ci);
}
cellStyle(ri, ci, sheetIndex = 0) {
return this.datas[sheetIndex].getCellStyle(ri, ci);
}
reRender() {
this.sheet.table.render();
return this;
}
on(eventName, func) {
this.sheet.on(eventName, func);
return this;
}
validate() {
const { validations } = this.data;
return validations.errors.size <= 0;
}
change(cb) {
this.sheet.on('change', cb);
return this;
}
static locale(lang, message) {
locale(lang, message);
}
}
export default Spreadsheet;

View File

@ -0,0 +1,292 @@
import { h, HComponent } from './component/element';
import DataProxy from './core/data_proxy';
import Sheet from './component/sheet';
import Bottombar from './component/bottombar';
import { cssPrefix } from './config';
import { locale } from './locale/locale';
import './index.less';
export type CELL_SELECTED = 'cell-selected';
export type CELLS_SELECTED = 'cells-selected';
export type CELL_EDITED = 'cell-edited';
export type CellMerge = [number, number];
export interface SpreadsheetEventHandler {
(envt: CELL_SELECTED, callback: (cell: Cell, rowIndex: number, colIndex: number) => void): void;
(
envt: CELLS_SELECTED,
callback: (cell: Cell, parameters: { sri: number; sci: number; eri: number; eci: number }) => void,
): void;
(evnt: CELL_EDITED, callback: (text: string, rowIndex: number, colIndex: number) => void): void;
}
export interface ColProperties {
width?: number;
}
/**
* Data for representing a cell
*/
export interface CellData {
text: string;
style?: number;
merge?: CellMerge;
}
/**
* Data for representing a row
*/
export interface RowData {
cells: {
[key: number]: CellData;
};
}
/**
* Data for representing a sheet
*/
export interface SheetData {
name?: string;
freeze?: string;
styles?: CellStyle[];
merges?: string[];
cols?: {
len?: number;
[key: number]: ColProperties;
};
rows?: {
[key: number]: RowData;
};
}
/**
* Data for representing a spreadsheet
*/
export interface SpreadsheetData {
[index: number]: SheetData;
}
export interface CellStyle {
align?: 'left' | 'center' | 'right';
valign?: 'top' | 'middle' | 'bottom';
font?: {
bold?: boolean;
};
bgcolor?: string;
textwrap?: boolean;
color?: string;
border?: {
top?: string[];
right?: string[];
bottom?: string[];
left?: string[];
};
}
export interface Editor {}
export interface Element {}
export interface Row {}
export interface Table {}
export interface Cell {}
export interface ExtendToolbarOption {
tip?: string;
el?: HTMLElement;
icon?: string;
onClick?: (data: object, sheet: object) => void;
}
export interface Options {
mode?: 'edit' | 'read';
showToolbar?: boolean;
showGrid?: boolean;
showContextmenu?: boolean;
showBottomBar?: boolean;
extendToolbar?: {
left?: ExtendToolbarOption[];
right?: ExtendToolbarOption[];
};
autoFocus?: boolean;
view?: {
height: () => number;
width: () => number;
};
row?: {
len: number;
height: number;
};
col?: {
len: number;
width: number;
indexWidth: number;
minWidth: number;
};
style?: {
bgcolor: string;
align: 'left' | 'center' | 'right';
valign: 'top' | 'middle' | 'bottom';
textwrap: boolean;
strike: boolean;
underline: boolean;
color: string;
font: {
name: 'Helvetica';
size: number;
bold: boolean;
italic: false;
};
};
}
class Spreadsheet {
private targetEl: HTMLElement;
private options: Options;
sheetIndex: number;
datas: any[];
bottombar: Bottombar;
sheet: any;
data: DataProxy;
rootEl: HComponent;
constructor(selectors: string | HTMLElement, options: Options = {}) {
this.options = { showBottomBar: true, ...options };
this.sheetIndex = 1;
this.datas = [];
// debugger;
if (typeof selectors === 'string') {
this.targetEl = document.querySelector(selectors) as HTMLElement;
} else {
this.targetEl = selectors;
}
this.bottombar = this.options.showBottomBar
? new Bottombar(
() => {
if (this.options.mode === 'read') return;
const d = this.addSheet();
this.sheet.resetData(d);
},
(index) => {
const d = this.datas[index];
this.sheet.resetData(d);
},
() => {
this.deleteSheet();
},
(index, value) => {
this.datas[index].name = value;
this.sheet.trigger('change');
},
)
: null;
this.data = this.addSheet();
this.rootEl = h('div', `${cssPrefix}`).on('contextmenu', (evt) => evt.preventDefault());
// create canvas element
this.targetEl.appendChild(this.rootEl.el);
this.sheet = new Sheet(this.rootEl, this.data);
if (this.bottombar !== null) {
this.rootEl.child(this.bottombar.el);
}
}
dispose() {
this.targetEl.removeChild(this.rootEl.el);
console.debug('x-sheet is diposed');
}
addSheet(name?: string, active = true) {
const n = name || `sheet${this.sheetIndex}`;
const d = new DataProxy(n, this.options);
d.change = (...args) => {
this.sheet.trigger('change', ...args);
};
this.datas.push(d);
// console.log('d:', n, d, this.datas);
if (this.bottombar !== null) {
this.bottombar.addItem(n, active, this.options);
}
this.sheetIndex += 1;
return d;
}
deleteSheet() {
if (this.bottombar === null) return;
const [oldIndex, nindex] = this.bottombar.deleteItem();
if (oldIndex >= 0) {
this.datas.splice(oldIndex, 1);
if (nindex >= 0) this.sheet.resetData(this.datas[nindex]);
this.sheet.trigger('change');
}
}
loadData(data: Record<string, any>): this {
const ds = Array.isArray(data) ? data : [data];
if (this.bottombar !== null) {
this.bottombar.clear();
}
this.datas = [];
if (ds.length > 0) {
for (let i = 0; i < ds.length; i += 1) {
const it = ds[i];
const nd = this.addSheet(it.name, i === 0);
nd.setData(it);
if (i === 0) {
this.sheet.resetData(nd);
}
}
}
return this;
}
getData(): Record<string, any> {
return this.datas.map((it) => it.getData());
}
cellText(ri: number, ci: number, text: string, sheetIndex = 0): this {
this.datas[sheetIndex].setCellText(ri, ci, text, 'finished');
return this;
}
cell(ri: number, ci: number, sheetIndex = 0) {
return this.datas[sheetIndex].getCell(ri, ci);
}
cellStyle(ri: number, ci: number, sheetIndex = 0): CellStyle {
return this.datas[sheetIndex].getCellStyle(ri, ci);
}
reRender() {
this.sheet.table.render();
return this;
}
on(eventName, func) {
this.sheet.on(eventName, func);
return this;
}
validate() {
const { validations } = this.data;
return validations.errors.size <= 0;
}
/**
* bind handler to change event, including data change and user actions
* @param cb
* @returns
*/
change(cb: (json: Record<string, any>) => void): this {
this.sheet.on('change', cb);
return this;
}
/**
* set locale
* @param lang
* @param message
*/
static locale(lang: string, message: unknown): void {
locale(lang, message);
}
}
export default Spreadsheet;

View File

@ -30,6 +30,7 @@ import {
CustomFieldInterface,
CustomAssociatedFieldInterface,
SignaturePadFieldInterface,
ExcelFieldInterface,
} from './interfaces';
import {
AssociatedField,
@ -41,6 +42,7 @@ import {
customComponentDispatcherSettings,
Expression,
SignatureInput,
ExcelFile,
} from './components';
import { AutoComplete, InternalPDFViewer } from './schema-components';
import {
@ -192,6 +194,7 @@ export class PluginCoreClient extends Plugin {
SheetBlockProvider,
SheetBlockToolbar,
SignatureInput,
ExcelFile,
});
}
@ -274,6 +277,7 @@ export class PluginCoreClient extends Plugin {
CustomFieldInterface,
CustomAssociatedFieldInterface,
SignaturePadFieldInterface,
ExcelFieldInterface,
]);
}

View File

@ -0,0 +1,25 @@
import { defaultProps, CollectionFieldInterface } from '@nocobase/client';
import { tval } from '../locale';
export class ExcelFieldInterface extends CollectionFieldInterface {
name = 'excelField';
type = 'json';
group = 'advanced';
order = 3;
title = tval('Excel table');
sortable = true;
default = {
interface: 'json',
type: 'json',
uiSchema: {
type: 'object',
'x-component': 'ExcelFile',
},
};
availableTypes = ['json'];
hasDefaultValue = false;
properties = {
...defaultProps,
};
}

View File

@ -1,3 +1,4 @@
export * from './ExcelFieldInterface';
export * from './AssociatedFieldInterface';
export * from './CalcFieldInterface';
export * from './CustomFieldInterface';

View File

@ -13,7 +13,7 @@ import {
import React, { createContext, useRef } from 'react';
import { uid } from '@nocobase/utils/client';
import { Button, Spin } from 'antd';
import Sheet, { SheetRef } from '../../components/Sheet';
import Sheet, { SheetRef } from '../../components/excel-table/Sheet';
export const SheetBlockContext = createContext<any>({});

View File

@ -76,9 +76,16 @@ export class ContractRuleService {
appends: ['lease_items', 'lease_items.products'],
});
const add = options.values.products;
const productData = plan.lease_items.filter(item => item.id !== options.values.id && item.products.length === add.length).map((item) => item.products).flat()
const productData = plan.lease_items
.filter((item) => item.id !== options.values.id && item.products.length === add.length)
.map((item) => item.products)
.flat();
productData.forEach((item) => {
const isHas = add.find((p) => (p.id < 99999 && (p.id === item.id || (item.id > 99999 && p.raw_category_id === item.raw_category_id))) || (p.id > 99999 && p.raw_category_id === item.raw_category_id));
const isHas = add.find(
(p) =>
(p.id < 99999 && (p.id === item.id || (item.id > 99999 && p.raw_category_id === item.raw_category_id))) ||
(p.id > 99999 && p.raw_category_id === item.raw_category_id),
);
if (isHas) {
throw new Error('方案中存在此产品');
}