Skip to content

Commit ed9a4e2

Browse files
1myuanfit2-zhao
authored andcommitted
feat: prompt when leaving the approval process page
1 parent 262c626 commit ed9a4e2

6 files changed

Lines changed: 158 additions & 11 deletions

File tree

frontend/packages/web/src/components/business/crm-flow/components/canvas/flowCanvas.vue

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,24 @@
199199
graphController.value.render(cellsWithSelection);
200200
}
201201
202+
const canvasRef = ref<HTMLElement | null>(null);
203+
204+
function refreshCanvas() {
205+
nextTick(() => {
206+
const graph = graphController.value?.getGraph();
207+
const resizeTarget = flowCanvasRef.value ?? canvasRef.value;
208+
if (!graph || !resizeTarget) {
209+
return;
210+
}
211+
212+
const { clientWidth, clientHeight } = resizeTarget;
213+
if (clientWidth && clientHeight) {
214+
graph.resize(clientWidth, clientHeight);
215+
}
216+
renderFlow();
217+
});
218+
}
219+
202220
function updateRenderedNodeState() {
203221
const graph = graphController.value?.getGraph();
204222
if (!graph) {
@@ -247,7 +265,6 @@
247265
}
248266
249267
let hasAutoFitted = false;
250-
const canvasRef = ref<HTMLElement | null>(null);
251268
252269
function fitAfterInit() {
253270
if (!graphController.value || !canvasRef.value || hasAutoFitted) {
@@ -383,6 +400,10 @@
383400
graphController.value = null;
384401
closeAddPopover();
385402
});
403+
404+
defineExpose({
405+
refreshCanvas,
406+
});
386407
</script>
387408

388409
<style scoped lang="less">

frontend/packages/web/src/components/business/crm-flow/index.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<div class="crm-flow relative flex h-full w-full">
33
<div class="crm-flow__main relative flex-1 overflow-hidden">
44
<FlowCanvas
5+
ref="flowCanvasRef"
56
:readonly="props.readonly"
67
:flow="flow"
78
:selection="selection"
@@ -25,7 +26,7 @@
2526
</template>
2627

2728
<script setup lang="ts">
28-
import { computed, useSlots } from 'vue';
29+
import { computed, ref, useSlots } from 'vue';
2930
3031
import { useI18n } from '@lib/shared/hooks/useI18n';
3132
@@ -114,9 +115,16 @@
114115
});
115116
}
116117
118+
const flowCanvasRef = ref<InstanceType<typeof FlowCanvas> | null>(null);
119+
120+
function refreshCanvas() {
121+
flowCanvasRef.value?.refreshCanvas();
122+
}
123+
117124
defineExpose({
118125
flow,
119126
selectNode,
127+
refreshCanvas,
120128
});
121129
</script>
122130

frontend/packages/web/src/components/business/crm-process-drawer/index.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
:footer="false"
77
:closable="false"
88
:close-on-esc="false"
9+
:mask-closable="false"
910
:loading="loading"
1011
header-class="crm-process-drawer-header"
1112
body-content-class="!p-0"
12-
@cancel="handleCancel"
1313
>
1414
<template #header>
1515
<div class="crm-process-drawer-header-content">
@@ -101,7 +101,6 @@
101101
102102
function handleCancel() {
103103
emit('cancel');
104-
visible.value = false;
105104
}
106105
107106
watchEffect(() => {

frontend/packages/web/src/views/system/process/process/components/addProcessDrawer.vue

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
:tabList="tabList"
66
:loading="loading"
77
:readonly="isDetail"
8+
@pointerdown.capture="handleUserInteraction"
9+
@keydown.capture="handleUserInteraction"
810
@save="handleSave"
911
@next-step="handleNextStep"
1012
@cancel="handleCancel"
@@ -41,6 +43,7 @@
4143
:need-detail="!!props.sourceId"
4244
:readonly="isDetail"
4345
:option-map="detailOptionMap"
46+
@change="markUnsaved"
4447
@switch-more-setting="activeTab = 'moreSetting'"
4548
/>
4649
<moreSetting
@@ -121,13 +124,54 @@
121124
},
122125
];
123126
124-
function handleCancel() {
127+
const unsaved = ref(false);
128+
const userInteracted = ref(false); // 防止没编辑就弹出提示
129+
const loading = ref(false);
130+
131+
function markUnsaved() {
132+
if (!props.readonly && !props.isDetail && userInteracted.value) {
133+
unsaved.value = true;
134+
}
135+
}
136+
137+
function handleUserInteraction() {
138+
if (!props.readonly && !props.isDetail) {
139+
userInteracted.value = true;
140+
}
141+
}
142+
143+
function closeDrawer() {
144+
unsaved.value = false;
145+
userInteracted.value = false;
125146
visible.value = false;
126147
form.value = cloneDeep(initForm);
127148
detailOptionMap.value = {};
128149
emit('cancel');
129150
}
130151
152+
function showUnsavedLeaveTip() {
153+
openModal({
154+
type: 'warning',
155+
title: t('common.unSaveLeaveTitle'),
156+
content: t('common.editUnsavedLeave'),
157+
positiveText: t('common.confirm'),
158+
negativeText: t('common.cancel'),
159+
onPositiveClick: async () => {
160+
closeDrawer();
161+
},
162+
});
163+
}
164+
165+
function handleCancel() {
166+
if (!loading.value) {
167+
if (unsaved.value) {
168+
showUnsavedLeaveTip();
169+
} else {
170+
closeDrawer();
171+
}
172+
}
173+
}
174+
131175
function handleNextStep() {
132176
const index = tabList.findIndex((item) => item.name === activeTab.value);
133177
if (index === tabList.length - 1) {
@@ -136,8 +180,6 @@
136180
activeTab.value = tabList[index + 1].name;
137181
}
138182
139-
const loading = ref(false);
140-
141183
async function handleSubmit() {
142184
try {
143185
loading.value = true;
@@ -157,7 +199,7 @@
157199
Message.success(t('common.addSuccess'));
158200
}
159201
emit('refresh');
160-
handleCancel();
202+
closeDrawer();
161203
} catch (error) {
162204
// eslint-disable-next-line no-console
163205
console.log(error);
@@ -173,9 +215,14 @@
173215
title: t('common.saveFailed'),
174216
positiveText: t('process.process.flow.toConfig'),
175217
content: t('process.process.flow.nodeNameNotSet'),
176-
onPositiveClick: async () => undefined,
218+
negativeText: t('common.cancel'),
219+
onPositiveClick: async () => {
220+
activeTab.value = 'process';
221+
nextTick(() => {
222+
approvalFlowDesignRef.value?.refreshCanvas();
223+
});
224+
},
177225
});
178-
activeTab.value = 'process';
179226
return;
180227
}
181228
@@ -207,6 +254,10 @@
207254
editingName.value = result.name;
208255
nextTick(() => {
209256
approvalFlowDesignRef.value?.setProcessData(result.nodes ?? [], result.links ?? []);
257+
nextTick(() => {
258+
unsaved.value = false;
259+
userInteracted.value = false;
260+
});
210261
});
211262
} catch (error) {
212263
// eslint-disable-next-line no-console
@@ -270,9 +321,26 @@
270321
(val) => {
271322
if (!val) {
272323
activeTab.value = 'process';
324+
userInteracted.value = false;
325+
return;
326+
}
327+
328+
if (!props.sourceId) {
329+
unsaved.value = false;
330+
userInteracted.value = false;
273331
}
274332
}
275333
);
334+
335+
watch(
336+
() => [form.value.basicConfig, form.value.moreConfig],
337+
() => {
338+
markUnsaved();
339+
},
340+
{
341+
deep: true,
342+
}
343+
);
276344
</script>
277345

278346
<style lang="less">

frontend/packages/web/src/views/system/process/process/components/approval-flow/approval-node/tabs/afterApprovalTab.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
</n-form-item>
5757
</div>
5858
<n-form-item :path="`${index}.enable`">
59-
<n-switch v-model:value="line.enable" :disabled="props.readonly" />
59+
<n-switch v-model:value="line.enable" :disabled="isFieldUpdateSwitchDisabled(line)" />
6060
</n-form-item>
6161
<n-button ghost class="px-[7px]" :disabled="props.readonly" @click="handleDeleteFieldUpdate(index)">
6262
<template #icon>
@@ -348,6 +348,18 @@
348348
return fieldType && isSupportedFieldValueType(fieldType) ? fieldValueComponentMap[fieldType] : null;
349349
}
350350
351+
function isEmptyFieldValue(value: unknown) {
352+
if (Array.isArray(value)) {
353+
return !value.length;
354+
}
355+
356+
return value === null || value === undefined || value === '';
357+
}
358+
359+
function isFieldUpdateSwitchDisabled(line: ApprovalFieldUpdateConfig) {
360+
return props.readonly || !line.fieldId || isEmptyFieldValue(line.fieldValue);
361+
}
362+
351363
// 切换左侧字段时,右侧值按批量编辑逻辑重置成该字段自己的默认值
352364
function handleFieldUpdate(line: ApprovalFieldUpdateConfig, fieldId: string) {
353365
if (props.readonly) {

frontend/packages/web/src/views/system/process/process/components/approval-flow/index.vue

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
106106
const emit = defineEmits<{
107107
(event: 'switchMoreSetting'): void;
108+
(event: 'change'): void;
108109
}>();
109110
const { t } = useI18n();
110111
@@ -169,6 +170,10 @@
169170
});
170171
}
171172
173+
function refreshCanvas() {
174+
crmFlowRef.value?.refreshCanvas();
175+
}
176+
172177
onMounted(() => {
173178
nextTick(() => {
174179
selectStartNodeOnInit();
@@ -253,11 +258,45 @@
253258
}
254259
);
255260
261+
function resetToDefaultFlow() {
262+
setConditionDrawerVisible.value = false;
263+
activeConditionBranch.value = null;
264+
flowSchema.value = createDefaultFlow(startNodeDescription.value);
265+
266+
nextTick(() => {
267+
selectStartNodeOnInit();
268+
});
269+
}
270+
271+
watch(
272+
() => basicConfig.value.formType,
273+
(formType, oldFormType) => {
274+
if (props.readonly || !oldFormType || formType === oldFormType) {
275+
return;
276+
}
277+
278+
resetToDefaultFlow();
279+
}
280+
);
281+
282+
watch(
283+
flowSchema,
284+
() => {
285+
if (!props.readonly) {
286+
emit('change');
287+
}
288+
},
289+
{
290+
deep: true,
291+
}
292+
);
293+
256294
defineExpose({
257295
validateFlowNodes,
258296
getProcessNodes,
259297
getProcessLinks,
260298
setProcessData,
299+
refreshCanvas,
261300
});
262301
</script>
263302

0 commit comments

Comments
 (0)