This commit is contained in:
gjd
2025-06-16 09:58:35 +08:00
648 changed files with 27087 additions and 5714 deletions

View File

@@ -19,7 +19,7 @@ const authStore = useAuthStore();
const activeName = ref('basicInfo');
/** 加载个人信息 */
const profile = ref<SystemUserProfileApi.UserProfileRespVO>();
const profile = ref<SystemUserProfileApi.UserProfileResp>();
async function loadProfile() {
profile.value = await getUserProfile();
}

View File

@@ -14,7 +14,7 @@ import { updateUserProfile } from '#/api/system/user/profile';
import { DICT_TYPE, getDictOptions } from '#/utils';
const props = defineProps<{
profile?: SystemUserProfileApi.UserProfileRespVO;
profile?: SystemUserProfileApi.UserProfileResp;
}>();
const emit = defineEmits<{
(e: 'success'): void;
@@ -77,7 +77,7 @@ async function handleSubmit(values: Recordable<any>) {
try {
formApi.setLoading(true);
// 提交表单
await updateUserProfile(values as SystemUserProfileApi.UpdateProfileReqVO);
await updateUserProfile(values as SystemUserProfileApi.UpdateProfileReq);
// 关闭并提示
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));

View File

@@ -14,7 +14,7 @@ import { CropperAvatar } from '#/components/cropper';
import { useUpload } from '#/components/upload/use-upload';
const props = defineProps<{
profile?: SystemUserProfileApi.UserProfileRespVO;
profile?: SystemUserProfileApi.UserProfileResp;
}>();
const emit = defineEmits<{

View File

@@ -1,12 +1,10 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { DocAlert, Page } from '@vben/common-ui';
import { Card, TabPane, Tabs } from 'ant-design-vue';
import { DocAlert } from '#/components/doc-alert';
import ChatConversationList from './modules/ChatConversationList.vue';
import ChatMessageList from './modules/ChatMessageList.vue';
@@ -15,7 +13,9 @@ const activeTabName = ref('conversation');
<template>
<Page auto-content-height>
<DocAlert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
<template #doc>
<DocAlert title="AI 对话聊天" url="https://doc.iocoder.cn/ai/chat/" />
</template>
<Card>
<Tabs v-model:active-key="activeTabName">
<TabPane tab="对话列表" key="conversation">

View File

@@ -5,7 +5,7 @@ import type { SystemUserApi } from '#/api/system/user';
import { onMounted, ref } from 'vue';
import { confirm, Page } from '@vben/common-ui';
import { confirm, DocAlert, Page } from '@vben/common-ui';
import { Image, message, Switch } from 'ant-design-vue';

View File

@@ -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' },
},
];
}

View File

@@ -1,18 +1,13 @@
<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 { DocAlert, Page, useVbenModal } from '@vben/common-ui';
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';
import { useGridColumns, useGridFormSchema } from './data';
@@ -22,12 +17,46 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建流程分类 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑流程分类 */
function handleEdit(row: BpmCategoryApi.Category) {
formModalApi.setData(row).open();
}
/** 删除流程分类 */
async function handleDelete(row: BpmCategoryApi.Category) {
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: {
@@ -48,56 +77,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<BpmCategoryApi.CategoryVO>,
} as VxeTableGridOptions<BpmCategoryApi.Category>,
});
/** 表格操作按钮的回调函数 */
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 +90,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>

View File

@@ -18,7 +18,7 @@ import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<BpmCategoryApi.CategoryVO>();
const formData = ref<BpmCategoryApi.Category>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['流程分类'])
@@ -39,7 +39,7 @@ const [Modal, modalApi] = useVbenModal({
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as BpmCategoryApi.CategoryVO;
const data = (await formApi.getValues()) as BpmCategoryApi.Category;
try {
await (formData.value?.id ? updateCategory(data) : createCategory(data));
// 关闭并提示
@@ -56,7 +56,7 @@ const [Modal, modalApi] = useVbenModal({
return;
}
// 加载数据
const data = modalApi.getData<BpmCategoryApi.CategoryVO>();
const data = modalApi.getData<BpmCategoryApi.Category>();
if (!data || !data.id) {
return;
}

View File

@@ -12,7 +12,7 @@ import { getCategory, updateCategory } from '#/api/bpm/category';
import { $t } from '#/locales';
const emit = defineEmits(['success']);
const formData = ref<BpmCategoryApi.CategoryVO>();
const formData = ref<BpmCategoryApi.Category>();
// 定义表单结构
const formSchema = [
@@ -53,7 +53,7 @@ const [Modal, modalApi] = useVbenModal({
status: formData.value?.status,
description: formData.value?.description,
sort: formData.value?.sort,
} as BpmCategoryApi.CategoryVO;
} as BpmCategoryApi.Category;
try {
await updateCategory(data);
@@ -74,7 +74,7 @@ const [Modal, modalApi] = useVbenModal({
}
// 加载数据
const data = modalApi.getData<BpmCategoryApi.CategoryVO>();
const data = modalApi.getData<BpmCategoryApi.Category>();
if (!data || !data.id) {
return;

View File

@@ -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' },
},
];
}

View File

@@ -106,6 +106,7 @@ async function initializeDesigner() {
}
}
// TODO @ziye注释使用 /** */ 风格,高亮更明显哈,方法注释;
// 保存表单
function handleSave() {
formModalApi

View File

@@ -1,36 +1,97 @@
<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 { DocAlert, Page, useVbenModal } from '@vben/common-ui';
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 { DocAlert } from '#/components/doc-alert';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteForm, getFormPage } from '#/api/bpm/form';
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.Form) {
router.push({
name: 'BpmFormEditor',
query: {
id: row.id,
type: 'edit',
},
});
}
/** 复制 */
function handleCopy(row: BpmFormApi.Form) {
router.push({
name: 'BpmFormEditor',
query: {
copyId: row.id,
type: 'copy',
},
});
}
/** 删除 */
async function handleDelete(row: BpmFormApi.Form) {
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.Form) {
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: {
@@ -54,104 +115,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
cellConfig: {
height: 64,
},
} as VxeTableGridOptions<BpmFormApi.FormVO>,
} as VxeTableGridOptions<BpmFormApi.Form>,
});
/** 表格操作按钮的回调函数 */
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 +137,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>

View 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>

View File

@@ -19,7 +19,7 @@ import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const designerComponent = ref<InstanceType<typeof FcDesigner>>();
const formData = ref<BpmFormApi.FormVO>();
const formData = ref<BpmFormApi.Form>();
const editorAction = ref<string>();
const getTitle = computed(() => {
@@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock();
try {
// 获取表单数据
const data = (await formApi.getValues()) as BpmFormApi.FormVO;
const data = (await formApi.getValues()) as BpmFormApi.Form;
// 编码表单配置和表单字段
data.conf = encodeConf(designerComponent);
@@ -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>

View File

@@ -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' },
},
];
}

View File

@@ -1,22 +1,17 @@
<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 { DocAlert, Page, useVbenModal } from '@vben/common-ui';
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';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -26,12 +21,53 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建用户分组 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑用户分组 */
function handleEdit(row: BpmUserGroupApi.UserGroup) {
formModalApi.setData(row).open();
}
/** 删除用户分组 */
async function handleDelete(row: BpmUserGroupApi.UserGroup) {
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: {
@@ -52,74 +88,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: { code: 'query' },
search: true,
},
} 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();
} as VxeTableGridOptions<BpmUserGroupApi.UserGroup>,
});
</script>
@@ -132,18 +101,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>

View File

@@ -18,7 +18,7 @@ import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<BpmUserGroupApi.UserGroupVO>();
const formData = ref<BpmUserGroupApi.UserGroup>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['用户分组'])
@@ -46,7 +46,7 @@ const [Modal, modalApi] = useVbenModal({
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as BpmUserGroupApi.UserGroupVO;
const data = (await formApi.getValues()) as BpmUserGroupApi.UserGroup;
try {
await (formData.value?.id
? updateUserGroup(data)
@@ -64,11 +64,10 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<BpmUserGroupApi.UserGroupVO>();
const data = modalApi.getData<BpmUserGroupApi.UserGroup>();
if (!data || !data.id) {
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>

View File

@@ -0,0 +1,65 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { BpmProcessDefinitionApi } from '#/api/bpm/definition';
import { DICT_TYPE } from '#/utils';
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<BpmProcessDefinitionApi.ProcessDefinition>['columns'] {
return [
{
field: 'id',
title: '定义编号',
minWidth: 250,
},
{
field: 'name',
title: '流程名称',
minWidth: 150,
},
{
field: 'icon',
title: '流程图标',
minWidth: 100,
slots: { default: 'icon' },
},
{
field: 'startUsers',
title: '可见范围',
minWidth: 100,
slots: { default: 'startUsers' },
},
{
field: 'modelType',
title: '流程类型',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.BPM_MODEL_TYPE },
},
},
{
field: 'formType',
title: '表单信息',
minWidth: 150,
slots: { default: 'formInfo' },
},
{
field: 'version',
title: '流程版本',
minWidth: 80,
slots: { default: 'version' },
},
{
field: 'deploymentTime',
title: '部署时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -0,0 +1,156 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { Button, Image, Tag, Tooltip } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProcessDefinitionPage } from '#/api/bpm/definition';
import { BpmModelFormType } from '#/utils';
// 导入 FormCreate 表单详情
import FormCreateDetail from '../../form/modules/detail.vue';
import { useGridColumns } from './data';
defineOptions({ name: 'BpmProcessDefinition' });
const [FormCreateDetailModal, formCreateDetailModalApi] = useVbenModal({
connectedComponent: FormCreateDetail,
destroyOnClose: true,
});
/** 查看表单详情 */
function handleFormDetail(row: any) {
if (row.formType === BpmModelFormType.NORMAL) {
const data = {
id: row.formId,
};
formCreateDetailModalApi.setData(data).open();
} else {
// TODO 待实现
console.warn('业务表单待实现', row);
}
}
const router = useRouter();
/** 恢复流程模型 */
async function openModelForm(id?: number) {
await router.push({
name: 'BpmModelUpdate',
params: { id, type: 'definition' },
});
}
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
const route = useRoute();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: true,
},
proxyConfig: {
ajax: {
query: async ({ page }) => {
const params = {
pageNo: page?.currentPage,
pageSize: page?.pageSize,
key: route.query.key,
};
return await getProcessDefinitionPage(params);
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
},
} as VxeTableGridOptions,
});
/** 初始化 */
onMounted(() => {
onRefresh();
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
</template>
<Grid table-title="流程定义列表">
<template #icon="{ row }">
<Image
v-if="row.icon"
:src="row.icon"
:width="24"
:height="24"
class="rounded"
/>
<span v-else> 无图标 </span>
</template>
<template #startUsers="{ row }">
<template v-if="!row.startUsers?.length">全部可见</template>
<template v-else-if="row.startUsers.length === 1">
{{ row.startUsers[0].nickname }}
</template>
<template v-else>
<Tooltip
placement="top"
:title="row.startUsers.map((user: any) => user.nickname).join(',')"
>
{{ row.startUsers[0].nickname }}
{{ row.startUsers.length }} 人可见
</Tooltip>
</template>
</template>
<template #formInfo="{ row }">
<Button
v-if="row.formType === BpmModelFormType.NORMAL"
type="link"
@click="handleFormDetail(row)"
>
<span>{{ row.formName }}</span>
</Button>
<Button
v-else-if="row.formType === BpmModelFormType.CUSTOM"
type="link"
@click="handleFormDetail(row)"
>
<span>{{ row.formCustomCreatePath }}</span>
</Button>
<span v-else>暂无表单</span>
</template>
<template #version="{ row }">
<Tag>v{{ row.version }}</Tag>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '恢复',
type: 'link',
auth: ['bpm:model:update'],
onClick: openModelForm.bind(null, row.id),
},
]"
/>
</template>
</Grid>
<FormCreateDetailModal />
</Page>
</template>

View File

@@ -29,6 +29,7 @@ import { getSimpleUserList } from '#/api/system/user';
import { BpmAutoApproveType, BpmModelFormType, BpmModelType } from '#/utils';
import BasicInfo from './modules/basic-info.vue';
import ExtraSetting from './modules/extra-setting.vue';
import FormDesign from './modules/form-design.vue';
import ProcessDesign from './modules/process-design.vue';
@@ -36,7 +37,7 @@ defineOptions({ name: 'BpmModelCreate' });
// 流程定义类型
type BpmProcessDefinitionType = Omit<
BpmProcessDefinitionApi.ProcessDefinitionVO,
BpmProcessDefinitionApi.ProcessDefinition,
'modelId' | 'modelType'
> & {
id?: string;
@@ -55,21 +56,28 @@ const basicInfoRef = ref<InstanceType<typeof BasicInfo>>();
const formDesignRef = ref<InstanceType<typeof FormDesign>>();
// 流程设计组件引用
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();
};
}
/** 更多设置校验 */
async function validateExtra() {
await extraSettingRef.value?.validate();
}
const currentStep = ref(-1); // 步骤控制。-1 用于,一开始全部不展示等当前页面数据初始化完成
@@ -77,7 +85,7 @@ const steps = [
{ title: '基本信息', validator: validateBasic },
{ title: '表单设计', validator: validateForm },
{ title: '流程设计', validator: validateProcess },
{ title: '更多设置', validator: null },
{ title: '更多设置', validator: validateExtra },
];
// 表单数据
@@ -124,14 +132,14 @@ provide('processData', processData);
provide('modelData', formData);
// 数据列表
const formList = ref<BpmFormApi.FormVO[]>([]);
const categoryList = ref<BpmCategoryApi.CategoryVO[]>([]);
const formList = ref<BpmFormApi.Form[]>([]);
const categoryList = ref<BpmCategoryApi.Category[]>([]);
const userList = ref<SystemUserApi.User[]>([]);
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;
@@ -190,9 +198,9 @@ const initData = async () => {
// 最终,设置 currentStep 切换到第一步
currentStep.value = 0;
// TODO 兼容,以前未配置更多设置的流程
// extraSettingsRef.value.initData()
};
// 以前未配置更多设置的流程
extraSettingRef.value?.initData();
}
/** 根据类型切换流程数据 */
watch(
@@ -210,7 +218,7 @@ watch(
);
/** 校验所有步骤数据是否完整 */
const validateAllSteps = async () => {
async function validateAllSteps() {
// 基本信息校验
try {
await validateBasic();
@@ -237,13 +245,19 @@ const validateAllSteps = async () => {
return false;
}
// TODO 更多设置校验
// 更多设置校验
try {
await validateExtra();
} catch {
currentStep.value = 3;
return false;
}
return true;
};
}
/** 保存操作 */
const handleSave = async () => {
async function handleSave() {
try {
// 保存前校验所有步骤的数据
const result = await validateAllSteps();
@@ -291,16 +305,16 @@ const handleSave = async () => {
// 返回列表页(排除更新的情况)
if (actionType !== 'update') {
await router.push({ name: 'BpmModel' });
router.push({ path: '/bpm/manager/model' });
}
} catch (error: any) {
console.error('保存失败:', error);
// message.warning(error.msg || '请完善所有步骤的必填信息');
}
};
}
/** 发布操作 */
const handleDeploy = async () => {
async function handleDeploy() {
try {
// 修改场景下直接发布,新增场景下需要先确认
if (!formData.value.id) {
@@ -325,16 +339,15 @@ const handleDeploy = async () => {
// 发布
await deployModel(formData.value.id);
message.success('发布成功');
// TODO 返回列表页
await router.push({ name: 'BpmModel' });
await router.push({ path: '/bpm/manager/model' });
} catch (error: any) {
console.error('发布失败:', error);
message.warning(error.message || '发布失败');
}
};
}
/** 步骤切换处理 */
const handleStepClick = async (index: number) => {
async function handleStepClick(index: number) {
try {
if (index !== 0) {
await validateBasic();
@@ -345,6 +358,9 @@ const handleStepClick = async (index: number) => {
if (index !== 2) {
await validateProcess();
}
if (index !== 3) {
await validateExtra();
}
// 切换步骤
currentStep.value = index;
} catch (error) {
@@ -353,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 () => {
@@ -402,11 +418,11 @@ onBeforeUnmount(() => {
<!-- 步骤条 -->
<div class="flex h-full flex-1 items-center justify-center">
<div class="flex h-full w-[400px] items-center justify-between">
<div class="flex h-full w-auto items-center justify-center">
<div
v-for="(step, index) in steps"
:key="index"
class="relative mx-[15px] flex h-full cursor-pointer items-center"
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'
@@ -475,8 +491,10 @@ onBeforeUnmount(() => {
ref="processDesignRef"
/>
<!-- 第四步更多设置 TODO -->
<div v-if="currentStep === 3" class="mx-auto w-4/6"></div>
<!-- 第四步更多设置 -->
<div v-if="currentStep === 3" class="mx-auto w-4/6">
<ExtraSetting v-model="formData" ref="extraSettingRef" />
</div>
</div>
</Card>
</div>

View File

@@ -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 {
@@ -28,7 +29,7 @@ import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '#/utils';
const props = defineProps({
categoryList: {
type: Array as PropType<BpmCategoryApi.CategoryVO[]>,
type: Array as PropType<BpmCategoryApi.Category[]>,
required: true,
},
userList: {
@@ -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,260 @@ 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-1"
>
<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="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 hover:bg-gray-200"
>
<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>
<Button
type="link"
@click="openStartUserSelect"
class="flex items-center"
>
<template #icon>
<IconifyIcon icon="lucide:user-plus" 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-1"
>
<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="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 shadow-sm hover:bg-gray-200"
>
<IconifyIcon icon="lucide: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>
<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-1">
<div
v-for="user in selectedManagerUsers"
:key="user.id"
class="relative flex h-9 items-center rounded-full bg-gray-100 pr-2 hover:bg-gray-200"
>
<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)"
/>
</div>
<Button
type="link"
@click="openManagerUserSelect"
class="flex items-center"
>
<template #icon>
<IconifyIcon icon="lucide:user-plus" 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 {

View File

@@ -0,0 +1,497 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { CircleHelp } from '@vben/icons';
import {
Checkbox,
Col,
Form,
FormItem,
Input,
InputNumber,
Mentions,
Radio,
RadioGroup,
Row,
Select,
Switch,
Tooltip,
TypographyText,
} from 'ant-design-vue';
import dayjs from 'dayjs';
import * as FormApi from '#/api/bpm/form';
import {
HttpRequestSetting,
parseFormFields,
} from '#/components/simple-process-design';
import {
BpmAutoApproveType,
BpmModelFormType,
ProcessVariableEnum,
} from '#/utils';
const modelData = defineModel<any>();
/** 自定义 ID 流程编码 */
const timeOptions = ref([
{
value: '',
label: '无',
},
{
value: 'DAY',
label: '精确到日',
},
{
value: 'HOUR',
label: '精确到时',
},
{
value: 'MINUTE',
label: '精确到分',
},
{
value: 'SECOND',
label: '精确到秒',
},
]);
const numberExample = computed(() => {
if (modelData.value.processIdRule.enable) {
let infix = '';
switch (modelData.value.processIdRule.infix) {
case 'DAY': {
infix = dayjs().format('YYYYMMDD');
break;
}
case 'HOUR': {
infix = dayjs().format('YYYYMMDDHH');
break;
}
case 'MINUTE': {
infix = dayjs().format('YYYYMMDDHHmm');
break;
}
case 'SECOND': {
infix = dayjs().format('YYYYMMDDHHmmss');
break;
}
default: {
break;
}
}
return (
modelData.value.processIdRule.prefix +
infix +
modelData.value.processIdRule.postfix +
'1'.padStart(modelData.value.processIdRule.length - 1, '0')
);
} else {
return '';
}
});
/** 是否开启流程前置通知 */
const processBeforeTriggerEnable = ref(false);
function handleProcessBeforeTriggerEnableChange(
val: boolean | number | string,
) {
modelData.value.processBeforeTriggerSetting = val
? {
url: '',
header: [],
body: [],
response: [],
}
: null;
}
/** 是否开启流程后置通知 */
const processAfterTriggerEnable = ref(false);
function handleProcessAfterTriggerEnableChange(val: boolean | number | string) {
modelData.value.processAfterTriggerSetting = val
? {
url: '',
header: [],
body: [],
response: [],
}
: null;
}
/** 是否开启任务前置通知 */
const taskBeforeTriggerEnable = ref(false);
function handleTaskBeforeTriggerEnableChange(val: boolean | number | string) {
modelData.value.taskBeforeTriggerSetting = val
? {
url: '',
header: [],
body: [],
response: [],
}
: null;
}
/** 是否开启任务后置通知 */
const taskAfterTriggerEnable = ref(false);
function handleTaskAfterTriggerEnableChange(val: boolean | number | string) {
modelData.value.taskAfterTriggerSetting = val
? {
url: '',
header: [],
body: [],
response: [],
}
: null;
}
/** 表单选项 */
const formField = ref<Array<{ field: string; title: string }>>([]);
const formFieldOptions4Title = computed(() => {
const cloneFormField = formField.value.map((item) => {
return {
label: item.title,
value: item.field,
};
});
// 固定添加发起人 ID 字段
cloneFormField.unshift({
label: '流程名称',
value: ProcessVariableEnum.PROCESS_DEFINITION_NAME,
});
cloneFormField.unshift({
label: '发起时间',
value: ProcessVariableEnum.START_TIME,
});
cloneFormField.unshift({
label: '发起人',
value: ProcessVariableEnum.START_USER_ID,
});
return cloneFormField;
});
const formFieldOptions4Summary = computed(() => {
return formField.value.map((item) => {
return {
label: item.title,
value: item.field,
};
});
});
/** 兼容以前未配置更多设置的流程 */
function initData() {
if (!modelData.value.processIdRule) {
modelData.value.processIdRule = {
enable: false,
prefix: '',
infix: '',
postfix: '',
length: 5,
};
}
if (!modelData.value.autoApprovalType) {
modelData.value.autoApprovalType = BpmAutoApproveType.NONE;
}
if (!modelData.value.titleSetting) {
modelData.value.titleSetting = {
enable: false,
title: '',
};
}
if (!modelData.value.summarySetting) {
modelData.value.summarySetting = {
enable: false,
summary: [],
};
}
if (modelData.value.processBeforeTriggerSetting) {
processBeforeTriggerEnable.value = true;
}
if (modelData.value.processAfterTriggerSetting) {
processAfterTriggerEnable.value = true;
}
if (modelData.value.taskBeforeTriggerSetting) {
taskBeforeTriggerEnable.value = true;
}
if (modelData.value.taskAfterTriggerSetting) {
taskAfterTriggerEnable.value = true;
}
}
/** 监听表单 ID 变化,加载表单数据 */
watch(
() => modelData.value.formId,
async (newFormId) => {
if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
const data = await FormApi.getFormDetail(newFormId);
const result: Array<{ field: string; title: string }> = [];
if (data.fields) {
data.fields.forEach((fieldStr: string) => {
parseFormFields(JSON.parse(fieldStr), result);
});
}
formField.value = result;
} else {
formField.value = [];
}
},
{ immediate: true },
);
// 表单引用
const formRef = ref();
/** 表单校验 */
async function validate() {
await formRef.value?.validate();
}
defineExpose({ initData, validate });
</script>
<template>
<Form
ref="formRef"
:model="modelData"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
class="mt-5 px-5"
>
<FormItem class="mb-5" label="提交人权限">
<div class="mt-1 flex flex-col">
<Checkbox v-model:checked="modelData.allowCancelRunningProcess">
允许撤销审批中的申请
</Checkbox>
<div class="ml-6">
<TypographyText type="warning">
第一个审批节点通过后提交人仍可撤销申请
</TypographyText>
</div>
</div>
</FormItem>
<FormItem v-if="modelData.processIdRule" class="mb-5" label="流程编码">
<Row :gutter="8" align="middle">
<Col :span="1">
<Checkbox v-model:checked="modelData.processIdRule.enable" />
</Col>
<Col :span="5">
<Input
v-model:value="modelData.processIdRule.prefix"
placeholder="前缀"
:disabled="!modelData.processIdRule.enable"
/>
</Col>
<Col :span="6">
<Select
v-model:value="modelData.processIdRule.infix"
allow-clear
placeholder="中缀"
:disabled="!modelData.processIdRule.enable"
:options="timeOptions"
/>
</Col>
<Col :span="4">
<Input
v-model:value="modelData.processIdRule.postfix"
placeholder="后缀"
:disabled="!modelData.processIdRule.enable"
/>
</Col>
<Col :span="4">
<InputNumber
v-model:value="modelData.processIdRule.length"
:min="5"
:disabled="!modelData.processIdRule.enable"
/>
</Col>
</Row>
<div class="ml-6 mt-2" v-if="modelData.processIdRule.enable">
<TypographyText type="success">
编码示例{{ numberExample }}
</TypographyText>
</div>
</FormItem>
<FormItem class="mb-5" label="自动去重">
<div class="mt-1">
<TypographyText class="mb-2 block">
同一审批人在流程中重复出现时
</TypographyText>
<RadioGroup v-model:value="modelData.autoApprovalType">
<Row :gutter="[0, 8]">
<Col :span="24">
<Radio :value="0">不自动通过</Radio>
</Col>
<Col :span="24">
<Radio :value="1">
仅审批一次后续重复的审批节点均自动通过
</Radio>
</Col>
<Col :span="24">
<Radio :value="2">仅针对连续审批的节点自动通过</Radio>
</Col>
</Row>
</RadioGroup>
</div>
</FormItem>
<FormItem v-if="modelData.titleSetting" class="mb-5" label="标题设置">
<div class="mt-1">
<RadioGroup v-model:value="modelData.titleSetting.enable">
<Row :gutter="[0, 8]">
<Col :span="24">
<Radio :value="false">
系统默认
<TypographyText type="success"> 展示流程名称 </TypographyText>
</Radio>
</Col>
<Col :span="24">
<Radio :value="true">
<div class="inline-flex items-center">
自定义标题
<Tooltip
title="输入字符 '{' 即可插入表单字段"
placement="top"
>
<CircleHelp class="ml-1 size-4 text-gray-500" />
</Tooltip>
</div>
</Radio>
</Col>
</Row>
</RadioGroup>
<div class="mt-2">
<Mentions
v-if="modelData.titleSetting.enable"
v-model:value="modelData.titleSetting.title"
style="width: 100%; max-width: 600px"
type="textarea"
prefix="{"
split="}"
:options="formFieldOptions4Title"
placeholder="请插入表单字段(输入 '{' 可以选择表单字段)或输入文本"
/>
</div>
</div>
</FormItem>
<FormItem
v-if="
modelData.summarySetting &&
modelData.formType === BpmModelFormType.NORMAL
"
class="mb-5"
label="摘要设置"
>
<div class="mt-1">
<RadioGroup v-model:value="modelData.summarySetting.enable">
<Row :gutter="[0, 8]">
<Col :span="24">
<Radio :value="false">
系统默认
<TypographyText type="secondary">
展示表单前 3 个字段
</TypographyText>
</Radio>
</Col>
<Col :span="24">
<Radio :value="true"> 自定义摘要 </Radio>
</Col>
</Row>
</RadioGroup>
<div class="mt-2">
<Select
v-if="modelData.summarySetting.enable"
v-model:value="modelData.summarySetting.summary"
mode="multiple"
placeholder="请选择要展示的表单字段"
:options="formFieldOptions4Summary"
/>
</div>
</div>
</FormItem>
<FormItem class="mb-5" label="流程前置通知">
<Row class="mt-1">
<Col :span="24">
<div class="flex items-center">
<Switch
v-model:checked="processBeforeTriggerEnable"
@change="handleProcessBeforeTriggerEnableChange"
/>
<span class="ml-4">流程启动后通知</span>
</div>
</Col>
</Row>
<Row v-if="processBeforeTriggerEnable">
<Col :span="24" class="mt-6">
<HttpRequestSetting
v-model:setting="modelData.processBeforeTriggerSetting"
:response-enable="true"
form-item-prefix="processBeforeTriggerSetting"
/>
</Col>
</Row>
</FormItem>
<FormItem class="mb-5" label="流程后置通知">
<Row class="mt-1">
<Col :span="24">
<div class="flex items-center">
<Switch
v-model:checked="processAfterTriggerEnable"
@change="handleProcessAfterTriggerEnableChange"
/>
<span class="ml-4">流程结束后通知</span>
</div>
</Col>
</Row>
<Row v-if="processAfterTriggerEnable" class="mt-2">
<Col :span="24">
<HttpRequestSetting
v-model:setting="modelData.processAfterTriggerSetting"
:response-enable="true"
form-item-prefix="processAfterTriggerSetting"
/>
</Col>
</Row>
</FormItem>
<FormItem class="mb-5" label="任务前置通知">
<Row class="mt-1">
<Col :span="24">
<div class="flex items-center">
<Switch
v-model:checked="taskBeforeTriggerEnable"
@change="handleTaskBeforeTriggerEnableChange"
/>
<span class="ml-4">任务执行时通知</span>
</div>
</Col>
</Row>
<Row v-if="taskBeforeTriggerEnable" class="mt-2">
<Col :span="24">
<HttpRequestSetting
v-model:setting="modelData.taskBeforeTriggerSetting"
:response-enable="true"
form-item-prefix="taskBeforeTriggerSetting"
/>
</Col>
</Row>
</FormItem>
<FormItem class="mb-5" label="任务后置通知">
<Row class="mt-1">
<Col :span="24">
<div class="flex items-center">
<Switch
v-model:checked="taskAfterTriggerEnable"
@change="handleTaskAfterTriggerEnableChange"
/>
<span class="ml-4">任务结束后通知</span>
</div>
</Col>
</Row>
<Row v-if="taskAfterTriggerEnable" class="mt-2">
<Col :span="24">
<HttpRequestSetting
v-model:setting="modelData.taskAfterTriggerSetting"
:response-enable="true"
form-item-prefix="taskAfterTriggerSetting"
/>
</Col>
</Row>
</FormItem>
</Form>
</template>

View File

@@ -29,7 +29,7 @@ import {
const props = defineProps({
formList: {
type: Array<BpmFormApi.FormVO>,
type: Array<BpmFormApi.Form>,
required: true,
},
});
@@ -80,9 +80,9 @@ const rules: Record<string, Rule[]> = {
};
/** 表单校验 */
const validate = async () => {
async function validate() {
await formRef.value?.validate();
};
}
defineExpose({ validate });
</script>
@@ -91,8 +91,8 @@ defineExpose({ validate });
ref="formRef"
:model="modelData"
:rules="rules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 21 }"
class="mt-5"
>
<FormItem label="表单类型" name="formType" class="mb-5">

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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,59 @@ const handleCategorySortSubmit = async () => {
if (sortableInstance.value) {
sortableInstance.value.option('disabled', true);
}
};
}
</script>
<template>
<Page auto-content-height>
<!-- TODO @jaosn没头像的图标展示文字头像哈 @芋艿 好像已经展示了文字头像是模型列表中吗? -->
<!-- TODO @jason体感整个页面的加载有点卡顿先看到分类大概 1-2 秒后上箭头变成下然后看到每个模型我本地大概 4 个分类每个分类下 20+ 模型 -->
<!-- 流程分类表单弹窗 -->
<CategoryFormModal @success="getList" />
<Card
:body-style="{ padding: '10px' }"
class="mb-4 h-[98%]"
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>
<div class="flex items-center justify-center">
<IconifyIcon icon="lucide:settings" />
</div>
</template>
</Button>
<template #overlay>
<Menu @click="(e) => handleCommand(e.key as string)">
<Menu.Item key="handleCategoryAdd">
<div class="flex items-center gap-1">
<IconifyIcon icon="lucide:plus" />
新建分类
</div>
</Menu.Item>
<Menu.Item key="handleCategorySort">
<div class="flex items-center gap-1">
<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 +209,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' : ''"

View File

@@ -1,9 +1,14 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { BpmModelApi, ModelCategoryInfo } from '#/api/bpm/model';
import { computed, ref, watchEffect } from 'vue';
import { useRouter } from 'vue-router';
import { confirm, useVbenModal } from '@vben/common-ui';
import { useAccess } from '@vben/access';
import { confirm, EllipsisText, useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { useUserStore } from '@vben/stores';
import { cloneDeep, formatDateTime, isEqual } from '@vben/utils';
import { useDebounceFn } from '@vueuse/core';
@@ -15,19 +20,27 @@ import {
Dropdown,
Menu,
message,
Table,
Tag,
Tooltip,
} from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteCategory } from '#/api/bpm/category';
import { updateModelSortBatch } from '#/api/bpm/model';
import { DictTag } from '#/components/dict-tag';
import {
cleanModel,
deleteModel,
deployModel,
updateModelSortBatch,
updateModelState,
} from '#/api/bpm/model';
import { $t } from '#/locales';
import { DICT_TYPE } from '#/utils';
import { BpmModelFormType } from '#/utils';
// 导入重命名表单
import CategoryRenameForm from '../../category/modules/rename-form.vue';
// 导入 FormCreate 表单详情
import FormCreateDetail from '../../form/modules/detail.vue';
import { useGridColumns } from './data';
const props = defineProps<{
categoryInfo: ModelCategoryInfo;
@@ -36,71 +49,73 @@ const props = defineProps<{
const emit = defineEmits(['success']);
/** 重命名分类对话框 */
const [CategoryRenameModal, categoryRenameModalApi] = useVbenModal({
connectedComponent: CategoryRenameForm,
destroyOnClose: true,
});
/** 流程表单详情对话框 */
const [FormCreateDetailModal, formCreateDetailModalApi] = useVbenModal({
connectedComponent: FormCreateDetail,
destroyOnClose: true,
});
const router = useRouter();
// 获取当前登录用户Id
const userStore = useUserStore();
const userId = userStore.userInfo?.id;
const isModelSorting = ref(false);
const originalData = ref<BpmModelApi.ModelVO[]>([]);
const modelList = ref<BpmModelApi.ModelVO[]>([]);
const originalData = ref<BpmModelApi.Model[]>([]);
const modelList = ref<BpmModelApi.Model[]>([]);
const isExpand = ref(false);
const tableRef = ref();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
pagerConfig: {
enabled: false,
},
data: modelList.value,
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
enabled: false, // 完全禁用工具栏
},
} as VxeTableGridOptions,
});
// 排序引用,以便后续启用或禁用排序
const sortableInstance = ref<any>(null);
/** 解决 v-model 问题,使用计算属性 */
const expandKeys = computed(() => (isExpand.value ? ['1'] : []));
// 表格列配置
const columns = [
{
title: '流程名',
dataIndex: 'name',
key: 'name',
align: 'left' as const,
ellipsis: true,
width: 250,
},
{
title: '可见范围',
dataIndex: 'startUserIds',
key: 'startUserIds',
align: 'center' as const,
ellipsis: true,
width: 150,
},
{
title: '流程类型',
dataIndex: 'type',
key: 'type',
align: 'center' as const,
ellipsis: true,
width: 120,
},
{
title: '表单信息',
dataIndex: 'formType',
key: 'formType',
align: 'center' as const,
ellipsis: true,
width: 150,
},
{
title: '最后发布',
dataIndex: 'deploymentTime',
key: 'deploymentTime',
align: 'center' as const,
width: 250,
},
{
title: '操作',
key: 'operation',
align: 'center' as const,
fixed: 'right' as const,
width: 150,
},
];
const { hasAccessByCodes } = useAccess();
/** 权限校验:通过 computed 解决列表的卡顿问题 */
const hasPermiUpdate = computed(() => {
return hasAccessByCodes(['bpm:model:update']);
});
const hasPermiDelete = computed(() => {
return hasAccessByCodes(['bpm:model:delete']);
});
const hasPermiDeploy = computed(() => {
return hasAccessByCodes(['bpm:model:deploy']);
});
/** 处理模型的排序 */
const handleModelSort = () => {
// 保存初始数据
originalData.value = cloneDeep(props.categoryInfo.modelList);
function handleModelSort() {
// 保存初始数据并确保数据完整
if (props.categoryInfo.modelList && props.categoryInfo.modelList.length > 0) {
originalData.value = cloneDeep(props.categoryInfo.modelList);
modelList.value = cloneDeep(props.categoryInfo.modelList);
}
// 更新表格数据
gridApi.setGridOptions({
data: modelList.value,
});
// 展开数据
isExpand.value = true;
isModelSorting.value = true;
@@ -109,16 +124,40 @@ const handleModelSort = () => {
// 已存在实例,则启用排序功能
sortableInstance.value.option('disabled', false);
} else {
const sortableClass = `.category-${props.categoryInfo.id} .ant-table .ant-table-tbody`;
sortableInstance.value = useSortable(sortableClass, modelList, {
disabled: false, // 启用排序
const sortableClass = `.category-${props.categoryInfo.id} .vxe-table .vxe-table--body-wrapper:not(.fixed-right--wrapper) .vxe-table--body tbody`;
// 确保使用最新的数据
modelList.value = cloneDeep(props.categoryInfo.modelList);
// 更新表格数据
gridApi.setGridOptions({
data: modelList.value,
});
sortableInstance.value = useSortable(sortableClass, modelList.value, {
draggable: '.vxe-body--row',
animation: 150,
handle: '.drag-handle',
disabled: false,
onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
if (oldDraggableIndex !== newDraggableIndex) {
modelList.value.splice(
newDraggableIndex ?? 0,
0,
modelList.value.splice(oldDraggableIndex ?? 0, 1)[0]!,
);
}
},
});
}
};
}
/** 处理模型的排序提交 */
const handleModelSortSubmit = async () => {
async function handleModelSortSubmit() {
try {
// 确保数据已经正确同步
if (!modelList.value || modelList.value.length === 0) {
message.error('排序数据异常,请重试');
return;
}
// 保存排序
const ids = modelList.value.map((item) => item.id);
await updateModelSortBatch(ids);
@@ -129,31 +168,37 @@ const handleModelSortSubmit = async () => {
} catch (error) {
console.error('排序保存失败', error);
}
};
}
/** 处理模型的排序取消 */
const handleModelSortCancel = () => {
function handleModelSortCancel() {
// 恢复初始数据
modelList.value = cloneDeep(originalData.value);
isModelSorting.value = false;
if (originalData.value && originalData.value.length > 0) {
modelList.value = cloneDeep(originalData.value);
// 更新表格数据
gridApi.setGridOptions({
data: modelList.value,
});
}
// 禁用排序功能
if (sortableInstance.value) {
sortableInstance.value.option('disabled', true);
}
};
isModelSorting.value = false;
}
/** 处理下拉菜单命令 */
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;
@@ -170,13 +215,161 @@ const handleDeleteCategory = async () => {
// 刷新列表
emit('success');
});
};
}
/** 处理表单详情点击 */
const handleFormDetail = (row: any) => {
// TODO 待实现
console.warn('待实现', row);
};
function handleFormDetail(row: any) {
if (row.formType === BpmModelFormType.NORMAL) {
const data = {
id: row.formId,
};
formCreateDetailModalApi.setData(data).open();
} else {
// TODO 待实现
console.warn('业务表单待实现', row);
}
}
/** 判断是否是流程管理员 */
function isManagerUser(row: any) {
return row.managerUserIds && row.managerUserIds.includes(userId);
}
async function modelOperation(type: string, id: number) {
await router.push({
name: 'BpmModelUpdate',
params: { id, type },
});
}
/** 发布流程 */
async function handleDeploy(row: any) {
confirm({
beforeClose: async ({ isConfirm }) => {
if (!isConfirm) return;
// 发起部署
await deployModel(row.id);
return true;
},
content: `确认要发布[${row.name}]流程吗?`,
icon: 'question',
}).then(async () => {
message.success(`发布[${row.name}]流程成功`);
// 刷新列表
emit('success');
});
}
/** '更多'操作按钮 */
function handleModelCommand(command: string, row: any) {
switch (command) {
case 'handleChangeState': {
handleChangeState(row);
break;
}
case 'handleClean': {
handleClean(row);
break;
}
case 'handleCopy': {
modelOperation('copy', row.id);
break;
}
case 'handleDefinitionList': {
handleDefinitionList(row);
break;
}
case 'handleDelete': {
handleDelete(row);
break;
}
case 'handleReport': {
handleReport(row);
break;
}
default: {
break;
}
}
}
/** 更新状态操作 */
function handleChangeState(row: any) {
const state = row.processDefinition.suspensionState;
const newState = state === 1 ? 2 : 1;
const statusState = state === 1 ? '停用' : '启用';
confirm({
beforeClose: async ({ isConfirm }) => {
if (!isConfirm) return;
// 发起更新状态
await updateModelState(row.id, newState);
return true;
},
content: `确认要${statusState}流程: "${row.name}" 吗?`,
icon: 'question',
}).then(async () => {
message.success(`${statusState} 流程: "${row.name}" 成功`);
// 刷新列表
emit('success');
});
}
/** 清理流程操作 */
function handleClean(row: any) {
confirm({
beforeClose: async ({ isConfirm }) => {
if (!isConfirm) return;
// 发起清理操作
await cleanModel(row.id);
return true;
},
content: `确认要清理流程: "${row.name}" 吗?`,
icon: 'question',
}).then(async () => {
message.success(`清理流程: "${row.name}" 成功`);
// 刷新列表
emit('success');
});
}
/** 删除流程操作 */
function handleDelete(row: any) {
confirm({
beforeClose: async ({ isConfirm }) => {
if (!isConfirm) return;
// 发起删除操作
await deleteModel(row.id);
return true;
},
content: `确认要删除流程: "${row.name}" 吗?`,
icon: 'question',
}).then(async () => {
message.success(`删除流程: "${row.name}" 成功`);
// 刷新列表
emit('success');
});
}
/** 跳转到指定流程定义列表 */
function handleDefinitionList(row: any) {
router.push({
name: 'BpmProcessDefinition',
query: {
key: row.key,
},
});
}
/** 跳转到流程报表页面 */
function handleReport(row: any) {
router.push({
name: 'BpmProcessInstanceReport',
query: {
processDefinitionId: row.processDefinition.id,
processDefinitionKey: row.key,
},
});
}
/** 更新 modelList 模型列表 */
const updateModelList = useDebounceFn(() => {
@@ -190,6 +383,10 @@ const updateModelList = useDebounceFn(() => {
isModelSorting.value = false;
// 重置排序实例
sortableInstance.value = null;
// 更新表格数据
gridApi.setGridOptions({
data: modelList.value,
});
}
}, 100);
@@ -204,19 +401,6 @@ watchEffect(() => {
}
});
/** 自定义表格行渲染 */
const customRow = (_record: any) => {
return {
class: isModelSorting.value ? 'cursor-move' : '',
};
};
// 重命名分类对话框
const [CategoryRenameModal, categoryRenameModalApi] = useVbenModal({
connectedComponent: CategoryRenameForm,
destroyOnClose: true,
});
// 处理重命名成功
const handleRenameSuccess = () => {
emit('success');
@@ -224,208 +408,196 @@ 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'">
<Grid
v-if="modelList && modelList.length > 0"
:class="`category-${categoryInfo.id}`"
>
<template #name="{ row }">
<div class="flex items-center">
<Tooltip v-if="isModelSorting" title="拖动排序">
<Tooltip
v-if="isModelSorting"
title="拖动排序"
placement="left"
>
<span
class="icon-[ic--round-drag-indicator] mr-2.5 cursor-move text-2xl text-gray-500"
class="icon-[ic--round-drag-indicator] drag-handle 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"
v-if="!row.icon"
class="mr-2.5 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded bg-blue-500 text-white"
>
<span style="font-size: 12px">{{
record.name.substring(0, 2)
}}</span>
<span style="font-size: 12px">
{{ row.name.substring(0, 2) }}
</span>
</div>
<img
v-else
:src="record.icon"
class="mr-2.5 h-9 w-9 rounded"
:src="row.icon"
class="mr-2.5 h-9 w-9 flex-shrink-0 rounded"
alt="图标"
/>
{{ record.name }}
<EllipsisText :max-width="160" :tooltip-when-ellipsis="true">
{{ row.name }}
</EllipsisText>
</div>
</template>
<!-- 可见范围列-->
<template v-else-if="column.key === 'startUserIds'">
<span
v-if="!record.startUsers?.length && !record.startDepts?.length"
>
<template #startUserIds="{ row }">
<span v-if="!row.startUsers?.length && !row.startDepts?.length">
全部可见
</span>
<span v-else-if="record.startUsers?.length === 1">
{{ record.startUsers[0].nickname }}
<span v-else-if="row.startUsers?.length === 1">
{{ row.startUsers[0].nickname }}
</span>
<span v-else-if="record.startDepts?.length === 1">
{{ record.startDepts[0].name }}
<span v-else-if="row.startDepts?.length === 1">
{{ row.startDepts[0].name }}
</span>
<span v-else-if="record.startDepts?.length > 1">
<span v-else-if="row.startDepts?.length > 1">
<Tooltip
placement="top"
:title="
record.startDepts.map((dept: any) => dept.name).join('、')
row.startDepts.map((dept: any) => dept.name).join('、')
"
>
{{ record.startDepts[0].name }}
{{ record.startDepts.length }} 个部门可见
{{ row.startDepts[0].name }}
{{ row.startDepts.length }} 个部门可见
</Tooltip>
</span>
<span v-else-if="record.startUsers?.length > 1">
<span v-else-if="row.startUsers?.length > 1">
<Tooltip
placement="top"
:title="
record.startUsers
.map((user: any) => user.nickname)
.join('、')
row.startUsers.map((user: any) => user.nickname).join('、')
"
>
{{ record.startUsers[0].nickname }}
{{ record.startUsers.length }} 人可见
{{ row.startUsers[0].nickname }}
{{ row.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 -->
<template #formInfo="{ row }">
<Button
v-if="record.formType === 10"
v-if="row.formType === BpmModelFormType.NORMAL"
type="link"
@click="handleFormDetail(record)"
@click="handleFormDetail(row)"
>
{{ record.formName }}
{{ row.formName }}
</Button>
<!-- TODO BpmModelFormType.CUSTOM -->
<Button
v-else-if="record.formType === 20"
v-else-if="row.formType === BpmModelFormType.CUSTOM"
type="link"
@click="handleFormDetail(record)"
@click="handleFormDetail(row)"
>
{{ record.formCustomCreatePath }}
{{ row.formCustomCreatePath }}
</Button>
<span v-else>暂无表单</span>
</template>
<!-- 最后发布列 -->
<template v-else-if="column.key === 'deploymentTime'">
<template #deploymentTime="{ row }">
<div class="flex items-center justify-center">
<span v-if="record.processDefinition" class="w-[150px]">
{{ formatDateTime(record.processDefinition.deploymentTime) }}
<span v-if="row.processDefinition" class="w-[150px]">
{{ formatDateTime(row.processDefinition.deploymentTime) }}
</span>
<Tag v-if="record.processDefinition">
v{{ record.processDefinition.version }}
<Tag v-if="row.processDefinition">
v{{ row.processDefinition.version }}
</Tag>
<Tag v-else color="warning">未部署</Tag>
<Tag
v-if="record.processDefinition?.suspensionState === 2"
v-if="row.processDefinition?.suspensionState === 2"
color="warning"
class="ml-[10px]"
>
@@ -433,38 +605,90 @@ const handleRenameSuccess = () => {
</Tag>
</div>
</template>
<!-- 操作列 -->
<template v-else-if="column.key === 'operation'">
<div class="flex items-center justify-center">待实现</div>
</template>
</template>
</Table>
</Collapse.Panel>
</Collapse>
</Card>
<template #actions="{ row }">
<div class="flex items-center space-x-0">
<Button
type="link"
size="small"
class="px-1"
@click="modelOperation('update', row.id)"
:disabled="!isManagerUser(row) || !hasPermiUpdate"
>
修改
</Button>
<Button
type="link"
size="small"
class="px-1"
@click="handleDeploy(row)"
:disabled="!isManagerUser(row) || !hasPermiDeploy"
>
发布
</Button>
<Dropdown placement="bottomRight" arrow>
<Button type="link" size="small" class="px-1">更多</Button>
<template #overlay>
<Menu
@click="(e) => handleModelCommand(e.key as string, row)"
>
<Menu.Item key="handleCopy"> 复制 </Menu.Item>
<Menu.Item key="handleDefinitionList"> 历史 </Menu.Item>
<!-- 重命名分类弹窗 -->
<CategoryRenameModal @success="handleRenameSuccess" />
<Menu.Item
key="handleReport"
:disabled="!isManagerUser(row)"
>
报表
</Menu.Item>
<Menu.Item
key="handleChangeState"
v-if="row.processDefinition"
:disabled="!isManagerUser(row)"
>
{{
row.processDefinition.suspensionState === 1
? '停用'
: '启用'
}}
</Menu.Item>
<Menu.Item
danger
key="handleClean"
:disabled="!isManagerUser(row)"
>
清理
</Menu.Item>
<Menu.Item
danger
key="handleDelete"
:disabled="!isManagerUser(row) || !hasPermiDelete"
>
删除
</Menu.Item>
</Menu>
</template>
</Dropdown>
</div>
</template>
</Grid>
</Collapse.Panel>
</Collapse>
</Card>
<!-- 重命名分类弹窗 -->
<CategoryRenameModal @success="handleRenameSuccess" />
<!-- 流程表单详情对话框 -->
<FormCreateDetailModal />
</div>
</template>
<style lang="scss" scoped>
.category-draggable-model {
// ant-table-tbody 自定义样式
:deep(.ant-table-tbody > tr > td) {
overflow: hidden;
border-bottom: none;
}
// ant-collapse-header 自定义样式
:deep(.ant-collapse-header) {
padding: 0;
}
// 优化表格渲染性能
:deep(.ant-table-tbody) {
transform: translateZ(0);
will-change: transform;
}
// 折叠面板样式
:deep(.ant-collapse-content-box) {
padding: 0;

View File

@@ -0,0 +1,49 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { BpmModelApi } from '#/api/bpm/model';
import { DICT_TYPE } from '#/utils';
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<BpmModelApi.Model>['columns'] {
return [
{
field: 'name',
title: '流程名称',
minWidth: 200,
slots: { default: 'name' },
},
{
field: 'startUserIds',
title: '可见范围',
minWidth: 150,
slots: { default: 'startUserIds' },
},
{
field: 'type',
title: '流程类型',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.BPM_MODEL_TYPE },
},
},
{
field: 'formType',
title: '表单信息',
minWidth: 150,
slots: { default: 'formInfo' },
},
{
field: 'deploymentTime',
title: '最后发布',
minWidth: 280,
slots: { default: 'deploymentTime' },
},
{
title: '操作',
width: 150,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -30,7 +30,7 @@ const tempStartUserSelectAssignees = ref({}); // 历史发起人选择审批人
const activityNodes = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>([]); // 审批节点信息
const processDefinitionId = ref('');
const formData = ref<BpmOALeaveApi.LeaveVO>();
const formData = ref<BpmOALeaveApi.Leave>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['请假'])
@@ -70,7 +70,7 @@ async function onSubmit() {
}
// 提交表单
const data = (await formApi.getValues()) as BpmOALeaveApi.LeaveVO;
const data = (await formApi.getValues()) as BpmOALeaveApi.Leave;
// 审批相关:设置指定审批人
if (startUserSelectTasks.value?.length > 0) {
@@ -78,7 +78,7 @@ async function onSubmit() {
}
// 格式化开始时间和结束时间的值
const submitData: BpmOALeaveApi.LeaveVO = {
const submitData: BpmOALeaveApi.Leave = {
...data,
startTime: Number(data.startTime),
endTime: Number(data.endTime),
@@ -95,8 +95,9 @@ async function onSubmit() {
key: 'action_process_msg',
});
router.push({
name: 'BpmOALeaveList',
// TODO @ziye、@jason好像跳转不了
await router.push({
name: 'BpmOALeave',
});
} catch (error: any) {
message.error(error.message);
@@ -105,36 +106,6 @@ async function onSubmit() {
}
}
/** 保存草稿 */
async function onDraft() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
const data = (await formApi.getValues()) as BpmOALeaveApi.LeaveVO;
// 格式化开始时间和结束时间的值
const submitData: BpmOALeaveApi.LeaveVO = {
...data,
startTime: Number(data.startTime),
endTime: Number(data.endTime),
};
try {
formLoading.value = true;
await (formData.value?.id
? updateLeave(submitData)
: createLeave(submitData));
// 关闭并提示
message.success({
content: '保存草稿成功',
});
} finally {
formLoading.value = false;
}
}
/** 返回上一页 */
function onBack() {
confirm({
@@ -142,6 +113,7 @@ function onBack() {
icon: 'warning',
beforeClose({ isConfirm }) {
if (isConfirm) {
// TODO @ziye、@jason是不是要关闭当前标签哈。
router.back();
}
return Promise.resolve(true);
@@ -152,7 +124,7 @@ function onBack() {
// ============================== 审核流程相关 ==============================
/** 审批相关:获取审批详情 */
const getApprovalDetail = async () => {
async function getApprovalDetail() {
try {
const data = await getApprovalDetailApi({
processDefinitionId: processDefinitionId.value,
@@ -188,13 +160,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(
@@ -259,8 +230,6 @@ onMounted(async () => {
<template #actions>
<Space warp :size="12" class="w-full px-6">
<Button type="primary" @click="onSubmit"> 提交 </Button>
<!-- TODO 后端接口暂不支持保存草稿 即仅保存数据不触发流程-->
<!-- <Button type="default" @click="onDraft"> 保存草稿 </Button> -->
</Space>
</template>
</Card>

View File

@@ -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: 220,
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' },
},
];
}

View File

@@ -14,19 +14,19 @@ const props = defineProps<{
id: string;
}>();
const datailLoading = ref(false);
const detailData = ref<BpmOALeaveApi.LeaveVO>();
const detailData = ref<BpmOALeaveApi.Leave>();
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();

View File

@@ -1,23 +1,16 @@
<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 { DocAlert, Page, prompt } from '@vben/common-ui';
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';
import { router } from '#/router';
import { GridFormSchema, useGridColumns } from './data';
@@ -27,12 +20,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,
@@ -48,11 +41,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<BpmOALeaveApi.LeaveVO>,
} as VxeTableGridOptions<BpmOALeaveApi.Leave>,
});
/** 创建请假 */
function onCreate() {
function handleCreate() {
router.push({
name: 'OALeaveCreate',
query: {
@@ -62,15 +55,15 @@ function onCreate() {
}
/** 查看请假详情 */
const onDetail = (row: BpmOALeaveApi.LeaveVO) => {
function handleDetail(row: BpmOALeaveApi.Leave) {
router.push({
name: 'OALeaveDetail',
query: { id: row.id },
});
};
}
/** 取消请假 */
const onCancel = (row: BpmOALeaveApi.LeaveVO) => {
function handleCancel(row: BpmOALeaveApi.Leave) {
prompt({
async beforeClose(scope) {
if (scope.isConfirm) {
@@ -100,35 +93,14 @@ const onCancel = (row: BpmOALeaveApi.LeaveVO) => {
title: '取消流程',
modelPropName: 'value',
});
};
}
/** 审批进度 */
const onProgress = (row: BpmOALeaveApi.LeaveVO) => {
function handleProgress(row: BpmOALeaveApi.Leave) {
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 +118,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>

View File

@@ -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' },
},
];
}

View File

@@ -1,21 +1,16 @@
<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 { DocAlert, Page, useVbenModal } from '@vben/common-ui';
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,
} from '#/api/bpm/processExpression';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -25,12 +20,46 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建流程表达式 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑流程表达式 */
function handleEdit(row: BpmProcessExpressionApi.ProcessExpression) {
formModalApi.setData(row).open();
}
/** 删除流程表达式 */
async function handleDelete(row: BpmProcessExpressionApi.ProcessExpression) {
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: {
@@ -51,57 +80,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<BpmProcessExpressionApi.ProcessExpressionVO>,
} as VxeTableGridOptions<BpmProcessExpressionApi.ProcessExpression>,
});
/** 表格操作按钮的回调函数 */
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 +92,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>

View File

@@ -18,7 +18,7 @@ import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<BpmProcessExpressionApi.ProcessExpressionVO>();
const formData = ref<BpmProcessExpressionApi.ProcessExpression>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['流程表达式'])
@@ -40,7 +40,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as BpmProcessExpressionApi.ProcessExpressionVO;
(await formApi.getValues()) as BpmProcessExpressionApi.ProcessExpression;
try {
await (formData.value?.id
? updateProcessExpression(data)
@@ -55,12 +55,10 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data =
modalApi.getData<BpmProcessExpressionApi.ProcessExpressionVO>();
const data = modalApi.getData<BpmProcessExpressionApi.ProcessExpression>();
if (!data || !data.id) {
return;
}
@@ -77,7 +75,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>

View File

@@ -26,20 +26,26 @@ import ProcessDefinitionDetail from './modules/form.vue';
defineOptions({ name: 'BpmProcessInstanceCreate' });
const route = useRoute(); // 路由
const route = useRoute();
const searchName = ref(''); // 当前搜索关键字
const isSearching = ref(false); // 是否处于搜索状态
const processInstanceId: any = route.query.processInstanceId; // 流程实例编号。场景:重新发起时
const loading = ref(true); // 加载中
const categoryList: any = ref([]); // 分类的列表
const activeCategory = ref(''); // 当前选中的分类
const processDefinitionList = ref<
BpmProcessDefinitionApi.ProcessDefinitionVO[]
>([]); // 流程定义的列表
// 当前搜索关键字
const searchName = ref('');
const isSearching = ref(false);
// 流程实例编号。场景:重新发起时
const processInstanceId: any = route.query.processInstanceId;
// 加载中
const loading = ref(true);
// 分类的列表
const categoryList: any = ref([]);
// 当前选中的分类
const activeCategory = ref('');
// 流程定义的列表
const processDefinitionList = ref<BpmProcessDefinitionApi.ProcessDefinition[]>(
[],
);
// 实现 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 +55,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 +85,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,15 +114,15 @@ const handleGetProcessDefinitionList = async () => {
} catch {
// 错误处理
}
};
}
/** 用于存储搜索过滤后的流程定义 */
const filteredProcessDefinitionList = ref<
BpmProcessDefinitionApi.ProcessDefinitionVO[]
BpmProcessDefinitionApi.ProcessDefinition[]
>([]);
/** 搜索流程 */
const handleQuery = () => {
function handleQuery() {
if (searchName.value.trim()) {
// 如果有搜索关键字,进行过滤
isSearching.value = true;
@@ -141,16 +147,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 {};
}
@@ -159,39 +165,39 @@ const processDefinitionGroup: any = computed(() => {
// 按照 categoryList 的顺序重新组织数据
const orderedGroup: Record<
string,
BpmProcessDefinitionApi.ProcessDefinitionVO[]
BpmProcessDefinitionApi.ProcessDefinition[]
> = {};
categoryList.value.forEach((category: BpmCategoryApi.CategoryVO) => {
categoryList.value.forEach((category: BpmCategoryApi.Category) => {
if (grouped[category.code]) {
orderedGroup[category.code] = grouped[
category.code
] as BpmProcessDefinitionApi.ProcessDefinitionVO[];
] as BpmProcessDefinitionApi.ProcessDefinition[];
}
});
return orderedGroup;
});
/** 通过分类 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 (
row: BpmProcessDefinitionApi.ProcessDefinitionVO,
async function handleSelect(
row: BpmProcessDefinitionApi.ProcessDefinition,
formVariables?: any,
) => {
) {
// 设置选择的流程
selectProcessDefinition.value = row;
// 初始化流程定义详情
await nextTick();
processDefinitionDetailRef.value?.initProcessInfo(row, formVariables);
};
}
/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */
const availableCategories = computed(() => {
@@ -203,7 +209,7 @@ const availableCategories = computed(() => {
const availableCategoryCodes = Object.keys(processDefinitionGroup.value);
// 过滤出有流程的分类
return categoryList.value.filter((category: BpmCategoryApi.CategoryVO) =>
return categoryList.value.filter((category: BpmCategoryApi.Category) =>
availableCategoryCodes.includes(category.code),
);
});
@@ -222,6 +228,7 @@ onMounted(() => {
<template>
<Page auto-content-height>
<!-- TODO @ziye优先级这里交互可以做成类似 vue3 + element-plus 那个一样滚动切换分类哈对标钉钉飞书哈 -->
<!-- 第一步通过流程定义的列表选择对应的流程 -->
<template v-if="!selectProcessDefinition">
<Card
@@ -274,6 +281,7 @@ onMounted(() => {
}"
>
<div class="flex items-center">
<!-- TODO @ziyeiconname 会告警~~ -->
<img
v-if="definition.icon"
:src="definition.icon"

View File

@@ -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 = {};
@@ -164,7 +166,7 @@ const initProcessInfo = async (row: any, formVariables?: any) => {
});
// 加载流程图
const processDefinitionDetail: BpmProcessDefinitionApi.ProcessDefinitionVO =
const processDefinitionDetail: BpmProcessDefinitionApi.ProcessDefinition =
await getProcessDefinition(row.id);
if (processDefinitionDetail) {
bpmnXML.value = processDefinitionDetail.bpmnXml;
@@ -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" />&nbsp; 返回
<IconifyIcon icon="lucide:arrow-left" />&nbsp; 返回
</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>

View File

@@ -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' },
},
];
}

View File

@@ -63,7 +63,7 @@ enum FieldPermissionType {
}
const processInstanceLoading = ref(false); // 流程实例的加载中
const processInstance = ref<BpmProcessInstanceApi.ProcessInstanceVO>(); // 流程实例
const processInstance = ref<BpmProcessInstanceApi.ProcessInstance>(); // 流程实例
const processDefinition = ref<any>({}); // 流程定义
const processModelView = ref<any>({}); // 流程模型视图
const operationButtonRef = ref(); // 操作按钮组件 ref
@@ -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();
});

View File

@@ -3,6 +3,7 @@ defineOptions({ name: 'ProcessInstanceBpmnViewer' });
</script>
<template>
<!-- TODO @ziye可以后续找下 antd 有没可以直接用的组件哈 -->
<div>
<h1>BPMN Viewer</h1>
</div>

View File

@@ -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>
@@ -1246,7 +1254,7 @@ defineExpose({ loadTodoTask });
"
>
<Button type="dashed" @click="openPopover('return')">
<IconifyIcon :size="14" icon="ep:back" />
<IconifyIcon :size="14" icon="lucide:arrow-left" />
{{ getButtonDisplayName(BpmTaskOperationButtonTypeEnum.RETURN) }}
</Button>
<template #content>
@@ -1387,5 +1395,5 @@ defineExpose({ loadTodoTask });
</div>
<!-- 签名弹窗 -->
<Signature ref="signRef" @success="handleSignFinish" />
<SignatureModal @success="handleSignFinish" />
</template>

View File

@@ -3,12 +3,13 @@ import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { base64ToFile } from '@vben/utils';
import { Button, message, Space, Tooltip } from 'ant-design-vue';
// TODO @ziye这个可能适合放到全局因为 element-plus 也用这个;
import Vue3Signature from 'vue3-signature';
import { uploadFile } from '#/api/infra/file';
import { download } from '#/utils';
defineOptions({
name: 'BpmProcessInstanceSignature',
@@ -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,28 @@ const [Modal, modalApi] = useVbenModal({
modalApi.close();
}
},
onConfirm: () => {
submit();
async onConfirm() {
message.success({
content: '签名上传中请稍等。。。',
});
const signFileUrl = await uploadFile({
file: 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 +56,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>

View File

@@ -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',
@@ -36,7 +36,7 @@ const columns = shallowRef([
field: 'approver',
title: '审批人',
slots: {
default: ({ row }: { row: BpmTaskApi.TaskManagerVO }) => {
default: ({ row }: { row: BpmTaskApi.TaskManager }) => {
return row.assigneeUser?.nickname || row.ownerUser?.nickname;
},
},
@@ -75,11 +75,7 @@ const columns = shallowRef([
field: 'durationInMillis',
title: '耗时',
minWidth: 180,
slots: {
default: ({ row }: { row: BpmTaskApi.TaskManagerVO }) => {
return formatPast2(row.durationInMillis);
},
},
formatter: 'formatPast2',
},
]);
@@ -110,15 +106,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
cellConfig: {
height: 60,
},
} as VxeTableGridOptions<BpmTaskApi.TaskVO>,
} as VxeTableGridOptions<BpmTaskApi.Task>,
});
/**
* 刷新表格数据
*/
const refresh = (): void => {
function refresh() {
gridApi.query();
};
}
// 表单相关
interface TaskForm {
@@ -141,7 +137,7 @@ const taskForm = ref<TaskForm>({
* 显示表单详情
* @param row 任务数据
*/
async function showFormDetail(row: BpmTaskApi.TaskManagerVO): Promise<void> {
async function showFormDetail(row: BpmTaskApi.TaskManager): Promise<void> {
// 设置表单配置和表单字段
taskForm.value = {
rule: [],
@@ -204,7 +200,7 @@ defineExpose({
ghost
class="ml-1"
>
<IconifyIcon icon="ep:document" />
<IconifyIcon icon="lucide:file-text" />
<span class="!ml-[2px] text-[12px]">查看表单</span>
</Button>
</div>

View File

@@ -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>

View File

@@ -1,23 +1,19 @@
<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';
import { Page, prompt } from '@vben/common-ui';
import { DocAlert, 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,
} from '#/api/bpm/processInstance';
import { DictTag } from '#/components/dict-tag';
import { DocAlert } from '#/components/doc-alert';
import { router } from '#/router';
import { BpmProcessInstanceStatus, DICT_TYPE } from '#/utils';
@@ -25,54 +21,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.Task) {
router.push({
name: 'BpmProcessInstanceDetail',
query: { id: row.id },
});
}
/** 取消流程实例 */
function onCancel(row: BpmTaskApi.TaskVO) {
function handleCancel(row: BpmTaskApi.Task) {
prompt({
async beforeClose(scope) {
if (scope.isConfirm) {
@@ -104,19 +67,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.Task>,
});
</script>
<template>
@@ -126,7 +107,6 @@ function onRefresh() {
url="https://doc.iocoder.cn/bpm/process-instance"
/>
<FormModal @success="onRefresh" />
<Grid table-title="流程状态">
<!-- 摘要 -->
<template #slot-summary="{ row }">
@@ -154,7 +134,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 +143,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 +158,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>

View File

@@ -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' },
},
];
}

View File

@@ -1,72 +1,29 @@
<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';
import { Page, prompt } from '@vben/common-ui';
import { DocAlert, 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 +32,17 @@ function onTaskClick(task: BpmProcessInstanceApi.Task) {
console.warn(task);
}
/** 查看流程实例 */
function handleDetail(row: BpmProcessInstanceApi.ProcessInstance) {
console.warn(row);
router.push({
name: 'BpmProcessInstanceDetail',
query: { id: row.id },
});
}
/** 取消流程实例 */
function onCancel(row: BpmProcessInstanceApi.ProcessInstanceVO) {
function handleCancel(row: BpmProcessInstanceApi.ProcessInstance) {
prompt({
async beforeClose(scope) {
if (scope.isConfirm) {
@@ -110,19 +76,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.ProcessInstance>,
});
</script>
<template>
@@ -131,6 +112,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>

View File

@@ -0,0 +1,153 @@
import type { VbenFormSchema } from '#/adapter/form';
import type {
VxeGridPropTypes,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 搜索的表单 */
export function useGridFormSchema(
userList: any[] = [],
formFields: any[] = [],
): VbenFormSchema[] {
// 基础搜索字段
const baseFormSchema = [
{
fieldName: 'startUserId',
label: '发起人',
component: 'Select',
componentProps: {
placeholder: '请选择发起人',
allowClear: true,
options: userList.map((user) => ({
label: user.nickname,
value: user.id,
})),
},
},
{
fieldName: 'name',
label: '流程名称',
component: 'Input',
componentProps: {
placeholder: '请输入流程名称',
allowClear: true,
},
},
{
fieldName: 'status',
label: '流程状态',
component: 'Select',
componentProps: {
placeholder: '请选择流程状态',
allowClear: true,
options: getDictOptions(
DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
'number',
),
},
},
{
fieldName: 'createTime',
label: '发起时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
placeholder: ['开始日期', '结束日期'],
allowClear: true,
},
},
{
fieldName: 'endTime',
label: '结束时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
placeholder: ['开始日期', '结束日期'],
allowClear: true,
},
},
];
// 动态表单字段 暂时只支持 input 和 textarea, TODO 其他类型的支持
const dynamicFormSchema = formFields
.filter((item) => item.type === 'input' || item.type === 'textarea')
// 根据类型选择合适的表单组件
.map((item) => {
return {
fieldName: `formFieldsParams.${item.field}`,
label: item.title,
component: 'Input',
componentProps: {
placeholder: `请输入${item.title}`,
allowClear: true,
},
};
});
return [...baseFormSchema, ...dynamicFormSchema];
}
/** 列表的字段 */
export function useGridColumns(
formFields: any[] = [],
): VxeTableGridOptions<BpmProcessInstanceApi.ProcessInstance>['columns'] {
const baseColumns: VxeGridPropTypes.Columns<BpmProcessInstanceApi.ProcessInstance> =
[
{
field: 'name',
title: '流程名称',
minWidth: 250,
fixed: 'left',
},
{
field: 'startUser.nickname',
title: '流程发起人',
minWidth: 200,
},
{
field: 'status',
title: '流程状态',
minWidth: 120,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS },
},
},
{
field: 'startTime',
title: '发起时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
field: 'endTime',
title: '结束时间',
minWidth: 180,
formatter: 'formatDateTime',
},
];
// 添加动态表单字段列暂时全部以字符串TODO 展示优化, 按 type 展示控制
const formFieldColumns = (formFields || []).map((item) => ({
field: `formVariables.${item.field}`,
title: item.title,
minWidth: 120,
formatter: ({ row }: any) => {
return row.formVariables?.[item.field] ?? '';
},
}));
return [
...baseColumns,
...formFieldColumns,
{
title: '操作',
width: 180,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -0,0 +1,200 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
import { h, nextTick, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { confirm, Page } from '@vben/common-ui';
import { Input, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProcessDefinition } from '#/api/bpm/definition';
import {
cancelProcessInstanceByAdmin,
getProcessInstanceManagerPage,
} from '#/api/bpm/processInstance';
import { getSimpleUserList } from '#/api/system/user';
import { parseFormFields } from '#/components/simple-process-design';
import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'BpmProcessInstanceReport' });
const router = useRouter(); // 路由
const { query } = useRoute();
const processDefinitionId = query.processDefinitionId as string;
const formFields = ref<any[]>([]);
const userList = ref<any[]>([]); // 用户列表
const gridReady = ref(false); // 表格是否准备好
const cancelReason = ref(''); // 取消原因
// 表格的列需要解析表单字段,这里定义成变量,解析表单字段后再渲染
let Grid: any = null;
let gridApi: any = null;
/** 获取流程定义 */
const getProcessDefinitionData = async () => {
try {
const processDefinition = await getProcessDefinition(processDefinitionId);
if (processDefinition && processDefinition.formFields) {
formFields.value = parseFormCreateFields(processDefinition.formFields);
}
} catch (error) {
console.error('获取流程定义失败', error);
}
};
/** 解析表单字段 */
const parseFormCreateFields = (formFields?: string[]) => {
const result: Array<Record<string, any>> = [];
if (formFields) {
formFields.forEach((fieldStr: string) => {
try {
parseFormFields(JSON.parse(fieldStr), result);
} catch (error) {
console.error('解析表单字段失败', error);
}
});
}
return result;
};
/** 刷新表格 */
function onRefresh() {
if (gridApi) {
gridApi.query();
}
}
/** 查看详情 */
const handleDetail = (row: BpmProcessInstanceApi.ProcessInstance) => {
router.push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.id,
},
});
};
/** 取消按钮操作 */
const handleCancel = async (row: BpmProcessInstanceApi.ProcessInstance) => {
cancelReason.value = ''; // 重置取消原因
confirm({
title: '取消流程',
content: h('div', [
h('p', '请输入取消原因:'),
h(Input, {
value: cancelReason.value,
'onUpdate:value': (val: string) => {
cancelReason.value = val;
},
placeholder: '请输入取消原因',
}),
]),
beforeClose: async ({ isConfirm }) => {
if (!isConfirm) return;
if (!cancelReason.value.trim()) {
message.warning('请输入取消原因');
return false;
}
await cancelProcessInstanceByAdmin(row.id, cancelReason.value);
return true;
},
}).then(() => {
message.success('取消成功');
onRefresh();
});
};
/** 创建表格 */
const createGrid = () => {
const [GridCompnent, api] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(userList.value, formFields.value),
},
gridOptions: {
columns: useGridColumns(formFields.value),
height: 'auto',
keepSource: true,
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
// 处理表单值,将 formFieldsParams 对象提取出来
const { formFieldsParams = {}, ...restValues } = formValues || {};
const params = {
pageNo: page.currentPage,
pageSize: page.pageSize,
...restValues,
processDefinitionKey: query.processDefinitionKey,
formFieldsParams: JSON.stringify(formFieldsParams),
};
return await getProcessInstanceManagerPage(params);
},
},
},
} as VxeTableGridOptions,
});
Grid = GridCompnent;
gridApi = api;
gridReady.value = true;
};
/** 初始化 */
onMounted(async () => {
// 获取用户列表
userList.value = await getSimpleUserList();
// 获取流程定义,并获取表单字段。
await getProcessDefinitionData();
// 解析表单字段后,再创建表格,表格的列依赖于表单字段
createGrid();
// 确保 DOM 更新完成
await nextTick();
// 加载表格数据
gridApi.query();
});
</script>
<template>
<Page auto-content-height>
<!-- 动态渲染表格 -->
<component :is="Grid" v-if="gridReady" table-title="流程实例列表">
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '详情',
type: 'link',
icon: ACTION_ICON.VIEW,
auth: ['bpm:process-instance:query'],
onClick: handleDetail.bind(null, row),
},
{
label: '取消',
type: 'link',
icon: ACTION_ICON.DELETE,
auth: ['bpm:process-instance:cancel'],
ifShow: row.status === 1,
onClick: handleCancel.bind(null, row),
},
]"
/>
</template>
</component>
</Page>
</template>

View File

@@ -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' },
},
];
}

View File

@@ -1,21 +1,16 @@
<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 { DocAlert, Page, useVbenModal } from '@vben/common-ui';
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,
} from '#/api/bpm/processListener';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -25,12 +20,46 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 创建流程监听器 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑流程监听器 */
function handleEdit(row: BpmProcessListenerApi.ProcessListener) {
formModalApi.setData(row).open();
}
/** 删除流程监听器 */
async function handleDelete(row: BpmProcessListenerApi.ProcessListener) {
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: {
@@ -51,59 +80,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<BpmProcessListenerApi.ProcessListenerVO>,
} as VxeTableGridOptions<BpmProcessListenerApi.ProcessListener>,
});
/** 表格操作按钮的回调函数 */
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 +92,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>

View File

@@ -15,10 +15,10 @@ 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>();
const formData = ref<BpmProcessListenerApi.ProcessListener>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['流程监听器'])
@@ -31,7 +31,7 @@ const [Form, formApi] = useVbenForm({
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 110,
labelWidth: 100,
},
layout: 'horizontal',
schema: useFormSchema(),
@@ -47,7 +47,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as BpmProcessListenerApi.ProcessListenerVO;
(await formApi.getValues()) as BpmProcessListenerApi.ProcessListener;
try {
await (formData.value?.id
? updateProcessListener(data)
@@ -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>();
const data = modalApi.getData<BpmProcessListenerApi.ProcessListener>();
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>

View File

@@ -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' },
},
];
}

View File

@@ -1,27 +1,36 @@
<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 { DocAlert, 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.Copy) {
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: {
@@ -45,38 +54,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
cellConfig: {
height: 64,
},
} as VxeTableGridOptions<BpmProcessInstanceApi.CopyVO>,
} as VxeTableGridOptions<BpmProcessInstanceApi.Copy>,
});
/** 表格操作按钮的回调函数 */
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 +67,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>

View File

@@ -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' },
},
];
}

View File

@@ -1,27 +1,34 @@
<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 { DocAlert, 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';
import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'BpmDoneTask' });
const [Grid, gridApi] = useVbenVxeGrid({
/** 查看历史 */
function handleHistory(row: BpmTaskApi.TaskManager) {
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: {
@@ -45,35 +52,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
cellConfig: {
height: 64,
},
} as VxeTableGridOptions<BpmTaskApi.TaskVO>,
} as VxeTableGridOptions<BpmTaskApi.Task>,
});
/** 表格操作按钮的回调函数 */
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 +72,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>

View File

@@ -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' },
},
];
}

View File

@@ -1,27 +1,33 @@
<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 { DocAlert, 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';
import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'BpmManagerTask' });
/** 查看历史 */
function handleHistory(row: BpmTaskApi.TaskManager) {
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: {
@@ -45,32 +51,8 @@ const [Grid] = useVbenVxeGrid({
cellConfig: {
height: 64,
},
} as VxeTableGridOptions<BpmTaskApi.TaskManagerVO>,
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
});
/** 表格操作按钮的回调函数 */
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 +60,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>

View File

@@ -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' },
},
];
}

View File

@@ -1,27 +1,35 @@
<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 { DocAlert, 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';
import { useGridColumns, useGridFormSchema } from './data';
defineOptions({ name: 'BpmTodoTask' });
const [Grid, gridApi] = useVbenVxeGrid({
/** 办理任务 */
function handleAudit(row: BpmTaskApi.Task) {
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: {
@@ -45,35 +53,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
cellConfig: {
height: 64,
},
} as VxeTableGridOptions<BpmTaskApi.TaskVO>,
} as VxeTableGridOptions<BpmTaskApi.Task>,
});
/** 表格操作按钮的回调函数 */
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 +71,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>

View File

@@ -21,8 +21,6 @@ import CustomerTodayContactList from './modules/customer-today-contact-list.vue'
import ReceivableAuditList from './modules/receivable-audit-list.vue';
import ReceivablePlanRemindList from './modules/receivable-plan-remind-list.vue';
defineOptions({ name: 'CrmBacklog' });
const leftMenu = ref('customerTodayContact');
const clueFollowCount = ref(0);

View File

@@ -1,13 +1,11 @@
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 { erpPriceMultiply } 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';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
@@ -93,6 +91,12 @@ export function useFormSchema(): VbenFormSchema[] {
valueFormat: 'x',
},
},
{
fieldName: 'product',
label: '产品清单',
component: 'Input',
formItemClass: 'col-span-3',
},
{
fieldName: 'totalProductPrice',
label: '产品总金额',
@@ -164,7 +168,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
{
field: 'totalPrice',
title: '商机金额(元)',
formatter: 'formatNumber',
formatter: 'formatAmount2',
},
{
field: 'dealTime',
@@ -225,123 +229,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',
},
];
}

View 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'),
);

View File

@@ -4,7 +4,7 @@ import type { CrmBusinessApi } from '#/api/crm/business';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
@@ -15,7 +15,6 @@ import {
exportBusiness,
getBusinessPage,
} from '#/api/crm/business';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';

View File

@@ -0,0 +1,124 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DescriptionItemSchema } from '#/components/description';
import { erpPriceInputFormatter, formatDateTime } from '@vben/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: 'formatAmount2',
},
{
field: 'dealTime',
title: '预计成交日期',
formatter: 'formatDate',
},
{
field: 'ownerUserName',
title: '负责人',
},
{
field: 'ownerUserDeptName',
title: '所属部门',
},
{
field: 'statusTypeName',
title: '商机状态组',
fixed: 'right',
},
{
field: 'statusName',
title: '商机阶段',
fixed: 'right',
},
];
}

View File

@@ -6,9 +6,7 @@ import { Divider } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
import { useDetailBaseSchema } from '../data';
defineOptions({ name: 'CrmCustomerDetailsInfo' });
import { useDetailBaseSchema } from './detail-data';
defineProps<{
business: CrmBusinessApi.Business; // 商机信息

View File

@@ -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<{

View File

@@ -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';

View File

@@ -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,44 +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 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);
@@ -124,9 +99,9 @@ async function handleUpdateStatus() {
}
// 加载数据
onMounted(async () => {
onMounted(() => {
businessId.value = Number(route.params.id);
await loadBusinessDetail();
loadBusinessDetail();
});
</script>
@@ -173,12 +148,16 @@ onMounted(async () => {
<ContactDetailsList
:biz-id="businessId"
:biz-type="BizTypeEnum.CRM_BUSINESS"
:business-id="business.id"
:business-id="businessId"
:customer-id="business.customerId"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="产品" key="4" :force-render="true">
<div>产品</div>
<ProductDetailsList
:biz-id="businessId"
:biz-type="BizTypeEnum.CRM_BUSINESS"
:business="business"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="合同" key="5" :force-render="true">
<ContractDetailsList
@@ -196,7 +175,7 @@ onMounted(async () => {
/>
</Tabs.TabPane>
<Tabs.TabPane tab="操作日志" key="7" :force-render="true">
<OperateLog :log-list="businessLogList" />
<AsyncOperateLog :log-list="businessLogList" />
</Tabs.TabPane>
</Tabs>
</Card>

View File

@@ -4,6 +4,7 @@ import type { CrmBusinessApi } from '#/api/crm/business';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { erpPriceMultiply } from '@vben/utils';
import { message } from 'ant-design-vue';
@@ -13,7 +14,9 @@ import {
getBusiness,
updateBusiness,
} from '#/api/crm/business';
import { BizTypeEnum } from '#/api/crm/permission';
import { $t } from '#/locales';
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>

View File

@@ -2,7 +2,7 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { CrmBusinessStatusApi } from '#/api/crm/business/status';
import { Page, useVbenModal } from '@vben/common-ui';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
@@ -11,7 +11,6 @@ import {
deleteBusinessStatus,
getBusinessStatusPage,
} from '#/api/crm/business/status';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns } from './data';

View File

@@ -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: '备注',
},
];
}

View File

@@ -4,14 +4,13 @@ import type { CrmClueApi } from '#/api/crm/clue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteClue, exportClue, getCluePage } from '#/api/crm/clue';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';

View 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: '备注',
},
];
}

View File

@@ -6,9 +6,7 @@ import { Divider } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
import { useDetailBaseSchema } from '../data';
defineOptions({ name: 'CrmClueDetailsInfo' });
import { useDetailBaseSchema } from './detail-data';
defineProps<{
clue: CrmClueApi.Clue; // 线索信息

View File

@@ -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);
@@ -127,9 +114,9 @@ async function handleTransform(): Promise<boolean | undefined> {
}
// 加载数据
onMounted(async () => {
onMounted(() => {
clueId.value = Number(route.params.id);
await loadClueDetail();
loadClueDetail();
});
</script>
@@ -184,7 +171,7 @@ onMounted(async () => {
/>
</Tabs.TabPane>
<Tabs.TabPane tab="操作日志" key="4" :force-render="true">
<OperateLog :log-list="clueLogList" />
<AsyncOperateLog :log-list="clueLogList" />
</Tabs.TabPane>
</Tabs>
</Card>

View File

@@ -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 },
},
},
];
}

View 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'),
);

View File

@@ -5,7 +5,7 @@ import type { CrmContactApi } from '#/api/crm/contact';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Tabs } from 'ant-design-vue';
@@ -16,7 +16,6 @@ import {
exportContact,
getContactPage,
} from '#/api/crm/contact';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';

View 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 },
},
},
];
}

View File

@@ -6,9 +6,7 @@ import { Divider } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
import { useDetailBaseSchema } from '../data';
defineOptions({ name: 'CrmContactDetailsInfo' });
import { useDetailBaseSchema } from './detail-data';
defineProps<{
contact: CrmContactApi.Contact; // 联系人信息

View File

@@ -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<{

View File

@@ -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';

View File

@@ -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);
@@ -107,9 +84,9 @@ function handleTransfer() {
}
// 加载数据
onMounted(async () => {
onMounted(() => {
contactId.value = Number(route.params.id);
await loadContactDetail();
loadContactDetail();
});
</script>
@@ -156,7 +133,7 @@ onMounted(async () => {
/>
</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(async () => {
/>
</Tabs.TabPane>
<Tabs.TabPane tab="操作日志" key="5" :force-render="true">
<OperateLog :log-list="contactLogList" />
<AsyncOperateLog :log-list="contactLogList" />
</Tabs.TabPane>
</Tabs>
</Card>

View File

@@ -1,28 +1,33 @@
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 { erpPriceMultiply, floatToFixed2 } 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 { DICT_TYPE } from '#/utils/dict';
import { getSimpleUserList } from '#/api/system/user';
import { DICT_TYPE } from '#/utils';
/** 新增/修改的表单 */
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 +39,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 +66,8 @@ export function useFormSchema(): VbenFormSchema[] {
},
{
fieldName: 'businessId',
label: '商机',
label: '商机名称',
component: 'ApiSelect',
rules: 'required',
componentProps: {
api: getSimpleBusinessList,
labelField: 'name',
@@ -58,49 +75,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 +138,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,
);
},
},
},
];
}
@@ -178,7 +243,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
title: '合同金额(元)',
field: 'totalPrice',
minWidth: 150,
formatter: 'formatNumber',
formatter: 'formatAmount2',
},
{
title: '下单时间',
@@ -213,7 +278,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
title: '已回款金额(元)',
field: 'totalReceivablePrice',
minWidth: 150,
formatter: 'formatNumber',
formatter: 'formatAmount2',
},
{
title: '未回款金额(元)',
@@ -280,182 +345,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 },
},
},
];
}

View 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'),
);

View File

@@ -5,7 +5,7 @@ import type { CrmContractApi } from '#/api/crm/contract';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Tabs } from 'ant-design-vue';
@@ -17,7 +17,6 @@ import {
getContractPage,
submitContract,
} from '#/api/crm/contract';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';

View File

@@ -0,0 +1,191 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DescriptionItemSchema } from '#/components/description';
import { h } from 'vue';
import {
erpPriceInputFormatter,
floatToFixed2,
formatDateTime,
} from '@vben/utils';
import { DictTag } from '#/components/dict-tag';
import { DICT_TYPE } 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: 'formatAmount2',
},
{
title: '合同开始时间',
field: 'startTime',
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '合同结束时间',
field: 'endTime',
minWidth: 150,
formatter: 'formatDateTime',
},
{
title: '已回款金额(元)',
field: 'totalReceivablePrice',
minWidth: 150,
formatter: 'formatAmount2',
},
{
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 },
},
},
];
}

View File

@@ -6,9 +6,7 @@ import { Divider } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
import { useDetailBaseSchema } from '../data';
defineOptions({ name: 'CrmContractDetailsInfo' });
import { useDetailBaseSchema } from './detail-data';
defineProps<{
contract: CrmContractApi.Contract; // 合同信息

View File

@@ -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<{

View File

@@ -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,29 +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 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'),
);
import { useDetailSchema } from './detail-data';
const loading = ref(false);
@@ -67,7 +55,7 @@ const [Description] = useDescription({
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: ClueForm,
connectedComponent: ContractForm,
destroyOnClose: true,
});
@@ -107,9 +95,9 @@ function handleTransfer() {
}
// 加载数据
onMounted(async () => {
onMounted(() => {
contractId.value = Number(route.params.id);
await loadContractDetail();
loadContractDetail();
});
</script>
@@ -147,8 +135,22 @@ onMounted(async () => {
<Tabs.TabPane tab="合同跟进" key="2" :force-render="true">
<FollowUp :biz-id="contractId" :biz-type="BizTypeEnum.CRM_CONTRACT" />
</Tabs.TabPane>
<Tabs.TabPane tab="产品" key="3" :force-render="true" />
<Tabs.TabPane tab="回款" key="4" :force-render="true" />
<Tabs.TabPane tab="产品" key="3" :force-render="true">
<ProductDetailsList
:biz-id="contractId"
:biz-type="BizTypeEnum.CRM_CONTRACT"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="回款" key="4" :force-render="true">
<ReceivablePlanDetailsList
:contract-id="contractId"
:customer-id="contract.customerId"
/>
<ReceivableDetailsList
:contract-id="contractId"
:customer-id="contract.customerId"
/>
</Tabs.TabPane>
<Tabs.TabPane tab="团队成员" key="5" :force-render="true">
<PermissionList
ref="permissionListRef"
@@ -159,7 +161,7 @@ onMounted(async () => {
/>
</Tabs.TabPane>
<Tabs.TabPane tab="操作日志" key="6" :force-render="true">
<OperateLog :log-list="contractLogList" />
<AsyncOperateLog :log-list="contractLogList" />
</Tabs.TabPane>
</Tabs>
</Card>

View File

@@ -4,6 +4,7 @@ import type { CrmContractApi } from '#/api/crm/contract';
import { computed, ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import { erpPriceMultiply } from '@vben/utils';
import { message } from 'ant-design-vue';
@@ -12,7 +13,9 @@ import {
getContract,
updateContract,
} from '#/api/crm/contract';
import { BizTypeEnum } from '#/api/crm/permission';
import { $t } from '#/locales';
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>

View File

@@ -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: '备注',
},
];
}

View 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'),
);

View File

@@ -5,7 +5,7 @@ import type { CrmCustomerApi } from '#/api/crm/customer';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Tabs } from 'ant-design-vue';
@@ -16,11 +16,11 @@ import {
exportCustomer,
getCustomerPage,
} from '#/api/crm/customer';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ImportForm from './modules/import-form.vue';
const { push } = useRouter();
const sceneType = ref('1');
@@ -35,6 +35,16 @@ function onRefresh() {
gridApi.query();
}
const [ImportModal, importModalApi] = useVbenModal({
connectedComponent: ImportForm,
destroyOnClose: true,
});
/** 导入客户 */
function handleImport() {
importModalApi.open();
}
/** 导出表格 */
async function handleExport() {
const data = await exportCustomer(await gridApi.formApi.getValues());
@@ -124,6 +134,7 @@ function onChangeSceneType(key: number | string) {
</template>
<FormModal @success="onRefresh" />
<ImportModal @success="onRefresh" />
<Grid>
<template #top>
<Tabs class="border-none" @change="onChangeSceneType">
@@ -142,6 +153,13 @@ function onChangeSceneType(key: number | string) {
auth: ['crm:customer:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.import'),
type: 'primary',
icon: ACTION_ICON.UPLOAD,
auth: ['crm:customer:import'],
onClick: handleImport,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',

View File

@@ -30,7 +30,7 @@ export function useFormSchema(confType: LimitConfType): VbenFormSchema[] {
label: 'nickname',
value: 'id',
},
multiple: true,
mode: 'tags',
allowClear: true,
},
rules: 'required',

View File

@@ -4,7 +4,7 @@ import type { CrmCustomerLimitConfigApi } from '#/api/crm/customer/limitConfig';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { message, Tabs } from 'ant-design-vue';
@@ -14,7 +14,6 @@ import {
getCustomerLimitConfigPage,
LimitConfType,
} from '#/api/crm/customer/limitConfig';
import { DocAlert } from '#/components/doc-alert';
import { $t } from '#/locales';
import { useGridColumns } from './data';

View 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: '备注',
},
];
}

View File

@@ -6,9 +6,7 @@ import { Divider } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useFollowUpDetailSchema } from '#/views/crm/followup/data';
import { useDetailBaseSchema } from '../data';
defineOptions({ name: 'CrmCustomerDetailsInfo' });
import { useDetailBaseSchema } from './detail-data';
defineProps<{
customer: CrmCustomerApi.Customer; // 客户信息

View File

@@ -1,4 +0,0 @@
<script lang="ts" setup></script>
<template>
<div>customerList</div>
</template>

Some files were not shown because too many files have changed in this diff Show More