fix: merge yudao/dev 解决冲突
This commit is contained in:
@@ -1,13 +1,9 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmCategoryApi } from '#/api/bpm/category';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
@@ -106,9 +102,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
@@ -146,29 +140,10 @@ export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程分类',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['bpm:category:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['bpm:category:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmCategoryApi } from '#/api/bpm/category';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteCategory, getCategoryPage } from '#/api/bpm/category';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
@@ -22,12 +18,46 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建流程分类 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑流程分类 */
|
||||
function handleEdit(row: BpmCategoryApi.CategoryVO) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除流程分类 */
|
||||
async function handleDelete(row: BpmCategoryApi.CategoryVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.code]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteCategory(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.code]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
@@ -50,54 +80,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeTableGridOptions<BpmCategoryApi.CategoryVO>,
|
||||
});
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<BpmCategoryApi.CategoryVO>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建流程分类 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑流程分类 */
|
||||
function onEdit(row: BpmCategoryApi.CategoryVO) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除流程分类 */
|
||||
async function onDelete(row: BpmCategoryApi.CategoryVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.code]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteCategory(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -109,14 +91,41 @@ async function onDelete(row: BpmCategoryApi.CategoryVO) {
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="流程分类">
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['bpm:category:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['流程分类']) }}
|
||||
</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['流程分类']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['bpm:category:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['bpm:category:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['bpm:category:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmFormApi } from '#/api/bpm/form';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { $t } from '@vben/locales';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
@@ -68,9 +62,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmFormApi.FormVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
@@ -103,41 +95,10 @@ export function useGridColumns<T = BpmFormApi.FormVO>(
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 200,
|
||||
align: 'center',
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程名称',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'copy',
|
||||
text: $t('ui.actionTitle.copy'),
|
||||
show: hasAccessByCodes(['bpm:form:update']),
|
||||
},
|
||||
{
|
||||
code: 'edit',
|
||||
text: $t('ui.actionTitle.edit'),
|
||||
show: hasAccessByCodes(['bpm:form:update']),
|
||||
},
|
||||
{
|
||||
code: 'detail',
|
||||
text: $t('ui.actionTitle.detail'),
|
||||
show: hasAccessByCodes(['bpm:form:query']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
text: $t('ui.actionTitle.delete'),
|
||||
show: hasAccessByCodes(['bpm:form:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ async function initializeDesigner() {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO @ziye:注释使用 /** */ 风格,高亮更明显哈,方法注释;
|
||||
// 保存表单
|
||||
function handleSave() {
|
||||
formModalApi
|
||||
|
||||
@@ -1,36 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmFormApi } from '#/api/bpm/form';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import FormCreate from '@form-create/ant-design-vue';
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteForm, getFormDetail, getFormPage } from '#/api/bpm/form';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteForm, getFormPage } from '#/api/bpm/form';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { router } from '#/router';
|
||||
import { setConfAndFields2 } from '#/utils';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
|
||||
defineOptions({ name: 'BpmForm' });
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleCreate() {
|
||||
router.push({
|
||||
name: 'BpmFormEditor',
|
||||
query: {
|
||||
type: 'create',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit(row: BpmFormApi.FormVO) {
|
||||
router.push({
|
||||
name: 'BpmFormEditor',
|
||||
query: {
|
||||
id: row.id,
|
||||
type: 'edit',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 复制 */
|
||||
function handleCopy(row: BpmFormApi.FormVO) {
|
||||
router.push({
|
||||
name: 'BpmFormEditor',
|
||||
query: {
|
||||
copyId: row.id,
|
||||
type: 'copy',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function handleDelete(row: BpmFormApi.FormVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteForm(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
async function handleDetail(row: BpmFormApi.FormVO) {
|
||||
detailModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 详情弹窗 */
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: Detail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 检测路由参数 */
|
||||
const route = useRoute();
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
@@ -57,101 +119,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
} as VxeTableGridOptions<BpmFormApi.FormVO>,
|
||||
});
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<BpmFormApi.FormVO>) {
|
||||
switch (code) {
|
||||
case 'copy': {
|
||||
onCopy(row);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 复制 */
|
||||
function onCopy(row: BpmFormApi.FormVO) {
|
||||
router.push({
|
||||
name: 'BpmFormEditor',
|
||||
query: {
|
||||
copyId: row.id,
|
||||
type: 'copy',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
async function onDelete(row: BpmFormApi.FormVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteForm(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 详情 */
|
||||
const formConfig = ref<any>({});
|
||||
async function onDetail(row: BpmFormApi.FormVO) {
|
||||
formConfig.value = await getFormDetail(row.id as number);
|
||||
|
||||
setConfAndFields2(
|
||||
formConfig.value,
|
||||
formConfig.value.conf,
|
||||
formConfig.value.fields,
|
||||
);
|
||||
detailModalApi.open();
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function onEdit(row: BpmFormApi.FormVO) {
|
||||
router.push({
|
||||
name: 'BpmFormEditor',
|
||||
query: {
|
||||
id: row.id,
|
||||
type: 'edit',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function onCreate() {
|
||||
router.push({
|
||||
name: 'BpmFormEditor',
|
||||
query: {
|
||||
type: 'create',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 详情弹窗 */
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
footer: false,
|
||||
});
|
||||
|
||||
/** 检测路由参数 */
|
||||
const route = useRoute();
|
||||
watch(
|
||||
() => route.query.refresh,
|
||||
(val) => {
|
||||
@@ -171,25 +138,60 @@ watch(
|
||||
url="https://doc.iocoder.cn/bpm/use-bpm-form/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<DetailModal />
|
||||
<Grid table-title="流程表单">
|
||||
<template #toolbar-tools>
|
||||
<Button type="primary" @click="onCreate">
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['流程表单']) }}
|
||||
</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['流程表单']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['bpm:form:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.copy'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.COPY,
|
||||
auth: ['bpm:form:update'],
|
||||
onClick: handleCopy.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['bpm:form:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['bpm:form:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['bpm:form:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<DetailModal
|
||||
title="流程表单详情"
|
||||
class="w-[800px]"
|
||||
:body-style="{
|
||||
maxHeight: '100px',
|
||||
}"
|
||||
>
|
||||
<div class="mx-4">
|
||||
<FormCreate :option="formConfig.option" :rule="formConfig.rule" />
|
||||
</div>
|
||||
</DetailModal>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
50
apps/web-antd/src/views/bpm/form/modules/detail.vue
Normal file
50
apps/web-antd/src/views/bpm/form/modules/detail.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import FormCreate from '@form-create/ant-design-vue';
|
||||
|
||||
import { getFormDetail } from '#/api/bpm/form';
|
||||
import { setConfAndFields2 } from '#/utils';
|
||||
|
||||
/** 详情 */
|
||||
const formConfig = ref<any>({});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
footer: false,
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formConfig.value = await getFormDetail(data.id as number);
|
||||
setConfAndFields2(
|
||||
formConfig.value,
|
||||
formConfig.value.conf,
|
||||
formConfig.value.fields,
|
||||
);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
class="w-[40%]"
|
||||
title="流程表单详情"
|
||||
:body-style="{
|
||||
maxHeight: '100px',
|
||||
}"
|
||||
>
|
||||
<FormCreate :option="formConfig.option" :rule="formConfig.rule" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -106,7 +106,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-[600px]">
|
||||
<Modal :title="getTitle" class="w-[40%]">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmCategoryApi } from '#/api/bpm/category';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
@@ -99,10 +95,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
getMemberNames: (userIds: number[]) => string,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
@@ -123,9 +116,7 @@ export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
||||
field: 'userIds',
|
||||
title: '成员',
|
||||
minWidth: 200,
|
||||
formatter: ({ cellValue }) => {
|
||||
return getMemberNames(cellValue);
|
||||
},
|
||||
slots: { default: 'userIds' },
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
@@ -143,29 +134,10 @@ export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '用户分组',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['bpm:user-group:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['bpm:user-group:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmUserGroupApi } from '#/api/bpm/userGroup';
|
||||
import type { SystemUserApi } from '#/api/system/user';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { message, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteUserGroup, getUserGroupPage } from '#/api/bpm/userGroup';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
@@ -26,12 +22,53 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建用户分组 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑用户分组 */
|
||||
function handleEdit(row: BpmUserGroupApi.UserGroupVO) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除用户分组 */
|
||||
async function handleDelete(row: BpmUserGroupApi.UserGroupVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteUserGroup(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const userList = ref<SystemUserApi.User[]>([]);
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 加载用户列表
|
||||
userList.value = await getSimpleUserList();
|
||||
});
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick, getMemberNames),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
@@ -54,73 +91,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeTableGridOptions<BpmUserGroupApi.UserGroupVO>,
|
||||
});
|
||||
|
||||
/** 获取分组成员姓名 */
|
||||
function getMemberNames(userIds: number[]) {
|
||||
const userMap = new Map(
|
||||
userList.value.map((user) => [user.id, user.nickname]),
|
||||
);
|
||||
return userIds
|
||||
.map((userId) => userMap.get(userId))
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<BpmUserGroupApi.UserGroupVO>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建用户分组 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑用户分组 */
|
||||
function onEdit(row: BpmUserGroupApi.UserGroupVO) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除用户分组 */
|
||||
async function onDelete(row: BpmUserGroupApi.UserGroupVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteUserGroup(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
const userList = ref<SystemUserApi.User[]>([]);
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 加载用户列表
|
||||
userList.value = await getSimpleUserList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -132,18 +102,46 @@ onMounted(async () => {
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="用户分组">
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['bpm:category:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['用户分组']) }}
|
||||
</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['用户分组']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['bpm:user-group:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #userIds-cell="{ row }">
|
||||
<span>{{ row.nicknames }}</span>
|
||||
<template #userIds="{ row }">
|
||||
<Tag v-for="userId in row.userIds" :key="userId" color="blue">
|
||||
{{ userList.find((u) => u.id === userId)?.nickname }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['bpm:user-group:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['bpm:user-group:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -64,7 +64,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
@@ -85,7 +84,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Modal :title="getTitle" class="w-[40%]">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -60,24 +60,24 @@ const processDesignRef = ref<InstanceType<typeof ProcessDesign>>();
|
||||
const extraSettingRef = ref<InstanceType<typeof ExtraSetting>>();
|
||||
|
||||
/** 步骤校验函数 */
|
||||
const validateBasic = async () => {
|
||||
async function validateBasic() {
|
||||
await basicInfoRef.value?.validate();
|
||||
};
|
||||
}
|
||||
|
||||
/** 表单设计校验 */
|
||||
const validateForm = async () => {
|
||||
async function validateForm() {
|
||||
await formDesignRef.value?.validate();
|
||||
};
|
||||
}
|
||||
|
||||
/** 流程设计校验 */
|
||||
const validateProcess = async () => {
|
||||
async function validateProcess() {
|
||||
await processDesignRef.value?.validate();
|
||||
};
|
||||
}
|
||||
|
||||
/** 更多设置校验 */
|
||||
const validateExtra = async () => {
|
||||
async function validateExtra() {
|
||||
await extraSettingRef.value?.validate();
|
||||
};
|
||||
}
|
||||
|
||||
const currentStep = ref(-1); // 步骤控制。-1 用于,一开始全部不展示等当前页面数据初始化完成
|
||||
|
||||
@@ -139,7 +139,7 @@ const deptList = ref<SystemDeptApi.Dept[]>([]);
|
||||
|
||||
/** 初始化数据 */
|
||||
const actionType = route.params.type as string;
|
||||
const initData = async () => {
|
||||
async function initData() {
|
||||
if (actionType === 'definition') {
|
||||
// 情况一:流程定义场景(恢复)
|
||||
const definitionId = route.params.id as string;
|
||||
@@ -200,7 +200,7 @@ const initData = async () => {
|
||||
|
||||
// 以前未配置更多设置的流程
|
||||
extraSettingRef.value?.initData();
|
||||
};
|
||||
}
|
||||
|
||||
/** 根据类型切换流程数据 */
|
||||
watch(
|
||||
@@ -218,7 +218,7 @@ watch(
|
||||
);
|
||||
|
||||
/** 校验所有步骤数据是否完整 */
|
||||
const validateAllSteps = async () => {
|
||||
async function validateAllSteps() {
|
||||
// 基本信息校验
|
||||
try {
|
||||
await validateBasic();
|
||||
@@ -254,10 +254,10 @@ const validateAllSteps = async () => {
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/** 保存操作 */
|
||||
const handleSave = async () => {
|
||||
async function handleSave() {
|
||||
try {
|
||||
// 保存前校验所有步骤的数据
|
||||
const result = await validateAllSteps();
|
||||
@@ -311,10 +311,10 @@ const handleSave = async () => {
|
||||
console.error('保存失败:', error);
|
||||
// message.warning(error.msg || '请完善所有步骤的必填信息');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 发布操作 */
|
||||
const handleDeploy = async () => {
|
||||
async function handleDeploy() {
|
||||
try {
|
||||
// 修改场景下直接发布,新增场景下需要先确认
|
||||
if (!formData.value.id) {
|
||||
@@ -344,10 +344,10 @@ const handleDeploy = async () => {
|
||||
console.error('发布失败:', error);
|
||||
message.warning(error.message || '发布失败');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 步骤切换处理 */
|
||||
const handleStepClick = async (index: number) => {
|
||||
async function handleStepClick(index: number) {
|
||||
try {
|
||||
if (index !== 0) {
|
||||
await validateBasic();
|
||||
@@ -369,17 +369,17 @@ const handleStepClick = async (index: number) => {
|
||||
message.warning('请先完善当前步骤必填信息');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const tabs = useTabs();
|
||||
|
||||
/** 返回列表页 */
|
||||
const handleBack = () => {
|
||||
function handleBack() {
|
||||
// 关闭当前页签
|
||||
tabs.closeCurrentTab();
|
||||
// 跳转到列表页,使用路径, 目前后端的路由 name: 'name'+ menuId
|
||||
router.push({ path: '/bpm/manager/model' });
|
||||
};
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
@@ -397,11 +397,9 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="mx-auto">
|
||||
<!-- 头部导航栏 -->
|
||||
<div
|
||||
class="absolute inset-x-0 top-0 z-10 flex h-12 items-center border-b bg-white px-5"
|
||||
>
|
||||
<!-- 主体内容 -->
|
||||
<Card class="mb-4">
|
||||
<template #title>
|
||||
<!-- 左侧标题 -->
|
||||
<div class="flex w-[200px] items-center overflow-hidden">
|
||||
<ArrowLeft
|
||||
@@ -415,88 +413,82 @@ onBeforeUnmount(() => {
|
||||
{{ formData.name || '创建流程' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 步骤条 -->
|
||||
<div class="flex h-full flex-1 items-center justify-center">
|
||||
<div class="flex h-full w-auto items-center justify-center">
|
||||
</template>
|
||||
<template #extra>
|
||||
<Button
|
||||
v-if="actionType === 'update'"
|
||||
type="primary"
|
||||
@click="handleDeploy"
|
||||
>
|
||||
发 布
|
||||
</Button>
|
||||
<Button type="primary" @click="handleSave">
|
||||
<span v-if="actionType === 'definition'">恢 复</span>
|
||||
<span v-else>保 存</span>
|
||||
</Button>
|
||||
</template>
|
||||
<!-- 步骤条 -->
|
||||
<div class="flex h-full flex-1 items-center justify-center">
|
||||
<div class="flex h-full w-auto items-center justify-center">
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="relative mx-6 flex h-full cursor-pointer items-center"
|
||||
:class="[
|
||||
currentStep === index
|
||||
? 'border-b-2 border-solid border-blue-500 text-blue-500'
|
||||
: 'text-gray-500',
|
||||
]"
|
||||
@click="handleStepClick(index)"
|
||||
>
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="relative mx-6 flex h-full cursor-pointer items-center"
|
||||
class="mr-2 flex h-7 w-7 items-center justify-center rounded-full border-2 border-solid text-[15px]"
|
||||
:class="[
|
||||
currentStep === index
|
||||
? 'border-b-2 border-solid border-blue-500 text-blue-500'
|
||||
: 'text-gray-500',
|
||||
? 'border-blue-500 bg-blue-500 text-white'
|
||||
: 'border-gray-300 bg-white text-gray-500',
|
||||
]"
|
||||
@click="handleStepClick(index)"
|
||||
>
|
||||
<div
|
||||
class="mr-2 flex h-7 w-7 items-center justify-center rounded-full border-2 border-solid text-[15px]"
|
||||
:class="[
|
||||
currentStep === index
|
||||
? 'border-blue-500 bg-blue-500 text-white'
|
||||
: 'border-gray-300 bg-white text-gray-500',
|
||||
]"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<span class="whitespace-nowrap text-base font-bold">{{
|
||||
step.title
|
||||
}}</span>
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<span class="whitespace-nowrap text-base font-bold">{{
|
||||
step.title
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧按钮 -->
|
||||
<div class="flex w-[200px] items-center justify-end gap-2">
|
||||
<Button
|
||||
v-if="actionType === 'update'"
|
||||
type="primary"
|
||||
@click="handleDeploy"
|
||||
>
|
||||
发 布
|
||||
</Button>
|
||||
<Button type="primary" @click="handleSave">
|
||||
<span v-if="actionType === 'definition'">恢 复</span>
|
||||
<span v-else>保 存</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主体内容 -->
|
||||
<Card :body-style="{ padding: '10px' }" class="mb-4">
|
||||
<div class="mt-[50px]">
|
||||
<!-- 第一步:基本信息 -->
|
||||
<div v-if="currentStep === 0" class="mx-auto w-4/6">
|
||||
<BasicInfo
|
||||
v-model="formData"
|
||||
:category-list="categoryList"
|
||||
:user-list="userList"
|
||||
:dept-list="deptList"
|
||||
ref="basicInfoRef"
|
||||
/>
|
||||
</div>
|
||||
<!-- 第二步:表单设计 -->
|
||||
<div v-if="currentStep === 1" class="mx-auto w-4/6">
|
||||
<FormDesign
|
||||
v-model="formData"
|
||||
:form-list="formList"
|
||||
ref="formDesignRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 第三步:流程设计 -->
|
||||
<ProcessDesign
|
||||
v-if="currentStep === 2"
|
||||
<div class="mt-[50px]">
|
||||
<!-- 第一步:基本信息 -->
|
||||
<div v-if="currentStep === 0" class="mx-auto w-4/6">
|
||||
<BasicInfo
|
||||
v-model="formData"
|
||||
ref="processDesignRef"
|
||||
:category-list="categoryList"
|
||||
:user-list="userList"
|
||||
:dept-list="deptList"
|
||||
ref="basicInfoRef"
|
||||
/>
|
||||
|
||||
<!-- 第四步:更多设置 -->
|
||||
<div v-if="currentStep === 3" class="mx-auto w-4/6">
|
||||
<ExtraSetting v-model="formData" ref="extraSettingRef" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<!-- 第二步:表单设计 -->
|
||||
<div v-if="currentStep === 1" class="mx-auto w-4/6">
|
||||
<FormDesign
|
||||
v-model="formData"
|
||||
:form-list="formList"
|
||||
ref="formDesignRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 第三步:流程设计 -->
|
||||
<ProcessDesign
|
||||
v-if="currentStep === 2"
|
||||
v-model="formData"
|
||||
ref="processDesignRef"
|
||||
/>
|
||||
|
||||
<!-- 第四步:更多设置 -->
|
||||
<div v-if="currentStep === 3" class="mx-auto w-4/6">
|
||||
<ExtraSetting v-model="formData" ref="extraSettingRef" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { SystemUserApi } from '#/api/system/user';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { CircleHelp, IconifyIcon, Plus, X } from '@vben/icons';
|
||||
|
||||
import {
|
||||
@@ -41,6 +42,16 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const [UserSelectModalComp, userSelectModalApi] = useVbenModal({
|
||||
connectedComponent: UserSelectModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DeptSelectModalComp, deptSelectModalApi] = useVbenModal({
|
||||
connectedComponent: DeptSelectModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref();
|
||||
|
||||
@@ -52,8 +63,6 @@ const selectedStartDepts = ref<SystemDeptApi.Dept[]>([]);
|
||||
|
||||
// 选中的流程管理员
|
||||
const selectedManagerUsers = ref<SystemUserApi.User[]>([]);
|
||||
const userSelectFormRef = ref();
|
||||
const deptSelectFormRef = ref();
|
||||
const currentSelectType = ref<'manager' | 'start'>('start');
|
||||
// 选中的用户
|
||||
const selectedUsers = ref<number[]>();
|
||||
@@ -98,37 +107,37 @@ watch(
|
||||
);
|
||||
|
||||
/** 打开发起人选择 */
|
||||
const openStartUserSelect = () => {
|
||||
function openStartUserSelect() {
|
||||
currentSelectType.value = 'start';
|
||||
selectedUsers.value = selectedStartUsers.value.map(
|
||||
(user) => user.id,
|
||||
) as number[];
|
||||
userSelectFormRef.value.open(selectedUsers.value);
|
||||
};
|
||||
userSelectModalApi.setData({ userIds: selectedUsers.value }).open();
|
||||
}
|
||||
|
||||
/** 打开部门选择 */
|
||||
const openStartDeptSelect = () => {
|
||||
deptSelectFormRef.value.open(selectedStartDepts.value);
|
||||
};
|
||||
function openStartDeptSelect() {
|
||||
deptSelectModalApi.setData({ selectedList: selectedStartDepts.value }).open();
|
||||
}
|
||||
/** 处理部门选择确认 */
|
||||
const handleDeptSelectConfirm = (depts: SystemDeptApi.Dept[]) => {
|
||||
function handleDeptSelectConfirm(depts: SystemDeptApi.Dept[]) {
|
||||
modelData.value = {
|
||||
...modelData.value,
|
||||
startDeptIds: depts.map((d) => d.id),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** 打开管理员选择 */
|
||||
const openManagerUserSelect = () => {
|
||||
function openManagerUserSelect() {
|
||||
currentSelectType.value = 'manager';
|
||||
selectedUsers.value = selectedManagerUsers.value.map(
|
||||
(user) => user.id,
|
||||
) as number[];
|
||||
userSelectFormRef.value.open(selectedUsers.value);
|
||||
};
|
||||
userSelectModalApi.setData({ userIds: selectedUsers.value }).open();
|
||||
}
|
||||
|
||||
/** 处理用户选择确认 */
|
||||
const handleUserSelectConfirm = (userList: SystemUserApi.User[]) => {
|
||||
function handleUserSelectConfirm(userList: SystemUserApi.User[]) {
|
||||
modelData.value =
|
||||
currentSelectType.value === 'start'
|
||||
? {
|
||||
@@ -139,20 +148,20 @@ const handleUserSelectConfirm = (userList: SystemUserApi.User[]) => {
|
||||
...modelData.value,
|
||||
managerUserIds: userList.map((u) => u.id),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** 用户选择弹窗关闭 */
|
||||
const handleUserSelectClosed = () => {
|
||||
function handleUserSelectClosed() {
|
||||
selectedUsers.value = [];
|
||||
};
|
||||
}
|
||||
|
||||
/** 用户选择弹窗取消 */
|
||||
const handleUserSelectCancel = () => {
|
||||
function handleUserSelectCancel() {
|
||||
selectedUsers.value = [];
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理发起人类型变化 */
|
||||
const handleStartUserTypeChange = (value: SelectValue) => {
|
||||
function handleStartUserTypeChange(value: SelectValue) {
|
||||
const numValue = Number(value);
|
||||
switch (numValue) {
|
||||
case 0: {
|
||||
@@ -181,270 +190,266 @@ const handleStartUserTypeChange = (value: SelectValue) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 移除发起人 */
|
||||
const handleRemoveStartUser = (user: SystemUserApi.User) => {
|
||||
function handleRemoveStartUser(user: SystemUserApi.User) {
|
||||
modelData.value = {
|
||||
...modelData.value,
|
||||
startUserIds: modelData.value.startUserIds.filter(
|
||||
(id: number) => id !== user.id,
|
||||
),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** 移除部门 */
|
||||
const handleRemoveStartDept = (dept: SystemDeptApi.Dept) => {
|
||||
function handleRemoveStartDept(dept: SystemDeptApi.Dept) {
|
||||
modelData.value = {
|
||||
...modelData.value,
|
||||
startDeptIds: modelData.value.startDeptIds.filter(
|
||||
(id: number) => id !== dept.id,
|
||||
),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** 移除管理员 */
|
||||
const handleRemoveManagerUser = (user: SystemUserApi.User) => {
|
||||
function handleRemoveManagerUser(user: SystemUserApi.User) {
|
||||
modelData.value = {
|
||||
...modelData.value,
|
||||
managerUserIds: modelData.value.managerUserIds.filter(
|
||||
(id: number) => id !== user.id,
|
||||
),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** 表单校验 */
|
||||
const validate = async () => {
|
||||
async function validate() {
|
||||
await formRef.value?.validate();
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({ validate });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="modelData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
class="mt-5"
|
||||
>
|
||||
<Form.Item label="流程标识" name="key" class="mb-5">
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="modelData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
class="mt-5"
|
||||
>
|
||||
<Form.Item label="流程标识" name="key" class="mb-5">
|
||||
<div class="flex items-center">
|
||||
<Input
|
||||
class="w-full"
|
||||
v-model:value="modelData.key"
|
||||
:disabled="!!modelData.id"
|
||||
placeholder="请输入流程标识,以字母或下划线开头"
|
||||
/>
|
||||
<Tooltip
|
||||
:title="
|
||||
modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'
|
||||
"
|
||||
placement="top"
|
||||
>
|
||||
<CircleHelp class="ml-1 size-5 text-gray-900" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="流程名称" name="name" class="mb-5">
|
||||
<Input
|
||||
class="w-full"
|
||||
v-model:value="modelData.key"
|
||||
v-model:value="modelData.name"
|
||||
:disabled="!!modelData.id"
|
||||
placeholder="请输入流程标识,以字母或下划线开头"
|
||||
allow-clear
|
||||
placeholder="请输入流程名称"
|
||||
/>
|
||||
<Tooltip
|
||||
:title="
|
||||
modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'
|
||||
"
|
||||
placement="top"
|
||||
</Form.Item>
|
||||
<Form.Item label="流程分类" name="category" class="mb-5">
|
||||
<Select
|
||||
class="w-full"
|
||||
v-model:value="modelData.category"
|
||||
allow-clear
|
||||
placeholder="请选择流程分类"
|
||||
>
|
||||
<CircleHelp class="ml-1 size-5 text-gray-900" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="流程名称" name="name" class="mb-5">
|
||||
<Input
|
||||
v-model:value="modelData.name"
|
||||
:disabled="!!modelData.id"
|
||||
allow-clear
|
||||
placeholder="请输入流程名称"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="流程分类" name="category" class="mb-5">
|
||||
<Select
|
||||
class="w-full"
|
||||
v-model:value="modelData.category"
|
||||
allow-clear
|
||||
placeholder="请选择流程分类"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="category in categoryList"
|
||||
:key="category.code"
|
||||
:value="category.code"
|
||||
<Select.Option
|
||||
v-for="category in categoryList"
|
||||
:key="category.code"
|
||||
:value="category.code"
|
||||
>
|
||||
{{ category.name }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="流程图标" class="mb-5">
|
||||
<ImageUpload v-model:value="modelData.icon" />
|
||||
</Form.Item>
|
||||
<Form.Item label="流程描述" name="description" class="mb-5">
|
||||
<Input.TextArea v-model:value="modelData.description" allow-clear />
|
||||
</Form.Item>
|
||||
<Form.Item label="流程类型" name="type" class="mb-5">
|
||||
<Radio.Group v-model:value="modelData.type">
|
||||
<!-- TODO BPMN 流程类型需要整合,暂时禁用 -->
|
||||
<Radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
:disabled="dict.value === 10"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否可见" name="visible" class="mb-5">
|
||||
<Radio.Group v-model:value="modelData.visible">
|
||||
<Radio
|
||||
v-for="(dict, index) in getBoolDictOptions(
|
||||
DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
)"
|
||||
:key="index"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="谁可以发起" name="startUserType" class="mb-5">
|
||||
<Select
|
||||
v-model:value="modelData.startUserType"
|
||||
placeholder="请选择谁可以发起"
|
||||
@change="handleStartUserTypeChange"
|
||||
>
|
||||
{{ category.name }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="流程图标" class="mb-5">
|
||||
<ImageUpload v-model:value="modelData.icon" />
|
||||
</Form.Item>
|
||||
<Form.Item label="流程描述" name="description" class="mb-5">
|
||||
<Input.TextArea v-model:value="modelData.description" allow-clear />
|
||||
</Form.Item>
|
||||
<Form.Item label="流程类型" name="type" class="mb-5">
|
||||
<Radio.Group v-model:value="modelData.type">
|
||||
<!-- TODO BPMN 流程类型需要整合,暂时禁用 -->
|
||||
<Radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
:disabled="dict.value === 10"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否可见" name="visible" class="mb-5">
|
||||
<Radio.Group v-model:value="modelData.visible">
|
||||
<Radio
|
||||
v-for="(dict, index) in getBoolDictOptions(
|
||||
DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
)"
|
||||
:key="index"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="谁可以发起" name="startUserType" class="mb-5">
|
||||
<Select
|
||||
v-model:value="modelData.startUserType"
|
||||
placeholder="请选择谁可以发起"
|
||||
@change="handleStartUserTypeChange"
|
||||
>
|
||||
<Select.Option :value="0">全员</Select.Option>
|
||||
<Select.Option :value="1">指定人员</Select.Option>
|
||||
<Select.Option :value="2">指定部门</Select.Option>
|
||||
</Select>
|
||||
<div
|
||||
v-if="modelData.startUserType === 1"
|
||||
class="mt-2 flex flex-wrap gap-2"
|
||||
>
|
||||
<Select.Option :value="0">全员</Select.Option>
|
||||
<Select.Option :value="1">指定人员</Select.Option>
|
||||
<Select.Option :value="2">指定部门</Select.Option>
|
||||
</Select>
|
||||
<div
|
||||
v-for="user in selectedStartUsers"
|
||||
:key="user.id"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2"
|
||||
v-if="modelData.startUserType === 1"
|
||||
class="mt-2 flex flex-wrap gap-2"
|
||||
>
|
||||
<Avatar
|
||||
class="m-1"
|
||||
:size="28"
|
||||
v-if="user.avatar"
|
||||
:src="user.avatar"
|
||||
/>
|
||||
<Avatar class="m-1" :size="28" v-else>
|
||||
{{ user.nickname?.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ user.nickname }}
|
||||
<X
|
||||
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
|
||||
@click="handleRemoveStartUser(user)"
|
||||
/>
|
||||
<div
|
||||
v-for="user in selectedStartUsers"
|
||||
:key="user.id"
|
||||
class="bg-destructive text-destructive-foreground hover:bg-destructive-hover relative flex h-9 items-center rounded-full pr-2"
|
||||
>
|
||||
<Avatar
|
||||
class="m-1"
|
||||
:size="28"
|
||||
v-if="user.avatar"
|
||||
:src="user.avatar"
|
||||
/>
|
||||
<Avatar class="m-1" :size="28" v-else>
|
||||
{{ user.nickname?.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ user.nickname }}
|
||||
<X
|
||||
class="ml-2 size-4 cursor-pointer"
|
||||
@click="handleRemoveStartUser(user)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
@click="openStartUserSelect"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon
|
||||
icon="mdi:account-plus-outline"
|
||||
class="size-[18px]"
|
||||
/>
|
||||
</template>
|
||||
选择人员
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
@click="openStartUserSelect"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="mdi:account-plus-outline" class="size-[18px]" />
|
||||
</template>
|
||||
选择人员
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
v-if="modelData.startUserType === 2"
|
||||
class="mt-2 flex flex-wrap gap-2"
|
||||
>
|
||||
<div
|
||||
v-for="dept in selectedStartDepts"
|
||||
:key="dept.id"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2"
|
||||
v-if="modelData.startUserType === 2"
|
||||
class="mt-2 flex flex-wrap gap-2"
|
||||
>
|
||||
<IconifyIcon icon="ep:office-building" class="size-6 px-1" />
|
||||
{{ dept.name }}
|
||||
<X
|
||||
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
|
||||
@click="handleRemoveStartDept(dept)"
|
||||
/>
|
||||
<div
|
||||
v-for="dept in selectedStartDepts"
|
||||
:key="dept.id"
|
||||
class="bg-destructive text-destructive-foreground hover:bg-destructive-hover relative flex h-9 items-center rounded-full pr-2 shadow-sm"
|
||||
>
|
||||
<IconifyIcon icon="ep:office-building" class="size-6 px-1" />
|
||||
{{ dept.name }}
|
||||
<X
|
||||
class="ml-2 size-4 cursor-pointer"
|
||||
@click="handleRemoveStartDept(dept)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
@click="openStartDeptSelect"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<Plus class="size-[18px]" />
|
||||
</template>
|
||||
选择部门
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
@click="openStartDeptSelect"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<Plus class="size-[18px]" />
|
||||
</template>
|
||||
选择部门
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="流程管理员" name="managerUserIds" class="mb-5">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="user in selectedManagerUsers"
|
||||
:key="user.id"
|
||||
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2"
|
||||
>
|
||||
<Avatar
|
||||
class="m-1"
|
||||
:size="28"
|
||||
v-if="user.avatar"
|
||||
:src="user.avatar"
|
||||
/>
|
||||
<Avatar class="m-1" :size="28" v-else>
|
||||
{{ user.nickname?.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ user.nickname }}
|
||||
<X
|
||||
class="ml-2 size-4 cursor-pointer text-gray-400 hover:text-red-500"
|
||||
@click="handleRemoveManagerUser(user)"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="流程管理员" name="managerUserIds" class="mb-5">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="user in selectedManagerUsers"
|
||||
:key="user.id"
|
||||
class="bg-destructive text-destructive-foreground hover:bg-destructive-hover relative flex h-9 items-center rounded-full pr-2"
|
||||
>
|
||||
<Avatar
|
||||
class="m-1"
|
||||
:size="28"
|
||||
v-if="user.avatar"
|
||||
:src="user.avatar"
|
||||
/>
|
||||
<Avatar class="m-1" :size="28" v-else>
|
||||
{{ user.nickname?.substring(0, 1) }}
|
||||
</Avatar>
|
||||
{{ user.nickname }}
|
||||
<X
|
||||
class="ml-2 size-4 cursor-pointer"
|
||||
@click="handleRemoveManagerUser(user)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
@click="openManagerUserSelect"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon
|
||||
icon="mdi:account-plus-outline"
|
||||
class="size-[18px]"
|
||||
/>
|
||||
</template>
|
||||
选择人员
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
@click="openManagerUserSelect"
|
||||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="mdi:account-plus-outline" class="size-[18px]" />
|
||||
</template>
|
||||
选择人员
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelectModal
|
||||
ref="userSelectFormRef"
|
||||
v-model:value="selectedUsers"
|
||||
:multiple="true"
|
||||
title="选择用户"
|
||||
@confirm="handleUserSelectConfirm"
|
||||
@closed="handleUserSelectClosed"
|
||||
@cancel="handleUserSelectCancel"
|
||||
/>
|
||||
<!-- 部门选择对话框 -->
|
||||
<DeptSelectModal
|
||||
ref="deptSelectFormRef"
|
||||
title="发起人部门选择"
|
||||
:check-strictly="true"
|
||||
@confirm="handleDeptSelectConfirm"
|
||||
/>
|
||||
<!-- 用户选择弹窗 -->
|
||||
<UserSelectModalComp
|
||||
v-model:value="selectedUsers"
|
||||
:multiple="true"
|
||||
title="选择用户"
|
||||
@confirm="handleUserSelectConfirm"
|
||||
@closed="handleUserSelectClosed"
|
||||
@cancel="handleUserSelectCancel"
|
||||
/>
|
||||
<!-- 部门选择对话框 -->
|
||||
<DeptSelectModalComp
|
||||
title="发起人部门选择"
|
||||
:check-strictly="true"
|
||||
@confirm="handleDeptSelectConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bg-gray-100 {
|
||||
background-color: #f5f7fa;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #e6e8eb;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-img-placeholder {
|
||||
cursor: pointer;
|
||||
background-color: #fafafa;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -91,9 +91,9 @@ const numberExample = computed(() => {
|
||||
|
||||
/** 是否开启流程前置通知 */
|
||||
const processBeforeTriggerEnable = ref(false);
|
||||
const handleProcessBeforeTriggerEnableChange = (
|
||||
function handleProcessBeforeTriggerEnableChange(
|
||||
val: boolean | number | string,
|
||||
) => {
|
||||
) {
|
||||
modelData.value.processBeforeTriggerSetting = val
|
||||
? {
|
||||
url: '',
|
||||
@@ -102,13 +102,11 @@ const handleProcessBeforeTriggerEnableChange = (
|
||||
response: [],
|
||||
}
|
||||
: null;
|
||||
};
|
||||
}
|
||||
|
||||
/** 是否开启流程后置通知 */
|
||||
const processAfterTriggerEnable = ref(false);
|
||||
const handleProcessAfterTriggerEnableChange = (
|
||||
val: boolean | number | string,
|
||||
) => {
|
||||
function handleProcessAfterTriggerEnableChange(val: boolean | number | string) {
|
||||
modelData.value.processAfterTriggerSetting = val
|
||||
? {
|
||||
url: '',
|
||||
@@ -117,13 +115,11 @@ const handleProcessAfterTriggerEnableChange = (
|
||||
response: [],
|
||||
}
|
||||
: null;
|
||||
};
|
||||
}
|
||||
|
||||
/** 是否开启任务前置通知 */
|
||||
const taskBeforeTriggerEnable = ref(false);
|
||||
const handleTaskBeforeTriggerEnableChange = (
|
||||
val: boolean | number | string,
|
||||
) => {
|
||||
function handleTaskBeforeTriggerEnableChange(val: boolean | number | string) {
|
||||
modelData.value.taskBeforeTriggerSetting = val
|
||||
? {
|
||||
url: '',
|
||||
@@ -132,11 +128,11 @@ const handleTaskBeforeTriggerEnableChange = (
|
||||
response: [],
|
||||
}
|
||||
: null;
|
||||
};
|
||||
}
|
||||
|
||||
/** 是否开启任务后置通知 */
|
||||
const taskAfterTriggerEnable = ref(false);
|
||||
const handleTaskAfterTriggerEnableChange = (val: boolean | number | string) => {
|
||||
function handleTaskAfterTriggerEnableChange(val: boolean | number | string) {
|
||||
modelData.value.taskAfterTriggerSetting = val
|
||||
? {
|
||||
url: '',
|
||||
@@ -145,7 +141,7 @@ const handleTaskAfterTriggerEnableChange = (val: boolean | number | string) => {
|
||||
response: [],
|
||||
}
|
||||
: null;
|
||||
};
|
||||
}
|
||||
|
||||
/** 表单选项 */
|
||||
const formField = ref<Array<{ field: string; title: string }>>([]);
|
||||
@@ -181,7 +177,7 @@ const formFieldOptions4Summary = computed(() => {
|
||||
});
|
||||
|
||||
/** 兼容以前未配置更多设置的流程 */
|
||||
const initData = () => {
|
||||
function initData() {
|
||||
if (!modelData.value.processIdRule) {
|
||||
modelData.value.processIdRule = {
|
||||
enable: false,
|
||||
@@ -218,7 +214,7 @@ const initData = () => {
|
||||
if (modelData.value.taskAfterTriggerSetting) {
|
||||
taskAfterTriggerEnable.value = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 监听表单 ID 变化,加载表单数据 */
|
||||
watch(
|
||||
@@ -242,9 +238,9 @@ watch(
|
||||
// 表单引用
|
||||
const formRef = ref();
|
||||
/** 表单校验 */
|
||||
const validate = async () => {
|
||||
async function validate() {
|
||||
await formRef.value?.validate();
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({ initData, validate });
|
||||
</script>
|
||||
|
||||
@@ -80,9 +80,9 @@ const rules: Record<string, Rule[]> = {
|
||||
};
|
||||
|
||||
/** 表单校验 */
|
||||
const validate = async () => {
|
||||
async function validate() {
|
||||
await formRef.value?.validate();
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({ validate });
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,7 @@ const processData = inject('processData') as Ref;
|
||||
const simpleDesign = ref();
|
||||
|
||||
/** 表单校验 */
|
||||
const validate = async () => {
|
||||
async function validate() {
|
||||
// 获取最新的流程数据
|
||||
if (!processData.value) {
|
||||
throw new Error('请设计流程');
|
||||
@@ -29,9 +29,9 @@ const validate = async () => {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
/** 处理设计器保存成功 */
|
||||
const handleDesignSuccess = async (data?: any) => {
|
||||
async function handleDesignSuccess(data?: any) {
|
||||
if (data) {
|
||||
// 创建新的对象以触发响应式更新
|
||||
const newModelData = {
|
||||
@@ -44,7 +44,7 @@ const handleDesignSuccess = async (data?: any) => {
|
||||
// 更新表单的模型数据部分
|
||||
modelData.value = newModelData;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 是否显示设计器 */
|
||||
const showDesigner = computed(() => {
|
||||
|
||||
@@ -18,15 +18,15 @@ const emit = defineEmits(['success']);
|
||||
const designerRef = ref();
|
||||
|
||||
/** 保存成功回调 */
|
||||
const handleSuccess = (data?: any) => {
|
||||
function handleSuccess(data?: any) {
|
||||
if (data) {
|
||||
emit('success', data);
|
||||
}
|
||||
};
|
||||
}
|
||||
/** 设计器配置校验 */
|
||||
const validateConfig = async () => {
|
||||
async function validateConfig() {
|
||||
return await designerRef.value.validate();
|
||||
};
|
||||
}
|
||||
defineExpose({ validateConfig });
|
||||
</script>
|
||||
<template>
|
||||
|
||||
@@ -4,21 +4,12 @@ import type { ModelCategoryInfo } from '#/api/bpm/model';
|
||||
import { onActivated, reactive, ref, useTemplateRef, watch } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus, Search, Settings } from '@vben/icons';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { refAutoReset } from '@vueuse/core';
|
||||
import { useSortable } from '@vueuse/integrations/useSortable';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Form,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
import { Button, Card, Dropdown, Input, Menu, message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getCategorySimpleList,
|
||||
@@ -72,7 +63,7 @@ watch(
|
||||
);
|
||||
|
||||
/** 加载数据 */
|
||||
const getList = async () => {
|
||||
async function getList() {
|
||||
modelListSpinning.value = true;
|
||||
try {
|
||||
const modelList = await getModelList(queryParams.name);
|
||||
@@ -89,27 +80,22 @@ const getList = async () => {
|
||||
} finally {
|
||||
modelListSpinning.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onActivated(() => {
|
||||
getList();
|
||||
});
|
||||
|
||||
/** 查询方法 */
|
||||
const handleQuery = () => {
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 新增模型 */
|
||||
const createModel = () => {
|
||||
function createModel() {
|
||||
router.push({
|
||||
name: 'BpmModelCreate',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理下拉菜单命令 */
|
||||
const handleCommand = (command: string) => {
|
||||
function handleCommand(command: string) {
|
||||
if (command === 'handleCategoryAdd') {
|
||||
// 打开新建流程分类弹窗
|
||||
categoryFormModalApi.open();
|
||||
@@ -126,10 +112,10 @@ const handleCommand = (command: string) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 取消分类排序 */
|
||||
const handleCategorySortCancel = () => {
|
||||
function handleCategorySortCancel() {
|
||||
// 恢复初始数据
|
||||
categoryGroup.value = cloneDeep(originalData.value);
|
||||
isCategorySorting.value = false;
|
||||
@@ -137,10 +123,10 @@ const handleCategorySortCancel = () => {
|
||||
if (sortableInstance.value) {
|
||||
sortableInstance.value.option('disabled', true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 提交分类排序 */
|
||||
const handleCategorySortSubmit = async () => {
|
||||
async function handleCategorySortSubmit() {
|
||||
saveSortLoading.value = true;
|
||||
try {
|
||||
// 保存排序逻辑
|
||||
@@ -157,76 +143,56 @@ const handleCategorySortSubmit = async () => {
|
||||
if (sortableInstance.value) {
|
||||
sortableInstance.value.option('disabled', true);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- TODO @jaosn:没头像的图标,展示文字头像哈 @芋艿 好像已经展示了文字头像。是模型列表中吗? -->
|
||||
<!-- 流程分类表单弹窗 -->
|
||||
<CategoryFormModal @success="getList" />
|
||||
<Card
|
||||
:body-style="{ padding: '10px' }"
|
||||
class="mb-4"
|
||||
title="流程模型"
|
||||
v-spinning="modelListSpinning"
|
||||
>
|
||||
<template #extra>
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="搜索流程"
|
||||
allow-clear
|
||||
@press-enter="getList"
|
||||
class="!w-60"
|
||||
/>
|
||||
<Button class="ml-2" type="primary" @click="createModel">
|
||||
<IconifyIcon icon="lucide:plus" /> 新建模型
|
||||
</Button>
|
||||
<Dropdown class="ml-2" placement="bottomRight" arrow>
|
||||
<Button>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:settings" />
|
||||
</template>
|
||||
</Button>
|
||||
<template #overlay>
|
||||
<Menu @click="(e) => handleCommand(e.key as string)">
|
||||
<Menu.Item key="handleCategoryAdd">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="lucide:plus" />
|
||||
新建分类
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="handleCategorySort">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="lucide:align-start-vertical" />
|
||||
分类排序
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<div class="flex h-full items-center justify-between pl-5">
|
||||
<span class="-mb-4 text-lg font-extrabold">流程模型</span>
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
v-if="!isCategorySorting"
|
||||
class="-mb-4 mr-2.5 flex"
|
||||
:model="queryParams"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item name="name" class="ml-auto">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="搜索流程"
|
||||
allow-clear
|
||||
@press-enter="handleQuery"
|
||||
class="!w-60"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="mx-2.5" />
|
||||
</template>
|
||||
</Input>
|
||||
</Form.Item>
|
||||
<!-- 右上角:新建模型、更多操作 -->
|
||||
<Form.Item>
|
||||
<Button type="primary" @click="createModel">
|
||||
<Plus class="size-5" /> 新建模型
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Dropdown placement="bottomRight" arrow>
|
||||
<Button>
|
||||
<template #icon>
|
||||
<Settings class="size-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<template #overlay>
|
||||
<Menu @click="(e) => handleCommand(e.key as string)">
|
||||
<Menu.Item key="handleCategoryAdd">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class="icon-[ant-design--plus-outlined] mr-1.5 text-[18px]"
|
||||
></span>
|
||||
新建分类
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="handleCategorySort">
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[fa--sort-amount-desc] mr-1.5"></span>
|
||||
分类排序
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div class="-mb-4 mr-6" v-else>
|
||||
<div class="mb-4 mr-6" v-if="isCategorySorting">
|
||||
<Button @click="handleCategorySortCancel" class="mr-3">
|
||||
取 消
|
||||
</Button>
|
||||
@@ -240,9 +206,8 @@ const handleCategorySortSubmit = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<!-- 按照分类,展示其所属的模型列表 -->
|
||||
<div class="px-5" ref="categoryGroupRef">
|
||||
<div class="px-3" ref="categoryGroupRef">
|
||||
<CategoryDraggableModel
|
||||
v-for="element in categoryGroup"
|
||||
:class="isCategorySorting ? 'cursor-move' : ''"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { computed, ref, watchEffect } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { cloneDeep, formatDateTime, isEqual } from '@vben/utils';
|
||||
|
||||
@@ -38,6 +39,12 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
// 重命名分类对话框
|
||||
const [CategoryRenameModal, categoryRenameModalApi] = useVbenModal({
|
||||
connectedComponent: CategoryRenameForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
// 获取当前登录用户Id
|
||||
const userStore = useUserStore();
|
||||
@@ -104,7 +111,7 @@ const columns = [
|
||||
];
|
||||
|
||||
/** 处理模型的排序 */
|
||||
const handleModelSort = () => {
|
||||
function handleModelSort() {
|
||||
// 保存初始数据
|
||||
originalData.value = cloneDeep(props.categoryInfo.modelList);
|
||||
// 展开数据
|
||||
@@ -120,10 +127,10 @@ const handleModelSort = () => {
|
||||
disabled: false, // 启用排序
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理模型的排序提交 */
|
||||
const handleModelSortSubmit = async () => {
|
||||
async function handleModelSortSubmit() {
|
||||
try {
|
||||
// 保存排序
|
||||
const ids = modelList.value.map((item) => item.id);
|
||||
@@ -135,10 +142,10 @@ const handleModelSortSubmit = async () => {
|
||||
} catch (error) {
|
||||
console.error('排序保存失败', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理模型的排序取消 */
|
||||
const handleModelSortCancel = () => {
|
||||
function handleModelSortCancel() {
|
||||
// 恢复初始数据
|
||||
modelList.value = cloneDeep(originalData.value);
|
||||
isModelSorting.value = false;
|
||||
@@ -146,20 +153,20 @@ const handleModelSortCancel = () => {
|
||||
if (sortableInstance.value) {
|
||||
sortableInstance.value.option('disabled', true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理下拉菜单命令 */
|
||||
const handleCommand = (command: string) => {
|
||||
function handleCommand(command: string) {
|
||||
if (command === 'renameCategory') {
|
||||
// 打开重命名分类对话框
|
||||
categoryRenameModalApi.setData(props.categoryInfo).open();
|
||||
} else if (command === 'deleteCategory') {
|
||||
handleDeleteCategory();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 删除流程分类 */
|
||||
const handleDeleteCategory = async () => {
|
||||
async function handleDeleteCategory() {
|
||||
if (props.categoryInfo.modelList.length > 0) {
|
||||
message.warning('该分类下仍有流程定义,不允许删除');
|
||||
return;
|
||||
@@ -176,13 +183,13 @@ const handleDeleteCategory = async () => {
|
||||
// 刷新列表
|
||||
emit('success');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理表单详情点击 */
|
||||
const handleFormDetail = (row: any) => {
|
||||
function handleFormDetail(row: any) {
|
||||
// TODO 待实现
|
||||
console.warn('待实现', row);
|
||||
};
|
||||
}
|
||||
|
||||
/** 判断是否是流程管理员 */
|
||||
function isManagerUser(row: any) {
|
||||
@@ -273,17 +280,11 @@ watchEffect(() => {
|
||||
});
|
||||
|
||||
/** 自定义表格行渲染 */
|
||||
const customRow = (_record: any) => {
|
||||
function customRow(_record: any) {
|
||||
return {
|
||||
class: isModelSorting.value ? 'cursor-move' : '',
|
||||
};
|
||||
};
|
||||
|
||||
// 重命名分类对话框
|
||||
const [CategoryRenameModal, categoryRenameModalApi] = useVbenModal({
|
||||
connectedComponent: CategoryRenameForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 处理重命名成功
|
||||
const handleRenameSuccess = () => {
|
||||
@@ -292,292 +293,307 @@ const handleRenameSuccess = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
:body-style="{ padding: 0 }"
|
||||
class="category-draggable-model mb-5 rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
|
||||
>
|
||||
<div class="flex h-12 items-center">
|
||||
<!-- 头部:分类名 -->
|
||||
<div class="flex items-center">
|
||||
<Tooltip v-if="isCategorySorting" title="拖动排序">
|
||||
<span
|
||||
class="icon-[ic--round-drag-indicator] ml-2.5 cursor-move text-2xl text-gray-500"
|
||||
></span>
|
||||
</Tooltip>
|
||||
<div class="ml-4 mr-2 text-lg font-medium">{{ categoryInfo.name }}</div>
|
||||
<div class="text-gray-500">
|
||||
({{ categoryInfo.modelList?.length || 0 }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 头部:操作 -->
|
||||
<div class="flex flex-1 items-center" v-show="!isCategorySorting">
|
||||
<div
|
||||
v-if="categoryInfo.modelList.length > 0"
|
||||
class="ml-3 flex cursor-pointer items-center transition-transform duration-300"
|
||||
:class="isExpand ? 'rotate-180' : 'rotate-0'"
|
||||
@click="isExpand = !isExpand"
|
||||
>
|
||||
<span
|
||||
class="icon-[ic--round-expand-more] text-3xl text-gray-400"
|
||||
></span>
|
||||
<div>
|
||||
<Card
|
||||
:body-style="{ padding: 0 }"
|
||||
class="category-draggable-model mb-5 rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
|
||||
>
|
||||
<div class="flex h-12 items-center">
|
||||
<!-- 头部:分类名 -->
|
||||
<div class="flex items-center">
|
||||
<Tooltip v-if="isCategorySorting" title="拖动排序">
|
||||
<span
|
||||
class="icon-[ic--round-drag-indicator] ml-2.5 cursor-move text-2xl text-gray-500"
|
||||
></span>
|
||||
</Tooltip>
|
||||
<div class="ml-4 mr-2 text-lg font-medium">
|
||||
{{ categoryInfo.name }}
|
||||
</div>
|
||||
<div class="text-gray-500">
|
||||
({{ categoryInfo.modelList?.length || 0 }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ml-auto flex items-center"
|
||||
:class="isModelSorting ? 'mr-4' : 'mr-8'"
|
||||
>
|
||||
<template v-if="!isModelSorting">
|
||||
<Button
|
||||
v-if="categoryInfo.modelList.length > 0"
|
||||
type="link"
|
||||
size="small"
|
||||
class="flex items-center text-[14px]"
|
||||
@click.stop="handleModelSort"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="icon-[fa--sort-amount-desc]"></span>
|
||||
</template>
|
||||
排序
|
||||
</Button>
|
||||
<Dropdown placement="bottom" arrow>
|
||||
<!-- 头部:操作 -->
|
||||
<div class="flex flex-1 items-center" v-show="!isCategorySorting">
|
||||
<div
|
||||
v-if="categoryInfo.modelList.length > 0"
|
||||
class="ml-3 flex cursor-pointer items-center transition-transform duration-300"
|
||||
:class="isExpand ? 'rotate-180' : 'rotate-0'"
|
||||
@click="isExpand = !isExpand"
|
||||
>
|
||||
<span
|
||||
class="icon-[ic--round-expand-more] text-3xl text-gray-400"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ml-auto flex items-center"
|
||||
:class="isModelSorting ? 'mr-4' : 'mr-8'"
|
||||
>
|
||||
<template v-if="!isModelSorting">
|
||||
<Button
|
||||
v-if="categoryInfo.modelList.length > 0"
|
||||
type="link"
|
||||
size="small"
|
||||
class="flex items-center text-[14px]"
|
||||
@click.stop="handleModelSort"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="icon-[ant-design--setting-outlined]"></span>
|
||||
<IconifyIcon icon="lucide:align-start-vertical" />
|
||||
</template>
|
||||
分类
|
||||
排序
|
||||
</Button>
|
||||
<template #overlay>
|
||||
<Menu @click="(e) => handleCommand(e.key as string)">
|
||||
<Menu.Item key="renameCategory"> 重命名 </Menu.Item>
|
||||
<Menu.Item key="deleteCategory"> 删除分类 </Menu.Item>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<Dropdown placement="bottom" arrow>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="flex items-center text-[14px]"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:settings" />
|
||||
</template>
|
||||
分类
|
||||
</Button>
|
||||
<template #overlay>
|
||||
<Menu @click="(e) => handleCommand(e.key as string)">
|
||||
<Menu.Item key="renameCategory"> 重命名 </Menu.Item>
|
||||
<Menu.Item key="deleteCategory"> 删除分类 </Menu.Item>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Button @click.stop="handleModelSortCancel" class="mr-2">
|
||||
取 消
|
||||
</Button>
|
||||
<Button type="primary" @click.stop="handleModelSortSubmit">
|
||||
保存排序
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Button @click.stop="handleModelSortCancel" class="mr-2">
|
||||
取 消
|
||||
</Button>
|
||||
<Button type="primary" @click.stop="handleModelSortSubmit">
|
||||
保存排序
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型列表 -->
|
||||
<Collapse :active-key="expandKeys" :bordered="false" class="bg-transparent">
|
||||
<Collapse.Panel
|
||||
key="1"
|
||||
:show-arrow="false"
|
||||
class="border-0 bg-transparent p-0"
|
||||
v-show="isExpand"
|
||||
<!-- 模型列表 -->
|
||||
<Collapse
|
||||
:active-key="expandKeys"
|
||||
:bordered="false"
|
||||
class="bg-transparent"
|
||||
>
|
||||
<Table
|
||||
v-if="modelList && modelList.length > 0"
|
||||
:class="`category-${categoryInfo.id}`"
|
||||
ref="tableRef"
|
||||
:data-source="modelList"
|
||||
:columns="columns"
|
||||
:pagination="false"
|
||||
:custom-row="customRow"
|
||||
:scroll="{ x: '100%' }"
|
||||
row-key="id"
|
||||
<Collapse.Panel
|
||||
key="1"
|
||||
:show-arrow="false"
|
||||
class="border-0 bg-transparent p-0"
|
||||
v-show="isExpand"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 流程名 -->
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="flex items-center">
|
||||
<Tooltip v-if="isModelSorting" title="拖动排序">
|
||||
<span
|
||||
class="icon-[ic--round-drag-indicator] mr-2.5 cursor-move text-2xl text-gray-500"
|
||||
></span>
|
||||
</Tooltip>
|
||||
<div
|
||||
v-if="!record.icon"
|
||||
class="mr-2.5 flex h-9 w-9 items-center justify-center rounded bg-blue-500 text-white"
|
||||
>
|
||||
<span style="font-size: 12px">{{
|
||||
record.name.substring(0, 2)
|
||||
}}</span>
|
||||
<Table
|
||||
v-if="modelList && modelList.length > 0"
|
||||
:class="`category-${categoryInfo.id}`"
|
||||
ref="tableRef"
|
||||
:data-source="modelList"
|
||||
:columns="columns"
|
||||
:pagination="false"
|
||||
:custom-row="customRow"
|
||||
:scroll="{ x: '100%' }"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 流程名 -->
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="flex items-center">
|
||||
<Tooltip v-if="isModelSorting" title="拖动排序">
|
||||
<span
|
||||
class="icon-[ic--round-drag-indicator] mr-2.5 cursor-move text-2xl text-gray-500"
|
||||
></span>
|
||||
</Tooltip>
|
||||
<div
|
||||
v-if="!record.icon"
|
||||
class="mr-2.5 flex h-9 w-9 items-center justify-center rounded bg-blue-500 text-white"
|
||||
>
|
||||
<span style="font-size: 12px">
|
||||
{{ record.name.substring(0, 2) }}
|
||||
</span>
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
:src="record.icon"
|
||||
class="mr-2.5 h-9 w-9 rounded"
|
||||
alt="图标"
|
||||
/>
|
||||
{{ record.name }}
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
:src="record.icon"
|
||||
class="mr-2.5 h-9 w-9 rounded"
|
||||
alt="图标"
|
||||
/>
|
||||
{{ record.name }}
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 可见范围列-->
|
||||
<template v-else-if="column.key === 'startUserIds'">
|
||||
<span
|
||||
v-if="!record.startUsers?.length && !record.startDepts?.length"
|
||||
>
|
||||
全部可见
|
||||
</span>
|
||||
<span v-else-if="record.startUsers?.length === 1">
|
||||
{{ record.startUsers[0].nickname }}
|
||||
</span>
|
||||
<span v-else-if="record.startDepts?.length === 1">
|
||||
{{ record.startDepts[0].name }}
|
||||
</span>
|
||||
<span v-else-if="record.startDepts?.length > 1">
|
||||
<Tooltip
|
||||
placement="top"
|
||||
:title="
|
||||
record.startDepts.map((dept: any) => dept.name).join('、')
|
||||
<!-- 可见范围列-->
|
||||
<template v-else-if="column.key === 'startUserIds'">
|
||||
<span
|
||||
v-if="
|
||||
!record.startUsers?.length && !record.startDepts?.length
|
||||
"
|
||||
>
|
||||
{{ record.startDepts[0].name }}等
|
||||
{{ record.startDepts.length }} 个部门可见
|
||||
</Tooltip>
|
||||
</span>
|
||||
<span v-else-if="record.startUsers?.length > 1">
|
||||
<Tooltip
|
||||
placement="top"
|
||||
:title="
|
||||
record.startUsers
|
||||
.map((user: any) => user.nickname)
|
||||
.join('、')
|
||||
"
|
||||
>
|
||||
{{ record.startUsers[0].nickname }}等
|
||||
{{ record.startUsers.length }} 人可见
|
||||
</Tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<!-- 流程类型列 -->
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<!-- <DictTag :value="record.type" :type="DICT_TYPE.BPM_MODEL_TYPE" /> -->
|
||||
<!-- <Tag>{{ record.type }}</Tag> -->
|
||||
<DictTag :type="DICT_TYPE.BPM_MODEL_TYPE" :value="record.type" />
|
||||
</template>
|
||||
<!-- 表单信息列 -->
|
||||
<template v-else-if="column.key === 'formType'">
|
||||
<!-- TODO BpmModelFormType.NORMAL -->
|
||||
<Button
|
||||
v-if="record.formType === 10"
|
||||
type="link"
|
||||
@click="handleFormDetail(record)"
|
||||
>
|
||||
{{ record.formName }}
|
||||
</Button>
|
||||
<!-- TODO BpmModelFormType.CUSTOM -->
|
||||
<Button
|
||||
v-else-if="record.formType === 20"
|
||||
type="link"
|
||||
@click="handleFormDetail(record)"
|
||||
>
|
||||
{{ record.formCustomCreatePath }}
|
||||
</Button>
|
||||
<span v-else>暂无表单</span>
|
||||
</template>
|
||||
<!-- 最后发布列 -->
|
||||
<template v-else-if="column.key === 'deploymentTime'">
|
||||
<div class="flex items-center justify-center">
|
||||
<span v-if="record.processDefinition" class="w-[150px]">
|
||||
{{ formatDateTime(record.processDefinition.deploymentTime) }}
|
||||
全部可见
|
||||
</span>
|
||||
<Tag v-if="record.processDefinition">
|
||||
v{{ record.processDefinition.version }}
|
||||
</Tag>
|
||||
<Tag v-else color="warning">未部署</Tag>
|
||||
<Tag
|
||||
v-if="record.processDefinition?.suspensionState === 2"
|
||||
color="warning"
|
||||
class="ml-[10px]"
|
||||
>
|
||||
已停用
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 操作列 -->
|
||||
<template v-else-if="column.key === 'operation'">
|
||||
<div class="flex items-center space-x-0">
|
||||
<!-- TODO 权限校验-->
|
||||
<span v-else-if="record.startUsers?.length === 1">
|
||||
{{ record.startUsers[0].nickname }}
|
||||
</span>
|
||||
<span v-else-if="record.startDepts?.length === 1">
|
||||
{{ record.startDepts[0].name }}
|
||||
</span>
|
||||
<span v-else-if="record.startDepts?.length > 1">
|
||||
<Tooltip
|
||||
placement="top"
|
||||
:title="
|
||||
record.startDepts.map((dept: any) => dept.name).join('、')
|
||||
"
|
||||
>
|
||||
{{ record.startDepts[0].name }}等
|
||||
{{ record.startDepts.length }} 个部门可见
|
||||
</Tooltip>
|
||||
</span>
|
||||
<span v-else-if="record.startUsers?.length > 1">
|
||||
<Tooltip
|
||||
placement="top"
|
||||
:title="
|
||||
record.startUsers
|
||||
.map((user: any) => user.nickname)
|
||||
.join('、')
|
||||
"
|
||||
>
|
||||
{{ record.startUsers[0].nickname }}等
|
||||
{{ record.startUsers.length }} 人可见
|
||||
</Tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<!-- 流程类型列 -->
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<!-- <DictTag :value="record.type" :type="DICT_TYPE.BPM_MODEL_TYPE" /> -->
|
||||
<!-- <Tag>{{ record.type }}</Tag> -->
|
||||
<DictTag
|
||||
:type="DICT_TYPE.BPM_MODEL_TYPE"
|
||||
:value="record.type"
|
||||
/>
|
||||
</template>
|
||||
<!-- 表单信息列 -->
|
||||
<template v-else-if="column.key === 'formType'">
|
||||
<!-- TODO BpmModelFormType.NORMAL -->
|
||||
<Button
|
||||
v-if="record.formType === 10"
|
||||
type="link"
|
||||
size="small"
|
||||
class="px-1"
|
||||
@click="modelOperation('update', record.id)"
|
||||
:disabled="!isManagerUser(record)"
|
||||
@click="handleFormDetail(record)"
|
||||
>
|
||||
修改
|
||||
{{ record.formName }}
|
||||
</Button>
|
||||
<!-- TODO BpmModelFormType.CUSTOM -->
|
||||
<Button
|
||||
v-else-if="record.formType === 20"
|
||||
type="link"
|
||||
size="small"
|
||||
class="px-1"
|
||||
@click="handleDeploy(record)"
|
||||
:disabled="!isManagerUser(record)"
|
||||
@click="handleFormDetail(record)"
|
||||
>
|
||||
发布
|
||||
{{ record.formCustomCreatePath }}
|
||||
</Button>
|
||||
<Dropdown placement="bottomRight" arrow>
|
||||
<Button type="link" size="small" class="px-1">更多</Button>
|
||||
<!-- TODO 待实现 -->
|
||||
<template #overlay>
|
||||
<Menu
|
||||
@click="
|
||||
(e) => handleModelCommand(e.key as string, record)
|
||||
"
|
||||
>
|
||||
<Menu.Item key="handleCopy"> 复制 </Menu.Item>
|
||||
<Menu.Item key="handleDefinitionList"> 历史 </Menu.Item>
|
||||
<Menu.Item
|
||||
key="handleReport"
|
||||
:disabled="!isManagerUser(record)"
|
||||
<span v-else>暂无表单</span>
|
||||
</template>
|
||||
<!-- 最后发布列 -->
|
||||
<template v-else-if="column.key === 'deploymentTime'">
|
||||
<div class="flex items-center justify-center">
|
||||
<span v-if="record.processDefinition" class="w-[150px]">
|
||||
{{
|
||||
formatDateTime(record.processDefinition.deploymentTime)
|
||||
}}
|
||||
</span>
|
||||
<Tag v-if="record.processDefinition">
|
||||
v{{ record.processDefinition.version }}
|
||||
</Tag>
|
||||
<Tag v-else color="warning">未部署</Tag>
|
||||
<Tag
|
||||
v-if="record.processDefinition?.suspensionState === 2"
|
||||
color="warning"
|
||||
class="ml-[10px]"
|
||||
>
|
||||
已停用
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 操作列 -->
|
||||
<template v-else-if="column.key === 'operation'">
|
||||
<div class="flex items-center space-x-0">
|
||||
<!-- TODO 权限校验-->
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="px-1"
|
||||
@click="modelOperation('update', record.id)"
|
||||
:disabled="!isManagerUser(record)"
|
||||
>
|
||||
修改
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="px-1"
|
||||
@click="handleDeploy(record)"
|
||||
:disabled="!isManagerUser(record)"
|
||||
>
|
||||
发布
|
||||
</Button>
|
||||
<Dropdown placement="bottomRight" arrow>
|
||||
<Button type="link" size="small" class="px-1">更多</Button>
|
||||
<!-- TODO 待实现 -->
|
||||
<template #overlay>
|
||||
<Menu
|
||||
@click="
|
||||
(e) => handleModelCommand(e.key as string, record)
|
||||
"
|
||||
>
|
||||
报表
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="handleChangeState"
|
||||
v-if="record.processDefinition"
|
||||
:disabled="!isManagerUser(record)"
|
||||
>
|
||||
{{
|
||||
record.processDefinition.suspensionState === 1
|
||||
? '停用'
|
||||
: '启用'
|
||||
}}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
danger
|
||||
key="handleClean"
|
||||
:disabled="!isManagerUser(record)"
|
||||
>
|
||||
清理
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
danger
|
||||
key="handleDelete"
|
||||
:disabled="!isManagerUser(record)"
|
||||
>
|
||||
删除
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<Menu.Item key="handleCopy"> 复制 </Menu.Item>
|
||||
<Menu.Item key="handleDefinitionList"> 历史 </Menu.Item>
|
||||
<Menu.Item
|
||||
key="handleReport"
|
||||
:disabled="!isManagerUser(record)"
|
||||
>
|
||||
报表
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="handleChangeState"
|
||||
v-if="record.processDefinition"
|
||||
:disabled="!isManagerUser(record)"
|
||||
>
|
||||
{{
|
||||
record.processDefinition.suspensionState === 1
|
||||
? '停用'
|
||||
: '启用'
|
||||
}}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
danger
|
||||
key="handleClean"
|
||||
:disabled="!isManagerUser(record)"
|
||||
>
|
||||
清理
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
danger
|
||||
key="handleDelete"
|
||||
:disabled="!isManagerUser(record)"
|
||||
>
|
||||
删除
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</Card>
|
||||
</Table>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</Card>
|
||||
|
||||
<!-- 重命名分类弹窗 -->
|
||||
<CategoryRenameModal @success="handleRenameSuccess" />
|
||||
<!-- 重命名分类弹窗 -->
|
||||
<CategoryRenameModal @success="handleRenameSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -152,7 +152,7 @@ function onBack() {
|
||||
// ============================== 审核流程相关 ==============================
|
||||
|
||||
/** 审批相关:获取审批详情 */
|
||||
const getApprovalDetail = async () => {
|
||||
async function getApprovalDetail() {
|
||||
try {
|
||||
const data = await getApprovalDetailApi({
|
||||
processDefinitionId: processDefinitionId.value,
|
||||
@@ -188,13 +188,12 @@ const getApprovalDetail = async () => {
|
||||
: [];
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
} finally {}
|
||||
}
|
||||
/** 审批相关:选择发起人 */
|
||||
const selectUserConfirm = (id: string, userList: any[]) => {
|
||||
function selectUserConfirm(id: string, userList: any[]) {
|
||||
startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id);
|
||||
};
|
||||
}
|
||||
|
||||
/** 审批相关:预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次, formData.value可改成实际业务中的特定字段 */
|
||||
watch(
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmCategoryApi } from '#/api/bpm/category';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
@@ -118,9 +114,7 @@ export function GridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
@@ -168,39 +162,11 @@ export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '请假',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '详情',
|
||||
show: hasAccessByCodes(['bpm:oa-leave:query']),
|
||||
},
|
||||
{
|
||||
code: 'progress',
|
||||
text: '进度',
|
||||
show: hasAccessByCodes(['bpm:oa-leave:query']),
|
||||
},
|
||||
{
|
||||
code: 'cancel',
|
||||
text: '取消',
|
||||
show: (row: any) =>
|
||||
row.status === 1 && hasAccessByCodes(['bpm:oa-leave:query']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ const detailData = ref<BpmOALeaveApi.LeaveVO>();
|
||||
const { query } = useRoute();
|
||||
const queryId = computed(() => query.id as string);
|
||||
|
||||
const getDetailData = async () => {
|
||||
async function getDetailData() {
|
||||
try {
|
||||
datailLoading.value = true;
|
||||
detailData.value = await getLeave(Number(props.id || queryId.value));
|
||||
} finally {
|
||||
datailLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDetailData();
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PageParam } from '@vben/request';
|
||||
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmOALeaveApi } from '#/api/bpm/oa/leave';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page, prompt } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message, Textarea } from 'ant-design-vue';
|
||||
import { message, Textarea } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getLeavePage } from '#/api/bpm/oa/leave';
|
||||
import { cancelProcessInstanceByStartUser } from '#/api/bpm/processInstance';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
@@ -27,12 +21,12 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
schema: GridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }: PageParam, formValues: any) => {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getLeavePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
@@ -52,7 +46,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
});
|
||||
|
||||
/** 创建请假 */
|
||||
function onCreate() {
|
||||
function handleCreate() {
|
||||
router.push({
|
||||
name: 'OALeaveCreate',
|
||||
query: {
|
||||
@@ -62,15 +56,15 @@ function onCreate() {
|
||||
}
|
||||
|
||||
/** 查看请假详情 */
|
||||
const onDetail = (row: BpmOALeaveApi.LeaveVO) => {
|
||||
function handleDetail(row: BpmOALeaveApi.LeaveVO) {
|
||||
router.push({
|
||||
name: 'OALeaveDetail',
|
||||
query: { id: row.id },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/** 取消请假 */
|
||||
const onCancel = (row: BpmOALeaveApi.LeaveVO) => {
|
||||
function handleCancel(row: BpmOALeaveApi.LeaveVO) {
|
||||
prompt({
|
||||
async beforeClose(scope) {
|
||||
if (scope.isConfirm) {
|
||||
@@ -100,35 +94,14 @@ const onCancel = (row: BpmOALeaveApi.LeaveVO) => {
|
||||
title: '取消流程',
|
||||
modelPropName: 'value',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/** 审批进度 */
|
||||
const onProgress = (row: BpmOALeaveApi.LeaveVO) => {
|
||||
function handleProgress(row: BpmOALeaveApi.LeaveVO) {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.processInstanceId },
|
||||
});
|
||||
};
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<BpmOALeaveApi.LeaveVO>) {
|
||||
switch (code) {
|
||||
case 'cancel': {
|
||||
onCancel(row);
|
||||
break;
|
||||
}
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
case 'progress': {
|
||||
onProgress(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
@@ -146,25 +119,48 @@ function onRefresh() {
|
||||
|
||||
<Grid table-title="请假列表">
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['bpm:category:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
发起请假
|
||||
</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '发起请假',
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['bpm:oa-leave:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #userIds-cell="{ row }">
|
||||
<span
|
||||
v-for="(userId, index) in row.userIds"
|
||||
:key="userId"
|
||||
class="pr-5px"
|
||||
>
|
||||
{{ dataList.find((user) => user.id === userId)?.nickname }}
|
||||
<span v-if="index < row.userIds.length - 1">、</span>
|
||||
</span>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['bpm:oa-leave:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '审批进度',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['bpm:oa-leave:query'],
|
||||
onClick: handleProgress.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '取消',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['bpm:user-group:query'],
|
||||
popConfirm: {
|
||||
title: '取消流程',
|
||||
confirm: handleCancel.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmProcessExpressionApi } from '#/api/bpm/processExpression';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
@@ -88,9 +83,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmProcessExpressionApi.ProcessExpressionVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
@@ -124,29 +117,10 @@ export function useGridColumns<T = BpmProcessExpressionApi.ProcessExpressionVO>(
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程表达式',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['bpm:process-expression:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['bpm:process-expression:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmProcessExpressionApi } from '#/api/bpm/processExpression';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteProcessExpression,
|
||||
getProcessExpressionPage,
|
||||
@@ -25,12 +21,46 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建流程表达式 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑流程表达式 */
|
||||
function handleEdit(row: BpmProcessExpressionApi.ProcessExpressionVO) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除流程表达式 */
|
||||
async function handleDelete(row: BpmProcessExpressionApi.ProcessExpressionVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteProcessExpression(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
@@ -53,55 +83,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeTableGridOptions<BpmProcessExpressionApi.ProcessExpressionVO>,
|
||||
});
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<BpmProcessExpressionApi.ProcessExpressionVO>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建流程表达式 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑流程表达式 */
|
||||
function onEdit(row: BpmProcessExpressionApi.ProcessExpressionVO) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除流程表达式 */
|
||||
async function onDelete(row: BpmProcessExpressionApi.ProcessExpressionVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
|
||||
try {
|
||||
await deleteProcessExpression(row.id as number);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
onRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -112,17 +93,45 @@ async function onDelete(row: BpmProcessExpressionApi.ProcessExpressionVO) {
|
||||
url="https://doc.iocoder.cn/bpm/expression/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="流程表达式">
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['bpm:process-expression:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['流程表达式']) }}
|
||||
</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['流程表达式']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['bpm:process-expression:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['bpm:process-expression:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['bpm:process-expression:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -55,7 +55,6 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
@@ -77,7 +76,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-[600px]">
|
||||
<Modal :title="getTitle" class="w-[40%]">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -39,7 +39,7 @@ const processDefinitionList = ref<
|
||||
>([]); // 流程定义的列表
|
||||
|
||||
// 实现 groupBy 功能
|
||||
const groupBy = (array: any[], key: string) => {
|
||||
function groupBy(array: any[], key: string) {
|
||||
const result: Record<string, any[]> = {};
|
||||
for (const item of array) {
|
||||
const groupKey = item[key];
|
||||
@@ -49,10 +49,10 @@ const groupBy = (array: any[], key: string) => {
|
||||
result[groupKey].push(item);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 所有流程分类数据
|
||||
@@ -79,20 +79,20 @@ const getList = async () => {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取所有流程分类数据 */
|
||||
const getCategoryList = async () => {
|
||||
async function getCategoryList() {
|
||||
try {
|
||||
// 流程分类
|
||||
categoryList.value = await getCategorySimpleList();
|
||||
} catch {
|
||||
// 错误处理
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取所有流程定义数据 */
|
||||
const handleGetProcessDefinitionList = async () => {
|
||||
async function handleGetProcessDefinitionList() {
|
||||
try {
|
||||
// 流程定义
|
||||
processDefinitionList.value = await getProcessDefinitionList({
|
||||
@@ -108,7 +108,7 @@ const handleGetProcessDefinitionList = async () => {
|
||||
} catch {
|
||||
// 错误处理
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 用于存储搜索过滤后的流程定义 */
|
||||
const filteredProcessDefinitionList = ref<
|
||||
@@ -116,7 +116,7 @@ const filteredProcessDefinitionList = ref<
|
||||
>([]);
|
||||
|
||||
/** 搜索流程 */
|
||||
const handleQuery = () => {
|
||||
function handleQuery() {
|
||||
if (searchName.value.trim()) {
|
||||
// 如果有搜索关键字,进行过滤
|
||||
isSearching.value = true;
|
||||
@@ -141,16 +141,16 @@ const handleQuery = () => {
|
||||
isSearching.value = false;
|
||||
filteredProcessDefinitionList.value = processDefinitionList.value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 判断流程定义是否匹配搜索 */
|
||||
const isDefinitionMatchSearch = (definition: any) => {
|
||||
function isDefinitionMatchSearch(definition: any) {
|
||||
if (!isSearching.value) return false;
|
||||
return definition.name.toLowerCase().includes(searchName.value.toLowerCase());
|
||||
};
|
||||
}
|
||||
|
||||
/** 流程定义的分组 */
|
||||
const processDefinitionGroup: any = computed(() => {
|
||||
const processDefinitionGroup = computed(() => {
|
||||
if (!processDefinitionList.value?.length) {
|
||||
return {};
|
||||
}
|
||||
@@ -172,26 +172,26 @@ const processDefinitionGroup: any = computed(() => {
|
||||
});
|
||||
|
||||
/** 通过分类 code 获取对应的名称 */
|
||||
const getCategoryName = (categoryCode: string) => {
|
||||
function getCategoryName(categoryCode: string) {
|
||||
return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)
|
||||
?.name;
|
||||
};
|
||||
}
|
||||
|
||||
// ========== 表单相关 ==========
|
||||
const selectProcessDefinition = ref(); // 选择的流程定义
|
||||
const processDefinitionDetailRef = ref();
|
||||
|
||||
/** 处理选择流程的按钮操作 */
|
||||
const handleSelect = async (
|
||||
async function handleSelect(
|
||||
row: BpmProcessDefinitionApi.ProcessDefinitionVO,
|
||||
formVariables?: any,
|
||||
) => {
|
||||
) {
|
||||
// 设置选择的流程
|
||||
selectProcessDefinition.value = row;
|
||||
// 初始化流程定义详情
|
||||
await nextTick();
|
||||
processDefinitionDetailRef.value?.initProcessInfo(row, formVariables);
|
||||
};
|
||||
}
|
||||
|
||||
/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */
|
||||
const availableCategories = computed(() => {
|
||||
@@ -222,6 +222,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- TODO @ziye:【优先级:低】这里交互,可以做成类似 vue3 + element-plus 那个一样,滚动切换分类哈?对标钉钉、飞书哈; -->
|
||||
<!-- 第一步,通过流程定义的列表,选择对应的流程 -->
|
||||
<template v-if="!selectProcessDefinition">
|
||||
<Card
|
||||
@@ -274,6 +275,7 @@ onMounted(() => {
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<!-- TODO @ziye:icon、name 会告警~~ -->
|
||||
<img
|
||||
v-if="definition.icon"
|
||||
:src="definition.icon"
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
} from '#/utils';
|
||||
import ProcessInstanceSimpleViewer from '#/views/bpm/processInstance/detail/modules/simple-bpm-viewer.vue';
|
||||
import ProcessInstanceTimeline from '#/views/bpm/processInstance/detail/modules/time-line.vue';
|
||||
// 类型定义
|
||||
|
||||
/** 类型定义 */
|
||||
interface ProcessFormData {
|
||||
rule: any[];
|
||||
option: Record<string, any>;
|
||||
@@ -91,7 +92,7 @@ const activityNodes = ref<ApprovalNodeInfo[]>([]);
|
||||
const processInstanceStartLoading = ref(false);
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = async () => {
|
||||
async function submitForm() {
|
||||
if (!fApi.value || !props.selectProcessDefinition) {
|
||||
return;
|
||||
}
|
||||
@@ -121,6 +122,7 @@ const submitForm = async () => {
|
||||
|
||||
message.success('发起流程成功');
|
||||
|
||||
// TODO @ziye:有告警哈;
|
||||
closeCurrentTab();
|
||||
|
||||
await router.push({ path: '/bpm/task/my' });
|
||||
@@ -130,10 +132,10 @@ const submitForm = async () => {
|
||||
} finally {
|
||||
processInstanceStartLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 设置表单信息、获取流程图数据 */
|
||||
const initProcessInfo = async (row: any, formVariables?: any) => {
|
||||
async function initProcessInfo(row: any, formVariables?: any) {
|
||||
// 重置指定审批人
|
||||
startUserSelectTasks.value = [];
|
||||
startUserSelectAssignees.value = {};
|
||||
@@ -177,7 +179,7 @@ const initProcessInfo = async (row: any, formVariables?: any) => {
|
||||
});
|
||||
// 这里暂时无需加载流程图,因为跳出到另外个 Tab;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次 */
|
||||
watch(
|
||||
@@ -200,10 +202,10 @@ watch(
|
||||
);
|
||||
|
||||
/** 获取审批详情 */
|
||||
const getApprovalDetail = async (row: {
|
||||
async function getApprovalDetail(row: {
|
||||
id: string;
|
||||
processVariablesStr: string;
|
||||
}) => {
|
||||
}) {
|
||||
try {
|
||||
const data = await getApprovalDetailApi({
|
||||
processDefinitionId: row.id,
|
||||
@@ -246,12 +248,12 @@ const getApprovalDetail = async (row: {
|
||||
message.error('获取审批详情失败');
|
||||
console.error('获取审批详情失败:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表单权限
|
||||
*/
|
||||
const setFieldPermission = (field: string, permission: string) => {
|
||||
function setFieldPermission(field: string, permission: string) {
|
||||
if (permission === BpmFieldPermissionType.READ) {
|
||||
// @ts-ignore
|
||||
fApi.value?.disabled(true, field);
|
||||
@@ -264,18 +266,18 @@ const setFieldPermission = (field: string, permission: string) => {
|
||||
// @ts-ignore
|
||||
fApi.value?.hidden(true, field);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 取消发起审批 */
|
||||
const handleCancel = () => {
|
||||
function handleCancel() {
|
||||
emit('cancel');
|
||||
};
|
||||
}
|
||||
|
||||
/** 选择发起人 */
|
||||
const selectUserConfirm = (activityId: string, userList: any[]) => {
|
||||
function selectUserConfirm(activityId: string, userList: any[]) {
|
||||
if (!activityId || !Array.isArray(userList)) return;
|
||||
startUserSelectAssignees.value[activityId] = userList.map((item) => item.id);
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({ initProcessInfo });
|
||||
</script>
|
||||
@@ -293,7 +295,7 @@ defineExpose({ initProcessInfo });
|
||||
<template #extra>
|
||||
<Space wrap>
|
||||
<Button plain type="default" @click="handleCancel">
|
||||
<IconifyIcon icon="mdi:arrow-left" /> 返回
|
||||
<IconifyIcon icon="lucide:arrow-left" /> 返回
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
@@ -345,11 +347,11 @@ defineExpose({ initProcessInfo });
|
||||
<template v-if="activeTab === 'form'">
|
||||
<Space wrap class="flex w-full justify-center">
|
||||
<Button plain type="primary" @click="submitForm">
|
||||
<IconifyIcon icon="icon-park-outline:check" />
|
||||
<IconifyIcon icon="lucide:check" />
|
||||
发起
|
||||
</Button>
|
||||
<Button plain type="default" @click="handleCancel">
|
||||
<IconifyIcon icon="icon-park-outline:close" />
|
||||
<IconifyIcon icon="lucide:x" />
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { getCategorySimpleList } from '#/api/bpm/category';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
BpmProcessInstanceStatus,
|
||||
DICT_TYPE,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
@@ -88,9 +77,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmProcessInstanceApi.ProcessInstanceVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
@@ -136,38 +123,11 @@ export function useGridColumns<T = BpmProcessInstanceApi.ProcessInstanceVO>(
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程名称',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: $t('ui.actionTitle.detail'),
|
||||
show: hasAccessByCodes(['bpm:process-instance:query']),
|
||||
},
|
||||
{
|
||||
code: 'cancel',
|
||||
text: $t('ui.actionTitle.cancel'),
|
||||
show: (row: BpmProcessInstanceApi.ProcessInstanceVO) => {
|
||||
return (
|
||||
row.status === BpmProcessInstanceStatus.RUNNING &&
|
||||
hasAccessByCodes(['bpm:process-instance:cancel'])
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ async function getApprovalDetail() {
|
||||
}
|
||||
|
||||
/** 获取流程模型视图*/
|
||||
const getProcessModelView = async () => {
|
||||
async function getProcessModelView() {
|
||||
if (BpmModelType.BPMN === processDefinition.value?.modelType) {
|
||||
// 重置,解决 BPMN 流程图刷新不会重新渲染问题
|
||||
processModelView.value = {
|
||||
@@ -186,14 +186,14 @@ const getProcessModelView = async () => {
|
||||
if (data) {
|
||||
processModelView.value = data;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 审批节点信息
|
||||
const activityNodes = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>([]);
|
||||
/**
|
||||
* 设置表单权限
|
||||
*/
|
||||
const setFieldPermission = (field: string, permission: string) => {
|
||||
function setFieldPermission(field: string, permission: string) {
|
||||
if (permission === FieldPermissionType.READ) {
|
||||
// @ts-ignore
|
||||
fApi.value?.disabled(true, field);
|
||||
@@ -208,7 +208,7 @@ const setFieldPermission = (field: string, permission: string) => {
|
||||
// @ts-ignore
|
||||
fApi.value?.hidden(true, field);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作成功后刷新
|
||||
@@ -222,7 +222,7 @@ const setFieldPermission = (field: string, permission: string) => {
|
||||
const activeTab = ref('form');
|
||||
const taskListRef = ref();
|
||||
|
||||
// 监听 Tab 切换,当切换到 "record" 标签时刷新任务列表
|
||||
/** 监听 Tab 切换,当切换到 "record" 标签时刷新任务列表 */
|
||||
watch(
|
||||
() => activeTab.value,
|
||||
(newVal) => {
|
||||
@@ -238,7 +238,7 @@ watch(
|
||||
/** 初始化 */
|
||||
const userOptions = ref<SystemUserApi.User[]>([]); // 用户列表
|
||||
onMounted(async () => {
|
||||
getDetail();
|
||||
await getDetail();
|
||||
// 获得用户列表
|
||||
userOptions.value = await getSimpleUserList();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ defineOptions({ name: 'ProcessInstanceBpmnViewer' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- TODO @ziye:可以后续,找下 antd 有没可以直接用的组件哈 -->
|
||||
<div>
|
||||
<h1>BPMN Viewer</h1>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
@@ -60,6 +61,17 @@ const props = defineProps<{
|
||||
writableFields: string[]; // 流程表单可以编辑的字段
|
||||
}>(); // 当前登录的编号
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [SignatureModal, signatureModalApi] = useVbenModal({
|
||||
connectedComponent: Signature,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建流程表达式 */
|
||||
function openSignatureModal() {
|
||||
signatureModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
const router = useRouter(); // 路由
|
||||
const userStore = useUserStore();
|
||||
const userId = userStore.userInfo?.id;
|
||||
@@ -86,7 +98,6 @@ const nodeTypeName = ref('审批'); // 节点类型名称
|
||||
// 审批通过意见表单
|
||||
const reasonRequire = ref();
|
||||
const approveFormRef = ref<FormInstance>();
|
||||
const signRef = ref();
|
||||
const approveSignFormRef = ref();
|
||||
const nextAssigneesActivityNode = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>(
|
||||
[],
|
||||
@@ -235,7 +246,7 @@ watch(
|
||||
);
|
||||
|
||||
/** 弹出气泡卡 */
|
||||
const openPopover = async (type: string) => {
|
||||
async function openPopover(type: string) {
|
||||
if (type === 'approve') {
|
||||
// 校验流程表单
|
||||
const valid = await validateNormalForm();
|
||||
@@ -258,19 +269,19 @@ const openPopover = async (type: string) => {
|
||||
});
|
||||
// await nextTick()
|
||||
// formRef.value.resetFields()
|
||||
};
|
||||
}
|
||||
|
||||
/** 关闭气泡卡 */
|
||||
const closePopover = (type: string, formRef: any | FormInstance) => {
|
||||
function closePopover(type: string, formRef: any | FormInstance) {
|
||||
if (formRef) {
|
||||
formRef.resetFields();
|
||||
}
|
||||
if (popOverVisible.value[type]) popOverVisible.value[type] = false;
|
||||
nextAssigneesActivityNode.value = [];
|
||||
};
|
||||
}
|
||||
|
||||
/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
|
||||
const initNextAssigneesFormField = async () => {
|
||||
async function initNextAssigneesFormField() {
|
||||
// 获取修改的流程变量, 暂时只支持流程表单
|
||||
const variables = getUpdatedProcessInstanceVariables();
|
||||
const data = await getNextApprovalNodes({
|
||||
@@ -293,14 +304,14 @@ const initNextAssigneesFormField = async () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 选择下一个节点的审批人 */
|
||||
const selectNextAssigneesConfirm = (id: string, userList: any[]) => {
|
||||
function selectNextAssigneesConfirm(id: string, userList: any[]) {
|
||||
approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id);
|
||||
};
|
||||
}
|
||||
/** 审批通过时,校验每个自选审批人的节点是否都已配置了审批人 */
|
||||
const validateNextAssignees = () => {
|
||||
function validateNextAssignees() {
|
||||
if (Object.keys(nextAssigneesActivityNode.value).length === 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -312,13 +323,10 @@ const validateNextAssignees = () => {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理审批通过和不通过的操作 */
|
||||
const handleAudit = async (
|
||||
pass: boolean,
|
||||
formRef: FormInstance | undefined,
|
||||
) => {
|
||||
async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 校验表单
|
||||
@@ -375,10 +383,10 @@ const handleAudit = async (
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理抄送 */
|
||||
const handleCopy = async () => {
|
||||
async function handleCopy() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 1. 校验表单
|
||||
@@ -397,10 +405,10 @@ const handleCopy = async () => {
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理转交 */
|
||||
const handleTransfer = async () => {
|
||||
async function handleTransfer() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 1.1 校验表单
|
||||
@@ -421,10 +429,10 @@ const handleTransfer = async () => {
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理委派 */
|
||||
const handleDelegate = async () => {
|
||||
async function handleDelegate() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 1.1 校验表单
|
||||
@@ -446,10 +454,10 @@ const handleDelegate = async () => {
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理加签 */
|
||||
const handlerAddSign = async (type: string) => {
|
||||
async function handlerAddSign(type: string) {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 1.1 校验表单
|
||||
@@ -471,10 +479,10 @@ const handlerAddSign = async (type: string) => {
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理退回 */
|
||||
const handleReturn = async () => {
|
||||
async function handleReturn() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 1.1 校验表单
|
||||
@@ -496,10 +504,10 @@ const handleReturn = async () => {
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理取消 */
|
||||
const handleCancel = async () => {
|
||||
async function handleCancel() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 1.1 校验表单
|
||||
@@ -518,26 +526,26 @@ const handleCancel = async () => {
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理再次提交 */
|
||||
const handleReCreate = async () => {
|
||||
async function handleReCreate() {
|
||||
// 跳转发起流程界面
|
||||
await router.push({
|
||||
path: '/bpm/task/create',
|
||||
query: { processInstanceId: props.processInstance?.id },
|
||||
});
|
||||
// router.push('/bpm/task/my');
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取减签人员标签 */
|
||||
const getDeleteSignUserLabel = (task: any): string => {
|
||||
function getDeleteSignUserLabel(task: any): string {
|
||||
const deptName = task?.assigneeUser?.deptName || task?.ownerUser?.deptName;
|
||||
const nickname = task?.assigneeUser?.nickname || task?.ownerUser?.nickname;
|
||||
return `${nickname} ( 所属部门:${deptName} )`;
|
||||
};
|
||||
}
|
||||
/** 处理减签 */
|
||||
const handlerDeleteSign = async () => {
|
||||
async function handlerDeleteSign() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 1.1 校验表单
|
||||
@@ -557,23 +565,23 @@ const handlerDeleteSign = async () => {
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
/** 重新加载数据 */
|
||||
const reload = () => {
|
||||
function reload() {
|
||||
emit('success');
|
||||
};
|
||||
}
|
||||
|
||||
/** 任务是否为处理中状态 */
|
||||
const isHandleTaskStatus = () => {
|
||||
function isHandleTaskStatus() {
|
||||
let canHandle = false;
|
||||
if (BpmTaskStatusEnum.RUNNING === runningTask.value?.status) {
|
||||
canHandle = true;
|
||||
}
|
||||
return canHandle;
|
||||
};
|
||||
}
|
||||
|
||||
/** 流程状态是否为结束状态 */
|
||||
const isEndProcessStatus = (status: number) => {
|
||||
function isEndProcessStatus(status: number) {
|
||||
let isEndStatus = false;
|
||||
if (
|
||||
BpmProcessInstanceStatus.APPROVE === status ||
|
||||
@@ -583,10 +591,10 @@ const isEndProcessStatus = (status: number) => {
|
||||
isEndStatus = true;
|
||||
}
|
||||
return isEndStatus;
|
||||
};
|
||||
}
|
||||
|
||||
/** 是否显示按钮 */
|
||||
const isShowButton = (btnType: BpmTaskOperationButtonTypeEnum): boolean => {
|
||||
function isShowButton(btnType: BpmTaskOperationButtonTypeEnum): boolean {
|
||||
let isShow = true;
|
||||
if (
|
||||
runningTask.value?.buttonsSetting &&
|
||||
@@ -595,10 +603,10 @@ const isShowButton = (btnType: BpmTaskOperationButtonTypeEnum): boolean => {
|
||||
isShow = runningTask.value.buttonsSetting[btnType].enable;
|
||||
}
|
||||
return isShow;
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取按钮的显示名称 */
|
||||
const getButtonDisplayName = (btnType: BpmTaskOperationButtonTypeEnum) => {
|
||||
function getButtonDisplayName(btnType: BpmTaskOperationButtonTypeEnum) {
|
||||
let displayName = OPERATION_BUTTON_NAME.get(btnType);
|
||||
if (
|
||||
runningTask.value?.buttonsSetting &&
|
||||
@@ -607,9 +615,9 @@ const getButtonDisplayName = (btnType: BpmTaskOperationButtonTypeEnum) => {
|
||||
displayName = runningTask.value.buttonsSetting[btnType].displayName;
|
||||
}
|
||||
return displayName;
|
||||
};
|
||||
}
|
||||
|
||||
const loadTodoTask = (task: any) => {
|
||||
function loadTodoTask(task: any) {
|
||||
approveForm.value = {};
|
||||
runningTask.value = task;
|
||||
approveFormFApi.value = {};
|
||||
@@ -629,10 +637,10 @@ const loadTodoTask = (task: any) => {
|
||||
} else {
|
||||
approveForm.value = {}; // 占位,避免为空
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 校验流程表单 */
|
||||
const validateNormalForm = async () => {
|
||||
async function validateNormalForm() {
|
||||
if (props.processDefinition?.formType === BpmModelFormType.NORMAL) {
|
||||
let valid = true;
|
||||
try {
|
||||
@@ -644,31 +652,31 @@ const validateNormalForm = async () => {
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
|
||||
const getUpdatedProcessInstanceVariables = () => {
|
||||
function getUpdatedProcessInstanceVariables() {
|
||||
const variables: any = {};
|
||||
props.writableFields.forEach((field: string) => {
|
||||
if (field && variables[field])
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
});
|
||||
return variables;
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理签名完成 */
|
||||
const handleSignFinish = (url: string) => {
|
||||
function handleSignFinish(url: string) {
|
||||
approveReasonForm.signPicUrl = url;
|
||||
approveFormRef.value?.validateFields(['signPicUrl']);
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理弹窗可见性 */
|
||||
const handlePopoverVisible = (visible: boolean) => {
|
||||
function handlePopoverVisible(visible: boolean) {
|
||||
if (!visible) {
|
||||
// 拦截关闭事件
|
||||
popOverVisible.value.approve = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({ loadTodoTask });
|
||||
</script>
|
||||
@@ -745,7 +753,7 @@ defineExpose({ loadTodoTask });
|
||||
name="signPicUrl"
|
||||
ref="approveSignFormRef"
|
||||
>
|
||||
<Button @click="signRef.open()" type="primary">
|
||||
<Button @click="openSignatureModal" type="primary">
|
||||
{{ approveReasonForm.signPicUrl ? '重新签名' : '点击签名' }}
|
||||
</Button>
|
||||
|
||||
@@ -802,7 +810,7 @@ defineExpose({ loadTodoTask });
|
||||
"
|
||||
>
|
||||
<Button ghost danger type="primary" @click="openPopover('reject')">
|
||||
<IconifyIcon icon="icon-park-outline:close" />
|
||||
<IconifyIcon icon="lucide:x" />
|
||||
{{ getButtonDisplayName(BpmTaskOperationButtonTypeEnum.REJECT) }}
|
||||
</Button>
|
||||
<template #content>
|
||||
@@ -862,7 +870,7 @@ defineExpose({ loadTodoTask });
|
||||
"
|
||||
>
|
||||
<Button type="dashed" @click="openPopover('copy')">
|
||||
<IconifyIcon icon="icon-park-outline:copy" />
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
{{ getButtonDisplayName(BpmTaskOperationButtonTypeEnum.COPY) }}
|
||||
</Button>
|
||||
<template #content>
|
||||
@@ -1387,5 +1395,5 @@ defineExpose({ loadTodoTask });
|
||||
</div>
|
||||
|
||||
<!-- 签名弹窗 -->
|
||||
<Signature ref="signRef" @success="handleSignFinish" />
|
||||
<SignatureModal @success="handleSignFinish" />
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, message, Space, Tooltip } from 'ant-design-vue';
|
||||
// TODO @ziye:这个可能,适合放到全局?!因为 element-plus 也用这个;
|
||||
import Vue3Signature from 'vue3-signature';
|
||||
|
||||
import { uploadFile } from '#/api/infra/file';
|
||||
@@ -16,6 +17,8 @@ defineOptions({
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
|
||||
const signature = ref<InstanceType<typeof Vue3Signature>>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
title: '流程签名',
|
||||
onOpenChange(visible) {
|
||||
@@ -23,42 +26,31 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.close();
|
||||
}
|
||||
},
|
||||
onConfirm: () => {
|
||||
submit();
|
||||
async onConfirm() {
|
||||
message.success({
|
||||
content: '签名上传中请稍等。。。',
|
||||
});
|
||||
const signFileUrl = await uploadFile({
|
||||
file: download.base64ToFile(
|
||||
signature?.value?.save('image/jpeg') || '',
|
||||
'签名',
|
||||
),
|
||||
});
|
||||
emits('success', signFileUrl);
|
||||
// TODO @ziye:下面有个告警哈;ps:所有告警,皆是错误,可以关注 ide 给的提示哈;
|
||||
modalApi.close();
|
||||
},
|
||||
});
|
||||
|
||||
const signature = ref<InstanceType<typeof Vue3Signature>>();
|
||||
|
||||
const open = async () => {
|
||||
modalApi.open();
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
const submit = async () => {
|
||||
message.success({
|
||||
content: '签名上传中请稍等。。。',
|
||||
});
|
||||
const signFileUrl = await uploadFile({
|
||||
file: download.base64ToFile(
|
||||
signature?.value?.save('image/jpeg') || '',
|
||||
'签名',
|
||||
),
|
||||
});
|
||||
emits('success', signFileUrl);
|
||||
modalApi.close();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="h-[500px] w-[900px]">
|
||||
<Modal class="h-[40%] w-[60%]">
|
||||
<div class="mb-2 flex justify-end">
|
||||
<Space>
|
||||
<Tooltip title="撤销上一步操作">
|
||||
<Button @click="signature?.undo()">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="mi:undo" class="mb-[4px] size-[16px]" />
|
||||
<IconifyIcon icon="lucide:undo" class="mb-[4px] size-[16px]" />
|
||||
</template>
|
||||
撤销
|
||||
</Button>
|
||||
@@ -67,10 +59,7 @@ const submit = async () => {
|
||||
<Tooltip title="清空画布">
|
||||
<Button @click="signature?.clear()">
|
||||
<template #icon>
|
||||
<IconifyIcon
|
||||
icon="mdi:delete-outline"
|
||||
class="mb-[4px] size-[16px]"
|
||||
/>
|
||||
<IconifyIcon icon="lucide:trash" class="mb-[4px] size-[16px]" />
|
||||
</template>
|
||||
<span>清除</span>
|
||||
</Button>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTaskListByProcessInstanceId } from '#/api/bpm/task';
|
||||
import { DICT_TYPE, formatPast2, setConfAndFields2 } from '#/utils';
|
||||
import { DICT_TYPE, setConfAndFields2 } from '#/utils';
|
||||
|
||||
defineOptions({
|
||||
name: 'BpmProcessInstanceTaskList',
|
||||
@@ -25,7 +25,7 @@ const props = defineProps<{
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
// 使用shallowRef减少不必要的深度响应
|
||||
// 使用 shallowRef 减少不必要的深度响应
|
||||
const columns = shallowRef([
|
||||
{
|
||||
field: 'name',
|
||||
@@ -75,11 +75,7 @@ const columns = shallowRef([
|
||||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
minWidth: 180,
|
||||
slots: {
|
||||
default: ({ row }: { row: BpmTaskApi.TaskManagerVO }) => {
|
||||
return formatPast2(row.durationInMillis);
|
||||
},
|
||||
},
|
||||
formatter: 'formatPast2',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -116,9 +112,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
/**
|
||||
* 刷新表格数据
|
||||
*/
|
||||
const refresh = (): void => {
|
||||
function refresh() {
|
||||
gridApi.query();
|
||||
};
|
||||
}
|
||||
|
||||
// 表单相关
|
||||
interface TaskForm {
|
||||
|
||||
@@ -108,12 +108,12 @@ const nodeTypeSvgMap = {
|
||||
const onlyStatusIconShow = [-1, 0, 1];
|
||||
|
||||
// 获取审批节点类型图标
|
||||
const getApprovalNodeTypeIcon = (nodeType: BpmNodeTypeEnum) => {
|
||||
function getApprovalNodeTypeIcon(nodeType: BpmNodeTypeEnum) {
|
||||
return nodeTypeSvgMap[nodeType]?.icon;
|
||||
};
|
||||
}
|
||||
|
||||
// 获取审批节点图标
|
||||
const getApprovalNodeIcon = (taskStatus: number, nodeType: BpmNodeTypeEnum) => {
|
||||
function getApprovalNodeIcon(taskStatus: number, nodeType: BpmNodeTypeEnum) {
|
||||
if (taskStatus === BpmTaskStatusEnum.NOT_START) {
|
||||
return statusIconMap[taskStatus]?.icon || 'mdi:clock-outline';
|
||||
}
|
||||
@@ -128,15 +128,15 @@ const getApprovalNodeIcon = (taskStatus: number, nodeType: BpmNodeTypeEnum) => {
|
||||
return statusIconMap[taskStatus]?.icon || 'mdi:clock-outline';
|
||||
}
|
||||
return 'mdi:clock-outline';
|
||||
};
|
||||
}
|
||||
|
||||
// 获取审批节点颜色
|
||||
const getApprovalNodeColor = (taskStatus: number) => {
|
||||
function getApprovalNodeColor(taskStatus: number) {
|
||||
return statusIconMap[taskStatus]?.color;
|
||||
};
|
||||
}
|
||||
|
||||
// 获取审批节点时间
|
||||
const getApprovalNodeTime = (node: BpmProcessInstanceApi.ApprovalNodeInfo) => {
|
||||
function getApprovalNodeTime(node: BpmProcessInstanceApi.ApprovalNodeInfo) {
|
||||
if (node.nodeType === BpmNodeTypeEnum.START_USER_NODE && node.startTime) {
|
||||
return formatDateTime(node.startTime);
|
||||
}
|
||||
@@ -147,7 +147,7 @@ const getApprovalNodeTime = (node: BpmProcessInstanceApi.ApprovalNodeInfo) => {
|
||||
return formatDateTime(node.startTime);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
}
|
||||
|
||||
// 选择自定义审批人
|
||||
const userSelectFormRef = ref();
|
||||
@@ -164,26 +164,26 @@ const handleSelectUser = (activityId: string, selectedList: any[]) => {
|
||||
|
||||
// 选择用户完成
|
||||
const selectedUsers = ref<number[]>([]);
|
||||
const handleUserSelectConfirm = (userList: any[]) => {
|
||||
function handleUserSelectConfirm(userList: any[]) {
|
||||
customApproveUsers.value[selectedActivityNodeId.value] = userList || [];
|
||||
|
||||
emit('selectUserConfirm', selectedActivityNodeId.value, userList);
|
||||
};
|
||||
}
|
||||
|
||||
/** 跳转子流程 */
|
||||
const handleChildProcess = (activity: any) => {
|
||||
function handleChildProcess(activity: any) {
|
||||
push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
id: activity.processInstanceId,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 判断是否需要显示自定义选择审批人
|
||||
const shouldShowCustomUserSelect = (
|
||||
function shouldShowCustomUserSelect(
|
||||
activity: BpmProcessInstanceApi.ApprovalNodeInfo,
|
||||
) => {
|
||||
) {
|
||||
return (
|
||||
isEmpty(activity.tasks) &&
|
||||
isEmpty(activity.candidateUsers) &&
|
||||
@@ -192,27 +192,27 @@ const shouldShowCustomUserSelect = (
|
||||
BpmCandidateStrategyEnum.APPROVE_USER_SELECT ===
|
||||
activity.candidateStrategy)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 判断是否需要显示审批意见
|
||||
const shouldShowApprovalReason = (task: any, nodeType: BpmNodeTypeEnum) => {
|
||||
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
|
||||
return (
|
||||
task.reason &&
|
||||
[BpmNodeTypeEnum.END_EVENT_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||
nodeType,
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// 用户选择弹窗关闭
|
||||
const handleUserSelectClosed = () => {
|
||||
function handleUserSelectClosed() {
|
||||
selectedUsers.value = [];
|
||||
};
|
||||
}
|
||||
|
||||
// 用户选择弹窗取消
|
||||
const handleUserSelectCancel = () => {
|
||||
function handleUserSelectCancel() {
|
||||
selectedUsers.value = [];
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmTaskApi } from '#/api/bpm/task';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -11,7 +8,7 @@ import { Page, prompt } from '@vben/common-ui';
|
||||
|
||||
import { Button, message, Textarea } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
cancelProcessInstanceByStartUser,
|
||||
getProcessInstanceMyPage,
|
||||
@@ -25,54 +22,21 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceMy' });
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getProcessInstanceMyPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
cellConfig: {
|
||||
height: 64,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskVO>,
|
||||
});
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<BpmTaskApi.TaskVO>) {
|
||||
switch (code) {
|
||||
case 'cancel': {
|
||||
onCancel(row);
|
||||
break;
|
||||
}
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/** 查看流程实例 */
|
||||
function handleDetail(row: BpmTaskApi.TaskVO) {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 取消流程实例 */
|
||||
function onCancel(row: BpmTaskApi.TaskVO) {
|
||||
function handleCancel(row: BpmTaskApi.TaskVO) {
|
||||
prompt({
|
||||
async beforeClose(scope) {
|
||||
if (scope.isConfirm) {
|
||||
@@ -104,19 +68,37 @@ function onCancel(row: BpmTaskApi.TaskVO) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 查看流程实例 */
|
||||
function onDetail(row: BpmTaskApi.TaskVO) {
|
||||
console.warn(row);
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getProcessInstanceMyPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
cellConfig: {
|
||||
height: 64,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskVO>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -126,7 +108,6 @@ function onRefresh() {
|
||||
url="https://doc.iocoder.cn/bpm/process-instance"
|
||||
/>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="流程状态">
|
||||
<!-- 摘要 -->
|
||||
<template #slot-summary="{ row }">
|
||||
@@ -154,7 +135,7 @@ function onRefresh() {
|
||||
<!-- 单人审批 -->
|
||||
<template v-if="row.tasks.length === 1">
|
||||
<span>
|
||||
<Button type="link" @click="onDetail(row)">
|
||||
<Button type="link" @click="handleDetail(row)">
|
||||
{{ row.tasks[0].assigneeUser?.nickname }}
|
||||
</Button>
|
||||
({{ row.tasks[0].name }}) 审批中
|
||||
@@ -163,7 +144,7 @@ function onRefresh() {
|
||||
<!-- 多人审批 -->
|
||||
<template v-else>
|
||||
<span>
|
||||
<Button type="link" @click="onDetail(row)">
|
||||
<Button type="link" @click="handleDetail(row)">
|
||||
{{ row.tasks[0].assigneeUser?.nickname }}
|
||||
</Button>
|
||||
等 {{ row.tasks.length }} 人 ({{ row.tasks[0].name }})审批中
|
||||
@@ -178,6 +159,28 @@ function onRefresh() {
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['bpm:process-instance:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.cancel'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
ifShow: row.status === BpmProcessInstanceStatus.RUNNING,
|
||||
auth: ['bpm:process-instance:cancel'],
|
||||
onClick: handleCancel.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { getCategorySimpleList } from '#/api/bpm/category';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
DICT_TYPE,
|
||||
formatPast2,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
|
||||
import { BpmProcessInstanceStatus } from '../../../../utils/constants';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
@@ -95,8 +83,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmProcessInstanceApi.ProcessInstanceVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
export function useGridColumns(
|
||||
onTaskClick: (task: BpmProcessInstanceApi.Task) => void,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
@@ -153,13 +140,7 @@ export function useGridColumns<T = BpmProcessInstanceApi.ProcessInstanceVO>(
|
||||
field: 'durationInMillis',
|
||||
title: '流程耗时',
|
||||
minWidth: 180,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return row.durationInMillis > 0
|
||||
? formatPast2(row.durationInMillis)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
formatter: 'formatPast2',
|
||||
},
|
||||
|
||||
// 当前审批任务 tasks
|
||||
@@ -192,36 +173,10 @@ export function useGridColumns<T = BpmProcessInstanceApi.ProcessInstanceVO>(
|
||||
minWidth: 320,
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程分类',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: $t('ui.actionTitle.detail'),
|
||||
show: hasAccessByCodes(['bpm:process-instance:query']),
|
||||
},
|
||||
{
|
||||
code: 'cancel',
|
||||
text: $t('ui.actionTitle.cancel'),
|
||||
show: (row: BpmProcessInstanceApi.ProcessInstanceVO) => {
|
||||
return (
|
||||
row.status === BpmProcessInstanceStatus.RUNNING &&
|
||||
hasAccessByCodes(['bpm:process-instance:cancel'])
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { h } from 'vue';
|
||||
@@ -11,62 +8,23 @@ import { Page, prompt } from '@vben/common-ui';
|
||||
|
||||
import { message, Textarea } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
cancelProcessInstanceByAdmin,
|
||||
getProcessInstanceManagerPage,
|
||||
} from '#/api/bpm/processInstance';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import { BpmProcessInstanceStatus } from '#/utils';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceManager' });
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick, onTaskClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getProcessInstanceManagerPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmProcessInstanceApi.ProcessInstanceVO>,
|
||||
});
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<BpmProcessInstanceApi.ProcessInstanceVO>) {
|
||||
switch (code) {
|
||||
case 'cancel': {
|
||||
onCancel(row);
|
||||
break;
|
||||
}
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 点击任务 */
|
||||
@@ -75,8 +33,17 @@ function onTaskClick(task: BpmProcessInstanceApi.Task) {
|
||||
console.warn(task);
|
||||
}
|
||||
|
||||
/** 查看流程实例 */
|
||||
function handleDetail(row: BpmProcessInstanceApi.ProcessInstanceVO) {
|
||||
console.warn(row);
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 取消流程实例 */
|
||||
function onCancel(row: BpmProcessInstanceApi.ProcessInstanceVO) {
|
||||
function handleCancel(row: BpmProcessInstanceApi.ProcessInstanceVO) {
|
||||
prompt({
|
||||
async beforeClose(scope) {
|
||||
if (scope.isConfirm) {
|
||||
@@ -110,19 +77,34 @@ function onCancel(row: BpmProcessInstanceApi.ProcessInstanceVO) {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/** 查看流程实例 */
|
||||
function onDetail(row: BpmProcessInstanceApi.ProcessInstanceVO) {
|
||||
console.warn(row);
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: { id: row.id },
|
||||
});
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onTaskClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getProcessInstanceManagerPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmProcessInstanceApi.ProcessInstanceVO>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -131,6 +113,29 @@ function onRefresh() {
|
||||
<DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm" />
|
||||
</template>
|
||||
|
||||
<Grid table-title="流程实例" />
|
||||
<Grid table-title="流程实例">
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['bpm:process-instance:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.cancel'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
ifShow: row.status === BpmProcessInstanceStatus.RUNNING,
|
||||
auth: ['bpm:process-instance:cancel'],
|
||||
onClick: handleCancel.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmCategoryApi } from '#/api/bpm/category';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
@@ -27,8 +24,6 @@ export const EVENT_OPTIONS = [
|
||||
{ label: 'timeout', value: 'timeout' },
|
||||
];
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
@@ -80,6 +75,16 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
allowClear: true,
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['type'],
|
||||
trigger: (values) => (values.event = undefined),
|
||||
componentProps: (values) => ({
|
||||
options:
|
||||
values.type === 'execution'
|
||||
? EVENT_EXECUTION_OPTIONS
|
||||
: EVENT_OPTIONS,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'valueType',
|
||||
@@ -96,9 +101,17 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
{
|
||||
fieldName: 'value',
|
||||
label: '表达式',
|
||||
label: '类路径|表达式',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['valueType'],
|
||||
trigger: (values) => (values.value = undefined),
|
||||
componentProps: (values) => ({
|
||||
placeholder:
|
||||
values.valueType === 'class' ? '请输入类路径' : '请输入表达式',
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -129,9 +142,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
@@ -178,29 +189,11 @@ export function useGridColumns<T = BpmCategoryApi.CategoryVO>(
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
minWidth: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程监听器',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'edit',
|
||||
show: hasAccessByCodes(['bpm:process-listener:update']),
|
||||
},
|
||||
{
|
||||
code: 'delete',
|
||||
show: hasAccessByCodes(['bpm:process-listener:delete']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmProcessListenerApi } from '#/api/bpm/processListener';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteProcessListener,
|
||||
getProcessListenerPage,
|
||||
@@ -25,12 +21,46 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建流程监听器 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑流程监听器 */
|
||||
function handleEdit(row: BpmProcessListenerApi.ProcessListenerVO) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除流程监听器 */
|
||||
async function handleDelete(row: BpmProcessListenerApi.ProcessListenerVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
try {
|
||||
await deleteProcessListener(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_key_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
@@ -53,57 +83,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeTableGridOptions<BpmProcessListenerApi.ProcessListenerVO>,
|
||||
});
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<BpmProcessListenerApi.ProcessListenerVO>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建流程监听器 */
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑流程监听器 */
|
||||
function onEdit(row: BpmProcessListenerApi.ProcessListenerVO) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除流程监听器 */
|
||||
async function onDelete(row: BpmProcessListenerApi.ProcessListenerVO) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
try {
|
||||
await deleteProcessListener(row.id as number);
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
onRefresh();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -114,17 +93,45 @@ async function onDelete(row: BpmProcessListenerApi.ProcessListenerVO) {
|
||||
url="https://doc.iocoder.cn/bpm/listener/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="流程监听器">
|
||||
<template #toolbar-tools>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="onCreate"
|
||||
v-access:code="['bpm:process-listener:create']"
|
||||
>
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', ['流程监听器']) }}
|
||||
</Button>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['流程监听器']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['bpm:process-listener:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['bpm:process-listener:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['bpm:process-listener:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '#/api/bpm/processListener';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { EVENT_EXECUTION_OPTIONS, EVENT_OPTIONS, useFormSchema } from '../data';
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<BpmProcessListenerApi.ProcessListenerVO>();
|
||||
@@ -31,7 +31,7 @@ const [Form, formApi] = useVbenForm({
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 110,
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
@@ -67,55 +67,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置事件
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'type',
|
||||
componentProps: {
|
||||
onChange: (value: string) => {
|
||||
formApi.setFieldValue('event', undefined);
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'event',
|
||||
componentProps: {
|
||||
options:
|
||||
value === 'execution'
|
||||
? EVENT_EXECUTION_OPTIONS
|
||||
: EVENT_OPTIONS,
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
fieldName: 'valueType',
|
||||
componentProps: {
|
||||
onChange: (value: string) => {
|
||||
formApi.setFieldValue('value', undefined);
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'value',
|
||||
label: value === 'class' ? '类路径' : '表达式',
|
||||
componentProps: {
|
||||
placeholder:
|
||||
value === 'class' ? '请输入类路径' : '请输入表达式',
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 加载数据
|
||||
const data = modalApi.getData<BpmProcessListenerApi.ProcessListenerVO>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getProcessListener(data.id as number);
|
||||
@@ -129,7 +85,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-[600px]">
|
||||
<Modal :title="getTitle" class="w-[40%]">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmTaskApi } from '#/api/bpm/task';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
@@ -32,9 +27,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'processInstanceName',
|
||||
@@ -46,8 +39,12 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
field: 'summary',
|
||||
title: '摘要',
|
||||
minWidth: 200,
|
||||
slots: {
|
||||
default: 'slot-summary',
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue && cellValue.length > 0
|
||||
? cellValue
|
||||
.map((item: any) => `${item.key} : ${item.value}`)
|
||||
.join('\n')
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -70,8 +67,8 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
field: 'createUser.nickname',
|
||||
title: '抄送人',
|
||||
minWidth: 180,
|
||||
slots: {
|
||||
default: 'slot-createUser',
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -86,26 +83,10 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 120,
|
||||
align: 'center',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程名称',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'detail',
|
||||
text: '详情',
|
||||
show: hasAccessByCodes(['bpm:task:query']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getProcessInstanceCopyPage } from '#/api/bpm/processInstance';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'BpmCopyTask' });
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
/** 任务详情 */
|
||||
function handleDetail(row: BpmProcessInstanceApi.CopyVO) {
|
||||
const query = {
|
||||
id: row.processInstanceId,
|
||||
...(row.activityId && { activityId: row.activityId }),
|
||||
};
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
@@ -47,36 +57,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeTableGridOptions<BpmProcessInstanceApi.CopyVO>,
|
||||
});
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<BpmProcessInstanceApi.CopyVO>) {
|
||||
switch (code) {
|
||||
case 'detail': {
|
||||
onDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 任务详情 */
|
||||
function onDetail(row: BpmProcessInstanceApi.CopyVO) {
|
||||
const query = {
|
||||
id: row.processInstanceId,
|
||||
...(row.activityId && { activityId: row.activityId }),
|
||||
};
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -88,27 +68,19 @@ function onRefresh() {
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="onRefresh" />
|
||||
<Grid table-title="抄送任务">
|
||||
<!-- 摘要 -->
|
||||
<template #slot-summary="{ row }">
|
||||
<div
|
||||
class="flex flex-col py-2"
|
||||
v-if="row.summary && row.summary.length > 0"
|
||||
>
|
||||
<div v-for="(item, index) in row.summary" :key="index">
|
||||
<span class="text-gray-500">
|
||||
{{ item.key }} : {{ item.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>-</div>
|
||||
</template>
|
||||
<!-- 抄送人 -->
|
||||
<template #slot-createUser="{ row }">
|
||||
<span class="text-gray-500">
|
||||
{{ row.createUser.nickname || '系统' }}
|
||||
</span>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.detail'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['bpm:task:query'],
|
||||
onClick: handleDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmTaskApi } from '#/api/bpm/task';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { getCategorySimpleList } from '#/api/bpm/category';
|
||||
import {
|
||||
DICT_TYPE,
|
||||
formatPast2,
|
||||
getDictOptions,
|
||||
getRangePickerDefaultProps,
|
||||
} from '#/utils';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
@@ -69,9 +63,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'processInstance.name',
|
||||
@@ -83,8 +75,12 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
field: 'processInstance.summary',
|
||||
title: '摘要',
|
||||
minWidth: 200,
|
||||
slots: {
|
||||
default: 'slot-summary',
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue && cellValue.length > 0
|
||||
? cellValue
|
||||
.map((item: any) => `${item.key} : ${item.value}`)
|
||||
.join('\n')
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -92,12 +88,6 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
title: '发起人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '发起时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '当前任务',
|
||||
@@ -133,9 +123,7 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
minWidth: 180,
|
||||
formatter: ({ cellValue }) => {
|
||||
return `${formatPast2(cellValue)}`;
|
||||
},
|
||||
formatter: 'formatPast2',
|
||||
},
|
||||
{
|
||||
field: 'processInstanceId',
|
||||
@@ -148,25 +136,10 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
minWidth: 280,
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 120,
|
||||
align: 'center',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程名称',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'history',
|
||||
text: '历史',
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmTaskApi } from '#/api/bpm/task';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTaskDonePage } from '#/api/bpm/task';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { router } from '#/router';
|
||||
@@ -16,12 +13,23 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'BpmDoneTask' });
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
/** 查看历史 */
|
||||
function handleHistory(row: BpmTaskApi.TaskManagerVO) {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
id: row.processInstance.id,
|
||||
taskId: row.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
@@ -47,33 +55,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskVO>,
|
||||
});
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<BpmTaskApi.TaskVO>) {
|
||||
switch (code) {
|
||||
case 'history': {
|
||||
onHistory(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看历史 */
|
||||
function onHistory(row: BpmTaskApi.TaskVO) {
|
||||
console.warn(row);
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
id: row.processInstance.id,
|
||||
taskId: row.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -92,25 +73,17 @@ function onRefresh() {
|
||||
</template>
|
||||
|
||||
<Grid table-title="已办任务">
|
||||
<!-- 摘要 -->
|
||||
<template #slot-summary="{ row }">
|
||||
<div
|
||||
class="flex flex-col py-2"
|
||||
v-if="
|
||||
row.processInstance.summary &&
|
||||
row.processInstance.summary.length > 0
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in row.processInstance.summary"
|
||||
:key="index"
|
||||
>
|
||||
<span class="text-gray-500">
|
||||
{{ item.key }} : {{ item.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>-</div>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '历史',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: handleHistory.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmTaskApi } from '#/api/bpm/task';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE, formatPast2, getRangePickerDefaultProps } from '#/utils';
|
||||
import { DICT_TYPE, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
@@ -28,9 +27,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'processInstance.name',
|
||||
@@ -89,9 +86,7 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
minWidth: 180,
|
||||
formatter: ({ cellValue }) => {
|
||||
return `${formatPast2(cellValue)}`;
|
||||
},
|
||||
formatter: 'formatPast2',
|
||||
},
|
||||
{
|
||||
field: 'processInstanceId',
|
||||
@@ -104,25 +99,10 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
minWidth: 280,
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 120,
|
||||
align: 'center',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程名称',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'history',
|
||||
text: '历史',
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmTaskApi } from '#/api/bpm/task';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTaskManagerPage } from '#/api/bpm/task';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { router } from '#/router';
|
||||
@@ -16,12 +13,22 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'BpmManagerTask' });
|
||||
|
||||
/** 查看历史 */
|
||||
function handleHistory(row: BpmTaskApi.TaskManagerVO) {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
id: row.processInstance.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
@@ -47,30 +54,6 @@ const [Grid] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskManagerVO>,
|
||||
});
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<BpmTaskApi.TaskManagerVO>) {
|
||||
switch (code) {
|
||||
case 'history': {
|
||||
onHistory(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看历史 */
|
||||
function onHistory(row: BpmTaskApi.TaskManagerVO) {
|
||||
console.warn(row);
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
id: row.processInstance.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -78,6 +61,20 @@ function onHistory(row: BpmTaskApi.TaskManagerVO) {
|
||||
<template #doc>
|
||||
<DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
|
||||
</template>
|
||||
<Grid table-title="流程任务" />
|
||||
<Grid table-title="流程任务">
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '历史',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['bpm:task:query'],
|
||||
onClick: handleHistory.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmTaskApi } from '#/api/bpm/task';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { getCategorySimpleList } from '#/api/bpm/category';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
@@ -69,9 +62,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'processInstance.name',
|
||||
@@ -83,29 +74,12 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
field: 'processInstance.summary',
|
||||
title: '摘要',
|
||||
minWidth: 200,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
const summary = row?.processInstance?.summary;
|
||||
|
||||
if (!summary || summary.length === 0) {
|
||||
return '-';
|
||||
}
|
||||
return summary.map((item: any) => {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
key: item.key,
|
||||
},
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: 'text-gray-500',
|
||||
},
|
||||
`${item.key} : ${item.value}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
formatter: ({ cellValue }) => {
|
||||
return cellValue && cellValue.length > 0
|
||||
? cellValue
|
||||
.map((item: any) => `${item.key} : ${item.value}`)
|
||||
.join('\n')
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -113,12 +87,6 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
title: '发起人',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '发起时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '当前任务',
|
||||
@@ -141,26 +109,10 @@ export function useGridColumns<T = BpmTaskApi.TaskVO>(
|
||||
minWidth: 280,
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
title: '操作',
|
||||
minWidth: 120,
|
||||
align: 'center',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: '流程名称',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'audit',
|
||||
text: '办理',
|
||||
show: hasAccessByCodes(['bpm:task:query']),
|
||||
},
|
||||
],
|
||||
},
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { BpmTaskApi } from '#/api/bpm/task';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTaskTodoPage } from '#/api/bpm/task';
|
||||
import { DocAlert } from '#/components/doc-alert';
|
||||
import { router } from '#/router';
|
||||
@@ -16,12 +13,24 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'BpmTodoTask' });
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
/** 办理任务 */
|
||||
function handleAudit(row: BpmTaskApi.TaskVO) {
|
||||
console.warn(row);
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
id: row.processInstance.id,
|
||||
taskId: row.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(onActionClick),
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
@@ -47,33 +56,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskVO>,
|
||||
});
|
||||
|
||||
/** 表格操作按钮的回调函数 */
|
||||
function onActionClick({ code, row }: OnActionClickParams<BpmTaskApi.TaskVO>) {
|
||||
switch (code) {
|
||||
case 'audit': {
|
||||
onAudit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 办理任务 */
|
||||
function onAudit(row: BpmTaskApi.TaskVO) {
|
||||
console.warn(row);
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
id: row.processInstance.id,
|
||||
taskId: row.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -90,6 +72,21 @@ function onRefresh() {
|
||||
/>
|
||||
<DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" />
|
||||
</template>
|
||||
<Grid table-title="待办任务" />
|
||||
|
||||
<Grid table-title="待办任务">
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '办理',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
auth: ['bpm:task:query'],
|
||||
onClick: handleAudit.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { getBusinessStatusTypeSimpleList } from '#/api/crm/business/status';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { erpPriceInputFormatter, erpPriceMultiply } from '#/utils';
|
||||
import { erpPriceMultiply } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
@@ -93,6 +90,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'product',
|
||||
label: '产品清单',
|
||||
component: 'Input',
|
||||
formItemClass: 'col-span-3',
|
||||
},
|
||||
{
|
||||
fieldName: 'totalProductPrice',
|
||||
label: '产品总金额',
|
||||
@@ -225,123 +228,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '商机金额(元)',
|
||||
content: (data) => erpPriceInputFormatter(data.totalPrice),
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
label: '商机组',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data) => formatDateTime(data?.createTime) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '商机名称',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '商机金额(元)',
|
||||
content: (data) => erpPriceInputFormatter(data.totalPrice),
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
label: '预计成交日期',
|
||||
content: (data) => formatDateTime(data?.dealTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.contactNextTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
label: '商机状态组',
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
label: '商机阶段',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
type: 'checkbox',
|
||||
width: 50,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '商机名称',
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '商机金额(元)',
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
title: '预计成交日期',
|
||||
formatter: 'formatDate',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
title: '商机状态组',
|
||||
fixed: 'right',
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
title: '商机阶段',
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
25
apps/web-antd/src/views/crm/business/index.ts
Normal file
25
apps/web-antd/src/views/crm/business/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export const BusinessForm = defineAsyncComponent(
|
||||
() => import('./modules/form.vue'),
|
||||
);
|
||||
|
||||
export const BusinessDetailsInfo = defineAsyncComponent(
|
||||
() => import('./modules/detail-info.vue'),
|
||||
);
|
||||
|
||||
export const BusinessDetailsList = defineAsyncComponent(
|
||||
() => import('./modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
export const BusinessDetails = defineAsyncComponent(
|
||||
() => import('./modules/detail.vue'),
|
||||
);
|
||||
|
||||
export const BusinessDetailsListModal = defineAsyncComponent(
|
||||
() => import('./modules/detail-list-modal.vue'),
|
||||
);
|
||||
|
||||
export const UpStatusForm = defineAsyncComponent(
|
||||
() => import('./modules/up-status-form.vue'),
|
||||
);
|
||||
126
apps/web-antd/src/views/crm/business/modules/detail-data.ts
Normal file
126
apps/web-antd/src/views/crm/business/modules/detail-data.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { erpPriceInputFormatter } from '#/utils';
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '商机金额(元)',
|
||||
content: (data) => erpPriceInputFormatter(data.totalPrice),
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
label: '商机组',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data) => formatDateTime(data?.createTime) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '商机名称',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '商机金额(元)',
|
||||
content: (data) => erpPriceInputFormatter(data.totalPrice),
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
label: '预计成交日期',
|
||||
content: (data) => formatDateTime(data?.dealTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.contactNextTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
label: '商机状态组',
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
label: '商机阶段',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
type: 'checkbox',
|
||||
width: 50,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '商机名称',
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '商机金额(元)',
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'dealTime',
|
||||
title: '预计成交日期',
|
||||
formatter: 'formatDate',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
title: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserDeptName',
|
||||
title: '所属部门',
|
||||
},
|
||||
{
|
||||
field: 'statusTypeName',
|
||||
title: '商机状态组',
|
||||
fixed: 'right',
|
||||
},
|
||||
{
|
||||
field: 'statusName',
|
||||
title: '商机阶段',
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Divider } from 'ant-design-vue';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
import { useDetailBaseSchema } from './detail-data';
|
||||
|
||||
defineProps<{
|
||||
business: CrmBusinessApi.Business; // 商机信息
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getBusinessPageByCustomer } from '#/api/crm/business';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDetailListColumns } from '../data';
|
||||
import { useDetailListColumns } from './detail-data';
|
||||
import Form from './form.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -19,7 +19,7 @@ import { createContactBusinessList } from '#/api/crm/contact';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDetailListColumns } from '../data';
|
||||
import { useDetailListColumns } from './detail-data';
|
||||
import ListModal from './detail-list-modal.vue';
|
||||
import Form from './form.vue';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
@@ -14,48 +14,19 @@ import { getBusiness } from '#/api/crm/business';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { AsyncOperateLog } from '#/components/operate-log';
|
||||
import {
|
||||
BusinessDetailsInfo,
|
||||
BusinessForm,
|
||||
UpStatusForm,
|
||||
} from '#/views/crm/business';
|
||||
import { ContactDetailsList } from '#/views/crm/contact';
|
||||
import { ContractDetailsList } from '#/views/crm/contract';
|
||||
import { FollowUp } from '#/views/crm/followup';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
import { ProductDetailsList } from '#/views/crm/product';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const BusinessDetailsInfo = defineAsyncComponent(
|
||||
() => import('./detail-info.vue'),
|
||||
);
|
||||
|
||||
const ContactDetailsList = defineAsyncComponent(
|
||||
() => import('#/views/crm/contact/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
const ContractDetailsList = defineAsyncComponent(
|
||||
() => import('#/views/crm/contract/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
const FollowUp = defineAsyncComponent(
|
||||
() => import('#/views/crm/followup/index.vue'),
|
||||
);
|
||||
|
||||
const ProductDetailsList = defineAsyncComponent(
|
||||
() => import('#/views/crm/product/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
const PermissionList = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/permission-list.vue'),
|
||||
);
|
||||
|
||||
const TransferForm = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/transfer-form.vue'),
|
||||
);
|
||||
|
||||
const OperateLog = defineAsyncComponent(
|
||||
() => import('#/components/operate-log'),
|
||||
);
|
||||
|
||||
const BusinessForm = defineAsyncComponent(
|
||||
() => import('#/views/crm/business/modules/form.vue'),
|
||||
);
|
||||
|
||||
const UpStatusForm = defineAsyncComponent(
|
||||
() => import('#/views/crm/business/modules/up-status-form.vue'),
|
||||
);
|
||||
import { useDetailSchema } from './detail-data';
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -183,6 +154,7 @@ onMounted(() => {
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="产品" key="4" :force-render="true">
|
||||
<ProductDetailsList
|
||||
:biz-id="businessId"
|
||||
:biz-type="BizTypeEnum.CRM_BUSINESS"
|
||||
:business="business"
|
||||
/>
|
||||
@@ -203,7 +175,7 @@ onMounted(() => {
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="操作日志" key="7" :force-render="true">
|
||||
<OperateLog :log-list="businessLogList" />
|
||||
<AsyncOperateLog :log-list="businessLogList" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
getBusiness,
|
||||
updateBusiness,
|
||||
} from '#/api/crm/business';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
import { erpPriceMultiply } from '#/utils';
|
||||
import { ProductEditTable } from '#/views/crm/product';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
@@ -25,15 +28,37 @@ const getTitle = computed(() => {
|
||||
: $t('ui.actionTitle.create', ['商机']);
|
||||
});
|
||||
|
||||
function handleUpdateProducts(products: any) {
|
||||
formData.value = modalApi.getData<CrmBusinessApi.Business>();
|
||||
formData.value!.products = products;
|
||||
if (formData.value) {
|
||||
const totalProductPrice =
|
||||
formData.value.products?.reduce(
|
||||
(prev, curr) => prev + curr.totalPrice,
|
||||
0,
|
||||
) ?? 0;
|
||||
const discountPercent = formData.value.discountPercent;
|
||||
const discountPrice =
|
||||
discountPercent === null
|
||||
? 0
|
||||
: erpPriceMultiply(totalProductPrice, discountPercent / 100);
|
||||
const totalPrice = totalProductPrice - (discountPrice ?? 0);
|
||||
formData.value!.totalProductPrice = totalProductPrice;
|
||||
formData.value!.totalPrice = totalPrice;
|
||||
formApi.setValues(formData.value!);
|
||||
}
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
// 一共3列
|
||||
wrapperClass: 'grid-cols-3',
|
||||
layout: 'vertical',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
@@ -47,6 +72,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmBusinessApi.Business;
|
||||
data.products = formData.value?.products;
|
||||
try {
|
||||
await (formData.value?.id ? updateBusiness(data) : createBusiness(data));
|
||||
// 关闭并提示
|
||||
@@ -80,7 +106,17 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-[40%]">
|
||||
<Form class="mx-4" />
|
||||
<Modal :title="getTitle" class="w-[50%]">
|
||||
<Form class="mx-4">
|
||||
<template #product="slotProps">
|
||||
<ProductEditTable
|
||||
v-bind="slotProps"
|
||||
class="w-full"
|
||||
:products="formData?.products ?? []"
|
||||
:biz-type="BizTypeEnum.CRM_BUSINESS"
|
||||
@update:products="handleUpdateProducts"
|
||||
/>
|
||||
</template>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
@@ -254,102 +248,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情头部的配置 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'source',
|
||||
label: '线索来源',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
|
||||
value: data?.source,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data) => formatDateTime(data?.createTime) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情基本信息的配置 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '线索名称',
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
label: '客户来源',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
|
||||
value: data?.source,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
label: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
label: '地址',
|
||||
content: (data) => data?.areaName + data?.detailAddress,
|
||||
},
|
||||
{
|
||||
field: 'qq',
|
||||
label: 'QQ',
|
||||
},
|
||||
{
|
||||
field: 'wechat',
|
||||
label: '微信',
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
label: '客户行业',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
value: data?.industryId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
label: '客户级别',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_LEVEL,
|
||||
value: data?.level,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.contactNextTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
107
apps/web-antd/src/views/crm/clue/modules/detail-data.ts
Normal file
107
apps/web-antd/src/views/crm/clue/modules/detail-data.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
/** 详情头部的配置 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'source',
|
||||
label: '线索来源',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
|
||||
value: data?.source,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data) => formatDateTime(data?.createTime) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情基本信息的配置 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '线索名称',
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
label: '客户来源',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
|
||||
value: data?.source,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
label: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
label: '地址',
|
||||
content: (data) => data?.areaName + data?.detailAddress,
|
||||
},
|
||||
{
|
||||
field: 'qq',
|
||||
label: 'QQ',
|
||||
},
|
||||
{
|
||||
field: 'wechat',
|
||||
label: '微信',
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
label: '客户行业',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
value: data?.industryId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
label: '客户级别',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_LEVEL,
|
||||
value: data?.level,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.contactNextTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Divider } from 'ant-design-vue';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
import { useDetailBaseSchema } from './detail-data';
|
||||
|
||||
defineProps<{
|
||||
clue: CrmClueApi.Clue; // 线索信息
|
||||
|
||||
@@ -15,26 +15,13 @@ import { getClue, transformClue } from '#/api/crm/clue';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { AsyncOperateLog } from '#/components/operate-log';
|
||||
import { FollowUp } from '#/views/crm/followup';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
import { useDetailSchema } from './detail-data';
|
||||
import ClueForm from './form.vue';
|
||||
|
||||
const FollowUp = defineAsyncComponent(
|
||||
() => import('#/views/crm/followup/index.vue'),
|
||||
);
|
||||
|
||||
const PermissionList = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/permission-list.vue'),
|
||||
);
|
||||
|
||||
const TransferForm = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/transfer-form.vue'),
|
||||
);
|
||||
|
||||
const OperateLog = defineAsyncComponent(
|
||||
() => import('#/components/operate-log'),
|
||||
);
|
||||
|
||||
const ClueDetailsInfo = defineAsyncComponent(() => import('./detail-info.vue'));
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -184,7 +171,7 @@ onMounted(() => {
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="操作日志" key="4" :force-render="true">
|
||||
<OperateLog :log-list="clueLogList" />
|
||||
<AsyncOperateLog :log-list="clueLogList" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { getSimpleContactList } from '#/api/crm/contact';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
@@ -278,159 +272,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
label: '职务',
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.createTime) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '姓名',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
label: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'qq',
|
||||
label: 'QQ',
|
||||
},
|
||||
{
|
||||
field: 'wechat',
|
||||
label: '微信',
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
label: '地址',
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
label: '详细地址',
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
label: '职务',
|
||||
},
|
||||
{
|
||||
field: 'parentName',
|
||||
label: '直属上级',
|
||||
},
|
||||
{
|
||||
field: 'master',
|
||||
label: '关键决策人',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
value: data?.master,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
label: '性别',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.SYSTEM_USER_SEX, value: data?.sex }),
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.contactNextTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
type: 'checkbox',
|
||||
width: 50,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '姓名',
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
title: '性别',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
title: '职位',
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
},
|
||||
{
|
||||
field: 'master',
|
||||
title: '关键决策人',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
17
apps/web-antd/src/views/crm/contact/index.ts
Normal file
17
apps/web-antd/src/views/crm/contact/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export const ContactDetailsInfo = defineAsyncComponent(
|
||||
() => import('./modules/detail-info.vue'),
|
||||
);
|
||||
|
||||
export const ContactForm = defineAsyncComponent(
|
||||
() => import('./modules/form.vue'),
|
||||
);
|
||||
|
||||
export const ContactDetails = defineAsyncComponent(
|
||||
() => import('./modules/detail.vue'),
|
||||
);
|
||||
|
||||
export const ContactDetailsList = defineAsyncComponent(
|
||||
() => import('./modules/detail-list.vue'),
|
||||
);
|
||||
165
apps/web-antd/src/views/crm/contact/modules/detail-data.ts
Normal file
165
apps/web-antd/src/views/crm/contact/modules/detail-data.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
label: '职务',
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.createTime) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '姓名',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
label: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'qq',
|
||||
label: 'QQ',
|
||||
},
|
||||
{
|
||||
field: 'wechat',
|
||||
label: '微信',
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
label: '地址',
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
label: '详细地址',
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
label: '职务',
|
||||
},
|
||||
{
|
||||
field: 'parentName',
|
||||
label: '直属上级',
|
||||
},
|
||||
{
|
||||
field: 'master',
|
||||
label: '关键决策人',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.INFRA_BOOLEAN_STRING,
|
||||
value: data?.master,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
label: '性别',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.SYSTEM_USER_SEX, value: data?.sex }),
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.contactNextTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
type: 'checkbox',
|
||||
width: 50,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '姓名',
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
title: '客户名称',
|
||||
fixed: 'left',
|
||||
slots: { default: 'customerName' },
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
title: '性别',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.SYSTEM_USER_SEX },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
title: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
title: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'post',
|
||||
title: '职位',
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
title: '地址',
|
||||
},
|
||||
{
|
||||
field: 'master',
|
||||
title: '关键决策人',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Divider } from 'ant-design-vue';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
import { useDetailBaseSchema } from './detail-data';
|
||||
|
||||
defineProps<{
|
||||
contact: CrmContactApi.Contact; // 联系人信息
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getContactPageByCustomer } from '#/api/crm/contact';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDetailListColumns } from '../data';
|
||||
import { useDetailListColumns } from './detail-data';
|
||||
import Form from './form.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDetailListColumns } from '../data';
|
||||
import { useDetailListColumns } from './detail-data';
|
||||
import ListModal from './detail-list-modal.vue';
|
||||
import Form from './form.vue';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { CrmContactApi } from '#/api/crm/contact';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
@@ -14,36 +14,13 @@ import { getContact } from '#/api/crm/contact';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { AsyncOperateLog } from '#/components/operate-log';
|
||||
import { BusinessDetailsList } from '#/views/crm/business';
|
||||
import { ContactDetailsInfo, ContactForm } from '#/views/crm/contact';
|
||||
import { FollowUp } from '#/views/crm/followup';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const ContactDetailsInfo = defineAsyncComponent(
|
||||
() => import('./detail-info.vue'),
|
||||
);
|
||||
|
||||
const ContactForm = defineAsyncComponent(
|
||||
() => import('#/views/crm/contact/modules/form.vue'),
|
||||
);
|
||||
|
||||
const BusinessList = defineAsyncComponent(
|
||||
() => import('#/views/crm/business/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
const FollowUp = defineAsyncComponent(
|
||||
() => import('#/views/crm/followup/index.vue'),
|
||||
);
|
||||
|
||||
const PermissionList = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/permission-list.vue'),
|
||||
);
|
||||
|
||||
const TransferForm = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/transfer-form.vue'),
|
||||
);
|
||||
|
||||
const OperateLog = defineAsyncComponent(
|
||||
() => import('#/components/operate-log'),
|
||||
);
|
||||
import { useDetailSchema } from './detail-data';
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -156,7 +133,7 @@ onMounted(() => {
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="商机" key="4" :force-render="true">
|
||||
<BusinessList
|
||||
<BusinessDetailsList
|
||||
:biz-id="contactId"
|
||||
:biz-type="BizTypeEnum.CRM_CONTACT"
|
||||
:contact-id="contactId"
|
||||
@@ -164,7 +141,7 @@ onMounted(() => {
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="操作日志" key="5" :force-render="true">
|
||||
<OperateLog :log-list="contactLogList" />
|
||||
<AsyncOperateLog :log-list="contactLogList" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleBusinessList } from '#/api/crm/business';
|
||||
import { getSimpleContactList } from '#/api/crm/contact';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { erpPriceInputFormatter, floatToFixed2 } from '#/utils';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { erpPriceMultiply, floatToFixed2 } from '#/utils';
|
||||
import { DICT_TYPE } from '#/utils/dict';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'no',
|
||||
label: '合同编号',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入合同编号',
|
||||
placeholder: '保存时自动生成',
|
||||
disabled: () => true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -34,9 +38,22 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
placeholder: '请输入合同名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'ownerUserId',
|
||||
label: '负责人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: () => getSimpleUserList(),
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'customerId',
|
||||
label: '客户',
|
||||
label: '客户名称',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
@@ -48,9 +65,8 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
{
|
||||
fieldName: 'businessId',
|
||||
label: '商机',
|
||||
label: '商机名称',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getSimpleBusinessList,
|
||||
labelField: 'name',
|
||||
@@ -58,49 +74,53 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
placeholder: '请选择商机',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'totalPrice',
|
||||
label: '合同金额',
|
||||
component: 'InputNumber',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入合同金额',
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'orderDate',
|
||||
label: '下单时间',
|
||||
label: '下单日期',
|
||||
component: 'DatePicker',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择下单时间',
|
||||
showTime: false,
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'startTime',
|
||||
label: '合同开始时间',
|
||||
component: 'DatePicker',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择合同开始时间',
|
||||
showTime: false,
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'endTime',
|
||||
label: '合同结束时间',
|
||||
component: 'DatePicker',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请选择合同结束时间',
|
||||
showTime: false,
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'signUserId',
|
||||
label: '公司签约人',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: () => getSimpleUserList(),
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'signContactId',
|
||||
label: '客户签约人',
|
||||
component: 'ApiSelect',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
api: getSimpleContactList,
|
||||
labelField: 'name',
|
||||
@@ -117,6 +137,50 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'product',
|
||||
label: '产品清单',
|
||||
component: 'Input',
|
||||
formItemClass: 'col-span-3',
|
||||
},
|
||||
{
|
||||
fieldName: 'totalProductPrice',
|
||||
label: '产品总金额',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'discountPercent',
|
||||
label: '整单折扣(%)',
|
||||
component: 'InputNumber',
|
||||
rules: z.number().min(0).max(100).default(0),
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'totalPrice',
|
||||
label: '折扣后金额',
|
||||
component: 'InputNumber',
|
||||
dependencies: {
|
||||
triggerFields: ['totalProductPrice', 'discountPercent'],
|
||||
disabled: () => true,
|
||||
trigger(values, form) {
|
||||
const discountPrice =
|
||||
erpPriceMultiply(
|
||||
values.totalProductPrice,
|
||||
values.discountPercent / 100,
|
||||
) ?? 0;
|
||||
form.setFieldValue(
|
||||
'totalPrice',
|
||||
values.totalProductPrice - discountPrice,
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -280,182 +344,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情头部的配置 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '合同金额(元)',
|
||||
content: (data) => erpPriceInputFormatter(data?.totalPrice) as string,
|
||||
},
|
||||
{
|
||||
field: 'orderDate',
|
||||
label: '下单时间',
|
||||
content: (data) => formatDateTime(data?.orderDate) as string,
|
||||
},
|
||||
{
|
||||
field: 'totalReceivablePrice',
|
||||
label: '回款金额(元)',
|
||||
content: (data) =>
|
||||
erpPriceInputFormatter(data?.totalReceivablePrice) as string,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情基本信息的配置 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '合同编号',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
label: '合同名称',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'businessName',
|
||||
label: '商机名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '合同金额(元)',
|
||||
content: (data) => erpPriceInputFormatter(data?.totalPrice) as string,
|
||||
},
|
||||
{
|
||||
field: 'orderDate',
|
||||
label: '下单时间',
|
||||
content: (data) => formatDateTime(data?.orderDate) as string,
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
label: '合同开始时间',
|
||||
content: (data) => formatDateTime(data?.startTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
label: '合同结束时间',
|
||||
content: (data) => formatDateTime(data?.endTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'signContactName',
|
||||
label: '客户签约人',
|
||||
},
|
||||
{
|
||||
field: 'signUserName',
|
||||
label: '公司签约人',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
{
|
||||
field: 'auditStatus',
|
||||
label: '合同状态',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_AUDIT_STATUS,
|
||||
value: data?.auditStatus,
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'no',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '合同名称',
|
||||
field: 'name',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
title: '合同金额(元)',
|
||||
field: 'totalPrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '合同开始时间',
|
||||
field: 'startTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '合同结束时间',
|
||||
field: 'endTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '已回款金额(元)',
|
||||
field: 'totalReceivablePrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '未回款金额(元)',
|
||||
field: 'unpaidPrice',
|
||||
minWidth: 150,
|
||||
formatter: ({ row }) => {
|
||||
return floatToFixed2(row.totalPrice - row.totalReceivablePrice);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '所属部门',
|
||||
field: 'ownerUserDeptName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
field: 'creatorName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同状态',
|
||||
field: 'auditStatus',
|
||||
fixed: 'right',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
17
apps/web-antd/src/views/crm/contract/index.ts
Normal file
17
apps/web-antd/src/views/crm/contract/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export const ContractDetailsInfo = defineAsyncComponent(
|
||||
() => import('./modules/detail-info.vue'),
|
||||
);
|
||||
|
||||
export const ContractForm = defineAsyncComponent(
|
||||
() => import('./modules/form.vue'),
|
||||
);
|
||||
|
||||
export const ContractDetails = defineAsyncComponent(
|
||||
() => import('./modules/detail.vue'),
|
||||
);
|
||||
|
||||
export const ContractDetailsList = defineAsyncComponent(
|
||||
() => import('./modules/detail-list.vue'),
|
||||
);
|
||||
188
apps/web-antd/src/views/crm/contract/modules/detail-data.ts
Normal file
188
apps/web-antd/src/views/crm/contract/modules/detail-data.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, erpPriceInputFormatter, floatToFixed2 } from '#/utils';
|
||||
|
||||
/** 详情头部的配置 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '合同金额(元)',
|
||||
content: (data) => erpPriceInputFormatter(data?.totalPrice) as string,
|
||||
},
|
||||
{
|
||||
field: 'orderDate',
|
||||
label: '下单时间',
|
||||
content: (data) => formatDateTime(data?.orderDate) as string,
|
||||
},
|
||||
{
|
||||
field: 'totalReceivablePrice',
|
||||
label: '回款金额(元)',
|
||||
content: (data) =>
|
||||
erpPriceInputFormatter(data?.totalReceivablePrice) as string,
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情基本信息的配置 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '合同编号',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
label: '合同名称',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'businessName',
|
||||
label: '商机名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '合同金额(元)',
|
||||
content: (data) => erpPriceInputFormatter(data?.totalPrice) as string,
|
||||
},
|
||||
{
|
||||
field: 'orderDate',
|
||||
label: '下单时间',
|
||||
content: (data) => formatDateTime(data?.orderDate) as string,
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
label: '合同开始时间',
|
||||
content: (data) => formatDateTime(data?.startTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'endTime',
|
||||
label: '合同结束时间',
|
||||
content: (data) => formatDateTime(data?.endTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'signContactName',
|
||||
label: '客户签约人',
|
||||
},
|
||||
{
|
||||
field: 'signUserName',
|
||||
label: '公司签约人',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
{
|
||||
field: 'auditStatus',
|
||||
label: '合同状态',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_AUDIT_STATUS,
|
||||
value: data?.auditStatus,
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'no',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '合同名称',
|
||||
field: 'name',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
title: '合同金额(元)',
|
||||
field: 'totalPrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '合同开始时间',
|
||||
field: 'startTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '合同结束时间',
|
||||
field: 'endTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '已回款金额(元)',
|
||||
field: 'totalReceivablePrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '未回款金额(元)',
|
||||
field: 'unpaidPrice',
|
||||
minWidth: 150,
|
||||
formatter: ({ row }) => {
|
||||
return floatToFixed2(row.totalPrice - row.totalReceivablePrice);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '所属部门',
|
||||
field: 'ownerUserDeptName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
field: 'createTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
field: 'creatorName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同状态',
|
||||
field: 'auditStatus',
|
||||
fixed: 'right',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Divider } from 'ant-design-vue';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
import { useDetailBaseSchema } from './detail-data';
|
||||
|
||||
defineProps<{
|
||||
contract: CrmContractApi.Contract; // 合同信息
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDetailListColumns } from '../data';
|
||||
import { useDetailListColumns } from './detail-data';
|
||||
import Form from './form.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
@@ -15,41 +15,17 @@ import { getContract } from '#/api/crm/contract';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { AsyncOperateLog } from '#/components/operate-log';
|
||||
import { ContractDetailsInfo, ContractForm } from '#/views/crm/contract';
|
||||
import { FollowUp } from '#/views/crm/followup';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
import { ProductDetailsList } from '#/views/crm/product';
|
||||
import {
|
||||
ReceivableDetailsList,
|
||||
ReceivablePlanDetailsList,
|
||||
} from '#/views/crm/receivable';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
import ClueForm from './form.vue';
|
||||
|
||||
const FollowUp = defineAsyncComponent(
|
||||
() => import('#/views/crm/followup/index.vue'),
|
||||
);
|
||||
|
||||
const ProductDetailsList = defineAsyncComponent(
|
||||
() => import('#/views/crm/product/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
const PermissionList = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/permission-list.vue'),
|
||||
);
|
||||
|
||||
const TransferForm = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/transfer-form.vue'),
|
||||
);
|
||||
|
||||
const OperateLog = defineAsyncComponent(
|
||||
() => import('#/components/operate-log'),
|
||||
);
|
||||
|
||||
const ContractDetailsInfo = defineAsyncComponent(
|
||||
() => import('./detail-info.vue'),
|
||||
);
|
||||
|
||||
const ReceivableDetailsList = defineAsyncComponent(
|
||||
() => import('#/views/crm/receivable/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
const ReceivablePlanDetailsList = defineAsyncComponent(
|
||||
() => import('#/views/crm/receivable/plan/modules/detail-list.vue'),
|
||||
);
|
||||
import { useDetailSchema } from './detail-data';
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -79,7 +55,7 @@ const [Description] = useDescription({
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: ClueForm,
|
||||
connectedComponent: ContractForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
@@ -161,8 +137,8 @@ onMounted(() => {
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="产品" key="3" :force-render="true">
|
||||
<ProductDetailsList
|
||||
:biz-id="contractId"
|
||||
:biz-type="BizTypeEnum.CRM_CONTRACT"
|
||||
:contract="contract"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="回款" key="4" :force-render="true">
|
||||
@@ -185,7 +161,7 @@ onMounted(() => {
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="操作日志" key="6" :force-render="true">
|
||||
<OperateLog :log-list="contractLogList" />
|
||||
<AsyncOperateLog :log-list="contractLogList" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
getContract,
|
||||
updateContract,
|
||||
} from '#/api/crm/contract';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { $t } from '#/locales';
|
||||
import { erpPriceMultiply } from '#/utils';
|
||||
import { ProductEditTable } from '#/views/crm/product';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
@@ -24,15 +27,37 @@ const getTitle = computed(() => {
|
||||
: $t('ui.actionTitle.create', ['合同']);
|
||||
});
|
||||
|
||||
function handleUpdateProducts(products: any) {
|
||||
formData.value = modalApi.getData<CrmContractApi.Contract>();
|
||||
formData.value!.products = products;
|
||||
if (formData.value) {
|
||||
const totalProductPrice =
|
||||
formData.value.products?.reduce(
|
||||
(prev, curr) => prev + curr.totalPrice,
|
||||
0,
|
||||
) ?? 0;
|
||||
const discountPercent = formData.value.discountPercent;
|
||||
const discountPrice =
|
||||
discountPercent === null
|
||||
? 0
|
||||
: erpPriceMultiply(totalProductPrice, discountPercent / 100);
|
||||
const totalPrice = totalProductPrice - (discountPrice ?? 0);
|
||||
formData.value!.totalProductPrice = totalProductPrice;
|
||||
formData.value!.totalPrice = totalPrice;
|
||||
formApi.setValues(formData.value!);
|
||||
}
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 120,
|
||||
},
|
||||
// 一共2列
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
// 一共3列
|
||||
wrapperClass: 'grid-cols-3',
|
||||
layout: 'vertical',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
@@ -46,6 +71,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as CrmContractApi.Contract;
|
||||
data.products = formData.value?.products;
|
||||
try {
|
||||
await (formData.value?.id ? updateContract(data) : createContract(data));
|
||||
// 关闭并提示
|
||||
@@ -79,7 +105,17 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-[40%]">
|
||||
<Form class="mx-4" />
|
||||
<Modal :title="getTitle" class="w-[50%]">
|
||||
<Form class="mx-4">
|
||||
<template #product="slotProps">
|
||||
<ProductEditTable
|
||||
v-bind="slotProps"
|
||||
class="w-full"
|
||||
:products="formData?.products ?? []"
|
||||
:biz-type="BizTypeEnum.CRM_CONTRACT"
|
||||
@update:products="handleUpdateProducts"
|
||||
/>
|
||||
</template>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { getAreaTree } from '#/api/system/area';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
@@ -240,96 +234,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'level',
|
||||
label: '客户级别',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: data?.level }),
|
||||
},
|
||||
{
|
||||
field: 'dealStatus',
|
||||
label: '成交状态',
|
||||
content: (data) => (data.dealStatus ? '已成交' : '未成交'),
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data) => formatDateTime(data?.createTime) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
label: '客户来源',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
|
||||
value: data?.source,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
label: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
label: '地址',
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
label: '详细地址',
|
||||
},
|
||||
{
|
||||
field: 'qq',
|
||||
label: 'QQ',
|
||||
},
|
||||
{
|
||||
field: 'wechat',
|
||||
label: '微信',
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
label: '客户行业',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
value: data?.industryId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
label: '客户级别',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: data?.level }),
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.contactNextTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
17
apps/web-antd/src/views/crm/customer/index.ts
Normal file
17
apps/web-antd/src/views/crm/customer/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export const CustomerDetailsInfo = defineAsyncComponent(
|
||||
() => import('./modules/detail-info.vue'),
|
||||
);
|
||||
|
||||
export const CustomerForm = defineAsyncComponent(
|
||||
() => import('./modules/form.vue'),
|
||||
);
|
||||
|
||||
export const CustomerDetails = defineAsyncComponent(
|
||||
() => import('./modules/detail.vue'),
|
||||
);
|
||||
|
||||
export const DistributeForm = defineAsyncComponent(
|
||||
() => import('./poolConfig/distribute-form.vue'),
|
||||
);
|
||||
101
apps/web-antd/src/views/crm/customer/modules/detail-data.ts
Normal file
101
apps/web-antd/src/views/crm/customer/modules/detail-data.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE } from '#/utils';
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'level',
|
||||
label: '客户级别',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: data?.level }),
|
||||
},
|
||||
{
|
||||
field: 'dealStatus',
|
||||
label: '成交状态',
|
||||
content: (data) => (data.dealStatus ? '已成交' : '未成交'),
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
content: (data) => formatDateTime(data?.createTime) as string,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'source',
|
||||
label: '客户来源',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_SOURCE,
|
||||
value: data?.source,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: '手机',
|
||||
},
|
||||
{
|
||||
field: 'telephone',
|
||||
label: '电话',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
field: 'areaName',
|
||||
label: '地址',
|
||||
},
|
||||
{
|
||||
field: 'detailAddress',
|
||||
label: '详细地址',
|
||||
},
|
||||
{
|
||||
field: 'qq',
|
||||
label: 'QQ',
|
||||
},
|
||||
{
|
||||
field: 'wechat',
|
||||
label: '微信',
|
||||
},
|
||||
{
|
||||
field: 'industryId',
|
||||
label: '客户行业',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
value: data?.industryId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
label: '客户级别',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_CUSTOMER_LEVEL, value: data?.level }),
|
||||
},
|
||||
{
|
||||
field: 'contactNextTime',
|
||||
label: '下次联系时间',
|
||||
content: (data) => formatDateTime(data?.contactNextTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Divider } from 'ant-design-vue';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
import { useDetailBaseSchema } from './detail-data';
|
||||
|
||||
defineProps<{
|
||||
customer: CrmCustomerApi.Customer; // 客户信息
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<script lang="ts" setup></script>
|
||||
<template>
|
||||
<div>customerList</div>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { CrmCustomerApi } from '#/api/crm/customer';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
@@ -14,56 +14,23 @@ import { getCustomer, updateCustomerDealStatus } from '#/api/crm/customer';
|
||||
import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { AsyncOperateLog } from '#/components/operate-log';
|
||||
import { BusinessDetailsList } from '#/views/crm/business';
|
||||
import { ContactDetailsList } from '#/views/crm/contact';
|
||||
import { ContractDetailsList } from '#/views/crm/contract';
|
||||
import {
|
||||
CustomerDetailsInfo,
|
||||
CustomerForm,
|
||||
DistributeForm,
|
||||
} from '#/views/crm/customer';
|
||||
import { FollowUp } from '#/views/crm/followup';
|
||||
import { PermissionList, TransferForm } from '#/views/crm/permission';
|
||||
import {
|
||||
ReceivableDetailsList,
|
||||
ReceivablePlanDetailsList,
|
||||
} from '#/views/crm/receivable';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const BusinessList = defineAsyncComponent(
|
||||
() => import('#/views/crm/business/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
const CustomerDetailsInfo = defineAsyncComponent(
|
||||
() => import('./detail-info.vue'),
|
||||
);
|
||||
|
||||
const ContactDetailsList = defineAsyncComponent(
|
||||
() => import('#/views/crm/contact/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
const ContractDetailsList = defineAsyncComponent(
|
||||
() => import('#/views/crm/contract/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
const CustomerForm = defineAsyncComponent(
|
||||
() => import('#/views/crm/customer/modules/form.vue'),
|
||||
);
|
||||
|
||||
const DistributeForm = defineAsyncComponent(
|
||||
() => import('#/views/crm/customer/poolConfig/distribute-form.vue'),
|
||||
);
|
||||
|
||||
const FollowUp = defineAsyncComponent(
|
||||
() => import('#/views/crm/followup/index.vue'),
|
||||
);
|
||||
|
||||
const PermissionList = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/permission-list.vue'),
|
||||
);
|
||||
|
||||
const TransferForm = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/transfer-form.vue'),
|
||||
);
|
||||
|
||||
const OperateLog = defineAsyncComponent(
|
||||
() => import('#/components/operate-log'),
|
||||
);
|
||||
|
||||
const ReceivableDetailsList = defineAsyncComponent(
|
||||
() => import('#/views/crm/receivable/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
const ReceivablePlanDetailsList = defineAsyncComponent(
|
||||
() => import('#/views/crm/receivable/plan/modules/detail-list.vue'),
|
||||
);
|
||||
import { useDetailSchema } from './detail-data';
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -278,7 +245,7 @@ onMounted(() => {
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="商机" key="5" :force-render="true">
|
||||
<BusinessList
|
||||
<BusinessDetailsList
|
||||
:biz-id="customerId"
|
||||
:biz-type="BizTypeEnum.CRM_CUSTOMER"
|
||||
:customer-id="customerId"
|
||||
@@ -295,7 +262,7 @@ onMounted(() => {
|
||||
<ReceivableDetailsList :customer-id="customerId" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="操作日志" key="8" :force-render="true">
|
||||
<OperateLog :log-list="customerLogList" />
|
||||
<AsyncOperateLog :log-list="customerLogList" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
3
apps/web-antd/src/views/crm/followup/index.ts
Normal file
3
apps/web-antd/src/views/crm/followup/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export const FollowUp = defineAsyncComponent(() => import('./index.vue'));
|
||||
@@ -1,2 +1,13 @@
|
||||
export { default as PermissionList } from './modules/permission-list.vue';
|
||||
export { default as TransferForm } from './modules/transfer-form.vue';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export const PermissionList = defineAsyncComponent(
|
||||
() => import('./modules/permission-list.vue'),
|
||||
);
|
||||
|
||||
export const PermissionForm = defineAsyncComponent(
|
||||
() => import('./modules/permission-form.vue'),
|
||||
);
|
||||
|
||||
export const TransferForm = defineAsyncComponent(
|
||||
() => import('./modules/transfer-form.vue'),
|
||||
);
|
||||
|
||||
@@ -84,7 +84,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[600px]" :title="getTitle">
|
||||
<Modal class="w-[40%]" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -86,7 +86,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[600px]" :title="getTitle">
|
||||
<Modal class="w-[40%]" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getProductCategoryList } from '#/api/crm/product/category';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import {
|
||||
CommonStatusEnum,
|
||||
DICT_TYPE,
|
||||
erpPriceInputFormatter,
|
||||
getDictOptions,
|
||||
} from '#/utils';
|
||||
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
@@ -184,86 +175,25 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
/** 代码生成表格列定义 */
|
||||
export function useProductEditTableColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'seq', title: '序号', minWidth: 50 },
|
||||
{
|
||||
field: 'categoryName',
|
||||
label: '产品类别',
|
||||
},
|
||||
{
|
||||
field: 'unit',
|
||||
label: '产品单位',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_PRODUCT_UNIT, value: data?.unit }),
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '产品价格',
|
||||
content: (data) => erpPriceInputFormatter(data.price),
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
label: '产品编码',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '产品名称',
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
label: '产品编码',
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '价格(元)',
|
||||
content: (data) => erpPriceInputFormatter(data.price),
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
label: '产品描述',
|
||||
},
|
||||
{
|
||||
field: 'categoryName',
|
||||
label: '产品类型',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '是否上下架',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_PRODUCT_STATUS, value: data?.status }),
|
||||
},
|
||||
{
|
||||
field: 'unit',
|
||||
label: '产品单位',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_PRODUCT_UNIT, value: data?.unit }),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(
|
||||
showBussinePrice: boolean,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'productName',
|
||||
field: 'productId',
|
||||
title: '产品名称',
|
||||
minWidth: 100,
|
||||
slots: { default: 'productId' },
|
||||
},
|
||||
{
|
||||
field: 'productNo',
|
||||
title: '产品条码',
|
||||
title: '条码',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'productUnit',
|
||||
title: '产品单位',
|
||||
title: '单位',
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
|
||||
@@ -271,24 +201,33 @@ export function useDetailListColumns(
|
||||
},
|
||||
{
|
||||
field: 'productPrice',
|
||||
title: '产品价格(元)',
|
||||
title: '价格(元)',
|
||||
minWidth: 100,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'businessPrice',
|
||||
title: '商机价格(元)',
|
||||
formatter: 'formatNumber',
|
||||
visible: showBussinePrice,
|
||||
field: 'sellingPrice',
|
||||
title: '售价(元)',
|
||||
minWidth: 100,
|
||||
slots: { default: 'sellingPrice' },
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '数量',
|
||||
formatter: 'formatNumber',
|
||||
minWidth: 100,
|
||||
slots: { default: 'count' },
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '合计金额(元)',
|
||||
title: '合计',
|
||||
minWidth: 100,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
21
apps/web-antd/src/views/crm/product/index.ts
Normal file
21
apps/web-antd/src/views/crm/product/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export const ProductDetailsInfo = defineAsyncComponent(
|
||||
() => import('./modules/detail-info.vue'),
|
||||
);
|
||||
|
||||
export const ProductForm = defineAsyncComponent(
|
||||
() => import('./modules/form.vue'),
|
||||
);
|
||||
|
||||
export const ProductDetails = defineAsyncComponent(
|
||||
() => import('./modules/detail.vue'),
|
||||
);
|
||||
|
||||
export const ProductDetailsList = defineAsyncComponent(
|
||||
() => import('./modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
export const ProductEditTable = defineAsyncComponent(
|
||||
() => import('./modules/product-table.vue'),
|
||||
);
|
||||
116
apps/web-antd/src/views/crm/product/modules/detail-data.ts
Normal file
116
apps/web-antd/src/views/crm/product/modules/detail-data.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, erpPriceInputFormatter } from '#/utils';
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'categoryName',
|
||||
label: '产品类别',
|
||||
},
|
||||
{
|
||||
field: 'unit',
|
||||
label: '产品单位',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_PRODUCT_UNIT, value: data?.unit }),
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '产品价格',
|
||||
content: (data) => erpPriceInputFormatter(data.price),
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
label: '产品编码',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
label: '产品名称',
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
label: '产品编码',
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '价格(元)',
|
||||
content: (data) => erpPriceInputFormatter(data.price),
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
label: '产品描述',
|
||||
},
|
||||
{
|
||||
field: 'categoryName',
|
||||
label: '产品类型',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '是否上下架',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_PRODUCT_STATUS, value: data?.status }),
|
||||
},
|
||||
{
|
||||
field: 'unit',
|
||||
label: '产品单位',
|
||||
content: (data) =>
|
||||
h(DictTag, { type: DICT_TYPE.CRM_PRODUCT_UNIT, value: data?.unit }),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(
|
||||
showBussinePrice: boolean,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'productName',
|
||||
title: '产品名称',
|
||||
},
|
||||
{
|
||||
field: 'productNo',
|
||||
title: '产品条码',
|
||||
},
|
||||
{
|
||||
field: 'productUnit',
|
||||
title: '产品单位',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_PRODUCT_UNIT },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'productPrice',
|
||||
title: '产品价格(元)',
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'businessPrice',
|
||||
title: '商机价格(元)',
|
||||
formatter: 'formatNumber',
|
||||
visible: showBussinePrice,
|
||||
},
|
||||
{
|
||||
field: 'count',
|
||||
title: '数量',
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
title: '合计金额(元)',
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type { CrmProductApi } from '#/api/crm/product';
|
||||
|
||||
import { useDescription } from '#/components/description';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
import { useDetailBaseSchema } from './detail-data';
|
||||
|
||||
defineProps<{
|
||||
product: CrmProductApi.Product; // 产品信息
|
||||
|
||||
@@ -1,32 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
import type { CrmProductApi } from '#/api/crm/product';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getBusiness } from '#/api/crm/business';
|
||||
import { getContract } from '#/api/crm/contract';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { erpPriceInputFormatter } from '#/utils';
|
||||
|
||||
import { useDetailListColumns } from '../data';
|
||||
import { useDetailListColumns } from './detail-data';
|
||||
|
||||
const props = defineProps<{
|
||||
bizId: number;
|
||||
bizType: BizTypeEnum;
|
||||
business?: CrmBusinessApi.Business; // 商机
|
||||
contract?: CrmContractApi.Contract; // 合同
|
||||
}>();
|
||||
|
||||
const discountPercent = ref(0);
|
||||
const totalProductPrice = ref(0);
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useDetailListColumns(props.bizType === BizTypeEnum.CRM_BUSINESS),
|
||||
data:
|
||||
props.bizType === BizTypeEnum.CRM_BUSINESS
|
||||
? props.business?.products
|
||||
: props.contract?.products,
|
||||
height: 600,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_params) => {
|
||||
const data =
|
||||
props.bizType === BizTypeEnum.CRM_BUSINESS
|
||||
? await getBusiness(props.bizId)
|
||||
: await getContract(props.bizId);
|
||||
discountPercent.value = data.discountPercent;
|
||||
totalProductPrice.value = data.totalProductPrice;
|
||||
return data.products;
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
},
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
@@ -38,21 +55,17 @@ const [Grid] = useVbenVxeGrid({
|
||||
<template>
|
||||
<div>
|
||||
<Grid />
|
||||
<div class="flex justify-end">
|
||||
<span class="font-bold text-red-500">
|
||||
整单折扣:{{
|
||||
erpPriceInputFormatter(
|
||||
business ? business.discountPercent : contract?.discountPercent,
|
||||
)
|
||||
}}%
|
||||
</span>
|
||||
<div class="flex flex-col items-end justify-end">
|
||||
<span class="ml-4 font-bold text-red-500">
|
||||
产品总金额:{{
|
||||
erpPriceInputFormatter(
|
||||
business ? business.totalProductPrice : contract?.totalProductPrice,
|
||||
)
|
||||
{{ `产品总金额:${erpPriceInputFormatter(totalProductPrice)}元` }}
|
||||
</span>
|
||||
<span class="font-bold text-red-500">
|
||||
{{ `整单折扣:${erpPriceInputFormatter(discountPercent)}%` }}
|
||||
</span>
|
||||
<span class="font-bold text-red-500">
|
||||
{{
|
||||
`实际金额:${erpPriceInputFormatter(totalProductPrice * (1 - discountPercent / 100))}元`
|
||||
}}
|
||||
元
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { CrmProductApi } from '#/api/crm/product';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
@@ -14,16 +14,10 @@ import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { getProduct } from '#/api/crm/product';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { AsyncOperateLog } from '#/components/operate-log';
|
||||
import { ProductDetailsInfo } from '#/views/crm/product';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
const ProductDetailsInfo = defineAsyncComponent(
|
||||
() => import('./detail-info.vue'),
|
||||
);
|
||||
|
||||
const OperateLog = defineAsyncComponent(
|
||||
() => import('#/components/operate-log'),
|
||||
);
|
||||
import { useDetailSchema } from './detail-data';
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -87,7 +81,7 @@ onMounted(() => {
|
||||
<ProductDetailsInfo :product="product" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="操作日志" key="2" :force-render="true">
|
||||
<OperateLog :log-list="productLogList" />
|
||||
<AsyncOperateLog :log-list="productLogList" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
@@ -76,7 +76,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[600px]" :title="getTitle">
|
||||
<Modal class="w-[40%]" :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
183
apps/web-antd/src/views/crm/product/modules/product-table.vue
Normal file
183
apps/web-antd/src/views/crm/product/modules/product-table.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CrmBusinessApi } from '#/api/crm/business';
|
||||
import type { CrmContractApi } from '#/api/crm/contract';
|
||||
import type { CrmProductApi } from '#/api/crm/product';
|
||||
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { InputNumber, Select } from 'ant-design-vue';
|
||||
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { getProductSimpleList } from '#/api/crm/product';
|
||||
import { erpPriceMultiply } from '#/utils';
|
||||
|
||||
import { useProductEditTableColumns } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
bizType: BizTypeEnum;
|
||||
products?:
|
||||
| CrmBusinessApi.BusinessProduct[]
|
||||
| CrmContractApi.ContractProduct[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:products']);
|
||||
|
||||
const tableData = ref<any[]>([]);
|
||||
|
||||
function handleAdd() {
|
||||
gridApi.grid.insertAt(null, -1);
|
||||
}
|
||||
|
||||
function handleDelete(row: CrmProductApi.Product) {
|
||||
gridApi.grid.remove(row);
|
||||
}
|
||||
|
||||
function handleProductChange(productId: any, row: any) {
|
||||
const product = productOptions.value.find((p) => p.id === productId);
|
||||
if (!product) {
|
||||
return;
|
||||
}
|
||||
row.productUnit = product.unit;
|
||||
row.productNo = product.no;
|
||||
row.productPrice = product.price;
|
||||
row.sellingPrice = product.price;
|
||||
row.count = 0;
|
||||
row.totalPrice = 0;
|
||||
handleUpdateValue(row);
|
||||
}
|
||||
|
||||
function handlePriceChange(row: any) {
|
||||
row.totalPrice = erpPriceMultiply(row.sellingPrice, row.count) ?? 0;
|
||||
handleUpdateValue(row);
|
||||
}
|
||||
|
||||
function handleUpdateValue(row: any) {
|
||||
const index = tableData.value.findIndex((item) => item.id === row.id);
|
||||
if (props.bizType === BizTypeEnum.CRM_BUSINESS) {
|
||||
row.businessPrice = row.sellingPrice;
|
||||
} else if (props.bizType === BizTypeEnum.CRM_CONTRACT) {
|
||||
row.contractPrice = row.sellingPrice;
|
||||
}
|
||||
if (index === -1) {
|
||||
row.id = tableData.value.length + 1;
|
||||
tableData.value.push(row);
|
||||
} else {
|
||||
tableData.value[index] = row;
|
||||
}
|
||||
emit('update:products', tableData.value);
|
||||
}
|
||||
|
||||
/** 表格配置 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
editConfig: {
|
||||
trigger: 'click',
|
||||
mode: 'cell',
|
||||
},
|
||||
columns: useProductEditTableColumns(),
|
||||
data: tableData.value,
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
autoResize: true,
|
||||
keepSource: true,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** 监听外部传入的列数据 */
|
||||
watch(
|
||||
() => props.products,
|
||||
async (products) => {
|
||||
if (!products) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
tableData.value = products;
|
||||
if (props.bizType === BizTypeEnum.CRM_BUSINESS) {
|
||||
tableData.value.forEach((item) => {
|
||||
item.sellingPrice = item.businessPrice;
|
||||
});
|
||||
} else if (props.bizType === BizTypeEnum.CRM_CONTRACT) {
|
||||
tableData.value.forEach((item) => {
|
||||
item.sellingPrice = item.contractPrice;
|
||||
});
|
||||
}
|
||||
gridApi.grid?.loadData(tableData.value);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
const productOptions = ref<CrmProductApi.Product[]>([]);
|
||||
onMounted(async () => {
|
||||
productOptions.value = await getProductSimpleList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid class="w-full">
|
||||
<template #productId="{ row }">
|
||||
<Select
|
||||
v-model:value="row.productId"
|
||||
:options="productOptions"
|
||||
:field-names="{ label: 'name', value: 'id' }"
|
||||
style="width: 100%"
|
||||
@change="handleProductChange($event, row)"
|
||||
/>
|
||||
</template>
|
||||
<template #sellingPrice="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.sellingPrice"
|
||||
:min="0.001"
|
||||
:precision="2"
|
||||
@change="handlePriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
<template #count="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.count"
|
||||
:min="0.001"
|
||||
:precision="3"
|
||||
@change="handlePriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
<template #bottom>
|
||||
<TableAction
|
||||
class="mt-4 flex justify-center"
|
||||
:actions="[
|
||||
{
|
||||
label: '添加产品',
|
||||
type: 'default',
|
||||
onClick: handleAdd,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</template>
|
||||
@@ -1,17 +1,10 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { getContractSimpleList } from '#/api/crm/contract';
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { getReceivablePlanSimpleList } from '#/api/crm/receivable/plan';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { erpPriceInputFormatter } from '#/utils';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
@@ -274,150 +267,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '合同金额',
|
||||
content: (data) => erpPriceInputFormatter(data.totalPrice),
|
||||
},
|
||||
{
|
||||
field: 'returnTime',
|
||||
label: '回款日期',
|
||||
content: (data) => formatDateTime(data?.returnTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '回款金额',
|
||||
content: (data) => erpPriceInputFormatter(data.price),
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '回款编号',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'contract',
|
||||
label: '合同编号',
|
||||
content: (data) => data?.contract?.no,
|
||||
},
|
||||
{
|
||||
field: 'returnTime',
|
||||
label: '回款日期',
|
||||
content: (data) => formatDateTime(data?.returnTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '回款金额',
|
||||
content: (data) => erpPriceInputFormatter(data.price),
|
||||
},
|
||||
{
|
||||
field: 'returnType',
|
||||
label: '回款方式',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE,
|
||||
value: data?.returnType,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '回款编号',
|
||||
field: 'no',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
field: 'customerName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'contract',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '回款日期',
|
||||
field: 'returnTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '回款金额(元)',
|
||||
field: 'price',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '回款方式',
|
||||
field: 'returnType',
|
||||
minWidth: 150,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同金额(元)',
|
||||
field: 'contract.totalPrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '回款状态',
|
||||
field: 'auditStatus',
|
||||
minWidth: 100,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
field: 'actions',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
29
apps/web-antd/src/views/crm/receivable/index.ts
Normal file
29
apps/web-antd/src/views/crm/receivable/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export const ReceivableDetailsInfo = defineAsyncComponent(
|
||||
() => import('./modules/detail-info.vue'),
|
||||
);
|
||||
|
||||
export const ReceivableForm = defineAsyncComponent(
|
||||
() => import('./modules/form.vue'),
|
||||
);
|
||||
|
||||
export const ReceivableDetails = defineAsyncComponent(
|
||||
() => import('./modules/detail.vue'),
|
||||
);
|
||||
|
||||
export const ReceivableDetailsList = defineAsyncComponent(
|
||||
() => import('./modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
export const ReceivablePlanDetailsInfo = defineAsyncComponent(
|
||||
() => import('./plan/modules/detail-info.vue'),
|
||||
);
|
||||
|
||||
export const ReceivablePlanDetailsList = defineAsyncComponent(
|
||||
() => import('./plan/modules/detail-list.vue'),
|
||||
);
|
||||
|
||||
export const ReceivablePlanDetails = defineAsyncComponent(
|
||||
() => import('./plan/modules/detail.vue'),
|
||||
);
|
||||
150
apps/web-antd/src/views/crm/receivable/modules/detail-data.ts
Normal file
150
apps/web-antd/src/views/crm/receivable/modules/detail-data.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DICT_TYPE, erpPriceInputFormatter } from '#/utils';
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '合同金额',
|
||||
content: (data) => erpPriceInputFormatter(data.totalPrice),
|
||||
},
|
||||
{
|
||||
field: 'returnTime',
|
||||
label: '回款日期',
|
||||
content: (data) => formatDateTime(data?.returnTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '回款金额',
|
||||
content: (data) => erpPriceInputFormatter(data.price),
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '回款编号',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'contract',
|
||||
label: '合同编号',
|
||||
content: (data) => data?.contract?.no,
|
||||
},
|
||||
{
|
||||
field: 'returnTime',
|
||||
label: '回款日期',
|
||||
content: (data) => formatDateTime(data?.returnTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '回款金额',
|
||||
content: (data) => erpPriceInputFormatter(data.price),
|
||||
},
|
||||
{
|
||||
field: 'returnType',
|
||||
label: '回款方式',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE,
|
||||
value: data?.returnType,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '回款编号',
|
||||
field: 'no',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
field: 'customerName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'contract.no',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '回款日期',
|
||||
field: 'returnTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '回款金额(元)',
|
||||
field: 'price',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '回款方式',
|
||||
field: 'returnType',
|
||||
minWidth: 150,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '回款状态',
|
||||
field: 'auditStatus',
|
||||
minWidth: 100,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
field: 'actions',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Divider } from 'ant-design-vue';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
|
||||
|
||||
import { useDetailBaseSchema } from '../data';
|
||||
import { useDetailBaseSchema } from './detail-data';
|
||||
|
||||
defineProps<{
|
||||
receivable: CrmReceivableApi.Receivable; // 收款信息
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
@@ -15,7 +13,7 @@ import {
|
||||
} from '#/api/crm/receivable';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDetailListColumns } from '../data';
|
||||
import { useDetailListColumns } from './detail-data';
|
||||
import Form from './form.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -28,15 +26,6 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const checkedRows = ref<CrmReceivableApi.Receivable[]>([]);
|
||||
function setCheckedRows({
|
||||
records,
|
||||
}: {
|
||||
records: CrmReceivableApi.Receivable[];
|
||||
}) {
|
||||
checkedRows.value = records;
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
@@ -73,15 +62,14 @@ async function handleDelete(row: CrmReceivableApi.Receivable) {
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useDetailListColumns(),
|
||||
height: 600,
|
||||
height: 500,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
query: async ({ page }) => {
|
||||
const queryParams: CrmReceivableApi.ReceivablePageParam = {
|
||||
page: page.currentPage,
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
if (props.customerId && !props.contractId) {
|
||||
queryParams.customerId = props.customerId;
|
||||
@@ -102,10 +90,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<CrmReceivableApi.Receivable>,
|
||||
gridEvents: {
|
||||
checkboxAll: setCheckedRows,
|
||||
checkboxChange: setCheckedRows,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { CrmReceivableApi } from '#/api/crm/receivable';
|
||||
import type { SystemOperateLogApi } from '#/api/system/operate-log';
|
||||
|
||||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
@@ -15,22 +15,13 @@ import { getOperateLogPage } from '#/api/crm/operateLog';
|
||||
import { BizTypeEnum } from '#/api/crm/permission';
|
||||
import { getReceivable } from '#/api/crm/receivable';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { AsyncOperateLog } from '#/components/operate-log';
|
||||
import { PermissionList } from '#/views/crm/permission';
|
||||
import { ReceivableDetailsInfo } from '#/views/crm/receivable';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
import { useDetailSchema } from './detail-data';
|
||||
import ReceivableForm from './form.vue';
|
||||
|
||||
const PermissionList = defineAsyncComponent(
|
||||
() => import('#/views/crm/permission/modules/permission-list.vue'),
|
||||
);
|
||||
|
||||
const OperateLog = defineAsyncComponent(
|
||||
() => import('#/components/operate-log'),
|
||||
);
|
||||
|
||||
const ReceivableDetailsInfo = defineAsyncComponent(
|
||||
() => import('./detail-info.vue'),
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
@@ -131,7 +122,7 @@ onMounted(() => {
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="操作日志" key="3" :force-render="true">
|
||||
<OperateLog :log-list="receivableLogList" />
|
||||
<AsyncOperateLog :log-list="receivableLogList" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
@@ -64,7 +64,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<CrmReceivableApi.Receivable>();
|
||||
const data = modalApi.getData();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { getCustomerSimpleList } from '#/api/crm/customer';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { erpPriceInputFormatter } from '#/utils';
|
||||
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
@@ -235,150 +228,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '合同金额',
|
||||
content: (data) => erpPriceInputFormatter(data.totalPrice),
|
||||
},
|
||||
{
|
||||
field: 'returnTime',
|
||||
label: '回款日期',
|
||||
content: (data) => formatDateTime(data?.returnTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '回款金额',
|
||||
content: (data) => erpPriceInputFormatter(data.price),
|
||||
},
|
||||
{
|
||||
field: 'ownerUserName',
|
||||
label: '负责人',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情页的基础字段 */
|
||||
export function useDetailBaseSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '回款编号',
|
||||
},
|
||||
{
|
||||
field: 'customerName',
|
||||
label: '客户名称',
|
||||
},
|
||||
{
|
||||
field: 'contract',
|
||||
label: '合同编号',
|
||||
content: (data) => data?.contract?.no,
|
||||
},
|
||||
{
|
||||
field: 'returnTime',
|
||||
label: '回款日期',
|
||||
content: (data) => formatDateTime(data?.returnTime) as string,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
label: '回款金额',
|
||||
content: (data) => erpPriceInputFormatter(data.price),
|
||||
},
|
||||
{
|
||||
field: 'returnType',
|
||||
label: '回款方式',
|
||||
content: (data) =>
|
||||
h(DictTag, {
|
||||
type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE,
|
||||
value: data?.returnType,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情列表的字段 */
|
||||
export function useDetailListColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
title: '回款编号',
|
||||
field: 'no',
|
||||
minWidth: 150,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
field: 'customerName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同编号',
|
||||
field: 'contract',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '回款日期',
|
||||
field: 'returnTime',
|
||||
minWidth: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '回款金额(元)',
|
||||
field: 'price',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '回款方式',
|
||||
field: 'returnType',
|
||||
minWidth: 150,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
field: 'ownerUserName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
field: 'remark',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '合同金额(元)',
|
||||
field: 'contract.totalPrice',
|
||||
minWidth: 150,
|
||||
formatter: 'formatNumber',
|
||||
},
|
||||
{
|
||||
title: '回款状态',
|
||||
field: 'auditStatus',
|
||||
minWidth: 100,
|
||||
fixed: 'right',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
field: 'actions',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user