diff --git a/src/components/Common/FlowDesign/Config/Components/FormBinding/option.tsx b/src/components/Common/FlowDesign/Config/Components/FormBinding/option.tsx index 089650dcfd4c8ee9da611df76dd3a3d28390eec3..d8b8ea7ddce93c532085a15f24c549669d8ad8e5 100644 --- a/src/components/Common/FlowDesign/Config/Components/FormBinding/option.tsx +++ b/src/components/Common/FlowDesign/Config/Components/FormBinding/option.tsx @@ -137,6 +137,7 @@ export const FormOption = (props: { {loadOperateRule('生成', props.operateRule, 'allowGenerate')} {loadOperateRule('选择文件', props.operateRule, 'allowSelectFile')} {loadOperateRule('单位空间', props.operateRule, 'selectBelong')} + {loadOperateRule('关闭锁', props.operateRule, 'closeLock')} {props.typeName === '子表' && loadOperateRule( '仅显示变更数据', diff --git a/src/executor/tools/editModal/formSelectModal.tsx b/src/executor/tools/editModal/formSelectModal.tsx index 6e5818b7ee76e6d2050403df44b9ba3807ee2f2e..f2d91d9d7bbfd0518f1d5228d56b5b1459573530 100644 --- a/src/executor/tools/editModal/formSelectModal.tsx +++ b/src/executor/tools/editModal/formSelectModal.tsx @@ -67,6 +67,19 @@ const FormSelectModal = ({ showCheckBoxesMode: 'always', }} pager={{ visible: true }} + onCellPrepared={(e) => { + if ( + e.column.type == 'selection' && + e.rowType == 'data' && + e.data?.locks?.exclusion + ) { + e.cellElement.style.pointerEvents = 'none'; + e.cellElement.style.whiteSpace = 'nowrap'; + e.cellElement.style.overflow = 'hidden'; + e.cellElement.style.textOverflow = 'ellipsis'; + e.cellElement.innerHTML = '锁定中 (' + e.data.locks.exclusion.name + ')'; + } + }} onSelectionChanged={(e) => { editData.rows = e.selectedRowsData; }} @@ -167,6 +180,19 @@ const FormSelectModal = ({ showCheckBoxesMode: 'always', }} pager={{ visible: true }} + onCellPrepared={(e) => { + if ( + e.column.type == 'selection' && + e.rowType == 'data' && + e.data?.locks?.exclusion + ) { + e.cellElement.style.pointerEvents = 'none'; + e.cellElement.style.whiteSpace = 'nowrap'; + e.cellElement.style.overflow = 'hidden'; + e.cellElement.style.textOverflow = 'ellipsis'; + e.cellElement.innerHTML = '锁定中 (' + e.data.locks.exclusion.name + ')'; + } + }} onSelectionChanged={(e) => { editData.rows = e.selectedRowsData; }} diff --git a/src/executor/tools/generate/thingTable.tsx b/src/executor/tools/generate/thingTable.tsx index 066916b6c2a2414e4864b982db5b37f6b7f3a0cb..a620a21ddd0bb854b0f4ecbf69f81667228eea53 100644 --- a/src/executor/tools/generate/thingTable.tsx +++ b/src/executor/tools/generate/thingTable.tsx @@ -81,16 +81,30 @@ const GenerateThingTable = (props: IProps) => { if (e.format === 'xlsx') { e.component.beginCustomLoading('正在导出数据...'); const options = e.component.getDataSource().loadOptions(); - const workbook = new Workbook(); - const worksheet = workbook.addWorksheet(props.form?.name); const columns = e.component.getVisibleColumns(); const filter = e.component.getCombinedFilter(); - worksheet.columns =e.component.getVisibleColumns().filter((it) => it.name !== '操作').map((column) => { - return { header: column.caption, key: column.dataField }; - }); let skip = 0; let take = 500; let pass = true; + let page = 1; + let buffer: any[] = []; + let generate = (buffer: any[]) => { + const workbook = new Workbook(); + const worksheet = workbook.addWorksheet(props.form?.name + '-' + page++); + worksheet.columns = e.component + .getVisibleColumns() + .filter((it) => it.name !== '操作') + .map((column) => { + return { header: column.caption, key: column.dataField }; + }); + worksheet.addRows(buffer); + workbook.xlsx.writeBuffer().then((buffer) => { + saveAs( + new Blob([buffer], { type: 'application/octet-stream' }), + (props.form?.name ?? '表单') + '.xlsx', + ); + }); + }; while (pass) { let result: model.LoadResult = { data: [], @@ -102,6 +116,7 @@ const GenerateThingTable = (props: IProps) => { code: 0, }; if (!e.selectedRowsOnly) { + options.requireTotalCount = false; result = (await e.component .getDataSource() .store() @@ -115,7 +130,8 @@ const GenerateThingTable = (props: IProps) => { } else { let data = result.data; const userTypeList = fields.filter((i) => i.valueType === '用户型'); - data.forEach((item: any, rowIndex) => { + for (let rowIndex = 0; rowIndex < data.length; rowIndex++) { + const item = data[rowIndex]; for (const column of columns) { const valueMap = (column.lookup as any)?.valueMap; if (valueMap && column.dataField) { @@ -126,32 +142,27 @@ const GenerateThingTable = (props: IProps) => { undefined; const regex = /^.*[\u4e00-\u9fa5]+.*$/; if (user && column.dataField && !regex.test(item[user.code])) { - item[user.code] = - orgCtrl.user.findMetadata(item[user.code]) - ?.name ?? item[user.code]; - } - } - worksheet.addRow(item); - if (item.redRowFlag) { - // 在这里添加条件判断并设置字体颜色 - for (let colIndex = 0; colIndex < columns.length; colIndex++) { - const cell = worksheet.getCell(rowIndex + 2, colIndex + 1); - cell.font = { color: { argb: 'FF0000' } }; + let entity = await orgCtrl.user.findEntityAsync(item[user.code]); + item[user.code] = entity?.name ?? item[user.code]; } } - }); + buffer.push(item); + } skip += result.data.length; if (e.selectedRowsOnly) { pass = false; } } + if (buffer.length >= 100000) { + generate(buffer); + buffer = []; + } + console.log('skip', skip); + } + if (buffer.length > 0) { + generate(buffer); + buffer = []; } - workbook.xlsx.writeBuffer().then((buffer) => { - saveAs( - new Blob([buffer], { type: 'application/octet-stream' }), - (props.form?.name ?? '表单') + '.xlsx', - ); - }); e.component.endCustomLoading(); } } finally { diff --git a/src/executor/tools/task/start/multitabTable/stagging.tsx b/src/executor/tools/task/start/multitabTable/stagging.tsx index d711691bcc2b6cd0e9cd58109018678aae4c95de..02d7a101da1d7ffae563180aabd5ff7f4b1317f8 100644 --- a/src/executor/tools/task/start/multitabTable/stagging.tsx +++ b/src/executor/tools/task/start/multitabTable/stagging.tsx @@ -7,6 +7,7 @@ import { AiFillRest } from 'react-icons/ai'; import useObjectUpdate from '@/hooks/useObjectUpdate'; import CustomStore from 'devextreme/data/custom_store'; import Confirm from '@/executor/open/reconfirm'; +import { Lock } from '@/ts/core/work/lock'; interface IProps { apply: IWorkApply; @@ -71,6 +72,17 @@ const WorkStagging: React.FC = ({ onShow, apply }) => { ...mainFormFields, ] as model.FieldModel[]; }; + const unlock = async (work: schema.XWorkInstance) => { + const stagings = await orgCtrl.user.workStagging.find([work.id]); + for (const staging of stagings) { + if (staging.data) { + const instance: model.InstanceDataModel = JSON.parse(staging.data); + if (instance) { + await new Lock(apply.target, instance).unlock(); + } + } + } + }; return ( <> = ({ onShow, apply }) => { options: { text: '移除', icon: 'remove', - onClick: () => { - orgCtrl.user.workStagging.removeMany(selectStaggings); + onClick: async () => { + for (const item of selectStaggings) { + await unlock(item); + } + await orgCtrl.user.workStagging.removeMany(selectStaggings); forceUpdate(); }, }, @@ -168,6 +183,7 @@ const WorkStagging: React.FC = ({ onShow, apply }) => { { + await unlock(item); const success = await orgCtrl.user.workStagging.remove(item); if (success) { forceUpdate(); diff --git a/src/executor/tools/workForm/detail.tsx b/src/executor/tools/workForm/detail.tsx index c4519616b84b76fc0bc5f855ef3f53c8ac7fc904..241683a42d99586b661e54a77215f1af7ff3a2b7 100644 --- a/src/executor/tools/workForm/detail.tsx +++ b/src/executor/tools/workForm/detail.tsx @@ -431,46 +431,30 @@ const DetailTable: React.FC = (props) => { try { if (e.format === 'xlsx') { e.component.beginCustomLoading('正在导出数据...'); - const options = e.component.getDataSource().loadOptions(); const workbook = new Workbook(); const worksheet = workbook.addWorksheet(form.name); const columns = e.component.getVisibleColumns(); - const filter = e.component.getCombinedFilter(); - worksheet.columns = e.component.getVisibleColumns().filter((it) => it.name !== '操作').map((column) => { - return { header: column.caption, key: column.dataField }; - }); - let skip = 0; - let take = 500; - let pass = true; - while (pass) { - const result: model.LoadResult = (await e.component - .getDataSource() - .store() - .load({ skip, take, options, filter })) as any; - if (!Array.isArray(result) || result.length == 0) { - pass = false; - break; - } else { - let data: any = result; - if (e.selectedRowsOnly) { - const selectedRowKeys: string[] = e.component.getSelectedRowKeys(); - data = data.filter((item) => selectedRowKeys.includes(item.id)); - } - data.forEach((item: any) => { - for (const column of columns) { - const valueMap = (column.lookup as any)?.valueMap; - if (valueMap && column.dataField) { - item[column.dataField] = valueMap[item[column.dataField]]; - } - } - worksheet.addRow(item); - }); - skip += data.length; - if (data.length < take) { - pass = false; + worksheet.columns = e.component + .getVisibleColumns() + .filter((it) => it.name !== '操作') + .map((column) => { + return { header: column.caption, key: column.dataField }; + }); + let result = await e.component.getDataSource().store().load(); + let data = deepClone(result) as schema.XThing[]; + if (e.selectedRowsOnly) { + const selectedRowKeys: string[] = e.component.getSelectedRowKeys(); + data = data.filter((item) => selectedRowKeys.includes(item.id)); + } + data.forEach((item) => { + for (const column of columns) { + const valueMap = (column.lookup as any)?.valueMap; + if (valueMap && column.dataField) { + item[column.dataField] = valueMap[item[column.dataField]]; } } - } + worksheet.addRow(item); + }); workbook.xlsx.writeBuffer().then((buffer) => { saveAs( new Blob([buffer], { type: 'application/octet-stream' }), diff --git a/src/executor/tools/workForm/primary.tsx b/src/executor/tools/workForm/primary.tsx index 5a950e8bd1664b8de7765f3993cea74b6f372a5b..f24e317c67ae6cef5f17c687cdfd122fc54b811a 100644 --- a/src/executor/tools/workForm/primary.tsx +++ b/src/executor/tools/workForm/primary.tsx @@ -74,6 +74,8 @@ const PrimaryForms: React.FC = (props) => { const loadItems = () => { const items = []; for (const form of props.node.primaryForms) { + let info = + props.node.forms.find((item) => item.id == form.id) ?? ({} as model.FormInfo); if ( props.service.model.rules?.find( (a) => a.destId == form.id && a.typeName == 'visible' && !a.value, @@ -88,7 +90,7 @@ const PrimaryForms: React.FC = (props) => { children: ( { } export interface AcquireExecutor extends ExecutorBase<'数据申领'> { - // 数据源空间(数据申领) - belongId: string; // 数据申领 acquires: Acquire[]; } diff --git a/src/ts/base/schema.ts b/src/ts/base/schema.ts index 9b4da0b745084aac8a13870f934046e1b9024054..6e391e2789b5b077028921b46d274cda8d8f009a 100644 --- a/src/ts/base/schema.ts +++ b/src/ts/base/schema.ts @@ -692,8 +692,28 @@ export type XThing = { belong: XTarget | undefined; // 标签 labels: string[]; + // 业务锁 + locks?: Locks; } & Xbase; +// 锁对象 +export interface Locks { + // 全局锁 + exclusion: Business; + // 自定义锁 + [lock: string]: Business; +} + +// 导致锁住的实例业务 +export interface Business { + // 实例 ID + id: string; + // 类型 + type: string; + // 实例名称 + name: string; +} + //物的属性值 export type XThingProp = { // 属性ID diff --git a/src/ts/core/work/apply.ts b/src/ts/core/work/apply.ts index 8f6153dbdbce6ad27f966c3ef5793f4bcb26cda3..03eeff3fcc9d06fc190867a3f56e8c99c3d93110 100644 --- a/src/ts/core/work/apply.ts +++ b/src/ts/core/work/apply.ts @@ -3,6 +3,7 @@ import { XCollection } from '../public/collection'; import { IForm } from '../thing/standard/form'; import { IPrint, ITarget } from '..'; import { XWorkInstance } from '@/ts/base/schema'; +import { Lock } from './lock'; export type InvalidItem = { id: string; @@ -134,6 +135,13 @@ export class WorkApply implements IWorkApply { data: JSON.stringify(this.instanceData), gateways: JSON.stringify(this.gatewayData), }); + if (res.success && res.data) { + await new Lock(this.target, this.instanceData).exclusionLock({ + id: res.data.id, + name: res.data.title, + type: '流程实例', + }); + } return res.data; } async staggingApply( @@ -161,6 +169,13 @@ export class WorkApply implements IWorkApply { data: JSON.stringify(this.instanceData), gateways: JSON.stringify(this.gatewayData), } as unknown as schema.XWorkInstance); + if (res) { + await new Lock(this.target, this.instanceData).exclusionLock({ + id: res.id, + name: res.title, + type: '流程草稿', + }); + } return res; } private getHideForms = () => { diff --git a/src/ts/core/work/financial/period.ts b/src/ts/core/work/financial/period.ts index 0ccc9e526114a9415e9bcdc2058819341aab40af..8e7a3983028b6c17e6f739804c22cb2c881a0421 100644 --- a/src/ts/core/work/financial/period.ts +++ b/src/ts/core/work/financial/period.ts @@ -179,10 +179,17 @@ export class Period extends Entity implements IPeriod { if (closings.filter((item) => !item.balanced).length > 0) { throw new Error('存在不平项!'); } + await this.unlockThings(); await this.financial.createSnapshots(this.period); await this.update({ ...this.metadata, closed: true }); await this.financial.createPeriod(this.getNextPeriod()); } + private async unlockThings(): Promise { + return await this.space.resource.thingColl.updateMatch( + { 'locks.exclusion.id': this.metadata.id }, + { _unset_: { locks: '' } }, + ); + } async reverseSettlement(): Promise { if (this.closed) { throw new Error('本月已结账,无法反结账!'); diff --git a/src/ts/core/work/lock.ts b/src/ts/core/work/lock.ts new file mode 100644 index 0000000000000000000000000000000000000000..4288bf2e951653d9b8ecc5e4873d9be1b613ada6 --- /dev/null +++ b/src/ts/core/work/lock.ts @@ -0,0 +1,72 @@ +import { model, schema } from '@/ts/base'; +import { ITarget } from '../target/base/target'; +import { XCollection } from '../public/collection'; + +interface Operate { + coll: XCollection; + ids: string[]; +} + +export class Lock { + target: ITarget; + instance: model.InstanceDataModel; + forms: schema.XForm[] = []; + + constructor(target: ITarget, instance: model.InstanceDataModel) { + this.target = target; + this.instance = instance; + this.forms = [...this.instance.node.primaryForms, ...this.instance.node.detailForms]; + } + + /** + * 遍历表单数据 + * @param operate 修改数据的回调函数 + * @returns 返回是否成功 + */ + private async ergodic(operate: (operate: Operate) => Promise) { + try { + for (const form of this.forms) { + const info = (this.instance.node.forms ?? []).find((item) => item.id == form.id); + if (info?.closeLock) { + continue; + } + const after = this.instance.data[form.id]?.at(-1)?.after; + if (after) { + if (this.target.hasDataAuth()) { + const coll = form.collName ?? '_system-things'; + await operate({ + coll: this.target.resource.genColl(coll), + ids: after.map((item) => item.id), + }); + } + } + } + return true; + } catch (e) { + return false; + } + } + + /** + * 排他锁 + * @param instance 业务实例 + * @returns 返回是否成功 + */ + async exclusionLock(instance: schema.Business) { + return await this.ergodic((operate) => { + return operate.coll.updateMany(operate.ids, { + _set_: { 'locks.exclusion': instance }, + }); + }); + } + + /** + * 解锁 + * @returns 返回是否成功 + */ + async unlock() { + return await this.ergodic((operate) => { + return operate.coll.updateMany(operate.ids, { _unset_: { locks: '' } }); + }); + } +} diff --git a/src/ts/core/work/task.ts b/src/ts/core/work/task.ts index db4b8c17e1f46e90ab329e0f5e72fa5e4b89bc8c..23b13e07fa65d355125b9d99149060911edfe911 100644 --- a/src/ts/core/work/task.ts +++ b/src/ts/core/work/task.ts @@ -13,6 +13,7 @@ import { Webhook } from './executor/webhook'; import message from '@/utils/message'; import { AcquireExecutor } from '@/ts/base/model'; import { ReceptionChange } from './executor/reception'; +import { Lock } from './lock'; export type TaskTypeName = '待办' | '已办' | '抄送' | '已发起' | '已完结' | '草稿'; export interface IWorkTask extends IFile { @@ -207,6 +208,9 @@ export class WorkTask extends FileInfo implements IWorkTask { if ((await this.loadInstance()) && this.instance) { if (this.instance.createUser === this.belong.userId) { if (await kernel.recallWorkInstance({ id: this.instance.id })) { + if (this.instanceData) { + await new Lock(this.belong, this.instanceData).unlock(); + } return true; } } diff --git a/src/ts/scripting/core/graph/CalcRefGraph.ts b/src/ts/scripting/core/graph/CalcRefGraph.ts index e4b543c7415e86e4983093079ece18e2be48f234..8ed6a33fc7c335c0fb7dff5aa87e05cd721908d1 100644 --- a/src/ts/scripting/core/graph/CalcRefGraph.ts +++ b/src/ts/scripting/core/graph/CalcRefGraph.ts @@ -57,10 +57,6 @@ export default class CalcRefGraph extends RefGraphBase<'calc'> { this.createNodeByRules(rules, formId); } - for (const [formId, rules] of Object.entries(this.rules.detail)) { - this.createNodeByRules(rules, formId); - } - const allRules: { calc?: NodeCalcRule[]; code?: NodeCodeRule[]; diff --git a/src/ts/scripting/core/services/FormService.ts b/src/ts/scripting/core/services/FormService.ts index 76e77b6d854f8d3a019844a0f606914831432e6b..f1aed6ded3ac27c6d58bb11fd2f057d1ff4d78a0 100644 --- a/src/ts/scripting/core/services/FormService.ts +++ b/src/ts/scripting/core/services/FormService.ts @@ -54,6 +54,7 @@ export async function setDefaultField( switch (field.options?.defaultType) { case 'currentPeriod': + await belong.financial.loadContent(); thing[field.id] = belong?.financial.current; onChange(field.id, belong?.financial.current); break;